From 10caa9d07f0fb5bf566ce624340fc4e30cdc00bb Mon Sep 17 00:00:00 2001 From: mistangl Date: Wed, 25 Feb 2026 15:50:06 +0100 Subject: [PATCH] Berichtigung von lib/translate.py aus dem letzen Merge --- bin/tr2dxf.bat | 23 +- lib/translate.py | 743 +++++++++++++++++++++++++++++++---------------- 2 files changed, 509 insertions(+), 257 deletions(-) diff --git a/bin/tr2dxf.bat b/bin/tr2dxf.bat index 30a305f..e8ed006 100644 --- a/bin/tr2dxf.bat +++ b/bin/tr2dxf.bat @@ -1,6 +1,6 @@ @echo off -REM Zentrale Batch-Datei für DXF/JSON-Übersetzungen +REM Zentrale Batch-Datei fďż˝r DXF/JSON-ďż˝bersetzungen REM Wird von tr2dxf_XX.bat Dateien mit Sprachparameter aufgerufen if [%1]==[] goto usage @@ -26,7 +26,7 @@ REM Zielverzeichnis set TARGET_DIR=%PROJECT_WORK% mkdir "%TARGET_DIR%" -REM Prüfe Dateityp und wähle entsprechenden Workflow +REM Prďż˝fe Dateityp und wďż˝hle entsprechenden Workflow if /I "%EXT%"==".json" goto retranslate_json if /I "%EXT%"==".txt" goto retranslate_txt if /I "%EXT%"==".dxf" goto translate_dxf @@ -45,9 +45,9 @@ if not exist "%PROJECT_WORK%\%RESULT_DXF%" ( pause goto :eof ) -move %PROJECT_WORK%\%RESULT_DXF% %PROJECT_BOM_RESULTS% +move %PROJECT_WORK%\%RESULT_DXF% %DIR% echo. -echo === Translation file created: %PROJECT_BOM_RESULTS%\%RESULT_DXF% === +echo === Translation file created: %DIR%\%RESULT_DXF% === pause goto :eof @@ -65,7 +65,6 @@ echo === JSON file updated: %INPUT_FILE% === pause goto :eof - :retranslate_txt echo. echo === Re-translating TXT file with current %LANG%.cfg === @@ -79,23 +78,23 @@ echo. echo === TXT file updated: %INPUT_FILE% === pause goto :eof - + :invalid_filetype -@echo FEHLER: Ungültiger Dateityp: %EXT% +@echo FEHLER: Ungďż˝ltiger Dateityp: %EXT% @echo Erlaubt sind: .dxf, .dwg, .json, .txt pause goto :eof :usage -@echo Usage: %0 ^ ^ +@echo Usage: %0 ^ ^ @echo LANG: Zielsprache (CS, EN, FR, IT, ES) -@echo - .dxf/.dwg: Extrahiert und übersetzt Texte zur angegebenen Sprache -@echo - .json: Re-übersetzt vorhandene Übersetzungen mit aktueller LANG.cfg -@echo - .txt: Re-übersetzt TXT Datei (translations/untranslated) mit aktueller LANG.cfg +@echo - .dxf/.dwg: Extrahiert und ďż˝bersetzt Texte zur angegebenen Sprache +@echo - .json: Re-ďż˝bersetzt vorhandene ďż˝bersetzungen mit aktueller LANG.cfg +@echo - .txt: Re-ďż˝bersetzt TXT-Datei (translations/untranslated) mit aktueller LANG.cfg @echo. @echo Beispiele: @echo %0 CS myfile.dxf @echo %0 EN translations.json -@echo %0 CS translation_texts.txt +@echo %0 CS translations_texts.txt exit /B 1 goto :eof diff --git a/lib/translate.py b/lib/translate.py index cf04ef1..ccc3ea1 100644 --- a/lib/translate.py +++ b/lib/translate.py @@ -32,9 +32,10 @@ SCHRITT 1 – Texte aus DXF extrahieren und Roh-JSON erzeugen Optional: zusätzliche Ausgabe als Excel oder Text (-t excel,json,text). ─────────────────────────────────────────────────────────────────────────────── -SCHRITT 2a – JSON auto-ĂĽbersetzen (Drop auf tr2dxf_CS.bat / tr2dxf_EN.bat) +SCHRITT 2a – JSON/TXT auto-ĂĽbersetzen (Drop auf tr2dxf_CS.bat / tr2dxf_EN.bat) ─────────────────────────────────────────────────────────────────────────────── --retranslate-json myfile_texts.json --translate CS[,EN,...] + --retranslate-txt myfile_texts.txt --translate CS Liest eine bestehende JSON-Datei (Legacy-Flat oder Multilang), ĂĽbersetzt alle Texte aus 'untranslated' automatisch und verschiebt Treffer nach @@ -277,8 +278,9 @@ def load_translation_config(translation_lang: str, translation_dir: Path) -> tup # PrĂĽfe auf fĂĽhrende Leerzeichen vor dem Parsen (wĂĽrden Zeilen zusammenziehen) _check_leading_spaces(config_file, translation_lang) - # RawConfigParser mit case-sensitive Keys - config = RawConfigParser() + # RawConfigParser mit case-sensitive Keys und UnterstĂĽtzung fĂĽr Einträge ohne Wert + # (allow_no_value=True damit der [transfer]-Abschnitt mit einzelnen Begriffen ohne '=' gelesen werden kann) + config = RawConfigParser(allow_no_value=True) # Ăśberschreibe optionxform um GroĂź-/Kleinschreibung zu erhalten config.optionxform = str try: @@ -292,37 +294,31 @@ def load_translation_config(translation_lang: str, translation_dir: Path) -> tup bigramme = {} single = {} - # Lade Multi (4+ Wörter) - if config.has_section('multi'): - for key in config['multi']: - original = key - translation = config['multi'][key] + def _load_section(section_name: str) -> dict[str, str]: + """Lädt eine Ăśbersetzungssektion und warnt bei Einträgen ohne '=' (value=None).""" + result = {} + if not config.has_section(section_name): + return result + for key in config[section_name]: + translation = config[section_name][key] + if translation is None: + print(f" WARNUNG [{translation_lang}] [{section_name}]: Eintrag ohne '=' ignoriert: '{key}'") + continue if translation and not translation.startswith('#'): - multi[original] = translation + result[key] = translation + return result + + # Lade Multi (4+ Wörter) + multi = _load_section('multi') # Lade Trigramme - if config.has_section('trigramme'): - for key in config['trigramme']: - original = key - translation = config['trigramme'][key] - if translation and not translation.startswith('#'): - trigramme[original] = translation + trigramme = _load_section('trigramme') # Lade Bigramme - if config.has_section('bigramme'): - for key in config['bigramme']: - original = key - translation = config['bigramme'][key] - if translation and not translation.startswith('#'): - bigramme[original] = translation + bigramme = _load_section('bigramme') # Lade Single - if config.has_section('single'): - for key in config['single']: - original = key - translation = config['single'][key] - if translation and not translation.startswith('#'): - single[original] = translation + single = _load_section('single') # PrĂĽfe SelbstĂĽbersetzungen in jedem Block und entferne sie mit Warnung multi = _filter_self_translations('multi', multi) @@ -385,20 +381,16 @@ def load_transfer_terms(translation_lang: str, translation_dir: Path) -> set[str return transfer -def load_symbol_attribute_mapping(cfg_path: Path) -> dict[str, str]: +def _load_translator_config(cfg_path: Path) -> RawConfigParser: """ - Liest die Zuordnung von Sprachcodes zu Symbol-Attributnamen aus der translator.cfg Datei. - - Verwendet den Abschnitt [symbol_attribute], z.B.: - DE = TEXT-D - EN = TEXT-E - CS = TEXT-CS + Lädt translator.cfg aus cfg_path und gibt einen RawConfigParser zurĂĽck. + Bricht mit Fehlermeldung ab wenn die Datei nicht existiert oder nicht gelesen werden kann. Args: cfg_path: Pfad zum Konfigurationsverzeichnis (PROJECT_CFG) Returns: - Dictionary mit Sprachcode -> Attributname Mapping (z.B. "DE" -> "TEXT-D", "EN" -> "TEXT-EN") + Gelesener RawConfigParser (case-sensitive Keys) """ config_file = cfg_path / "translator.cfg" @@ -415,6 +407,26 @@ def load_symbol_attribute_mapping(cfg_path: Path) -> dict[str, str]: print(f"Fehler beim Lesen der Konfigurationsdatei: {e}") sys.exit(1) + return config + + +def load_symbol_attribute_mapping(cfg_path: Path) -> dict[str, str]: + """ + Liest die Zuordnung von Sprachcodes zu Symbol-Attributnamen aus der translator.cfg Datei. + + Verwendet den Abschnitt [symbol_attribute], z.B.: + DE = TEXT-D + EN = TEXT-E + CS = TEXT-CS + + Args: + cfg_path: Pfad zum Konfigurationsverzeichnis (PROJECT_CFG) + + Returns: + Dictionary mit Sprachcode -> Attributname Mapping (z.B. "DE" -> "TEXT-D", "EN" -> "TEXT-EN") + """ + config = _load_translator_config(cfg_path) + symbol_mapping: dict[str, str] = {} # Lade Symbol-Attribut-Zuordnung @@ -446,20 +458,7 @@ def load_symbol_layer_mapping(cfg_path: Path) -> dict[str, str]: Returns: Dictionary mit Sprachcode -> Layername Mapping """ - config_file = cfg_path / "translator.cfg" - - if not config_file.exists(): - print(f"FEHLER: Konfigurationsdatei nicht gefunden: {config_file}") - print(f"Bitte erstellen Sie die Datei 'translator.cfg' im Verzeichnis: {cfg_path}") - sys.exit(1) - - config = RawConfigParser() - config.optionxform = str - try: - config.read(config_file, encoding='utf-8') - except Exception as e: - print(f"Fehler beim Lesen der Konfigurationsdatei: {e}") - sys.exit(1) + config = _load_translator_config(cfg_path) layer_mapping: dict[str, str] = {} @@ -487,22 +486,8 @@ def load_ignore_patterns(cfg_path: Path) -> tuple[list[str], list[str]]: Returns: Tuple mit (Wildcard-Mustern, Regex-Mustern) """ - config_file = cfg_path / "translator.cfg" - - if not config_file.exists(): - print(f"FEHLER: Konfigurationsdatei nicht gefunden: {config_file}") - print(f"Bitte erstellen Sie die Datei 'translator.cfg' im Verzeichnis: {cfg_path}") - sys.exit(1) - # RawConfigParser verhindert dass Backslashes in Regex-Mustern escaped werden - config = RawConfigParser() - # Ăśberschreibe optionxform um GroĂź-/Kleinschreibung zu erhalten - config.optionxform = str - try: - config.read(config_file, encoding='utf-8') - except Exception as e: - print(f"Fehler beim Lesen der Konfigurationsdatei: {e}") - sys.exit(1) + config = _load_translator_config(cfg_path) wildcard_patterns = [] regex_patterns = [] @@ -708,12 +693,6 @@ def extract_text_from_dxf(filename: Path, text_attr_name: str = 'TEXT-D') -> lis # PrĂĽfe ob Block alle erforderlichen Attribute hat if required_attributes.issubset(block_attributes) and text_d_value: block_count += 1 - - # Ignoriere leere Texte - if not text_d_value: - empty_count += 1 - continue - texts_set.add(text_d_value) # Konvertiere Set zu sortierter Liste @@ -1221,6 +1200,50 @@ def translate_block_attribute(entity, return 1, 0, 0, 0 +def _translate_text_entities(msp, entity_type: str, + translations: dict[str, str], + wildcard_patterns: list[str], + regex_patterns: list[str]) -> tuple[int, int, int]: + """ + Ăśbersetzt alle TEXT oder MTEXT Entitäten im Modelraum in-place. + + Args: + msp: ezdxf Modelspace-Objekt + entity_type: "TEXT" oder "MTEXT" + translations: Dictionary mit source -> target Mappings + wildcard_patterns: Wildcard-Muster zum Filtern + regex_patterns: Regex-Muster zum Filtern + + Returns: + Tuple (translated, skipped, ignored) + """ + translated = 0 + skipped = 0 + ignored = 0 + + for entity in msp.query(entity_type): + raw_text = entity.dxf.text if entity_type == 'TEXT' else entity.text + if not raw_text: + continue + + original_text = raw_text.strip() + + if should_ignore_text(original_text, wildcard_patterns, regex_patterns): + ignored += 1 + continue + + if original_text in translations: + if entity_type == 'TEXT': + entity.dxf.text = translations[original_text] + else: + entity.text = translations[original_text] + translated += 1 + else: + skipped += 1 + + return translated, skipped, ignored + + def translate_dxf_texts(input_dxf: Path, output_dxf: Path, translations: dict[str, str], @@ -1256,51 +1279,16 @@ def translate_dxf_texts(input_dxf: Path, doc = ezdxf.readfile(input_dxf) msp = doc.modelspace() - text_translated = 0 - text_skipped = 0 - text_ignored = 0 - mtext_translated = 0 - mtext_skipped = 0 - mtext_ignored = 0 + text_translated, text_skipped, text_ignored = _translate_text_entities( + msp, 'TEXT', translations, wildcard_patterns, regex_patterns) + mtext_translated, mtext_skipped, mtext_ignored = _translate_text_entities( + msp, 'MTEXT', translations, wildcard_patterns, regex_patterns) + block_translated = 0 block_skipped = 0 block_ignored = 0 block_transferred = 0 - # Ăśbersetze TEXT Objekte - for entity in msp.query('TEXT'): - if entity.dxf.text: - original_text = entity.dxf.text.strip() - - # PrĂĽfe ob Text ignoriert werden soll - if should_ignore_text(original_text, wildcard_patterns, regex_patterns): - text_ignored += 1 - continue - - # Suche Ăśbersetzung - if original_text in translations: - entity.dxf.text = translations[original_text] - text_translated += 1 - else: - text_skipped += 1 - - # Ăśbersetze MTEXT Objekte - for entity in msp.query('MTEXT'): - if entity.text: - original_text = entity.text.strip() - - # PrĂĽfe ob Text ignoriert werden soll - if should_ignore_text(original_text, wildcard_patterns, regex_patterns): - mtext_ignored += 1 - continue - - # Suche Ăśbersetzung - if original_text in translations: - entity.text = translations[original_text] - mtext_translated += 1 - else: - mtext_skipped += 1 - # Ăśbersetze Text-Attribute in Blöcken for entity in msp.query('INSERT'): translated, skipped, ignored, transferred = translate_block_attribute( @@ -1387,6 +1375,78 @@ def write_texts_to_excel(texts: list[str], output_path: Path, sys.exit(1) +def _load_existing_lang_blocks(output_path: Path, translation_languages: list[str]) -> dict: + """ + Lädt vorhandene Sprachblöcke aus einer bestehenden JSON-Datei fĂĽr Merge-UnterstĂĽtzung. + Gibt ein leeres Dict zurĂĽck wenn die Datei nicht existiert oder nicht gelesen werden kann. + + Args: + output_path: Pfad zur bestehenden JSON-Datei + translation_languages: Sprachen die neu generiert werden (werden aus dem Merge ausgeschlossen) + + Returns: + Dict mit bestehenden Sprachblöcken die NICHT neu generiert werden + """ + if not output_path.exists(): + return {} + + try: + with open(output_path, 'r', encoding='utf-8') as f: + existing = json.load(f) + if not isinstance(existing, dict): + return {} + + existing_lang_blocks = { + key: value for key, value in existing.items() + if key.startswith('[') and key.endswith(']') and len(key) > 2 + } + + if existing_lang_blocks: + preserved = [k for k in existing_lang_blocks + if k not in {f"[{lang}]" for lang in translation_languages}] + if preserved: + print(f" Bestehende Sprachblöcke ĂĽbernommen: {', '.join(preserved)}") + + new_lang_keys = {f"[{lang}]" for lang in translation_languages} + return {key: value for key, value in existing_lang_blocks.items() + if key not in new_lang_keys} + + except Exception: + return {} # Fehler beim Laden → einfach neu schreiben + + +def _build_lang_block( + texts: list[str], + multi: dict[str, str], + trigramme: dict[str, str], + bigramme: dict[str, str], + single: dict[str, str]) -> dict: + """ + Ăśbersetzt alle Texte fĂĽr eine Sprache und baut den Sprach-Block fĂĽr die JSON-Ausgabe. + + Args: + texts: Liste der zu ĂĽbersetzenden Texte + multi: Multi-Phrasen-Dict + trigramme: Trigramme-Dict + bigramme: Bigramme-Dict + single: Einzelwörter-Dict + + Returns: + Dict mit {"translations": [...], "untranslated": [...]} + """ + translations = [] + untranslated = [] + + for text in texts: + translated = translate_text(text, multi, trigramme, bigramme, single) + if translated: + translations.append({"source": text, "target": translated}) + else: + untranslated.append({"source": text, "target": ""}) + + return {"translations": translations, "untranslated": untranslated} + + 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_all: dict[str, tuple] = None, @@ -1426,49 +1486,14 @@ def write_texts_to_json(texts: list[str], ignored_texts: list[str], output_path: if translation_dicts_all and translation_languages: # Lade bestehende Sprachblöcke aus vorhandener Datei (Merge-UnterstĂĽtzung) - existing_lang_blocks = {} - if output_path.exists(): - try: - with open(output_path, 'r', encoding='utf-8') as f: - existing = json.load(f) - if isinstance(existing, dict): - for key, value in existing.items(): - if key.startswith('[') and key.endswith(']') and len(key) > 2: - existing_lang_blocks[key] = value - if existing_lang_blocks: - preserved = [k for k in existing_lang_blocks - if k not in {f"[{lang}]" for lang in translation_languages}] - if preserved: - print(f" Bestehende Sprachblöcke ĂĽbernommen: {', '.join(preserved)}") - except Exception: - pass # Fehler beim Laden → einfach neu schreiben - - # Ăśbernehme Sprachblöcke, die NICHT neu generiert werden - new_lang_keys = {f"[{lang}]" for lang in translation_languages} - for key, value in existing_lang_blocks.items(): - if key not in new_lang_keys: - output_data[key] = value + output_data.update(_load_existing_lang_blocks(output_path, translation_languages)) # Generiere neue/aktualisierte Sprachblöcke 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: - translations.append({"source": text, "target": translated}) - else: - untranslated.append({"source": text, "target": ""}) - - output_data[f"[{lang}]"] = { - "translations": translations, - "untranslated": untranslated - } + output_data[f"[{lang}]"] = _build_lang_block(texts, multi, trigramme, bigramme, single) # Ignorierte Texte am Ende (ohne Sprachzuordnung) output_data["ignored"] = ignored_texts @@ -1477,18 +1502,9 @@ def write_texts_to_json(texts: list[str], ignored_texts: list[str], output_path: else: if translation_dicts: multi, trigramme, bigramme, single = translation_dicts - translations = [] - untranslated = [] - - for text in texts: - translated = translate_text(text, multi, trigramme, bigramme, single) - if translated: - translations.append({"source": text, "target": translated}) - else: - untranslated.append({"source": text, "target": ""}) - - output_data["translations"] = translations - output_data["untranslated"] = untranslated + lang_block = _build_lang_block(texts, multi, trigramme, bigramme, single) + output_data["translations"] = lang_block["translations"] + output_data["untranslated"] = lang_block["untranslated"] else: # Ohne Ăśbersetzungs-Config: alle akzeptierten Texte sind nicht ĂĽbersetzt output_data["translations"] = [] @@ -1591,7 +1607,7 @@ def write_texts_to_text(texts: list[str], ignored_texts: list[str], output_path: print(f"Text-Datei erfolgreich erstellt: {output_path}") if translation_dicts: - translations_count = sum(1 for text in texts if translate_text(text, multi, trigramme, bigramme, single)) + translations_count = len(translations) untranslated_count = len(texts) - translations_count print(f" Ăśbersetzungen: {translations_count}") print(f" Nicht ĂĽbersetzte Texte: {untranslated_count}") @@ -1736,6 +1752,119 @@ def _retranslate_language_block( return updated, unchanged, newly +def _retranslate_legacy(data: dict, + translation_languages: list[str], + translation_dicts_all: dict[str, tuple]) -> dict: + """ + Verarbeitet eine Legacy-Flat-JSON-Struktur und konvertiert sie ins Multilang-Format. + + Args: + data: Geladene JSON-Daten (flache Struktur mit 'translations'/'untranslated') + translation_languages: Sprachen in der Reihenfolge der Verarbeitung + translation_dicts_all: Dict mit {LANG: (multi, trigramme, bigramme, single), ...} + + Returns: + Konvertierte Daten im Multilang-Format + """ + if 'translations' not in data or not isinstance(data['translations'], list): + print(f"FEHLER: JSON-Datei enthält keinen gĂĽltigen 'translations' Block") + sys.exit(1) + if not translation_languages or translation_languages[0] not in translation_dicts_all: + print(f"FEHLER: Keine Ăśbersetzungs-Config fĂĽr Re-Ăśbersetzung verfĂĽgbar") + sys.exit(1) + + if 'untranslated' not in data: + data['untranslated'] = [] + + lang = translation_languages[0] + multi, trigramme, bigramme, single = translation_dicts_all[lang] + + updated, unchanged, newly = _retranslate_language_block( + data['translations'], data['untranslated'], + multi, trigramme, bigramme, single, + f"[{lang}]" + ) + + print(f"\nRe-Ăśbersetzung abgeschlossen (alte Struktur → wird zu multilang konvertiert, Sprache: {lang}):") + print(f" Aktualisiert (translations): {updated}") + print(f" Unverändert: {unchanged}") + if newly: + print(f" Neu aus 'untranslated' ĂĽbersetzt/verschoben: {newly}") + + # Legacy → Multilang konvertieren: flache Struktur in [LANG]-Block einpacken + return { + f"[{lang}]": { + "translations": data['translations'], + "untranslated": data['untranslated'], + }, + "ignored": data.get('ignored', []), + } + + +def _retranslate_multilang(data: dict, + existing_languages: list[str], + translation_languages: list[str], + translation_dicts_all: dict[str, tuple]) -> None: + """ + Verarbeitet eine Multilang-JSON-Struktur in-place. + + Args: + data: Geladene JSON-Daten (hierarchische Multilang-Struktur, wird in-place verändert) + existing_languages: Sprachen die bereits in der Datei vorhanden sind + translation_languages: Sprachen in der Reihenfolge der Verarbeitung + translation_dicts_all: Dict mit {LANG: (multi, trigramme, bigramme, single), ...} + """ + print(f"Erkannte mehrsprachige Struktur mit Sprachen: {', '.join(existing_languages)}") + + total_updated = 0 + total_unchanged = 0 + total_newly = 0 + new_block_count = 0 + + for lang in translation_languages: + lang_key = f"[{lang}]" + if lang_key not in data: + print(f" Erstelle neuen Sprach-Block: [{lang}]") + data[lang_key] = {"translations": [], "untranslated": []} + new_block_count += 1 + + lang_data = data[lang_key] + if not isinstance(lang_data, dict): + print(f"WARNUNG: [{lang}] Block ist kein Object, ĂĽberspringe") + continue + if 'translations' not in lang_data: + lang_data['translations'] = [] + if 'untranslated' not in lang_data: + lang_data['untranslated'] = [] + if not isinstance(lang_data['translations'], list): + print(f"WARNUNG: [{lang}][translations] ist kein Array, ĂĽberspringe") + continue + if lang not in translation_dicts_all: + print(f"WARNUNG: Keine Ăśbersetzungs-Config fĂĽr [{lang}] verfĂĽgbar, ĂĽberspringe") + continue + + multi, trigramme, bigramme, single = translation_dicts_all[lang] + updated, unchanged, newly = _retranslate_language_block( + lang_data['translations'], lang_data['untranslated'], + multi, trigramme, bigramme, single, + f"[{lang}]" + ) + total_updated += updated + newly + total_unchanged += unchanged + total_newly += newly + + print(f" [{lang}] Aktualisiert: {updated}, Unverändert: {unchanged}", end="") + if newly: + print(f", Neu aus 'untranslated': {newly}", end="") + print() + + print(f"\nRe-Ăśbersetzung abgeschlossen (mehrsprachige Struktur):") + print(f" Gesamt aktualisiert: {total_updated}") + print(f" Gesamt unverändert: {total_unchanged}") + if new_block_count: + print(f" Neue Sprach-Blöcke erstellt: {new_block_count}") + + def retranslate_json_file(json_path: Path, translation_dicts_all: dict[str, tuple], translation_languages: list[str]): @@ -1764,93 +1893,10 @@ def retranslate_json_file(json_path: Path, structure_type, existing_languages = detect_json_structure(data) - total_updated = 0 - total_unchanged = 0 - total_newly = 0 - new_block_count = 0 - if structure_type == "legacy": - if 'translations' not in data or not isinstance(data['translations'], list): - print(f"FEHLER: JSON-Datei enthält keinen gĂĽltigen 'translations' Block") - sys.exit(1) - if not translation_languages or translation_languages[0] not in translation_dicts_all: - print(f"FEHLER: Keine Ăśbersetzungs-Config fĂĽr Re-Ăśbersetzung verfĂĽgbar") - sys.exit(1) - - if 'untranslated' not in data: - data['untranslated'] = [] - - lang = translation_languages[0] - multi, trigramme, bigramme, single = translation_dicts_all[lang] - - updated, unchanged, newly = _retranslate_language_block( - data['translations'], data['untranslated'], - multi, trigramme, bigramme, single, - f"[{lang}]" - ) - total_updated, total_unchanged, total_newly = updated, unchanged, newly - - print(f"\nRe-Ăśbersetzung abgeschlossen (alte Struktur → wird zu multilang konvertiert, Sprache: {lang}):") - print(f" Aktualisiert (translations): {updated}") - print(f" Unverändert: {unchanged}") - if newly: - print(f" Neu aus 'untranslated' ĂĽbersetzt/verschoben: {newly}") - - # Legacy → Multilang konvertieren: flache Struktur in [LANG]-Block einpacken - data = { - f"[{lang}]": { - "translations": data['translations'], - "untranslated": data['untranslated'], - }, - "ignored": data.get('ignored', []), - } - + data = _retranslate_legacy(data, translation_languages, translation_dicts_all) elif structure_type == "multilang": - print(f"Erkannte mehrsprachige Struktur mit Sprachen: {', '.join(existing_languages)}") - - for lang in translation_languages: - lang_key = f"[{lang}]" - if lang_key not in data: - print(f" Erstelle neuen Sprach-Block: [{lang}]") - data[lang_key] = {"translations": [], "untranslated": []} - new_block_count += 1 - - lang_data = data[lang_key] - if not isinstance(lang_data, dict): - print(f"WARNUNG: [{lang}] Block ist kein Object, ĂĽberspringe") - continue - if 'translations' not in lang_data: - lang_data['translations'] = [] - if 'untranslated' not in lang_data: - lang_data['untranslated'] = [] - if not isinstance(lang_data['translations'], list): - print(f"WARNUNG: [{lang}][translations] ist kein Array, ĂĽberspringe") - continue - if lang not in translation_dicts_all: - print(f"WARNUNG: Keine Ăśbersetzungs-Config fĂĽr [{lang}] verfĂĽgbar, ĂĽberspringe") - continue - - multi, trigramme, bigramme, single = translation_dicts_all[lang] - updated, unchanged, newly = _retranslate_language_block( - lang_data['translations'], lang_data['untranslated'], - multi, trigramme, bigramme, single, - f"[{lang}]" - ) - total_updated += updated + newly - total_unchanged += unchanged - total_newly += newly - - print(f" [{lang}] Aktualisiert: {updated}, Unverändert: {unchanged}", end="") - if newly: - print(f", Neu aus 'untranslated': {newly}", end="") - print() - - print(f"\nRe-Ăśbersetzung abgeschlossen (mehrsprachige Struktur):") - print(f" Gesamt aktualisiert: {total_updated}") - print(f" Gesamt unverändert: {total_unchanged}") - if new_block_count: - print(f" Neue Sprach-Blöcke erstellt: {new_block_count}") - + _retranslate_multilang(data, existing_languages, translation_languages, translation_dicts_all) else: print(f"FEHLER: JSON-Datei hat unbekannte Struktur") sys.exit(1) @@ -1892,6 +1938,201 @@ def resolve_input_file(name: str, work_dir: Path, label: str = "Datei") -> Path: return path +def _parse_txt_sections(filename: Path) -> tuple[list[tuple[str, str]], list[str], list[str]]: + """ + Parst eine Textdatei im Format von write_texts_to_text. + + Erkannte Abschnitte: + - "--- translations:" → Zeilen im Format "source -> target" + - "--- untranslated:" → einfache Texte (eine pro Zeile) + - "--- ignored:" → einfache Texte (eine pro Zeile) + + Args: + filename: Pfad zur Textdatei + + Returns: + Tuple (translations, untranslated, ignored): + - translations: Liste von (source, target) Tupeln + - untranslated: Liste von source-Texten ohne Ăśbersetzung + - ignored: Liste von ignorierten Texten + """ + translations: list[tuple[str, str]] = [] + untranslated: list[str] = [] + ignored: list[str] = [] + current_section: str | None = None + + try: + with open(filename, 'r', encoding='utf-8') as f: + for line in f: + stripped = line.rstrip('\n\r').strip() + + if stripped.startswith('---'): + low = stripped.lower() + if 'translations' in low: + current_section = 'translations' + elif 'untranslated' in low: + current_section = 'untranslated' + elif 'ignored' in low: + current_section = 'ignored' + else: + current_section = None + continue + + if not stripped or current_section is None: + continue + + if current_section == 'translations': + if ' -> ' in stripped: + source, target = stripped.split(' -> ', 1) + translations.append((source, target)) + else: + # Eintrag ohne Ziel → als untranslated behandeln + untranslated.append(stripped) + elif current_section == 'untranslated': + untranslated.append(stripped) + elif current_section == 'ignored': + ignored.append(stripped) + + except Exception as e: + print(f"Fehler beim Lesen der Textdatei: {e}") + sys.exit(1) + + return translations, untranslated, ignored + + +def retranslate_txt_file(txt_path: Path, + translation_dicts_all: dict[str, tuple], + translation_languages: list[str], + wildcard_patterns: list[str] | None = None, + regex_patterns: list[str] | None = None) -> None: + """ + Re-ĂĽbersetzt eine Textdatei im Format von write_texts_to_text. + Ăśberschreibt die ursprĂĽngliche Datei mit aktualisierten Inhalten. + + Verarbeitungsreihenfolge: + 1. Bestehende translations werden mit aktueller Config neu ĂĽbersetzt. + Texte fĂĽr die keine Ăśbersetzung mehr gefunden wird, wandern nach untranslated. + 2. Einträge aus untranslated werden gegen die Ignore-Pattern geprĂĽft: + - Treffer → ignored (neu erkannte Pattern seit dem letzten Extrakt) + - Kein Treffer → auto-ĂĽbersetzen; Erfolg → translations, sonst bleibt in untranslated. + + Arbeitet immer mit der ersten Sprache aus translation_languages. + + Args: + txt_path: Pfad zur Textdatei (wird in-place ĂĽberschrieben) + translation_dicts_all: Dict mit {LANG: (multi, trigramme, bigramme, single), ...} + translation_languages: Liste der Sprachen (nur die erste wird verwendet) + wildcard_patterns: Wildcard-Muster zum Filtern (aus translator.cfg) + regex_patterns: Regex-Muster zum Filtern (aus translator.cfg) + """ + if not translation_languages or translation_languages[0] not in translation_dicts_all: + print(f"FEHLER: Keine Ăśbersetzungs-Config fĂĽr Re-Ăśbersetzung verfĂĽgbar") + sys.exit(1) + + wildcard_patterns = wildcard_patterns or [] + regex_patterns = regex_patterns or [] + + lang = translation_languages[0] + multi, trigramme, bigramme, single = translation_dicts_all[lang] + + existing_translations, untranslated, ignored = _parse_txt_sections(txt_path) + + print(f" Vorhandene Ăśbersetzungen: {len(existing_translations)}") + print(f" Nicht ĂĽbersetzt: {len(untranslated)}") + print(f" Ignoriert: {len(ignored)}") + + new_translations: list[tuple[str, str]] = [] + updated = 0 + unchanged = 0 + moved_to_untranslated: list[str] = [] + + # Schritt 1: Bestehende Ăśbersetzungen neu ĂĽbersetzen + for source, old_target in existing_translations: + new_target = translate_text(source, multi, trigramme, bigramme, single) + if new_target: + new_translations.append((source, new_target)) + if new_target != old_target: + updated += 1 + else: + unchanged += 1 + else: + moved_to_untranslated.append(source) + print(f" WARNUNG: Ăśbersetzung nicht mehr verfĂĽgbar fĂĽr '{source}', " + f"verschoben zu 'untranslated'") + + # Schritt 2: Untranslated Texte prĂĽfen und auto-ĂĽbersetzen + still_untranslated: list[str] = list(moved_to_untranslated) + newly = 0 + newly_ignored = 0 + for text in untranslated: + # Neu hinzugefĂĽgte Ignore-Pattern prĂĽfen + if should_ignore_text(text, wildcard_patterns, regex_patterns): + ignored.append(text) + newly_ignored += 1 + continue + new_target = translate_text(text, multi, trigramme, bigramme, single) + if new_target: + new_translations.append((text, new_target)) + newly += 1 + else: + still_untranslated.append(text) + + # Ergebnis zurĂĽckschreiben + try: + with open(txt_path, 'w', encoding='utf-8') as f: + f.write("--- translations:\n\n") + for source, target in new_translations: + f.write(f"{source} -> {target}\n") + f.write("\n--- untranslated:\n\n") + for text in still_untranslated: + f.write(f"{text}\n") + f.write("\n--- ignored:\n\n") + for text in ignored: + f.write(f"{text}\n") + except Exception as e: + print(f"Fehler beim Schreiben der Textdatei: {e}") + sys.exit(1) + + print(f"\nRe-Ăśbersetzung abgeschlossen (Sprache: {lang}):") + print(f" Aktualisiert: {updated}") + print(f" Unverändert: {unchanged}") + if newly: + print(f" Neu ĂĽbersetzt: {newly}") + if newly_ignored: + print(f" Neu ignoriert (Pattern): {newly_ignored}") + if moved_to_untranslated: + print(f" ZurĂĽck zu untranslated: {len(moved_to_untranslated)}") + print(f" Noch nicht ĂĽbersetzt: {len(still_untranslated)}") + print(f"\nDatei aktualisiert: {txt_path}") + + +def _run_retranslate_txt( + args, + work_dir: Path, + translation_dicts_all: dict, + translation_languages: list, + wildcard_patterns: list, + regex_patterns: list) -> None: + """Workflow: Re-Ăśbersetzung einer bestehenden TXT-Datei (--retranslate-txt).""" + print("=== Re-Ăśbersetzung TXT-Datei ===\n") + + input_filename = args.retranslate_txt + if not input_filename.lower().endswith('.txt'): + print("Mit --retranslate-txt nur .txt Dateien erlaubt") + sys.exit(1) + + txt_file = resolve_input_file(input_filename, work_dir, "TXT-Datei") + + print(f"TXT-Datei: {txt_file}") + print(f"Sprache: {translation_languages[0]}") + + retranslate_txt_file(txt_file, translation_dicts_all, translation_languages, + wildcard_patterns, regex_patterns) + + print("\nVerarbeitung erfolgreich abgeschlossen.") + sys.exit(0) + + def _build_arg_parser() -> argparse.ArgumentParser: """Erstellt und konfiguriert den Argument-Parser fĂĽr translate.py.""" parser = argparse.ArgumentParser( @@ -1924,6 +2165,12 @@ def _build_arg_parser() -> argparse.ArgumentParser: metavar='FILE.json', help='Re-ĂĽbersetzt translations Block in JSON-Datei mit aktueller Sprachkonfig (benötigt --translate)' ) + input_group.add_argument( + '--retranslate-txt', + action='store', + metavar='FILE.txt', + help='Re-ĂĽbersetzt translations/untranslated Blöcke in TXT-Datei mit aktueller Sprachkonfig (benötigt --translate)' + ) input_group.add_argument( '--todxf', action='store', @@ -2163,6 +2410,8 @@ def main() -> None: parser.error("--fromjson benötigt --translate (Ăśbersetzung ist erforderlich)") if args.retranslate_json and not args.translate: parser.error("--retranslate-json benötigt --translate (Ăśbersetzung ist erforderlich)") + if args.retranslate_txt and not args.translate: + parser.error("--retranslate-txt benötigt --translate (Ăśbersetzung ist erforderlich)") if args.todxf and not args.filename: parser.error("--todxf benötigt --filename (Quell-DXF muss angegeben werden)") if args.translation_json and not args.todxf: @@ -2217,6 +2466,10 @@ def main() -> None: if args.retranslate_json: _run_retranslate(args, work_dir, translation_dicts_all, translation_languages) + if args.retranslate_txt: + _run_retranslate_txt(args, work_dir, translation_dicts_all, translation_languages, + wildcard_patterns, regex_patterns) + if args.todxf: _run_todxf(args, work_dir, translation_languages, wildcard_patterns, regex_patterns, source_attr_name, target_attr_name, target_layer_name)