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>
This commit is contained in:
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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<String> refLines = Files.readAllLines(refPath);
|
||||
|
||||
try (Stream<Path> 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<String> refLines, Path path) throws IOException {
|
||||
List<String> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<String> refFileLines = Files.readAllLines(refPath, StandardCharsets.UTF_8);
|
||||
try (Stream<Path> 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<String, String> parseProperties(List<String> lines) {
|
||||
Map<String, String> 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<String> refFileLines, Path targetPath) throws IOException {
|
||||
List<String> originalTargetLines = Files.readAllLines(targetPath, StandardCharsets.UTF_8);
|
||||
Map<String, String> targetProperties = parseProperties(originalTargetLines);
|
||||
List<String> 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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=로그 뷰어
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=Просмотр логов
|
||||
|
||||
Reference in New Issue
Block a user