Multi Language translation impl. Eine .json Datei enthält die ÜBersetztungen von allen Sprachen die übersetzt werden sollen. Dazu müssen diese Sektione allerdings auch befüllt sein

This commit is contained in:
2025-12-15 13:39:39 +01:00
parent ccbfcafb63
commit 6524a76c2a
3 changed files with 154766 additions and 108 deletions
+100 -6
View File
@@ -29,8 +29,11 @@ class TestRunner:
self.testdata_dir = Path(os.environ.get('PROJECT_TEST', self.project_root / "testdata"))
self.work_dir = Path(os.environ.get('PROJECT_WORK', self.project_root / "work"))
self.bin_dir = Path(os.environ.get('PROJECT_BIN', self.project_root / "bin"))
self.lib_dir = Path(os.environ.get('PROJECT_LIB', self.project_root / "lib"))
self.cfg_dir = Path(os.environ.get('PROJECT_CFG', self.project_root / "cfg"))
self.getexdraw_bat = self.bin_dir / "getexdraw.bat"
self.ioconverter_bat = self.bin_dir / "ioconverter.bat"
self.translate_py = self.lib_dir / "translate.py"
self.test_results = []
self.failed_tests = []
@@ -128,6 +131,80 @@ class TestRunner:
except Exception as e:
return False, f"Exception during execution: {str(e)}"
def run_translate(self, test_filename):
"""Execute translate.py for given test file with three variants: raw, cs, multilang"""
dxf_file = self.testdata_dir / f"{test_filename}.dxf"
if not dxf_file.exists():
return False, f"Test file {dxf_file} not found"
try:
# Set up environment for translate.py
env = os.environ.copy()
env['PROJECT_TRANSLATION'] = str(self.cfg_dir / "translation")
# Variant 1: Raw extraction (no translation)
cmd_raw = [
sys.executable,
str(self.translate_py),
'--extract',
'--filename', str(dxf_file),
'--export-type', 'json',
'--outname', f'{test_filename}_raw'
]
result_raw = subprocess.run(cmd_raw, capture_output=True, text=True, env=env)
# Variant 2: CS translation (single language)
cmd_cs = [
sys.executable,
str(self.translate_py),
'--extract',
'--filename', str(dxf_file),
'--export-type', 'json',
'--outname', f'{test_filename}_cs',
'--translate', 'CS'
]
result_cs = subprocess.run(cmd_cs, capture_output=True, text=True, env=env)
# Variant 3: Multilang translation (CS,EN)
cmd_multilang = [
sys.executable,
str(self.translate_py),
'--extract',
'--filename', str(dxf_file),
'--export-type', 'json',
'--outname', f'{test_filename}_multilang',
'--translate', 'CS,EN'
]
result_multilang = subprocess.run(cmd_multilang, capture_output=True, text=True, env=env)
# Check if all expected output files were created
expected_raw = self.work_dir / f"{test_filename}_raw.json"
expected_cs = self.work_dir / f"{test_filename}_cs.json"
expected_multilang = self.work_dir / f"{test_filename}_multilang.json"
missing_files = []
if not expected_raw.exists():
missing_files.append(f"{test_filename}_raw.json")
if not expected_cs.exists():
missing_files.append(f"{test_filename}_cs.json")
if not expected_multilang.exists():
missing_files.append(f"{test_filename}_multilang.json")
if missing_files:
error_msg = f"translate.py failed - expected output files not found: {', '.join(missing_files)}"
if result_raw.returncode != 0:
error_msg += f"\n\nRaw variant STDOUT:\n{result_raw.stdout}\nSTDERR:\n{result_raw.stderr}"
if result_cs.returncode != 0:
error_msg += f"\n\nCS variant STDOUT:\n{result_cs.stdout}\nSTDERR:\n{result_cs.stderr}"
if result_multilang.returncode != 0:
error_msg += f"\n\nMultilang variant STDOUT:\n{result_multilang.stdout}\nSTDERR:\n{result_multilang.stderr}"
return False, error_msg
else:
return True, "Success"
except Exception as e:
return False, f"Exception during execution: {str(e)}"
def cleanup_test_files(self, test_filename):
"""Remove the work folder for the given test"""
if not self.clean:
@@ -149,7 +226,11 @@ class TestRunner:
# Check what files actually exist and determine comparison strategy based on mode
errors_file = self.work_dir / test_filename / f"{test_filename}_errors.json"
if self.mode == 'ioconverter':
if self.mode == 'translation':
# For translation mode: compare _raw, _cs, and _multilang variants
file_suffixes = ['_raw', '_cs', '_multilang']
# Translation files are directly in work_dir, not in subdirectory
elif self.mode == 'ioconverter':
# For ioconverter mode: check for _positionsconv.json
positions_file = self.work_dir / test_filename / f"{test_filename}_positionsconv.json"
@@ -178,7 +259,11 @@ class TestRunner:
return False, f"No generated files found for {test_filename}", []
for suffix in file_suffixes:
# Generated file should be in work/<testfilename>/<testfilename><suffix>.json
# For translation mode, files are directly in work_dir
if self.mode == 'translation':
generated_file = self.work_dir / f"{test_filename}{suffix}.json"
else:
# For other modes, files are in work/<testfilename>/ subdirectory
generated_file = self.work_dir / test_filename / f"{test_filename}{suffix}.json"
# Reference file should be in testdata/<testfilename><suffix>.json
@@ -237,8 +322,10 @@ class TestRunner:
result = TestResult(test_filename)
# Step 1: Run the appropriate batch script based on mode
if self.mode == 'ioconverter':
# Step 1: Run the appropriate script based on mode
if self.mode == 'translation':
success, msg = self.run_translate(test_filename)
elif self.mode == 'ioconverter':
success, msg = self.run_ioconverter(test_filename)
else:
success, msg = self.run_getexdraw(test_filename)
@@ -370,11 +457,18 @@ def main():
action='store_true',
help='Run ioconverter.bat tests'
)
mode_group.add_argument(
'--translation',
action='store_true',
help='Run translate.py tests (extract text with raw, CS, and multilang variants)'
)
args = parser.parse_args()
# Determine mode - default to getexdraw if neither is specified
if args.ioconverter:
# Determine mode - default to getexdraw if none is specified
if args.translation:
mode = 'translation'
elif args.ioconverter:
mode = 'ioconverter'
else:
mode = 'getexdraw'
+239 -33
View File
@@ -525,16 +525,49 @@ def read_texts_from_file(filename: Path) -> list[str]:
sys.exit(1)
def detect_json_structure(data: dict) -> tuple[str, list[str]]:
"""
Erkennt die Struktur einer JSON-Datei (alte oder hierarchische mehrsprachige Struktur).
Args:
data: Geladene JSON-Daten als Dictionary
Returns:
Tuple mit (Struktur-Typ, Liste der Sprachen)
- Struktur-Typ: "legacy" (alte Struktur) oder "multilang" (mehrsprachige hierarchische Struktur)
- Liste der Sprachen: leer bei legacy, z.B. ["EN", "FR", "CS"] bei multilang
"""
# Prüfe ob alte Struktur (hat 'untranslated' oder 'translations' Keys)
if 'untranslated' in data or 'translations' in data:
return "legacy", []
# Prüfe ob neue hierarchische Struktur (hat [LANG] Keys)
languages = []
for key in data.keys():
if key.startswith('[') and key.endswith(']') and len(key) > 2:
# Extrahiere Sprachcode aus [LANG]
lang = key[1:-1]
languages.append(lang)
if languages:
return "multilang", languages
# Unbekannte Struktur
return "unknown", []
def read_texts_from_json(filename: Path) -> list[str]:
"""
Liest Texte aus dem 'untranslated' Block einer JSON-Datei.
Erwartet eine Struktur mit "untranslated": [{"source": "...", "target": ""}, ...]
Liest Texte aus einer JSON-Datei.
Unterstützt beide Strukturen:
- Alte Struktur: "untranslated": [{"source": "...", "target": ""}, ...]
- Hierarchische Struktur: "[LANG]": {"untranslated": [...], ...}
Args:
filename: Pfad zur JSON-Datei
Returns:
Alphabetisch sortierte Liste aller "source" Texte aus dem untranslated Block
Alphabetisch sortierte Liste aller "source" Texte aus den untranslated Blöcken
"""
try:
with open(filename, 'r', encoding='utf-8') as f:
@@ -544,6 +577,13 @@ def read_texts_from_json(filename: Path) -> list[str]:
print(f"FEHLER: JSON-Datei hat ungültige Struktur (erwartet Object, bekam {type(data).__name__})")
sys.exit(1)
# Erkenne Struktur
structure_type, languages = detect_json_structure(data)
texts_set = set() # Verwende Set um Duplikate zu vermeiden
if structure_type == "legacy":
# Alte Struktur
if 'untranslated' not in data:
print(f"FEHLER: JSON-Datei enthält keinen 'untranslated' Block")
sys.exit(1)
@@ -553,7 +593,6 @@ def read_texts_from_json(filename: Path) -> list[str]:
print(f"FEHLER: 'untranslated' Block hat ungültige Struktur (erwartet Array)")
sys.exit(1)
texts = []
for idx, entry in enumerate(untranslated):
if not isinstance(entry, dict):
print(f"WARNUNG: Eintrag {idx} in 'untranslated' ist kein Object, überspringe")
@@ -565,12 +604,50 @@ def read_texts_from_json(filename: Path) -> list[str]:
source_text = entry['source']
if source_text: # Ignoriere leere Texte
texts.append(source_text)
texts_set.add(source_text)
# Sortiere alphabetisch
texts.sort()
print(f"Texte aus JSON-Datei gelesen (alte Struktur): {len(texts_set)}")
print(f"Texte aus JSON-Datei gelesen: {len(texts)}")
elif structure_type == "multilang":
# Hierarchische mehrsprachige Struktur
print(f"Erkannte mehrsprachige Struktur mit Sprachen: {', '.join(languages)}")
for lang in languages:
lang_key = f"[{lang}]"
if lang_key not in data:
continue
lang_data = data[lang_key]
if not isinstance(lang_data, dict):
print(f"WARNUNG: [{lang}] Block ist kein Object, überspringe")
continue
# Lese aus untranslated Block
if 'untranslated' in lang_data:
untranslated = lang_data['untranslated']
if not isinstance(untranslated, list):
print(f"WARNUNG: [{lang}][untranslated] ist kein Array, überspringe")
continue
for idx, entry in enumerate(untranslated):
if not isinstance(entry, dict):
continue
if 'source' not in entry:
continue
source_text = entry['source']
if source_text:
texts_set.add(source_text)
print(f"Texte aus JSON-Datei gelesen (mehrsprachige Struktur): {len(texts_set)}")
else:
print(f"FEHLER: JSON-Datei hat unbekannte Struktur")
sys.exit(1)
# Konvertiere Set zu sortierter Liste
texts = sorted(texts_set)
return texts
except json.JSONDecodeError as e:
@@ -581,13 +658,19 @@ def read_texts_from_json(filename: Path) -> list[str]:
sys.exit(1)
def load_translations_from_json(filename: Path) -> dict[str, str]:
def load_translations_from_json(filename: Path, target_lang: str = None) -> dict[str, str]:
"""
Liest Übersetzungen aus dem 'translations' Block einer JSON-Datei.
Erwartet eine Struktur mit "translations": [{"source": "...", "target": "..."}, ...]
Liest Übersetzungen aus einer JSON-Datei.
Unterstützt beide Strukturen:
- Alte Struktur: "translations": [{"source": "...", "target": "..."}, ...]
- Hierarchische Struktur: "[LANG]": {"translated": [...], ...}
Bei hierarchischer Struktur werden Übersetzungen aus allen Sprachen oder nur der angegebenen Sprache gelesen.
Wenn mehrere Übersetzungen für denselben source existieren, wird die letzte verwendet (Sprache mit höherer Priorität).
Args:
filename: Pfad zur JSON-Datei
target_lang: Optional: Bei hierarchischer Struktur nur Übersetzungen dieser Sprache laden
Returns:
Dictionary mit source -> target Mappings
@@ -600,6 +683,13 @@ def load_translations_from_json(filename: Path) -> dict[str, str]:
print(f"FEHLER: JSON-Datei hat ungültige Struktur (erwartet Object, bekam {type(data).__name__})")
sys.exit(1)
# Erkenne Struktur
structure_type, languages = detect_json_structure(data)
translations = {}
if structure_type == "legacy":
# Alte Struktur
if 'translations' not in data:
print(f"FEHLER: JSON-Datei enthält keinen 'translations' Block")
sys.exit(1)
@@ -609,7 +699,6 @@ def load_translations_from_json(filename: Path) -> dict[str, str]:
print(f"FEHLER: 'translations' Block hat ungültige Struktur (erwartet Array)")
sys.exit(1)
translations = {}
for idx, entry in enumerate(translations_list):
if not isinstance(entry, dict):
print(f"WARNUNG: Eintrag {idx} in 'translations' ist kein Object, überspringe")
@@ -624,7 +713,53 @@ def load_translations_from_json(filename: Path) -> dict[str, str]:
if source and target: # Ignoriere leere Einträge
translations[source] = target
print(f"Übersetzungen aus JSON-Datei geladen: {len(translations)}")
print(f"Übersetzungen aus JSON-Datei geladen (alte Struktur): {len(translations)}")
elif structure_type == "multilang":
# Hierarchische mehrsprachige Struktur
print(f"Erkannte mehrsprachige Struktur mit Sprachen: {', '.join(languages)}")
# Filtere Sprachen falls target_lang angegeben
langs_to_load = [target_lang] if target_lang and target_lang in languages else languages
for lang in langs_to_load:
lang_key = f"[{lang}]"
if lang_key not in data:
continue
lang_data = data[lang_key]
if not isinstance(lang_data, dict):
print(f"WARNUNG: [{lang}] Block ist kein Object, überspringe")
continue
# Lese aus translated Block
if 'translated' in lang_data:
translated = lang_data['translated']
if not isinstance(translated, list):
print(f"WARNUNG: [{lang}][translated] ist kein Array, überspringe")
continue
for idx, entry in enumerate(translated):
if not isinstance(entry, dict):
continue
if 'source' not in entry or 'target' not in entry:
continue
source = entry['source']
target = entry['target']
if source and target:
translations[source] = target
if target_lang:
print(f"Übersetzungen für Sprache [{target_lang}] aus JSON-Datei geladen: {len(translations)}")
else:
print(f"Übersetzungen aus JSON-Datei geladen (mehrsprachige Struktur, alle Sprachen): {len(translations)}")
else:
print(f"FEHLER: JSON-Datei hat unbekannte Struktur")
sys.exit(1)
return translations
except json.JSONDecodeError as e:
@@ -959,23 +1094,66 @@ def write_texts_to_excel(texts: list[str], output_path: Path,
def write_texts_to_json(texts: list[str], ignored_texts: list[str], output_path: Path,
translation_dicts: tuple[dict[str, str], dict[str, str], dict[str, str], dict[str, str]] = None):
translation_dicts: tuple[dict[str, str], dict[str, str], dict[str, str], dict[str, str]] = None,
translation_dicts_all: dict[str, tuple] = None,
translation_languages: list[str] = None):
"""
Schreibt die Texte in eine JSON-Datei mit drei Blöcken.
Schreibt die Texte in eine JSON-Datei.
Die JSON-Struktur enthält:
Bei einer Sprache (alte Struktur):
- "translations": Liste von {source, target} für übersetzte Texte
- "untranslated": Liste von {source, target: ""} für nicht übersetzte Texte
- "ignored": Liste von Strings für gefilterte/ignorierte Texte
Bei mehreren Sprachen (hierarchische Struktur):
- "[EN]": {"translated": [...], "untranslated": [...]}
- "[FR]": {"translated": [...], "untranslated": [...]}
- "ignored": Liste von Strings für gefilterte/ignorierte Texte
Args:
texts: Liste der akzeptierten Texte
ignored_texts: Liste der ignorierten Texte
output_path: Pfad zur Ausgabe-JSON-Datei
translation_dicts: Optional: Tuple mit (Multi, Trigramme, Bigramme, Single) für Übersetzungen
translation_dicts: Optional: Tuple mit (Multi, Trigramme, Bigramme, Single) für eine Sprache (backwards compatibility)
translation_dicts_all: Optional: Dict mit {LANG: (multi, trigramme, bigramme, single)} für mehrere Sprachen
translation_languages: Optional: Liste der Sprachen in der Reihenfolge der Verarbeitung
"""
output_data = {}
# Mehrsprachige hierarchische Struktur
if translation_dicts_all and translation_languages and len(translation_languages) > 1:
for lang in translation_languages:
if lang not in translation_dicts_all:
continue
multi, trigramme, bigramme, single = translation_dicts_all[lang]
translations = []
untranslated = []
for text in texts:
translated = translate_text(text, multi, trigramme, bigramme, single)
if translated: # Übersetzung vorhanden
translations.append({
"source": text,
"target": translated
})
else: # Keine Übersetzung vorhanden
untranslated.append({
"source": text,
"target": ""
})
# Hierarchische Struktur: [LANG] als Schlüssel
output_data[f"[{lang}]"] = {
"translated": translations,
"untranslated": untranslated
}
# Ignorierte Texte am Ende (ohne Sprachzuordnung)
output_data["ignored"] = ignored_texts
# Alte Struktur (eine Sprache oder keine Übersetzung)
else:
# Block 1: Übersetzte Texte (als Liste von Dicts mit source/target)
# Block 2: Nicht übersetzte Texte (als Liste von Dicts mit source und leerem target)
if translation_dicts:
@@ -1011,11 +1189,22 @@ def write_texts_to_json(texts: list[str], ignored_texts: list[str], output_path:
json.dump(output_data, f, ensure_ascii=False, indent=2)
print(f"JSON-Datei erfolgreich erstellt: {output_path}")
# Ausgabe-Statistiken
if translation_dicts_all and translation_languages and len(translation_languages) > 1:
# Mehrsprachige Struktur
for lang in translation_languages:
if f"[{lang}]" in output_data:
lang_data = output_data[f"[{lang}]"]
print(f" [{lang}] Übersetzungen: {len(lang_data['translated'])}, Nicht übersetzt: {len(lang_data['untranslated'])}")
else:
# Alte Struktur
if translation_dicts:
print(f" Übersetzungen: {len(output_data['translations'])}")
print(f" Nicht übersetzte Texte: {len(output_data['untranslated'])}")
else:
print(f" Nicht übersetzte Texte: {len(output_data['untranslated'])}")
print(f" Ignorierte Texte: {len(ignored_texts)}")
except Exception as e:
print(f"Fehler beim Schreiben der JSON-Datei: {e}")
@@ -1160,7 +1349,7 @@ if __name__ == '__main__':
'--translate',
action='store',
metavar='LANG',
help='Übersetzt Texte mit Config aus translation/<LANG>.cfg (z.B. CS, EN, FR)'
help='Übersetzt Texte mit Config aus translation/<LANG>.cfg (z.B. CS, EN, FR). Mehrere Sprachen kommasepariert möglich (z.B. EN,FR,CS)'
)
parser.add_argument(
'--lang_source',
@@ -1222,25 +1411,41 @@ if __name__ == '__main__':
symbol_attr_mapping = load_symbol_attribute_mapping(cfg_dir)
symbol_layer_mapping = load_symbol_layer_mapping(cfg_dir)
# Bestimme Quell- und Ziel-Attributnamen basierend auf Sprachcodes
source_attr_name = symbol_attr_mapping.get(args.lang_source, 'TEXT-D')
target_attr_name = symbol_attr_mapping.get(args.translate, 'TEXT-CS') if args.translate else 'TEXT-CS'
# Parse Sprachen aus --translate Parameter (kommaseparierte Liste)
translation_languages = []
if args.translate:
translation_languages = [lang.strip().upper() for lang in args.translate.split(',')]
# Entferne Duplikate und behalte Reihenfolge
seen = set()
translation_languages = [lang for lang in translation_languages if lang and not (lang in seen or seen.add(lang))]
# Bestimme Ziel-Layer für Blockattribute (nur relevant bei --todxf)
target_layer_name = symbol_layer_mapping.get(args.translate) if args.translate else None
# Bestimme Quell-Attributnamen basierend auf Sprachcode
source_attr_name = symbol_attr_mapping.get(args.lang_source, 'TEXT-D')
# Für --todxf: verwende nur die erste Sprache als Ziel (backwards compatibility)
target_attr_name = symbol_attr_mapping.get(translation_languages[0], 'TEXT-CS') if translation_languages else 'TEXT-CS'
target_layer_name = symbol_layer_mapping.get(translation_languages[0]) if translation_languages else None
print(f"\nVerwendete Attribut-Namen:")
print(f" Quell-Attribut (--lang_source {args.lang_source}): {source_attr_name}")
if args.translate:
print(f" Ziel-Attribut (--translate {args.translate}): {target_attr_name}")
if translation_languages:
if len(translation_languages) == 1:
print(f" Ziel-Attribut (--translate {translation_languages[0]}): {target_attr_name}")
if target_layer_name:
print(f" Ziel-Layer (--translate {args.translate}): {target_layer_name}")
print(f" Ziel-Layer (--translate {translation_languages[0]}): {target_layer_name}")
else:
print(f" Ziel-Sprachen (--translate): {', '.join(translation_languages)}")
# Optional: Lade Übersetzungs-Config
translation_dicts = None
if args.translate:
# Optional: Lade Übersetzungs-Configs für alle Sprachen
# translation_dicts_all ist ein Dict: {LANG: (multi, trigramme, bigramme, single), ...}
translation_dicts_all = {}
if translation_languages:
translation_dir = check_environment_var('PROJECT_TRANSLATION')
translation_dicts = load_translation_config(args.translate, translation_dir)
for lang in translation_languages:
translation_dicts_all[lang] = load_translation_config(lang, translation_dir)
# Für backwards compatibility: wenn nur eine Sprache, setze translation_dicts wie früher
translation_dicts = translation_dicts_all.get(translation_languages[0]) if len(translation_languages) == 1 else None
if args.debug:
print("DEBUG-Modus aktiviert")
@@ -1286,8 +1491,9 @@ if __name__ == '__main__':
print(f"Übersetzungs-JSON: {translation_json}")
# Lade Übersetzungen
translations = load_translations_from_json(translation_json)
# Lade Übersetzungen (verwende erste Sprache als Ziel-Sprache)
target_lang = translation_languages[0] if translation_languages else None
translations = load_translations_from_json(translation_json, target_lang)
# Bestimme Ausgabe-DXF
output_dxf_name = args.todxf
@@ -1415,7 +1621,7 @@ if __name__ == '__main__':
if export_type == 'excel':
write_texts_to_excel(texts, output_path, translation_dicts)
elif export_type == 'json':
write_texts_to_json(texts, ignored_texts, output_path, translation_dicts)
write_texts_to_json(texts, ignored_texts, output_path, translation_dicts, translation_dicts_all, translation_languages)
else: # text
write_texts_to_text(texts, ignored_texts, output_path, translation_dicts)
+154358
View File
File diff suppressed because it is too large Load Diff