Commit b09c7ba6 authored by Ahmed Ashour's avatar Ahmed Ashour Committed by skylot

feat(gui): support project (#526) (PR #543)

parent ec66476a
......@@ -231,10 +231,6 @@ public class JadxCLIArgs {
return deobfuscationUseSourceNameAsAlias;
}
public boolean escapeUnicode() {
return escapeUnicode;
}
public boolean isEscapeUnicode() {
return escapeUnicode;
}
......
......@@ -26,7 +26,7 @@ public class JadxGUI {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
}
NLS.setLocale(settings.getLangLocale());
SwingUtilities.invokeLater(new MainWindow(settings)::open);
SwingUtilities.invokeLater(new MainWindow(settings)::init);
} catch (Exception e) {
LOG.error("Error: {}", e.getMessage(), e);
System.exit(1);
......
......@@ -47,27 +47,24 @@ public class JadxWrapper {
}
public void saveAll(final File dir, final ProgressMonitor progressMonitor) {
Runnable save = new Runnable() {
@Override
public void run() {
try {
decompiler.getArgs().setRootDir(dir);
ThreadPoolExecutor ex = (ThreadPoolExecutor) decompiler.getSaveExecutor();
ex.shutdown();
while (ex.isTerminating()) {
long total = ex.getTaskCount();
long done = ex.getCompletedTaskCount();
progressMonitor.setProgress((int) (done * 100.0 / total));
Thread.sleep(500);
}
progressMonitor.close();
LOG.info("decompilation complete, freeing memory ...");
decompiler.getClasses().forEach(JavaClass::unload);
LOG.info("done");
} catch (InterruptedException e) {
LOG.error("Save interrupted", e);
Thread.currentThread().interrupt();
Runnable save = () -> {
try {
decompiler.getArgs().setRootDir(dir);
ThreadPoolExecutor ex = (ThreadPoolExecutor) decompiler.getSaveExecutor();
ex.shutdown();
while (ex.isTerminating()) {
long total = ex.getTaskCount();
long done = ex.getCompletedTaskCount();
progressMonitor.setProgress((int) (done * 100.0 / total));
Thread.sleep(500);
}
progressMonitor.close();
LOG.info("decompilation complete, freeing memory ...");
decompiler.getClasses().forEach(JavaClass::unload);
LOG.info("done");
} catch (InterruptedException e) {
LOG.error("Save interrupted", e);
Thread.currentThread().interrupt();
}
};
new Thread(save).start();
......
......@@ -58,8 +58,7 @@ public class BackgroundWorker extends SwingWorker<Void, Void> {
if (searchIndex != null && searchIndex.getSkippedCount() > 0) {
LOG.warn("Indexing of some classes skipped, count: {}, low memory: {}",
searchIndex.getSkippedCount(), Utils.memoryInfo());
String msg = NLS.str("message.indexingClassesSkipped");
msg = String.format(msg, searchIndex.getSkippedCount());
String msg = NLS.str("message.indexingClassesSkipped", searchIndex.getSkippedCount());
JOptionPane.showMessageDialog(null, msg);
}
} catch (Exception e) {
......
package jadx.gui.settings;
import java.io.BufferedWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import jadx.gui.utils.PathTypeAdapter;
public class JadxProject {
private static final Logger LOG = LoggerFactory.getLogger(JadxProject.class);
private static final int CURRENT_SETTINGS_VERSION = 0;
public static final String PROJECT_EXTENSION = "jadx";
private static final Gson GSON = new GsonBuilder()
.registerTypeHierarchyAdapter(Path.class, PathTypeAdapter.singleton())
.create();
private transient JadxSettings settings;
private transient String name = "New Project";
private transient Path projectPath;
private List<Path> filesPath;
private transient boolean saved;
private transient boolean initial = true;
private int projectVersion = 0;
public JadxProject(JadxSettings settings) {
this.settings = settings;
}
public Path getProjectPath() {
return projectPath;
}
private void setProjectPath(Path projectPath) {
this.projectPath = projectPath;
if (projectVersion != CURRENT_SETTINGS_VERSION) {
upgradeSettings(projectVersion);
}
name = projectPath.getFileName().toString();
name = name.substring(0, name.lastIndexOf('.'));
changed();
}
public Path getFilePath() {
return filesPath == null ? null : filesPath.get(0);
}
public void setFilePath(Path filePath) {
if (!filePath.equals(getFilePath())) {
this.filesPath = Arrays.asList(filePath);
changed();
}
}
private void changed() {
if (settings.isAutoSaveProject()) {
save();
}
else {
saved = false;
}
initial = false;
}
public String getName() {
return name;
}
public boolean isSaved() {
return saved;
}
public boolean isInitial() {
return initial;
}
public void saveAs(Path path) {
setProjectPath(path);
save();
}
public void save() {
try (BufferedWriter writer = Files.newBufferedWriter(getProjectPath())) {
writer.write(GSON.toJson(this));
saved = true;
} catch (Exception e) {
LOG.error("Error saving project", e);
}
}
public static JadxProject from(Path path, JadxSettings settings) {
try {
List<String> lines = Files.readAllLines(path);
if (!lines.isEmpty()) {
JadxProject project = GSON.fromJson(lines.get(0), JadxProject.class);
project.settings = settings;
project.setProjectPath(path);
project.saved = true;
return project;
}
} catch (Exception e) {
LOG.error("Error loading project", e);
}
return null;
}
private void upgradeSettings(int fromVersion) {
LOG.debug("upgrade settings from version: {} to {}", fromVersion, CURRENT_SETTINGS_VERSION);
if (fromVersion == 0) {
fromVersion++;
}
projectVersion = CURRENT_SETTINGS_VERSION;
save();
}
}
......@@ -4,6 +4,8 @@ import java.awt.Font;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Window;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
......@@ -27,25 +29,27 @@ import jadx.gui.utils.Utils;
public class JadxSettings extends JadxCLIArgs {
private static final Logger LOG = LoggerFactory.getLogger(JadxSettings.class);
private static final String USER_HOME = System.getProperty("user.home");
private static final int RECENT_FILES_COUNT = 15;
private static final int CURRENT_SETTINGS_VERSION = 8;
private static final Path USER_HOME = Paths.get(System.getProperty("user.home"));
private static final int RECENT_PROJECTS_COUNT = 15;
private static final int CURRENT_SETTINGS_VERSION = 9;
private static final Font DEFAULT_FONT = new RSyntaxTextArea().getFont();
static final Set<String> SKIP_FIELDS = new HashSet<>(Arrays.asList(
"files", "input", "outDir", "outDirSrc", "outDirRes", "verbose", "printVersion", "printHelp"
));
private String lastOpenFilePath = USER_HOME;
private String lastSaveFilePath = USER_HOME;
private Path lastSaveProjectPath = USER_HOME;
private Path lastOpenFilePath = USER_HOME;
private Path lastSaveFilePath = USER_HOME;
private boolean flattenPackage = false;
private boolean checkForUpdates = false;
private List<String> recentFiles = new ArrayList<>();
private List<Path> recentProjects = new ArrayList<>();
private String fontStr = "";
private String editorThemePath = "";
private LangLocale langLocale = NLS.defaultLocale();
private boolean autoStartJobs = false;
protected String excludedPackages = "";
private boolean autoSaveProject = false;
private boolean showHeapUsageBar = true;
......@@ -84,20 +88,29 @@ public class JadxSettings extends JadxCLIArgs {
}
}
public String getLastOpenFilePath() {
public Path getLastOpenFilePath() {
return lastOpenFilePath;
}
public void setLastOpenFilePath(String lastOpenFilePath) {
public void setLastOpenFilePath(Path lastOpenFilePath) {
this.lastOpenFilePath = lastOpenFilePath;
partialSync(settings -> settings.lastOpenFilePath = JadxSettings.this.lastOpenFilePath);
}
public String getLastSaveFilePath() {
public Path getLastSaveProjectPath() {
return lastSaveProjectPath;
}
public Path getLastSaveFilePath() {
return lastSaveFilePath;
}
public void setLastSaveFilePath(String lastSaveFilePath) {
public void setLastSaveProjectPath(Path lastSaveProjectPath) {
this.lastSaveProjectPath = lastSaveProjectPath;
partialSync(settings -> settings.lastSaveProjectPath = JadxSettings.this.lastSaveProjectPath);
}
public void setLastSaveFilePath(Path lastSaveFilePath) {
this.lastSaveFilePath = lastSaveFilePath;
partialSync(settings -> settings.lastSaveFilePath = JadxSettings.this.lastSaveFilePath);
}
......@@ -120,18 +133,18 @@ public class JadxSettings extends JadxCLIArgs {
sync();
}
public Iterable<String> getRecentFiles() {
return recentFiles;
public Iterable<Path> getRecentProjects() {
return recentProjects;
}
public void addRecentFile(String filePath) {
recentFiles.remove(filePath);
recentFiles.add(0, filePath);
int count = recentFiles.size();
if (count > RECENT_FILES_COUNT) {
recentFiles.subList(RECENT_FILES_COUNT, count).clear();
public void addRecentProject(Path projectPath) {
recentProjects.remove(projectPath);
recentProjects.add(0, projectPath);
int count = recentProjects.size();
if (count > RECENT_PROJECTS_COUNT) {
recentProjects.subList(RECENT_PROJECTS_COUNT, count).clear();
}
partialSync(settings -> settings.recentFiles = recentFiles);
partialSync(settings -> settings.recentProjects = recentProjects);
}
public void saveWindowPos(Window window) {
......@@ -265,6 +278,14 @@ public class JadxSettings extends JadxCLIArgs {
this.autoStartJobs = autoStartJobs;
}
public boolean isAutoSaveProject() {
return autoSaveProject;
}
public void setAutoSaveProject(boolean autoSaveProject) {
this.autoSaveProject = autoSaveProject;
}
public void setExportAsGradleProject(boolean exportAsGradleProject) {
this.exportAsGradleProject = exportAsGradleProject;
}
......@@ -343,6 +364,10 @@ public class JadxSettings extends JadxCLIArgs {
outDir = null;
outDirSrc = null;
outDirRes = null;
fromVersion++;
}
if (fromVersion == 8) {
fromVersion++;
}
settingsVersion = CURRENT_SETTINGS_VERSION;
sync();
......
package jadx.gui.settings;
import java.lang.reflect.Modifier;
import java.nio.file.Path;
import java.util.prefs.Preferences;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.ExclusionStrategy;
import com.google.gson.FieldAttributes;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.InstanceCreator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.gui.JadxGUI;
import jadx.gui.utils.PathTypeAdapter;
public class JadxSettingsAdapter {
......@@ -34,7 +37,10 @@ public class JadxSettingsAdapter {
return false;
}
};
private static final GsonBuilder GSON_BUILDER = new GsonBuilder().setExclusionStrategies(EXCLUDE_FIELDS);
private static final GsonBuilder GSON_BUILDER = new GsonBuilder()
.setExclusionStrategies(EXCLUDE_FIELDS)
.registerTypeHierarchyAdapter(Path.class, PathTypeAdapter.singleton())
;
private static final Gson GSON = GSON_BUILDER.create();
private JadxSettingsAdapter() {
......
......@@ -52,6 +52,7 @@ public class JadxSettingsWindow extends JDialog {
panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
panel.add(makeDeobfuscationGroup());
panel.add(makeDecompilationGroup());
panel.add(makeProjectGroup());
panel.add(makeEditorGroup());
panel.add(makeOtherGroup());
......@@ -168,6 +169,18 @@ public class JadxSettingsWindow extends JDialog {
connectedComponents.forEach(comp -> comp.setEnabled(enabled));
}
private SettingsGroup makeProjectGroup() {
JCheckBox autoSave = new JCheckBox();
autoSave.setSelected(settings.isAutoSaveProject());
autoSave.addItemListener(e ->
settings.setAutoSaveProject(e.getStateChange() == ItemEvent.SELECTED));
SettingsGroup group = new SettingsGroup(NLS.str("preferences.project"));
group.addRow(NLS.str("preferences.autoSave"), autoSave);
return group;
}
private SettingsGroup makeEditorGroup() {
JButton fontBtn = new JButton(NLS.str("preferences.select_font"));
......@@ -186,9 +199,9 @@ public class JadxSettingsWindow extends JDialog {
mainWindow.loadSettings();
});
SettingsGroup other = new SettingsGroup(NLS.str("preferences.editor"));
JLabel fontLabel = other.addRow(getFontLabelStr(), fontBtn);
other.addRow(NLS.str("preferences.theme"), themesCbx);
SettingsGroup group = new SettingsGroup(NLS.str("preferences.editor"));
JLabel fontLabel = group.addRow(getFontLabelStr(), fontBtn);
group.addRow(NLS.str("preferences.theme"), themesCbx);
fontBtn.addMouseListener(new MouseAdapter() {
@Override
......@@ -205,7 +218,7 @@ public class JadxSettingsWindow extends JDialog {
}
}
});
return other;
return group;
}
private String getFontLabelStr() {
......@@ -263,7 +276,7 @@ public class JadxSettingsWindow extends JDialog {
autoStartJobs.addItemListener(e -> settings.setAutoStartJobs(e.getStateChange() == ItemEvent.SELECTED));
JCheckBox escapeUnicode = new JCheckBox();
escapeUnicode.setSelected(settings.escapeUnicode());
escapeUnicode.setSelected(settings.isEscapeUnicode());
escapeUnicode.addItemListener(e -> {
settings.setEscapeUnicode(e.getStateChange() == ItemEvent.SELECTED);
needReload();
......@@ -333,12 +346,12 @@ public class JadxSettingsWindow extends JDialog {
needReload();
});
SettingsGroup other = new SettingsGroup(NLS.str("preferences.other"));
other.addRow(NLS.str("preferences.language"), languageCbx);
other.addRow(NLS.str("preferences.check_for_updates"), update);
other.addRow(NLS.str("preferences.cfg"), cfg);
other.addRow(NLS.str("preferences.raw_cfg"), rawCfg);
return other;
SettingsGroup group = new SettingsGroup(NLS.str("preferences.other"));
group.addRow(NLS.str("preferences.language"), languageCbx);
group.addRow(NLS.str("preferences.check_for_updates"), update);
group.addRow(NLS.str("preferences.cfg"), cfg);
group.addRow(NLS.str("preferences.raw_cfg"), rawCfg);
return group;
}
private void needReload() {
......
......@@ -77,15 +77,15 @@ public class ApkSignature extends JNode {
final String err = NLS.str("apkSignature.errors");
final String warn = NLS.str("apkSignature.warnings");
final String sigSucc = NLS.str("apkSignature.signatureSuccess");
final String sigFail = NLS.str("apkSignature.signatureFailed");
final String sigSuccKey = "apkSignature.signatureSuccess";
final String sigFailKey = "apkSignature.signatureFailed";
writeIssues(builder, err, result.getErrors());
writeIssues(builder, warn, result.getWarnings());
if (!result.getV1SchemeSigners().isEmpty()) {
builder.append("<h2>");
builder.escape(String.format(result.isVerifiedUsingV1Scheme() ? sigSucc : sigFail, 1));
builder.escape(NLS.str(result.isVerifiedUsingV1Scheme() ? sigSuccKey : sigFailKey, 1));
builder.append("</h2>\n");
builder.append("<blockquote>");
......@@ -106,7 +106,7 @@ public class ApkSignature extends JNode {
}
if (!result.getV2SchemeSigners().isEmpty()) {
builder.append("<h2>");
builder.escape(String.format(result.isVerifiedUsingV2Scheme() ? sigSucc : sigFail, 2));
builder.escape(NLS.str(result.isVerifiedUsingV2Scheme() ? sigSuccKey : sigFailKey, 2));
builder.append("</h2>\n");
builder.append("<blockquote>");
......
......@@ -232,11 +232,10 @@ public abstract class CommonSearchDialog extends JDialog {
}
protected void updateProgressLabel() {
String statusText = String.format(
NLS.str("search_dialog.info_label"),
resultsModel.getDisplayedResultsStart(),
resultsModel.getDisplayedResultsEnd(),
resultsModel.getResultCount()
String statusText = NLS.str("search_dialog.info_label",
resultsModel.getDisplayedResultsStart(),
resultsModel.getDisplayedResultsEnd(),
resultsModel.getResultCount()
);
resultsInfoLabel.setText(statusText);
}
......
......@@ -25,11 +25,9 @@ public class HeapUsageBar extends JProgressBar implements ActionListener {
private final transient Runtime runtime = Runtime.getRuntime();
private final transient Timer timer;
private final String textFormat;
private final double maxGB;
public HeapUsageBar() {
this.textFormat = NLS.str("heapUsage.text");
setBorderPainted(false);
setStringPainted(true);
setValue(10);
......@@ -54,7 +52,7 @@ public class HeapUsageBar extends JProgressBar implements ActionListener {
long used = runtime.totalMemory() - runtime.freeMemory();
int usedKB = (int) (used / 1024);
setValue(usedKB);
setString(String.format(textFormat, (usedKB / TWO_TO_20), maxGB));
setString(NLS.str("heapUsage.text", (usedKB / TWO_TO_20), maxGB));
if ((used + Utils.MIN_FREE_MEMORY) > runtime.maxMemory()) {
setForeground(RED);
......
......@@ -62,7 +62,7 @@ public class MainDropTarget implements DropTargetListener {
if (!transferData.isEmpty()) {
dtde.dropComplete(true);
// load first file
mainWindow.openFile(transferData.get(0));
mainWindow.open(transferData.get(0).toPath());
}
} catch (Exception e) {
LOG.error("File drop operation failed", e);
......
......@@ -61,12 +61,14 @@ public class NLS {
i18nMessagesMap.put(locale, bundle);
}
public static String str(String key) {
public static String str(String key, Object... parameters) {
String value;
try {
return localizedMessagesMap.getString(key);
value = localizedMessagesMap.getString(key);
} catch (MissingResourceException e) {
return fallbackMessagesMap.getString(key); // definitely exists
value = fallbackMessagesMap.getString(key); // definitely exists
}
return String.format(value, parameters);
}
public static String str(String key, LangLocale locale) {
......
package jadx.gui.utils;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
public class PathTypeAdapter {
private static TypeAdapter<Path> SINGLETON;
public static TypeAdapter<Path> singleton() {
if (SINGLETON == null) {
SINGLETON = new TypeAdapter<Path>() {
@Override
public void write(JsonWriter out, Path value) throws IOException {
if (value == null) {
out.nullValue();
} else {
out.value(value.toAbsolutePath().toString());
}
}
@Override
public Path read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
return Paths.get(in.nextString());
}
};
}
return SINGLETON;
}
private PathTypeAdapter() {
}
}
......@@ -2,8 +2,8 @@ language.name=English
menu.file=File
menu.view=View
menu.recent_files=Recent Files
menu.no_recent_files=No recent files
menu.recent_projects=Recent projects
menu.no_recent_projects=No recent projects
menu.preferences=Preferences
menu.sync=Sync with editor
menu.flatten=Show flatten packages
......@@ -20,17 +20,18 @@ menu.update_label=New version %s available!
file.open_action=Open file...
file.open_title=Open file
file.new_project=New project
file.save_project=Save project
file.save_project_as=Save project as...
file.save_all=Save all
file.export_gradle=Save as gradle project
file.save_all_msg=Select directory for save decompiled sources
file.select=Select
file.exit=Exit
tree.sources_title=Source code
tree.resources_title=Resources
tree.loading=Loading...
search=Search
search.previous=Previous
search.next=Next
search.mark_all=Mark All
......@@ -79,6 +80,7 @@ preferences.title=Preferences
preferences.deobfuscation=Deobfuscation
preferences.editor=Editor
preferences.decompile=Decompilation
preferences.project=Project
preferences.other=Other
preferences.language=Language
preferences.check_for_updates=Check for updates on startup
......@@ -89,6 +91,7 @@ preferences.replaceConsts=Replace constants
preferences.respectBytecodeAccessModifiers=Respect bytecode access modifiers
preferences.useImports=Use import statements
preferences.skipResourcesDecode=Don't decode resources
preferences.autoSave=Auto save
preferences.threads=Processing threads count
preferences.excludedPackages=Excluded packages
preferences.excludedPackages.tooltip=List of space separated package names that will not be decompiled or indexed (saves RAM)
......@@ -116,6 +119,8 @@ msg.saving_sources=Saving sources...
msg.language_changed_title=Language changed
msg.language_changed=New language will be displayed the next time application starts.
msg.index_not_initialized=Index not initialized, search will be disabled!
msg.project_error_title=Error
msg.project_error=Project could not be loaded
popup.undo=Undo
popup.redo=Redo
......@@ -127,11 +132,15 @@ popup.select_all=Select All
popup.find_usage=Find Usage
popup.exclude=Exclude
confirm.save_as_title=Confirm Save as
confirm.save_as_message=%s already exists.\nDo you want to replace it?
confirm.not_saved_title=Save project
confirm.not_saved_message=Save the current project before opening the new one?
certificate.title=Certificate
certificate.cert_type=Type
certificate.serialSigVer=Version
certificate.serialNumber=Serial number
certificate.cert_issuer=Issuer
certificate.cert_subject=Subject
certificate.serialValidFrom=Valid from
certificate.serialValidUntil=Valid until
......
......@@ -2,8 +2,8 @@ language.name=Español
menu.file=Archivo
menu.view=Vista
menu.recent_files=Archivos recientes
menu.no_recent_files=No hay archivos recientes
#menu.recent_projects=
#menu.no_recent_projects=
menu.preferences=Preferencias
menu.sync=Sincronizar con el editor
menu.flatten=Mostrar paquetes en vista plana
......@@ -20,17 +20,18 @@ menu.update_label=¡Nueva versión %s disponible!
file.open_action=Abrir archivo...
file.open_title=Abrir archivo
#file.new_project=
#file.save_project=
#file.save_project_as=
file.save_all=Guardar todo
file.export_gradle=Guardar como proyecto Gradle
file.save_all_msg=Seleccionar carpeta para guardar fuentes descompiladas
file.select=Seleccionar
file.exit=Salir
tree.sources_title=Código fuente
tree.resources_title=Recursos
tree.loading=Cargando...
search=Buscar
search.previous=Anterior
search.next=Siguiente
search.mark_all=Marcar todo
......@@ -79,6 +80,7 @@ preferences.title=Preferencias
preferences.deobfuscation=Desofuscación
preferences.editor=Editor
preferences.decompile=Descompilación
#preferences.project=
preferences.other=Otros
preferences.language=Idioma
preferences.check_for_updates=Buscar actualizaciones al iniciar
......@@ -89,6 +91,7 @@ preferences.replaceConsts=Reemplazar constantes
#preferences.respectBytecodeAccessModifiers=
#preferences.useImports=
preferences.skipResourcesDecode=No descodificar recursos
#preferences.autoSave=
preferences.threads=Número de hilos a procesar
#preferences.excludedPackages=
#preferences.excludedPackages.tooltip=
......@@ -116,6 +119,8 @@ msg.saving_sources=Guardando fuente...
msg.language_changed_title=Idioma cambiado
msg.language_changed=El nuevo idioma se mostrará la próxima vez que la aplicación se inicie.
msg.index_not_initialized=Índice no inicializado, ¡la bósqueda se desactivará!
#msg.project_error_title=
#msg.project_error=
popup.undo=Deshacer
popup.redo=Rehacer
......@@ -127,11 +132,15 @@ popup.select_all=Seleccionar todo
#popup.find_usage=
#popup.exclude=
#confirm.save_as_title=
#confirm.save_as_message=
#confirm.not_saved_title=
#confirm.not_saved_message=
certificate.title=Certificado
certificate.cert_type=Tipo
certificate.serialSigVer=Versión
certificate.serialNumber=Número de serial
certificate.cert_issuer=Issuer
certificate.cert_subject=Subject
certificate.serialValidFrom=Válido desde
certificate.serialValidUntil=Válido hasta
......
......@@ -2,8 +2,8 @@ language.name=中文(简体)
menu.file=文件
menu.view=视图
menu.recent_files=最近打开的文件
menu.no_recent_files=无最近打开的文件
#menu.recent_projects=
#menu.no_recent_projects=
menu.preferences=首选项
menu.sync=与编辑器同步
menu.flatten=展开显示代码包
......@@ -20,17 +20,18 @@ menu.update_label=发现新版本 %s!
file.open_action=打开文件...
file.open_title=打开文件
#file.new_project=
#file.save_project=
#file.save_project_as=
file.save_all=全部保存
file.export_gradle=另存为 Gradle 项目
file.save_all_msg=选择反编译资源路径
file.select=选择
file.exit=退出
tree.sources_title=源代码
tree.resources_title=资源文件
tree.loading=稍等...
search=搜索
search.previous=上一个
search.next=下一个
search.mark_all=标记全部
......@@ -79,6 +80,7 @@ preferences.title=首选项
preferences.deobfuscation=反混淆
preferences.editor=编辑器
preferences.decompile=反编译
#preferences.project=
preferences.other=其他
preferences.language=语言
preferences.check_for_updates=启动时检查更新
......@@ -89,6 +91,7 @@ preferences.replaceConsts=替换常量
preferences.respectBytecodeAccessModifiers=遵守字节码访问修饰符
preferences.useImports=使用 import 语句
preferences.skipResourcesDecode=不反编译资源文件
#preferences.autoSave=
preferences.threads=并行线程数
preferences.excludedPackages=排除的包
preferences.excludedPackages.tooltip=将不被解压缩或索引的以空格分隔的包名称列表(节省 RAM)
......@@ -116,6 +119,8 @@ msg.saving_sources=正在导出源代码...
msg.language_changed_title=语言已更改
msg.language_changed=在下次启动时将会显示新的语言。
msg.index_not_initialized=索引尚未初始化,无法进行搜索!
#msg.project_error_title=
#msg.project_error=
popup.undo=撤销
popup.redo=重做
......@@ -127,11 +132,15 @@ popup.select_all=全选
popup.find_usage=查找用例
#popup.exclude=
#confirm.save_as_title=
#confirm.save_as_message=
#confirm.not_saved_title=
#confirm.not_saved_message=
certificate.title=证书
certificate.cert_type=类型
certificate.serialSigVer=版本
certificate.serialNumber=序列号
certificate.cert_issuer=颁发者
certificate.cert_subject=主题
certificate.serialValidFrom=有效期始
certificate.serialValidUntil=有效期至
......
package jadx.gui;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import java.io.IOException;
import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
public class TestI18n {
private static Path guiJavaPath;
private static Path i18nPath;
private List<String> reference;
private String referenceName;
@BeforeAll
public static void init() {
i18nPath = Paths.get("src/main/resources/i18n");
assertTrue(Files.exists(i18nPath));
guiJavaPath = Paths.get("src/main/java");
assertTrue(Files.exists(guiJavaPath));
}
@Test
public void filesExactlyMatch() throws IOException {
Path path = Paths.get("./src/main/resources/i18n");
assertTrue(Files.exists(path));
Files.list(path).forEach(p -> {
Files.list(i18nPath).forEach(p -> {
List<String> lines;
try {
lines = Files.readAllLines(p);
......@@ -45,12 +64,12 @@ public class TestI18n {
if (p0 != -1) {
String prefix = line.substring(0, p0 + 1);
if (i >= lines.size() || !trimComment(lines.get(i)).startsWith(prefix)) {
fail(path, i + 1);
failLine(path, i + 1);
}
}
}
if (lines.size() != reference.size()) {
fail(path, reference.size());
failLine(path, reference.size());
}
}
......@@ -58,7 +77,37 @@ public class TestI18n {
return string.startsWith("#") ? string.substring(1) : string;
}
private void fail(Path path, int line) {
Assertions.fail("I18n files " + path.getFileName() + " and " + referenceName + " differ in line " + line);
private void failLine(Path path, int line) {
fail("I18n files " + path.getFileName() + " and " + referenceName + " differ in line " + line);
}
@Test
public void keyIsUsed() throws IOException {
Properties properties = new Properties();
try (Reader reader = Files.newBufferedReader(i18nPath.resolve("Messages_en_US.properties"))) {
properties.load(reader);
}
Set<String> keys = new HashSet<>();
for (Object key : properties.keySet()) {
keys.add("\"" + key + '"');
}
Files.walk(guiJavaPath).filter(p -> Files.isRegularFile(p)).forEach(p -> {
try {
List<String> lines = Files.readAllLines(p);
for (String line : lines) {
for (Iterator<String> it = keys.iterator(); it.hasNext(); ) {
if (line.contains(it.next())) {
it.remove();
}
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
});
assertThat("keys not used", keys, empty());
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment