From bee476895c8b03aca7b6e51eb9cf8d78817e157b Mon Sep 17 00:00:00 2001 From: Yaroslav <43380144+MrIkso@users.noreply.github.com> Date: Mon, 19 May 2025 21:17:07 +0300 Subject: [PATCH] feat(tools): improve tool for sync and update I18N lines (PR #2494) * feat(tools): add to NLSAddNewLines tool remove lines if it not found on default reference & update commented untranslated line from reference * fix: sync i18n translation from english reference file * rename class and task, adjust code style --------- Co-authored-by: Skylot <118523+skylot@users.noreply.github.com> --- jadx-gui/build.gradle.kts | 6 +- .../jadx/gui/utils/tools/NLSAddNewLines.java | 124 ------------ .../jadx/gui/utils/tools/SyncNLSLines.java | 184 ++++++++++++++++++ .../resources/i18n/Messages_es_ES.properties | 16 +- .../resources/i18n/Messages_id_ID.properties | 6 +- .../resources/i18n/Messages_ko_KR.properties | 10 +- .../resources/i18n/Messages_pt_BR.properties | 8 +- .../resources/i18n/Messages_ru_RU.properties | 6 +- 8 files changed, 210 insertions(+), 150 deletions(-) delete mode 100644 jadx-gui/src/main/java/jadx/gui/utils/tools/NLSAddNewLines.java create mode 100644 jadx-gui/src/main/java/jadx/gui/utils/tools/SyncNLSLines.java diff --git a/jadx-gui/build.gradle.kts b/jadx-gui/build.gradle.kts index 0377278c8..09fbd2a84 100644 --- a/jadx-gui/build.gradle.kts +++ b/jadx-gui/build.gradle.kts @@ -228,10 +228,10 @@ val copyDistWinWithJre by tasks.registering(Copy::class) { duplicatesStrategy = DuplicatesStrategy.EXCLUDE } -val addNewNLSLines by tasks.registering(JavaExec::class) { +val syncNLSLines by tasks.registering(JavaExec::class) { group = "jadx-dev" - description = "Utility task to add new/missing translation lines" + description = "Utility task to sync new/missing translation using EN as a reference" classpath = sourceSets.main.get().runtimeClasspath - mainClass.set("jadx.gui.utils.tools.NLSAddNewLines") + mainClass.set("jadx.gui.utils.tools.SyncNLSLines") } diff --git a/jadx-gui/src/main/java/jadx/gui/utils/tools/NLSAddNewLines.java b/jadx-gui/src/main/java/jadx/gui/utils/tools/NLSAddNewLines.java deleted file mode 100644 index 1ad40d3c2..000000000 --- a/jadx-gui/src/main/java/jadx/gui/utils/tools/NLSAddNewLines.java +++ /dev/null @@ -1,124 +0,0 @@ -package jadx.gui.utils.tools; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.List; -import java.util.stream.Stream; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Automatically add new i18n lines from reference (EN) into other languages - */ -public class NLSAddNewLines { - private static final Logger LOG = LoggerFactory.getLogger(NLSAddNewLines.class); - - private static final Path I18N_PATH = Paths.get("src/main/resources/i18n/"); - private static final String GUI_MODULE_DIR = "jadx-gui"; - - public static void main(String[] args) { - try { - process(); - } catch (Exception e) { - LOG.error("Failed to add new i18n lines", e); - } - } - - private static void process() throws Exception { - String reference = "Messages_en_US.properties"; - Path refPath = getRefPath(reference); - List refLines = Files.readAllLines(refPath); - - try (Stream pathStream = Files.list(refPath.toAbsolutePath().getParent())) { - pathStream.forEach(path -> { - if (path.getFileName().equals(refPath.getFileName())) { - return; - } - try { - applyFix(refLines, path); - } catch (Exception e) { - throw new RuntimeException(e); - } - }); - } - } - - private static void applyFix(List refLines, Path path) throws IOException { - List lines = Files.readAllLines(path); - int linesCount = lines.size(); - boolean updated = false; - for (int i = 0; i < linesCount; i++) { - String line = lines.get(i); - String refLine = refLines.get(i); - if (isSameKey(refLine, line)) { - if (line.startsWith("#") && line.endsWith("=")) { - // add ref text if missing to simplify translation - lines.set(i, '#' + refLine); - updated = true; - } - } else { - if (refLine.isEmpty()) { - lines.add(i, ""); - } else { - lines.add(i, '#' + refLine); - } - updated = true; - } - - } - if (updated) { - LOG.info("Updating {}", path); - Files.write(path, lines, StandardCharsets.UTF_8); - } - } - - private static boolean isSameKey(String refLine, String line) { - int refLen = refLine.length(); - int len = line.length(); - if (refLen == 0) { - return len == 0; - } - if (len == 0) { - return false; - } - int pos = 0; - // skip comment and spaces - while (pos < len) { - char ch = line.charAt(pos); - if (ch == '#' || ch == ' ') { - pos++; - } else { - break; - } - } - int refPos = 0; - while (true) { - char refCh = refLine.charAt(refPos); - if (refCh == ' ' || refCh == '=') { - return true; - } - char ch = line.charAt(pos); - if (refCh != ch) { - return false; - } - refPos++; - pos++; - } - } - - private static Path getRefPath(String reference) { - Path path = I18N_PATH.resolve(reference); - if (Files.exists(path)) { - return path; - } - Path rootPath = Paths.get(GUI_MODULE_DIR).resolve(I18N_PATH).resolve(reference); - if (Files.exists(rootPath)) { - return rootPath; - } - throw new RuntimeException("Can't find reference I18N: " + reference); - } -} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/tools/SyncNLSLines.java b/jadx-gui/src/main/java/jadx/gui/utils/tools/SyncNLSLines.java new file mode 100644 index 000000000..db9cdaf10 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/tools/SyncNLSLines.java @@ -0,0 +1,184 @@ +package jadx.gui.utils.tools; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Automatically synchronizes i18n files with a reference (EN) file. + * - Adds new lines from the reference file (commented out) to other language files. + * - Removes lines from other language files that are not present in the reference file. + * - Updates commented-out empty translations in other files with the reference text (commented). + */ +public class SyncNLSLines { + private static final Logger LOG = LoggerFactory.getLogger(SyncNLSLines.class); + + private static final Path I18N_PATH = Paths.get("src/main/resources/i18n/"); + private static final String REFERENCE_FILE_NAME = "Messages_en_US.properties"; + + /** + * Assumes this tool runs from the project root and jadx-gui is a direct subdirectory. + * If jadx-gui is the project root, this might need adjustment or to be removed + * if I18N_PATH is already relative to jadx-gui. + */ + private static final String GUI_MODULE_DIR_NAME = "jadx-gui"; + private static final Path GUI_MODULE_PREFIX_PATH = Paths.get(GUI_MODULE_DIR_NAME); + + public static void main(String[] args) { + try { + process(); + } catch (Exception e) { + LOG.error("Failed to process i18n files", e); + } + } + + private static void process() throws Exception { + Path refPath = getRefPath(REFERENCE_FILE_NAME); + if (!Files.exists(refPath)) { + LOG.error("Reference i18n file not found: {}", REFERENCE_FILE_NAME); + return; + } + LOG.info("Using reference file: {}", refPath.toAbsolutePath()); + Path i18nDir = refPath.toAbsolutePath().getParent(); + if (i18nDir == null) { + LOG.error("Could not determine i18n directory from reference path: {}", refPath); + return; + } + List refFileLines = Files.readAllLines(refPath, StandardCharsets.UTF_8); + try (Stream pathStream = Files.list(i18nDir)) { + pathStream.filter(path -> { + String fileName = path.getFileName().toString(); + return !fileName.equals(REFERENCE_FILE_NAME) + && fileName.startsWith("Messages_") + && fileName.endsWith(".properties"); + }) + .forEach(targetPath -> { + try { + LOG.info("Processing target file: {}", targetPath.toAbsolutePath()); + applySync(refFileLines, targetPath); + } catch (Exception e) { + LOG.error("Failed to sync file: {}", targetPath, e); + } + }); + } + LOG.info("I18N synchronization process finished."); + } + + /** + * Parses a list of lines from a properties file into a map of key-value pairs. + * Preserves the full line as value. Comments and empty lines will have null keys. + */ + private static Map parseProperties(List lines) { + Map properties = new LinkedHashMap<>(); + for (String line : lines) { + String key = extractKey(line); + // If the key is null, it's a comment or blank line, we might not need these in the map. + // If we iterate over the original refFileLines later. + // For simplicity here, we only store actual properties. + if (key != null) { + properties.put(key, line); + } + } + return properties; + } + + /** + * Extracts the key from a properties file line. + * Returns null if the line is a comment, empty, or doesn't seem to be a key-value pair. + */ + private static String extractKey(String line) { + String trimmedLine = line.trim(); + if (trimmedLine.isEmpty() || trimmedLine.startsWith("#")) { + // Comment or empty line + return null; + } + int separatorIndex = trimmedLine.indexOf('='); + if (separatorIndex == -1) { + // Line without a separator could be a key with no value (less common in i18n) + // or just a malformed line. For now, let's consider it a key if it's not empty. + return trimmedLine; + } + return trimmedLine.substring(0, separatorIndex).trim(); + } + + private static void applySync(List refFileLines, Path targetPath) throws IOException { + List originalTargetLines = Files.readAllLines(targetPath, StandardCharsets.UTF_8); + Map targetProperties = parseProperties(originalTargetLines); + List newTargetLines = new ArrayList<>(refFileLines.size()); + boolean updated = false; + for (String refLine : refFileLines) { + String refKey = extractKey(refLine); + if (refKey == null) { + // It's a comment or blank line from reference + newTargetLines.add(refLine); + } else { + // It's a property line from reference + if (targetProperties.containsKey(refKey)) { + String targetLine = targetProperties.get(refKey); + // Original logic: if target line is like "#key=" (commented, no value) + // then use the commented reference value. + String trimmed = targetLine.trim(); + if (trimmed.startsWith("#") + && trimmed.substring(1).trim().startsWith(refKey) // ensure it's the same key + && trimmed.endsWith("=")) { + // Use reference line, commented + newTargetLines.add('#' + refLine.trim()); + } else { + // Use existing target line + newTargetLines.add(targetLine); + } + } else { + // Key from reference is missing in target, add it commented out + newTargetLines.add('#' + refLine.trim()); + } + } + } + // Check if files are different + if (originalTargetLines.size() != newTargetLines.size()) { + updated = true; + } else { + for (int i = 0; i < originalTargetLines.size(); i++) { + if (!originalTargetLines.get(i).equals(newTargetLines.get(i))) { + updated = true; + break; + } + } + } + if (updated) { + LOG.info("Updating {} ({} lines -> {} lines)", targetPath.getFileName(), originalTargetLines.size(), newTargetLines.size()); + Files.write(targetPath, newTargetLines, StandardCharsets.UTF_8); + } else { + LOG.info("No changes needed for {}", targetPath.getFileName()); + } + } + + private static Path getRefPath(String referenceFileName) { + // Path relative to project root (where src/main/resources exists) + Path projectRootRelative = I18N_PATH.resolve(referenceFileName); + if (Files.exists(projectRootRelative)) { + return projectRootRelative.toAbsolutePath(); + } + // Path relative to a module (e.g. jadx-gui/src/main/resources) + // This assumes the script is run from one level above GUI_MODULE_DIR_NAME + Path moduleRelative = GUI_MODULE_PREFIX_PATH.resolve(I18N_PATH).resolve(referenceFileName); + if (Files.exists(moduleRelative)) { + return moduleRelative.toAbsolutePath(); + } + // Path if tool runs from within the GUI_MODULE_DIR_NAME itself + Path currentDirRelative = Paths.get(".").resolve(I18N_PATH).resolve(referenceFileName); + if (Files.exists(currentDirRelative)) { + return currentDirRelative.toAbsolutePath(); + } + throw new RuntimeException("Can't find reference I18N: " + referenceFileName); + } +} diff --git a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties index d5146b8a4..e95bd730a 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties @@ -8,10 +8,10 @@ menu.preferences=Preferencias menu.sync=Sincronizar con el editor menu.flatten=Mostrar paquetes en vista plana #menu.enable_preview_tab=Enable Preview Tab -#menu.heapUsageBar=Show memory usage bar -#menu.alwaysSelectOpened=Always Select Opened File/Class -#menu.dock_log=Dock log viewer -#menu.dock_quick_tabs=Dock quick tabs +#menu.heapUsageBar=Show Memory Usage Bar +#menu.alwaysSelectOpened=Auto Select in Tree +#menu.dock_log=Dock Log Viewer +#menu.dock_quick_tabs=Show Quick Tabs menu.navigation=Navegación menu.text_search=Buscar texto menu.class_search=Buscar clase @@ -33,7 +33,7 @@ menu.update_label=¡Nueva versión %s disponible! #menu.hex_viewer=Hex Viewer file.open_action=Abrir archivo... -#file.add_files_action=Add files ... +#file.add_files_action=Add files file.open_title=Abrir archivo #file.open_project=Open project #file.new_project=New project @@ -192,11 +192,11 @@ usage_dialog.label=Usage for: #rename_dialog.class_help=Enter full name to move class to another package. Start with '.' to move to default (empty) package -#export_dialog.title=Export to source code +#export_dialog.title=Export #export_dialog.save_path=Save path: #export_dialog.browse=Browse #export_dialog.export_options=Export options -#export_dialog.export_gradle=Export as gradle project +#export_dialog.export_gradle=Export as a Gradle project #export_dialog.export_gradle_type=Gradle template: log_viewer.title=Visor log @@ -288,7 +288,7 @@ preferences.rename_use_source_name_as_class_name_alias=Usar el nombre del source #preferences.search_group_title=Search #preferences.search_results_per_page=Results per page (0 - no limit) #preferences.res_file_ext=Resource files extensions ('xml|html', * for all) -#preferences.res_skip_file=Skip resources files if larger (MB) +#preferences.res_skip_file=Skip resources files if larger (MB) (0 - disable) #preferences.tab_dnd_appearance=Dragging tab appearance #preferences.plugins.install=Install plugin diff --git a/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties b/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties index 0545731e2..d3bbb53b3 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties @@ -11,7 +11,7 @@ menu.flatten=Tampilkan paket yang diratakan menu.heapUsageBar=Tampilkan penggunaan memori menu.alwaysSelectOpened=Selalu Pilih Berkas/Kelas yang Terbuka menu.dock_log=Kaitkan pemantau log -#menu.dock_quick_tabs=Dock quick tabs +#menu.dock_quick_tabs=Show Quick Tabs menu.navigation=Navigasi menu.text_search=Pencarian Teks menu.class_search=Pencarian Kelas @@ -192,11 +192,11 @@ comment_dialog.usage=Gunakan Shift + Enter untuk memulai baris baru #rename_dialog.class_help=Enter full name to move class to another package. Start with '.' to move to default (empty) package -#export_dialog.title=Export to source code +#export_dialog.title=Export #export_dialog.save_path=Save path: #export_dialog.browse=Browse #export_dialog.export_options=Export options -#export_dialog.export_gradle=Export as gradle project +#export_dialog.export_gradle=Export as a Gradle project #export_dialog.export_gradle_type=Gradle template: log_viewer.title=Pemantau Log diff --git a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties index 3a9a55f3a..c3e635d73 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties @@ -10,8 +10,8 @@ menu.flatten=플랫 패키지 표시 #menu.enable_preview_tab=Enable Preview Tab menu.heapUsageBar=메모리 사용량 표시 menu.alwaysSelectOpened=항상 열린 파일/클래스 선택 -#menu.dock_log=Dock log viewer -#menu.dock_quick_tabs=Dock quick tabs +#menu.dock_log=Dock Log Viewer +#menu.dock_quick_tabs=Show Quick Tabs menu.navigation=네비게이션 menu.text_search=텍스트 검색 menu.class_search=클래스 검색 @@ -45,7 +45,7 @@ file.live_reload_desc=파일 내용 변경 시 자동으로 다시 로드 #file.open_mappings=Open mappings... file.save_mappings=다른 이름으로 매핑 내보내기... #file.save_mappings_as=Save mappings as... -#file.close_mappings=다른 이름으로 매핑 내보내기... +#file.close_mappings=Close mappings file.save_all=모두 저장 #file.save=Save #file.export=Export project @@ -192,11 +192,11 @@ comment_dialog.usage=Shift + Enter 를 입력해 새 라인에 입력 #rename_dialog.class_help=Enter full name to move class to another package. Start with '.' to move to default (empty) package -#export_dialog.title=Export to source code +#export_dialog.title=Export #export_dialog.save_path=Save path: #export_dialog.browse=Browse #export_dialog.export_options=Export options -#export_dialog.export_gradle=Export as gradle project +#export_dialog.export_gradle=Export as a Gradle project #export_dialog.export_gradle_type=Gradle template: log_viewer.title=로그 뷰어 diff --git a/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties b/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties index d77730c14..060308bc0 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties @@ -10,8 +10,8 @@ menu.flatten=Mostrar pacotes achatados #menu.enable_preview_tab=Enable Preview Tab menu.heapUsageBar=Mostrar uso de memória menu.alwaysSelectOpened=Sempre selecionar arquivo/classe aberta -#menu.dock_log=Dock log viewer -#menu.dock_quick_tabs=Dock quick tabs +#menu.dock_log=Dock Log Viewer +#menu.dock_quick_tabs=Show Quick Tabs menu.navigation=Navegação menu.text_search=Buscar por texto menu.class_search=Buscar por classe @@ -192,11 +192,11 @@ comment_dialog.usage=Use Shift + Enter para pular uma linha #rename_dialog.class_help=Enter full name to move class to another package. Start with '.' to move to default (empty) package -#export_dialog.title=Export to source code +#export_dialog.title=Export #export_dialog.save_path=Save path: #export_dialog.browse=Browse #export_dialog.export_options=Export options -#export_dialog.export_gradle=Export as gradle project +#export_dialog.export_gradle=Export as a Gradle project #export_dialog.export_gradle_type=Gradle template: log_viewer.title=Visualizador de log diff --git a/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties b/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties index 73f30cb4a..899c9573a 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties @@ -11,7 +11,7 @@ menu.flatten=Плоская структура пакетов menu.heapUsageBar=Использование ОЗУ menu.alwaysSelectOpened=Выбирать открытый файл/класс menu.dock_log=Просмотр логов в панели -#menu.dock_quick_tabs=Dock quick tabs +#menu.dock_quick_tabs=Show Quick Tabs menu.navigation=Навигация menu.text_search=Поиск строк menu.class_search=Поиск классов @@ -192,11 +192,11 @@ comment_dialog.usage=Используйте Shift + Enter для перенос rename_dialog.class_help=Введите полный путь к пакету, в который вы хотите переместить этот класс. Введите '.' для перемещения в пакет по умолчанию (пустой) пакет -#export_dialog.title=Export to source code +#export_dialog.title=Export #export_dialog.save_path=Save path: #export_dialog.browse=Browse #export_dialog.export_options=Export options -#export_dialog.export_gradle=Export as gradle project +#export_dialog.export_gradle=Export as a Gradle project #export_dialog.export_gradle_type=Gradle template: log_viewer.title=Просмотр логов