diff --git a/lib/translate.py b/lib/translate.py index 60ec229..04c3547 100644 --- a/lib/translate.py +++ b/lib/translate.py @@ -380,20 +380,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" @@ -410,6 +406,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 @@ -441,20 +457,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] = {} @@ -482,22 +485,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 = [] @@ -703,12 +692,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 @@ -1216,6 +1199,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], @@ -1251,51 +1278,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( @@ -1382,6 +1374,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, @@ -1421,49 +1485,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 @@ -1472,18 +1501,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"] = [] @@ -1586,7 +1606,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}") @@ -1731,6 +1751,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]): @@ -1759,93 +1892,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)