Berichtigung von lib/translate.py aus dem letzen Merge

This commit is contained in:
2026-02-25 15:50:06 +01:00
parent 7ec65d6c77
commit 10caa9d07f
2 changed files with 509 additions and 257 deletions
+498 -245
View File
@@ -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)