2532 lines
100 KiB
Python
2532 lines
100 KiB
Python
import argparse
|
||
import json
|
||
import re
|
||
import sys
|
||
from pathlib import Path
|
||
from configparser import RawConfigParser
|
||
from fnmatch import fnmatch
|
||
|
||
import ezdxf
|
||
from openpyxl import Workbook
|
||
|
||
from utils import check_environment_var, check_file_in_work
|
||
|
||
|
||
"""
|
||
translate.py – Texte aus DXF extrahieren, übersetzen und DXF-Dateien neu beschriften.
|
||
|
||
Alle Ausgabedateien landen im PROJECT_WORK Verzeichnis.
|
||
Die Übersetzungs-Configs liegen in PROJECT_TRANSLATION/<LANG>.cfg (z.B. CS.cfg, EN.cfg).
|
||
Beim Laden einer Config werden Selbstübersetzungen entfernt und Übersetzungsketten gemeldet.
|
||
|
||
───────────────────────────────────────────────────────────────────────────────
|
||
SCHRITT 1 – Texte aus DXF extrahieren und Roh-JSON erzeugen
|
||
───────────────────────────────────────────────────────────────────────────────
|
||
-e --extract -f myfile.dxf [-t json,excel,text] [-o output]
|
||
|
||
Liest alle TEXT/MTEXT-Objekte und Symbol-Attribute aus einer DXF/DWG-Datei,
|
||
filtert sie über translator.cfg und schreibt eine JSON-Datei mit drei Blöcken:
|
||
"translations": [] (leer – keine Sprache angegeben)
|
||
"untranslated": [...] (alle gefilterten Texte)
|
||
"ignored": [...] (herausgefilterte Texte)
|
||
Optional: zusätzliche Ausgabe als Excel oder Text (-t excel,json,text).
|
||
|
||
───────────────────────────────────────────────────────────────────────────────
|
||
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
|
||
'translations'. Bestehende Übersetzungen werden mit der aktuellen Config
|
||
aktualisiert. Duplikate zwischen und innerhalb der Listen werden bereinigt.
|
||
Ausgabe immer im Multilang-Format: "[CS]": { "translations": [...], ... }
|
||
Legacy-Dateien werden dabei automatisch ins Multilang-Format konvertiert.
|
||
|
||
───────────────────────────────────────────────────────────────────────────────
|
||
SCHRITT 2b – Mehrere Sprachen gleichzeitig extrahieren und übersetzen
|
||
───────────────────────────────────────────────────────────────────────────────
|
||
-e --extract -f myfile.dxf --translate CS,EN [-t json] [-o output]
|
||
|
||
Wie Schritt 1, aber mit sofortiger Auto-Übersetzung in eine oder mehrere
|
||
Sprachen. Ausgabe direkt im Multilang-Format. Wird die Ausgabedatei erneut
|
||
mit einer weiteren Sprache erzeugt, bleiben bestehende Sprachblöcke erhalten
|
||
(Merge): z.B. erst --translate CS, dann --translate EN → Datei enthält [CS] + [EN].
|
||
|
||
───────────────────────────────────────────────────────────────────────────────
|
||
SCHRITT 3 – Übersetzte DXF erzeugen (Drop auf tr2dxf_CS.bat / tr2dxf_EN.bat)
|
||
───────────────────────────────────────────────────────────────────────────────
|
||
-f myfile.dxf --todxf myfile_CS.dxf --translate CS
|
||
[-f myfile.dxf --todxf myfile_CS.dxf --translation-json custom_texts.json]
|
||
|
||
Liest die Übersetzungstabelle aus <filename>_texts.json (oder --translation-json),
|
||
übersetzt TEXT/MTEXT-Objekte und Symbol-Attribute direkt in der DXF-Datei
|
||
und schreibt eine neue DXF mit den Zielsprachen-Texten.
|
||
|
||
───────────────────────────────────────────────────────────────────────────────
|
||
WEITERE EINGABEQUELLEN
|
||
───────────────────────────────────────────────────────────────────────────────
|
||
--fromtxt FILE.txt Texte aus TXT-Datei lesen (ein Text pro Zeile)
|
||
--fromjson FILE.json Nur 'untranslated'-Block aus JSON lesen und
|
||
neu übersetzen (schreibt neue Ausgabedatei)
|
||
|
||
───────────────────────────────────────────────────────────────────────────────
|
||
GEMEINSAME OPTIONEN
|
||
───────────────────────────────────────────────────────────────────────────────
|
||
--translate LANG[,LANG2,...] Zielsprache(n), z.B. CS oder EN,CS,FR
|
||
-t / --export-type TYPE[,TYPE] Ausgabeformat(e): json (Standard), excel, text
|
||
-o / --outname NAME Ausgabedateiname (ohne Pfad)
|
||
--lang_source LANG Quellsprache für Symbol-Attribute (Standard: DE)
|
||
--translation-json FILE.json Übersetzungs-JSON für --todxf (Standard: <name>_texts.json)
|
||
-d / --debug Debug-Ausgabe für Pattern-Matching
|
||
"""
|
||
|
||
|
||
def _filter_self_translations(section_name: str, translations: dict[str, str]) -> dict[str, str]:
|
||
"""
|
||
Prüft ob in einem Übersetzungs-Block auf beiden Seiten dasselbe steht (z.B. AMR ILS=AMR ILS).
|
||
Gibt eine bereinigte Kopie zurück und gibt für jeden Fund eine Warnung aus.
|
||
|
||
Args:
|
||
section_name: Name des Abschnitts (für die Warnmeldung)
|
||
translations: Zu prüfendes Dictionary
|
||
|
||
Returns:
|
||
Bereinigtes Dictionary ohne Selbstübersetzungen
|
||
"""
|
||
filtered = {}
|
||
for key, value in translations.items():
|
||
if key == value:
|
||
print(f" WARNUNG [{section_name}]: Selbstübersetzung: '{key}'")
|
||
else:
|
||
filtered[key] = value
|
||
return filtered
|
||
|
||
|
||
def _check_translation_chains(
|
||
lang: str,
|
||
multi: dict[str, str],
|
||
trigramme: dict[str, str],
|
||
bigramme: dict[str, str],
|
||
single: dict[str, str]) -> None:
|
||
"""
|
||
Prüft ob Übersetzungsketten entstehen würden: wenn die Ausgabe eines Blocks
|
||
durch einen nachgelagerten Block weiter übersetzt würde.
|
||
|
||
Verarbeitungsreihenfolge: multi → trigramme → bigramme → single
|
||
- multi-Ausgaben werden gegen trigramme/bigramme/single geprüft
|
||
- trigramme-Ausgaben werden gegen bigramme/single geprüft
|
||
- bigramme-Ausgaben werden gegen single geprüft
|
||
|
||
Ändert nichts, gibt nur Warnungen aus.
|
||
|
||
Args:
|
||
lang: Sprach-Code (für die Warnmeldung)
|
||
multi: Multi-Phrasen-Dict
|
||
trigramme: Trigramme-Dict
|
||
bigramme: Bigramme-Dict
|
||
single: Einzelwörter-Dict
|
||
"""
|
||
|
||
def find_substring_matches(value: str, d: dict[str, str]) -> list[str]:
|
||
"""Findet alle Keys aus d, die als Teilstring in value vorkommen."""
|
||
return [key for key in d if key in value]
|
||
|
||
def find_single_matches(value: str) -> list[str]:
|
||
"""
|
||
Findet alle Single-Keys, die in value vorkommen würden.
|
||
Spiegelt die Logik von translate_text wider:
|
||
- Direkt-Match wenn ein Leerzeichen-Token in single enthalten ist
|
||
- Sub-Match wenn ein [\\w-]+ Teilausdruck eines Tokens in single enthalten ist
|
||
"""
|
||
found = []
|
||
seen = set()
|
||
words = value.split()
|
||
for word in words:
|
||
if word in single:
|
||
if word not in seen:
|
||
found.append(word)
|
||
seen.add(word)
|
||
else:
|
||
for m in re.finditer(r'[\w-]+', word):
|
||
sub_word = m.group(0)
|
||
if sub_word in single and sub_word not in seen:
|
||
found.append(sub_word)
|
||
seen.add(sub_word)
|
||
return found
|
||
|
||
chain_warnings = []
|
||
|
||
# multi → trigramme, bigramme, single
|
||
for orig, transl in multi.items():
|
||
for key in find_substring_matches(transl, trigramme):
|
||
chain_warnings.append(
|
||
f"[multi] '{orig}' → '{transl}' → [trigramme] würde Treffer auf '{key}' erzeugen"
|
||
)
|
||
for key in find_substring_matches(transl, bigramme):
|
||
chain_warnings.append(
|
||
f"[multi] '{orig}' → '{transl}' → [bigramme] würde Treffer auf '{key}' erzeugen"
|
||
)
|
||
for key in find_single_matches(transl):
|
||
chain_warnings.append(
|
||
f"[multi] '{orig}' → '{transl}' → [single] würde Treffer auf '{key}' erzeugen"
|
||
)
|
||
|
||
# trigramme → bigramme, single
|
||
for orig, transl in trigramme.items():
|
||
for key in find_substring_matches(transl, bigramme):
|
||
chain_warnings.append(
|
||
f"[trigramme] '{orig}' → '{transl}' → [bigramme] würde Treffer auf '{key}' erzeugen"
|
||
)
|
||
for key in find_single_matches(transl):
|
||
chain_warnings.append(
|
||
f"[trigramme] '{orig}' → '{transl}' → [single] würde Treffer auf '{key}' erzeugen"
|
||
)
|
||
|
||
# bigramme → single
|
||
for orig, transl in bigramme.items():
|
||
for key in find_single_matches(transl):
|
||
chain_warnings.append(
|
||
f"[bigramme] '{orig}' → '{transl}' → [single] würde Treffer auf '{key}' erzeugen"
|
||
)
|
||
|
||
if chain_warnings:
|
||
print(f"\nWARNUNG [{lang}]: {len(chain_warnings)} mögliche Übersetzungskette(n) gefunden:")
|
||
for w in chain_warnings:
|
||
print(f" WARNUNG: {w}")
|
||
else:
|
||
print(f" Keine Übersetzungsketten gefunden.")
|
||
|
||
|
||
def _check_leading_spaces(config_file: Path, translation_lang: str) -> None:
|
||
"""
|
||
Liest die Config-Datei zeilenweise und prüft ob Zeilen mit einem Leerzeichen
|
||
oder Tabulator beginnen. Solche Zeilen werden vom RawConfigParser als Fortsetzung
|
||
der vorherigen Zeile interpretiert (zusammengezogen), was zu fehlerhaften
|
||
Übersetzungseinträgen führt.
|
||
|
||
Gibt für jede betroffene Zeile eine Warnung mit Zeilennummer und Inhalt aus.
|
||
Leere Zeilen und Kommentarzeilen (# oder ;) werden ignoriert.
|
||
|
||
Args:
|
||
config_file: Pfad zur Config-Datei
|
||
translation_lang: Sprach-Code (für die Warnmeldung)
|
||
"""
|
||
relevant_sections = {'multi', 'trigramme', 'bigramme', 'single'}
|
||
current_section = None
|
||
warnings_found = []
|
||
|
||
try:
|
||
with open(config_file, 'r', encoding='utf-8') as f:
|
||
for line_nr, line in enumerate(f, start=1):
|
||
# Zeilenende entfernen, führende Leerzeichen NICHT entfernen
|
||
raw_line = line.rstrip('\n\r')
|
||
stripped = raw_line.strip()
|
||
|
||
# Abschnitts-Header erkennen
|
||
if stripped.startswith('[') and stripped.endswith(']'):
|
||
section_name = stripped[1:-1].lower()
|
||
current_section = section_name if section_name in relevant_sections else None
|
||
continue
|
||
|
||
# Nur Zeilen innerhalb der relevanten Blöcke prüfen
|
||
if current_section is None:
|
||
continue
|
||
|
||
# Leere Zeilen und Kommentarzeilen überspringen
|
||
if not stripped or stripped.startswith('#') or stripped.startswith(';'):
|
||
continue
|
||
|
||
# Prüfe ob Zeile mit einem Whitespace-Zeichen beginnt (Leerzeichen, Tabulator,
|
||
# non-breaking space U+00A0 usw.) - ConfigParser behandelt alle isspace()-Zeichen
|
||
# als Fortsetzungszeichen und zieht die Zeile mit der vorherigen zusammen
|
||
if raw_line and raw_line[0].isspace():
|
||
warnings_found.append((line_nr, current_section, raw_line))
|
||
|
||
except Exception as e:
|
||
print(f"WARNUNG: Fehler beim Prüfen auf führende Leerzeichen: {e}")
|
||
return
|
||
|
||
if warnings_found:
|
||
print(f"\nWARNUNG [{translation_lang}]: {len(warnings_found)} Zeile(n) mit führendem Whitespace gefunden:")
|
||
print(f" (Leerzeichen, Tabulator, non-breaking space u.a. werden vom ConfigParser")
|
||
print(f" als Fortsetzungszeichen interpretiert und mit der vorherigen Zeile zusammengezogen!)")
|
||
for line_nr, section, line_content in warnings_found:
|
||
print(f" Zeile {line_nr:4d} [{section}]: '{line_content}'")
|
||
else:
|
||
print(f" Keine führenden Leerzeichen gefunden.")
|
||
|
||
|
||
def load_translation_config(translation_lang: str, translation_dir: Path) -> tuple[dict[str, str], dict[str, str], dict[str, str], dict[str, str]]:
|
||
"""
|
||
Lädt eine Übersetzungs-Config-Datei (z.B. CS.cfg, EN.cfg, FR.cfg).
|
||
|
||
Args:
|
||
translation_lang: Sprach-Code (z.B. "CS", "EN", "FR")
|
||
translation_dir: Verzeichnis mit Übersetzungs-Configs (aus PROJECT_TRANSLATION)
|
||
|
||
Returns:
|
||
Tuple mit (Multi-Dict, Trigramme-Dict, Bigramme-Dict, Single-Dict)
|
||
"""
|
||
config_file = translation_dir / f"{translation_lang}.cfg"
|
||
|
||
if not config_file.exists():
|
||
print(f"FEHLER: Übersetzungs-Konfiguration nicht gefunden: {config_file}")
|
||
sys.exit(1)
|
||
|
||
# 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 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:
|
||
config.read(config_file, encoding='utf-8')
|
||
except Exception as e:
|
||
print(f"Fehler beim Lesen der Übersetzungs-Konfiguration: {e}")
|
||
sys.exit(1)
|
||
|
||
multi = {}
|
||
trigramme = {}
|
||
bigramme = {}
|
||
single = {}
|
||
|
||
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('#'):
|
||
result[key] = translation
|
||
return result
|
||
|
||
# Lade Multi (4+ Wörter)
|
||
multi = _load_section('multi')
|
||
|
||
# Lade Trigramme
|
||
trigramme = _load_section('trigramme')
|
||
|
||
# Lade Bigramme
|
||
bigramme = _load_section('bigramme')
|
||
|
||
# Lade Single
|
||
single = _load_section('single')
|
||
|
||
# Prüfe Selbstübersetzungen in jedem Block und entferne sie mit Warnung
|
||
multi = _filter_self_translations('multi', multi)
|
||
trigramme = _filter_self_translations('trigramme', trigramme)
|
||
bigramme = _filter_self_translations('bigramme', bigramme)
|
||
single = _filter_self_translations('single', single)
|
||
|
||
print(f"\nÜbersetzungs-Config geladen ({translation_lang}):")
|
||
print(f" Multi-Phrasen: {len(multi)}")
|
||
print(f" Trigramme: {len(trigramme)}")
|
||
print(f" Bigramme: {len(bigramme)}")
|
||
print(f" Einzelwörter: {len(single)}")
|
||
|
||
# Prüfe Übersetzungsketten zwischen den Blöcken
|
||
_check_translation_chains(translation_lang, multi, trigramme, bigramme, single)
|
||
|
||
return multi, trigramme, bigramme, single
|
||
|
||
|
||
def load_transfer_terms(translation_lang: str, translation_dir: Path) -> set[str]:
|
||
"""
|
||
Lädt die Transfer-Begriffe aus einer Übersetzungs-Config-Datei (Abschnitt [transfer]).
|
||
|
||
Transfer-Begriffe werden beim Übersetzen von Symbol-Attributen (Block-Inserts) ohne
|
||
Übersetzung direkt in das Ziel-Attribut übernommen. Für TEXT/MTEXT-Objekte gilt
|
||
diese Regel nicht.
|
||
|
||
Einträge im Abschnitt [transfer] stehen einzeln pro Zeile (ohne Wert/Gleichheitszeichen).
|
||
|
||
Args:
|
||
translation_lang: Sprach-Code (z.B. "CS", "EN", "FR")
|
||
translation_dir: Verzeichnis mit Übersetzungs-Configs (aus PROJECT_TRANSLATION)
|
||
|
||
Returns:
|
||
Set mit den Transfer-Begriffen (Groß-/Kleinschreibung wie in der Config)
|
||
"""
|
||
config_file = translation_dir / f"{translation_lang}.cfg"
|
||
|
||
if not config_file.exists():
|
||
return set()
|
||
|
||
config = RawConfigParser(allow_no_value=True)
|
||
config.optionxform = str
|
||
try:
|
||
config.read(config_file, encoding='utf-8')
|
||
except Exception as e:
|
||
print(f"Warnung: Fehler beim Lesen der Transfer-Begriffe aus {config_file}: {e}")
|
||
return set()
|
||
|
||
transfer: set[str] = set()
|
||
if config.has_section('transfer'):
|
||
for key in config['transfer']:
|
||
term = key.strip()
|
||
if term and not term.startswith('#'):
|
||
transfer.add(term)
|
||
|
||
if transfer:
|
||
print(f" Transfer-Begriffe ({translation_lang}): {len(transfer)} ({', '.join(sorted(transfer))})")
|
||
|
||
return transfer
|
||
|
||
|
||
def _load_translator_config(cfg_path: Path) -> RawConfigParser:
|
||
"""
|
||
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:
|
||
Gelesener RawConfigParser (case-sensitive Keys)
|
||
"""
|
||
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)
|
||
|
||
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
|
||
if config.has_section('symbol_attribute'):
|
||
for key in config['symbol_attribute']:
|
||
attr_name = config['symbol_attribute'][key].strip()
|
||
if attr_name and not attr_name.startswith('#'):
|
||
symbol_mapping[key] = attr_name
|
||
|
||
print(f"\nGeladene Symbol-Attribut-Zuordnungen: {len(symbol_mapping)}")
|
||
for lang, attr in symbol_mapping.items():
|
||
print(f" {lang} -> {attr}")
|
||
|
||
return symbol_mapping
|
||
|
||
|
||
def load_symbol_layer_mapping(cfg_path: Path) -> dict[str, str]:
|
||
"""
|
||
Liest die Zuordnung von Sprachcodes zu Symbol-Layern aus der translator.cfg Datei.
|
||
|
||
Verwendet den Abschnitt [symbol_layer], z.B.:
|
||
DE = 0-0_TEXT-D
|
||
EN = 0-0_TEXT-E
|
||
CS = 0-0_TEXT-CS
|
||
|
||
Args:
|
||
cfg_path: Pfad zum Konfigurationsverzeichnis (PROJECT_CFG)
|
||
|
||
Returns:
|
||
Dictionary mit Sprachcode -> Layername Mapping
|
||
"""
|
||
config = _load_translator_config(cfg_path)
|
||
|
||
layer_mapping: dict[str, str] = {}
|
||
|
||
if config.has_section('symbol_layer'):
|
||
for key in config['symbol_layer']:
|
||
layer_name = config['symbol_layer'][key].strip()
|
||
if layer_name and not layer_name.startswith('#'):
|
||
layer_mapping[key] = layer_name
|
||
|
||
print(f"\nGeladene Symbol-Layer-Zuordnungen: {len(layer_mapping)}")
|
||
for lang, layer in layer_mapping.items():
|
||
print(f" {lang} -> {layer}")
|
||
|
||
return layer_mapping
|
||
|
||
|
||
def load_ignore_patterns(cfg_path: Path) -> tuple[list[str], list[str]]:
|
||
"""
|
||
Liest die ignore_pattern aus der translator.cfg Datei.
|
||
Unterstützt sowohl Wildcard-Muster (*,?) als auch reguläre Ausdrücke.
|
||
|
||
Args:
|
||
cfg_path: Pfad zum Konfigurationsverzeichnis (PROJECT_CFG)
|
||
|
||
Returns:
|
||
Tuple mit (Wildcard-Mustern, Regex-Mustern)
|
||
"""
|
||
# RawConfigParser verhindert dass Backslashes in Regex-Mustern escaped werden
|
||
config = _load_translator_config(cfg_path)
|
||
|
||
wildcard_patterns = []
|
||
regex_patterns = []
|
||
|
||
# Lade Wildcard-Muster
|
||
if config.has_section('ignore_pattern_wildcard'):
|
||
for key in config['ignore_pattern_wildcard']:
|
||
pattern = config['ignore_pattern_wildcard'][key].strip()
|
||
if pattern and not pattern.startswith('#'):
|
||
wildcard_patterns.append(pattern)
|
||
|
||
# Lade Regex-Muster
|
||
if config.has_section('ignore_pattern_regex'):
|
||
for key in config['ignore_pattern_regex']:
|
||
pattern = config['ignore_pattern_regex'][key].strip()
|
||
if pattern and not pattern.startswith('#'):
|
||
regex_patterns.append(pattern)
|
||
|
||
print(f"Geladene Wildcard-Muster: {len(wildcard_patterns)}")
|
||
for pattern in wildcard_patterns:
|
||
print(f" - {pattern}")
|
||
|
||
print(f"Geladene Regex-Muster: {len(regex_patterns)}")
|
||
for pattern in regex_patterns:
|
||
print(f" - {pattern}")
|
||
|
||
return wildcard_patterns, regex_patterns
|
||
|
||
|
||
def translate_text(text: str, multi: dict[str, str], trigramme: dict[str, str], bigramme: dict[str, str], single: dict[str, str], transfer_terms: set[str] | None = None) -> str:
|
||
"""
|
||
Übersetzt einen Text durch Ersetzen von Multi-Phrasen, Trigrammen, Bigrammen und Einzelwörtern.
|
||
Ersetzungen erfolgen in dieser Reihenfolge: Transfer → Multi → Trigramme → Bigramme → Einzelwörter.
|
||
|
||
Bei Einzelwörtern werden auch Satzzeichen und eingebettete Wörter berücksichtigt:
|
||
- "Weight:" wird zu "Hmotnost:" (Doppelpunkt bleibt erhalten)
|
||
- "(Sub-distributor)" wird zu "(Rozvaděč)" (Klammern bleiben erhalten)
|
||
- "\\fArial Black|b0|i0;Firewall 1" wird zu "\\fArial Black|b0|i0;Požární 1"
|
||
(alle Wörter in einem Token werden durchsucht, Trennzeichen bleiben erhalten)
|
||
Bindestriche im Wort (wie "Sub-distributor") bleiben Teil des Kern-Worts.
|
||
|
||
Transfer-Begriffe werden 1:1 übernommen (ohne Übersetzung) wenn der Text exakt übereinstimmt.
|
||
|
||
Args:
|
||
text: Zu übersetzender Text
|
||
multi: Dictionary mit Multi-Wort-Phrasen (4+ Wörter)
|
||
trigramme: Dictionary mit Drei-Wort-Phrasen
|
||
bigramme: Dictionary mit Zwei-Wort-Phrasen
|
||
single: Dictionary mit Einzelwörtern
|
||
transfer_terms: Set mit Begriffen die 1:1 übernommen werden (aus [transfer] Sektion)
|
||
|
||
Returns:
|
||
Übersetzter Text (leer wenn keine Übersetzung gefunden wurde)
|
||
"""
|
||
# 0. Prüfe Transfer-Begriffe (exakte Übereinstimmung, 1:1 Übernahme)
|
||
if transfer_terms is not None and text in transfer_terms:
|
||
return text
|
||
|
||
translated_text = text
|
||
has_translation = False
|
||
|
||
# 1. Multi-Phrasen ersetzen (längste Phrasen zuerst)
|
||
for original, translation in multi.items():
|
||
if original in translated_text:
|
||
translated_text = translated_text.replace(original, translation)
|
||
has_translation = True
|
||
|
||
# 2. Trigramme ersetzen
|
||
for original, translation in trigramme.items():
|
||
if original in translated_text:
|
||
translated_text = translated_text.replace(original, translation)
|
||
has_translation = True
|
||
|
||
# 3. Bigramme ersetzen
|
||
for original, translation in bigramme.items():
|
||
if original in translated_text:
|
||
translated_text = translated_text.replace(original, translation)
|
||
has_translation = True
|
||
|
||
# 4. Einzelwörter ersetzen
|
||
words = translated_text.split()
|
||
translated_words = []
|
||
for word in words:
|
||
if word in single:
|
||
# Direkter Match
|
||
translated_words.append(single[word])
|
||
has_translation = True
|
||
else:
|
||
# Suche ALLE Wörter im Token und ersetze sie (z.B. "Black|b0|i0;Firewall" enthält "Black" und "Firewall")
|
||
# Pattern matcht Wörter (Buchstaben, Zahlen, Unterstriche, Bindestriche)
|
||
word_found = False
|
||
|
||
def replace_word(match):
|
||
nonlocal word_found, has_translation
|
||
matched_word = match.group(0)
|
||
if matched_word in single:
|
||
word_found = True
|
||
has_translation = True
|
||
return single[matched_word]
|
||
return matched_word
|
||
|
||
# Ersetze alle gefundenen Wörter im Token
|
||
translated_word = re.sub(r'[\w-]+', replace_word, word)
|
||
translated_words.append(translated_word)
|
||
translated_text = ' '.join(translated_words)
|
||
|
||
# Gib nur übersetzten Text zurück wenn tatsächlich etwas übersetzt wurde
|
||
return translated_text if has_translation else ""
|
||
|
||
|
||
def should_ignore_text(text: str, wildcard_patterns: list[str], regex_patterns: list[str], debug: bool = False) -> bool:
|
||
"""
|
||
Prüft ob ein Text ignoriert werden soll basierend auf den Mustern.
|
||
|
||
Args:
|
||
text: Zu prüfender Text
|
||
wildcard_patterns: Liste der Wildcard-Muster (*,?)
|
||
regex_patterns: Liste der regulären Ausdrücke
|
||
debug: Wenn True, gibt Debug-Informationen aus
|
||
|
||
Returns:
|
||
True wenn Text ignoriert werden soll, False sonst
|
||
"""
|
||
# Prüfe Wildcard-Muster
|
||
for pattern in wildcard_patterns:
|
||
if fnmatch(text, pattern):
|
||
if debug:
|
||
print(f" DEBUG: '{text}' matched wildcard '{pattern}'")
|
||
return True
|
||
|
||
# Prüfe Regex-Muster
|
||
for pattern in regex_patterns:
|
||
try:
|
||
if re.search(pattern, text):
|
||
if debug:
|
||
print(f" DEBUG: '{text}' matched regex '{pattern}'")
|
||
return True
|
||
except re.error:
|
||
# Falls ungültiger Regex, ignoriere dieses Muster
|
||
print(f"WARNUNG: Ungültiger regulärer Ausdruck: {pattern}")
|
||
continue
|
||
|
||
return False
|
||
|
||
|
||
def extract_text_from_dxf(filename: Path, text_attr_name: str = 'TEXT-D') -> list[str]:
|
||
"""
|
||
Extrahiert alle TEXT und MTEXT Objekte sowie TEXT-{attr} Attribute aus Blöcken einer DXF Datei.
|
||
Verwendet ein Set, um Duplikate zu vermeiden.
|
||
Filtert leere Texte heraus.
|
||
|
||
Args:
|
||
filename: Pfad zur DXF Datei
|
||
text_attr_name: Name des Text-Attributs in Blöcken (z.B. "TEXT-D", "TEXT-EN", "TEXT-CS"). Standard: "TEXT-D"
|
||
|
||
Returns:
|
||
Alphabetisch sortierte Liste aller gefundenen Texte (ohne Duplikate, ohne leere Texte)
|
||
"""
|
||
texts_set = set() # Set für eindeutige Texte
|
||
text_count = 0
|
||
mtext_count = 0
|
||
block_count = 0
|
||
empty_count = 0
|
||
|
||
# Attribute die ein Block haben muss, damit das Text-Attribut extrahiert wird
|
||
required_attributes = {'IO', 'ID', 'VERW', 'BEZEICHNUNG', 'KENNZEICHNUNG', text_attr_name}
|
||
|
||
try:
|
||
doc = ezdxf.readfile(filename)
|
||
msp = doc.modelspace()
|
||
|
||
# Sammle alle TEXT Objekte
|
||
for entity in msp.query('TEXT'):
|
||
if entity.dxf.text:
|
||
text_count += 1
|
||
text = entity.dxf.text.strip() # Entferne führende/nachfolgende Leerzeichen
|
||
|
||
# Ignoriere leere Texte
|
||
if not text:
|
||
empty_count += 1
|
||
continue
|
||
|
||
texts_set.add(text)
|
||
|
||
# Sammle alle MTEXT Objekte
|
||
for entity in msp.query('MTEXT'):
|
||
if entity.text:
|
||
mtext_count += 1
|
||
text = entity.text.strip() # Entferne führende/nachfolgende Leerzeichen
|
||
|
||
# Ignoriere leere Texte
|
||
if not text:
|
||
empty_count += 1
|
||
continue
|
||
|
||
texts_set.add(text)
|
||
|
||
# Sammle TEXT-D aus Blöcken mit den erforderlichen Attributen
|
||
for entity in msp.query('INSERT'):
|
||
if entity.has_attrib:
|
||
# Sammle alle vorhandenen Attribute des Blocks
|
||
block_attributes = set()
|
||
text_d_value = None
|
||
|
||
for attrib in entity.attribs:
|
||
block_attributes.add(attrib.dxf.tag)
|
||
if attrib.dxf.tag == text_attr_name:
|
||
text_d_value = attrib.dxf.text.strip() if attrib.dxf.text else None
|
||
|
||
# Prüfe ob Block alle erforderlichen Attribute hat
|
||
if required_attributes.issubset(block_attributes) and text_d_value:
|
||
block_count += 1
|
||
texts_set.add(text_d_value)
|
||
|
||
# Konvertiere Set zu sortierter Liste
|
||
texts = sorted(texts_set)
|
||
|
||
print(f"Anzahl gefundener TEXT Objekte: {text_count}")
|
||
print(f"Anzahl gefundener MTEXT Objekte: {mtext_count}")
|
||
print(f"Anzahl gefundener {text_attr_name} in Blöcken: {block_count}")
|
||
print(f"Leere Texte ignoriert: {empty_count}")
|
||
total_count = text_count + mtext_count + block_count
|
||
print(f"Duplikate entfernt: {total_count - empty_count - len(texts)}")
|
||
print(f"Eindeutige Texte extrahiert: {len(texts)}")
|
||
|
||
except Exception as e:
|
||
print(f"Fehler beim Lesen der DXF Datei: {e}")
|
||
sys.exit(1)
|
||
|
||
return texts
|
||
|
||
|
||
def filter_texts(texts: list[str], wildcard_patterns: list[str], regex_patterns: list[str], debug: bool = False) -> tuple[list[str], list[str]]:
|
||
"""
|
||
Filtert eine Liste von Texten basierend auf Wildcard- und Regex-Mustern.
|
||
Diese Funktion ist unabhängig von der Quelle der Texte (DXF oder TXT).
|
||
|
||
Args:
|
||
texts: Liste der zu filternden Texte
|
||
wildcard_patterns: Liste der Wildcard-Muster zum Filtern
|
||
regex_patterns: Liste der Regex-Muster zum Filtern
|
||
debug: Wenn True, gibt Debug-Informationen aus
|
||
|
||
Returns:
|
||
Tuple mit (akzeptierte Texte, ignorierte Texte) - beide alphabetisch sortiert
|
||
"""
|
||
accepted_texts = []
|
||
ignored_texts = []
|
||
|
||
for text in texts:
|
||
if should_ignore_text(text, wildcard_patterns, regex_patterns, debug):
|
||
ignored_texts.append(text)
|
||
else:
|
||
accepted_texts.append(text)
|
||
|
||
# Sortiere beide Listen alphabetisch
|
||
accepted_texts.sort()
|
||
ignored_texts.sort()
|
||
|
||
print(f"\nFilterung abgeschlossen:")
|
||
print(f" Akzeptierte Texte: {len(accepted_texts)}")
|
||
print(f" Ignorierte Texte: {len(ignored_texts)}")
|
||
|
||
return accepted_texts, ignored_texts
|
||
|
||
|
||
def read_texts_from_file(filename: Path) -> list[str]:
|
||
"""
|
||
Liest Texte aus einer UTF-8 Textdatei (ein Text pro Zeile).
|
||
Entfernt Duplikate und leere Zeilen.
|
||
Ignoriert Abschnitts-Marker (Zeilen die mit "---" beginnen).
|
||
|
||
Args:
|
||
filename: Pfad zur Textdatei
|
||
|
||
Returns:
|
||
Alphabetisch sortierte Liste aller Texte (ohne Duplikate, ohne leere Zeilen)
|
||
"""
|
||
texts_set = set()
|
||
|
||
try:
|
||
with open(filename, 'r', encoding='utf-8') as f:
|
||
for line in f:
|
||
text = line.strip()
|
||
if text: # Ignoriere leere Zeilen
|
||
# Ignoriere Abschnitts-Marker (z.B. "--- translations:", "--- ignored:")
|
||
if text.startswith('---'):
|
||
continue
|
||
# Konvertiere escaped Zeilenumbrüche zurück
|
||
text = text.replace('\\n', '\n').replace('\\r', '\r')
|
||
texts_set.add(text)
|
||
|
||
texts = sorted(texts_set)
|
||
print(f"Texte aus Datei gelesen: {len(texts)}")
|
||
return texts
|
||
|
||
except Exception as e:
|
||
print(f"Fehler beim Lesen der Textdatei: {e}")
|
||
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 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 den untranslated Blöcken
|
||
"""
|
||
try:
|
||
with open(filename, 'r', encoding='utf-8') as f:
|
||
data = json.load(f)
|
||
|
||
if not isinstance(data, dict):
|
||
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)
|
||
|
||
untranslated = data['untranslated']
|
||
if not isinstance(untranslated, list):
|
||
print(f"FEHLER: 'untranslated' Block hat ungültige Struktur (erwartet Array)")
|
||
sys.exit(1)
|
||
|
||
for idx, entry in enumerate(untranslated):
|
||
if not isinstance(entry, dict):
|
||
print(f"WARNUNG: Eintrag {idx} in 'untranslated' ist kein Object, überspringe")
|
||
continue
|
||
|
||
if 'source' not in entry:
|
||
print(f"WARNUNG: Eintrag {idx} in 'untranslated' hat kein 'source' Feld, überspringe")
|
||
continue
|
||
|
||
source_text = entry['source']
|
||
if source_text: # Ignoriere leere Texte
|
||
texts_set.add(source_text)
|
||
|
||
print(f"Texte aus JSON-Datei gelesen (alte Struktur): {len(texts_set)}")
|
||
|
||
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:
|
||
print(f"Fehler beim Parsen der JSON-Datei: {e}")
|
||
sys.exit(1)
|
||
except Exception as e:
|
||
print(f"Fehler beim Lesen der JSON-Datei: {e}")
|
||
sys.exit(1)
|
||
|
||
|
||
def load_translations_from_json(filename: Path, target_lang: str = None) -> dict[str, str]:
|
||
"""
|
||
Liest Übersetzungen aus einer JSON-Datei.
|
||
Unterstützt beide Strukturen:
|
||
- Alte Struktur: "translations": [{"source": "...", "target": "..."}, ...]
|
||
- Hierarchische Struktur: "[LANG]": {"translations": [...], ...}
|
||
|
||
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
|
||
"""
|
||
try:
|
||
with open(filename, 'r', encoding='utf-8') as f:
|
||
data = json.load(f)
|
||
|
||
if not isinstance(data, dict):
|
||
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)
|
||
|
||
translations_list = data['translations']
|
||
if not isinstance(translations_list, list):
|
||
print(f"FEHLER: 'translations' Block hat ungültige Struktur (erwartet Array)")
|
||
sys.exit(1)
|
||
|
||
for idx, entry in enumerate(translations_list):
|
||
if not isinstance(entry, dict):
|
||
print(f"WARNUNG: Eintrag {idx} in 'translations' ist kein Object, überspringe")
|
||
continue
|
||
|
||
if 'source' not in entry or 'target' not in entry:
|
||
print(f"WARNUNG: Eintrag {idx} in 'translations' fehlt 'source' oder 'target', überspringe")
|
||
continue
|
||
|
||
source = entry['source']
|
||
target = entry['target']
|
||
if source and target: # Ignoriere leere Einträge
|
||
translations[source] = target
|
||
|
||
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 translations Block (oder translated für Rückwärtskompatibilität)
|
||
translations_key = 'translations' if 'translations' in lang_data else 'translated'
|
||
if translations_key in lang_data:
|
||
translations_list = lang_data[translations_key]
|
||
if not isinstance(translations_list, list):
|
||
print(f"WARNUNG: [{lang}][{translations_key}] ist kein Array, überspringe")
|
||
continue
|
||
|
||
for idx, entry in enumerate(translations_list):
|
||
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:
|
||
print(f"Fehler beim Parsen der JSON-Datei: {e}")
|
||
sys.exit(1)
|
||
except Exception as e:
|
||
print(f"Fehler beim Lesen der JSON-Datei: {e}")
|
||
sys.exit(1)
|
||
|
||
|
||
def ensure_block_attdef_and_layer(doc, block_name: str,
|
||
text_source_attr: str,
|
||
text_target_attr: str,
|
||
target_layer: str | None = None) -> None:
|
||
"""
|
||
Stellt sicher, dass im Block-Definitionseintrag ein ATTDEF für das Ziel-Attribut
|
||
existiert und der passende Layer vorhanden ist.
|
||
|
||
- Wenn der Layer aus translator.cfg ([symbol_layer]) noch nicht existiert, wird er angelegt.
|
||
- Wenn das ATTDEF für text_target_attr noch nicht existiert, wird es auf Basis des
|
||
ATTDEF von text_source_attr erzeugt (Position, Höhe, Stil, Rotation werden übernommen).
|
||
"""
|
||
if doc is None or not block_name:
|
||
return
|
||
|
||
try:
|
||
block = doc.blocks[block_name]
|
||
except KeyError:
|
||
# Kein Block mit diesem Namen vorhanden (z.B. anonyme Blöcke) -> nichts zu tun
|
||
return
|
||
|
||
# Layer sicherstellen
|
||
if target_layer:
|
||
if target_layer not in doc.layers:
|
||
doc.layers.new(name=target_layer)
|
||
|
||
source_def = None
|
||
target_def = None
|
||
|
||
for attdef in block.attdefs():
|
||
tag = attdef.dxf.tag
|
||
if tag == text_target_attr:
|
||
target_def = attdef
|
||
elif tag == text_source_attr:
|
||
source_def = attdef
|
||
|
||
# Ziel-ATTDEF existiert bereits -> ggf. nur Layer korrigieren
|
||
if target_def is not None:
|
||
if target_layer:
|
||
target_def.dxf.layer = target_layer
|
||
return
|
||
|
||
# Neues ATTDEF anlegen, möglichst anhand der Quell-Definition
|
||
insert = (0.0, 0.0)
|
||
height = 2.5
|
||
layer_name = target_layer or "0"
|
||
|
||
if source_def is not None:
|
||
insert = source_def.dxf.insert
|
||
if source_def.dxf.hasattr('height'):
|
||
height = source_def.dxf.height
|
||
if source_def.dxf.hasattr('layer') and not target_layer:
|
||
layer_name = source_def.dxf.layer
|
||
|
||
# `add_attdef` unterstützt keinen `layer`-Parameter direkt, daher Layer danach setzen
|
||
new_def = block.add_attdef(
|
||
tag=text_target_attr,
|
||
insert=insert,
|
||
height=height,
|
||
)
|
||
new_def.dxf.layer = layer_name
|
||
|
||
# Weitere Eigenschaften (Rotation, Stil) vom Quell-ATTDEF übernehmen
|
||
if source_def is not None:
|
||
if source_def.dxf.hasattr('rotation'):
|
||
new_def.dxf.rotation = source_def.dxf.rotation
|
||
if source_def.dxf.hasattr('style'):
|
||
new_def.dxf.style = source_def.dxf.style
|
||
|
||
|
||
def translate_block_attribute(entity,
|
||
doc,
|
||
translations: dict[str, str],
|
||
wildcard_patterns: list[str],
|
||
regex_patterns: list[str],
|
||
required_attributes: set[str],
|
||
text_source_attr: str,
|
||
text_target_attr: str,
|
||
target_layer: str | None = None,
|
||
transfer_terms: set[str] | None = None) -> tuple[int, int, int, int]:
|
||
"""
|
||
Übersetzt Text-Attribute eines einzelnen Block-Inserts (INSERT entity).
|
||
|
||
Args:
|
||
entity: INSERT entity aus der DXF-Datei
|
||
doc: ezdxf-Dokument, um Blockdefinitionen und Layer zu ändern
|
||
translations: Dictionary mit source -> target Mappings
|
||
wildcard_patterns: Wildcard-Muster zum Filtern
|
||
regex_patterns: Regex-Muster zum Filtern
|
||
required_attributes: Set mit erforderlichen Attributnamen für den Block
|
||
text_source_attr: Quell-Attributname (z.B. "TEXT-D", "TEXT-EN")
|
||
text_target_attr: Ziel-Attributname (z.B. "TEXT-EN", "TEXT-CS")
|
||
target_layer: Ziel-Layername für das Ziel-Attribut (aus [symbol_layer])
|
||
transfer_terms: Begriffe die ohne Übersetzung direkt übernommen werden (aus [transfer])
|
||
|
||
Returns:
|
||
Tuple mit (translated_count, skipped_count, ignored_count, transferred_count) für diesen Block
|
||
"""
|
||
if not entity.has_attrib:
|
||
return 0, 0, 0, 0
|
||
|
||
# Sammle alle vorhandenen Attribute des Blocks
|
||
block_attributes = {}
|
||
text_source_attrib = None
|
||
text_target_attrib = None
|
||
|
||
for attrib in entity.attribs:
|
||
block_attributes[attrib.dxf.tag] = attrib
|
||
if attrib.dxf.tag == text_source_attr:
|
||
text_source_attrib = attrib
|
||
elif attrib.dxf.tag == text_target_attr:
|
||
text_target_attrib = attrib
|
||
|
||
# Prüfe ob Block alle erforderlichen Attribute hat
|
||
if not (required_attributes.issubset(block_attributes.keys()) and text_source_attrib):
|
||
return 0, 0, 0, 0
|
||
|
||
original_text = text_source_attrib.dxf.text.strip() if text_source_attrib.dxf.text else None
|
||
|
||
if not original_text:
|
||
return 0, 0, 0, 0
|
||
|
||
# Prüfe ob Text ignoriert werden soll
|
||
if should_ignore_text(original_text, wildcard_patterns, regex_patterns):
|
||
return 0, 0, 1, 0
|
||
|
||
# Bestimme Zieltext: Transfer (1:1 übernehmen) oder Übersetzung
|
||
is_transfer = transfer_terms is not None and original_text in transfer_terms
|
||
if is_transfer:
|
||
target_text = original_text
|
||
elif original_text in translations:
|
||
target_text = translations[original_text]
|
||
else:
|
||
return 0, 1, 0, 0
|
||
|
||
# Schreibe Zieltext in Ziel-Attribut
|
||
if text_target_attrib:
|
||
# Ziel-Attribut existiert bereits, aktualisiere es
|
||
text_target_attrib.dxf.text = target_text
|
||
# Layer ggf. anpassen
|
||
if target_layer:
|
||
text_target_attrib.dxf.layer = target_layer
|
||
else:
|
||
# Vor dem Anlegen sicherstellen, dass im Block ein passender ATTDEF existiert
|
||
ensure_block_attdef_and_layer(doc, entity.dxf.name, text_source_attr, text_target_attr, target_layer)
|
||
|
||
# Ziel-Attribut existiert nicht, erstelle es basierend auf Quell-Attribut
|
||
new_attrib = entity.add_attrib(
|
||
tag=text_target_attr,
|
||
text=target_text,
|
||
insert=text_source_attrib.dxf.insert,
|
||
)
|
||
# Kopiere weitere Eigenschaften
|
||
if text_source_attrib.dxf.hasattr('height'):
|
||
new_attrib.dxf.height = text_source_attrib.dxf.height
|
||
if text_source_attrib.dxf.hasattr('style'):
|
||
new_attrib.dxf.style = text_source_attrib.dxf.style
|
||
if text_source_attrib.dxf.hasattr('rotation'):
|
||
new_attrib.dxf.rotation = text_source_attrib.dxf.rotation
|
||
if text_source_attrib.dxf.hasattr('width'):
|
||
new_attrib.dxf.width = text_source_attrib.dxf.width
|
||
# Layer explizit auf Ziel-Layer setzen, falls konfiguriert, sonst wie Quell-Attribut
|
||
if target_layer:
|
||
new_attrib.dxf.layer = target_layer
|
||
elif text_source_attrib.dxf.hasattr('layer'):
|
||
new_attrib.dxf.layer = text_source_attrib.dxf.layer
|
||
|
||
if is_transfer:
|
||
return 0, 0, 0, 1
|
||
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],
|
||
wildcard_patterns: list[str],
|
||
regex_patterns: list[str],
|
||
text_source_attr: str = 'TEXT-D',
|
||
text_target_attr: str = 'TEXT-CS',
|
||
target_layer: str | None = None,
|
||
transfer_terms: set[str] | None = None):
|
||
"""
|
||
Übersetzt alle TEXT, MTEXT Objekte und Text-Attribute in Blöcken einer DXF-Datei.
|
||
|
||
Für Block-Attribute:
|
||
- Liest die Übersetzung aus dem Quell-Attribut (z.B. TEXT-D)
|
||
- Schreibt die Übersetzung in das Ziel-Attribut (z.B. TEXT-EN, TEXT-CS) (erstellt das Attribut falls nicht vorhanden)
|
||
- Das Quell-Attribut bleibt unverändert (Original wird bewahrt)
|
||
- Begriffe aus transfer_terms werden ohne Übersetzung direkt übernommen (nur bei Block-Attributen)
|
||
|
||
Args:
|
||
input_dxf: Pfad zur Quell-DXF
|
||
output_dxf: Pfad zur Ziel-DXF
|
||
translations: Dictionary mit source -> target Mappings
|
||
wildcard_patterns: Wildcard-Muster zum Filtern
|
||
regex_patterns: Regex-Muster zum Filtern
|
||
text_source_attr: Quell-Attributname für Texte in Blöcken (z.B. "TEXT-D", "TEXT-EN"). Standard: "TEXT-D"
|
||
text_target_attr: Ziel-Attributname für Übersetzungen in Blöcken (z.B. "TEXT-EN", "TEXT-CS"). Standard: "TEXT-CS"
|
||
transfer_terms: Begriffe die in Symbol-Attributen ohne Übersetzung übernommen werden (aus [transfer])
|
||
"""
|
||
# Attribute die ein Block haben muss, damit das Text-Attribut übersetzt wird
|
||
required_attributes = {'IO', 'ID', 'VERW', 'BEZEICHNUNG', 'KENNZEICHNUNG', text_source_attr}
|
||
|
||
try:
|
||
doc = ezdxf.readfile(input_dxf)
|
||
msp = doc.modelspace()
|
||
|
||
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-Attribute in Blöcken
|
||
for entity in msp.query('INSERT'):
|
||
translated, skipped, ignored, transferred = translate_block_attribute(
|
||
entity,
|
||
doc,
|
||
translations,
|
||
wildcard_patterns,
|
||
regex_patterns,
|
||
required_attributes,
|
||
text_source_attr,
|
||
text_target_attr,
|
||
target_layer,
|
||
transfer_terms,
|
||
)
|
||
block_translated += translated
|
||
block_skipped += skipped
|
||
block_ignored += ignored
|
||
block_transferred += transferred
|
||
|
||
# Speichere neue DXF
|
||
doc.saveas(output_dxf)
|
||
|
||
translated_count = text_translated + mtext_translated + block_translated
|
||
skipped_count = text_skipped + mtext_skipped + block_skipped
|
||
ignored_count = text_ignored + mtext_ignored + block_ignored
|
||
|
||
print(f"\nDXF-Übersetzung abgeschlossen:")
|
||
print(f" TEXT übersetzt: {text_translated}, übersprungen: {text_skipped}, ignoriert: {text_ignored}")
|
||
print(f" MTEXT übersetzt: {mtext_translated}, übersprungen: {mtext_skipped}, ignoriert: {mtext_ignored}")
|
||
print(f" {text_target_attr} (Blöcke) erstellt/aktualisiert: {block_translated}, übersprungen: {block_skipped}, ignoriert: {block_ignored}, übernommen (transfer): {block_transferred}")
|
||
print(f" Gesamt: {translated_count} übersetzt, {block_transferred} übernommen, {skipped_count} übersprungen, {ignored_count} ignoriert")
|
||
print(f" Hinweis: Übersetzungen in Blöcken wurden in {text_target_attr} geschrieben, {text_source_attr} bleibt unverändert")
|
||
print(f" Ausgabe-Datei: {output_dxf}")
|
||
|
||
except Exception as e:
|
||
print(f"Fehler beim Übersetzen der DXF-Datei: {e}")
|
||
sys.exit(1)
|
||
|
||
|
||
def write_texts_to_excel(texts: list[str], output_path: Path,
|
||
translation_dicts: tuple[dict[str, str], dict[str, str], dict[str, str], dict[str, str]] = None):
|
||
"""
|
||
Schreibt die Texte in die erste Spalte einer Excel-Datei.
|
||
Optional: Schreibt Übersetzungen in die zweite Spalte.
|
||
|
||
Args:
|
||
texts: Liste der zu schreibenden Texte
|
||
output_path: Pfad zur Ausgabe-Excel-Datei
|
||
translation_dicts: Optional: Tuple mit (Multi, Trigramme, Bigramme, Single) für Übersetzungen
|
||
"""
|
||
wb = Workbook()
|
||
ws = wb.active
|
||
ws.title = "Texte"
|
||
|
||
# Kopfzeile
|
||
ws['A1'] = "Text"
|
||
if translation_dicts:
|
||
ws['B1'] = "Übersetzung"
|
||
|
||
# Texte schreiben
|
||
for idx, text in enumerate(texts, start=2):
|
||
ws[f'A{idx}'] = text
|
||
|
||
# Optional: Übersetzung in zweite Spalte
|
||
if translation_dicts:
|
||
multi, trigramme, bigramme, single = translation_dicts
|
||
translated = translate_text(text, multi, trigramme, bigramme, single)
|
||
if translated: # Nur wenn Übersetzung gefunden wurde
|
||
ws[f'B{idx}'] = translated
|
||
# Sonst bleibt die Zelle leer
|
||
|
||
# Spaltenbreiten anpassen
|
||
ws.column_dimensions['A'].width = 50
|
||
if translation_dicts:
|
||
ws.column_dimensions['B'].width = 50
|
||
|
||
try:
|
||
wb.save(output_path)
|
||
print(f"Excel-Datei erfolgreich erstellt: {output_path}")
|
||
if translation_dicts:
|
||
print(f" Mit Übersetzungen in Spalte B")
|
||
except Exception as e:
|
||
print(f"Fehler beim Schreiben der Excel-Datei: {e}")
|
||
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,
|
||
translation_languages: list[str] = None):
|
||
"""
|
||
Schreibt die Texte in eine JSON-Datei.
|
||
|
||
Immer wenn Übersetzungssprachen angegeben sind (auch nur eine), wird die
|
||
hierarchische Struktur verwendet:
|
||
- "[CS]": {"translations": [...], "untranslated": [...]}
|
||
- "[EN]": {"translations": [...], "untranslated": [...]}
|
||
- "ignored": Liste von Strings für gefilterte/ignorierte Texte
|
||
|
||
Existiert die Ausgabedatei bereits und enthält sie Sprachblöcke, werden
|
||
diese ERHALTEN und nur die neu angegebenen Sprachen aktualisiert. So können
|
||
CS- und EN-Übersetzungen schrittweise in dieselbe Datei geschrieben werden.
|
||
|
||
Nur ohne --translate (kein translation_languages) wird die flache Legacy-Struktur
|
||
erzeugt – sie dient als reiner Rohextrakt ohne Sprachzuordnung:
|
||
- "translations": [] (immer leer, da keine Sprache bekannt)
|
||
- "untranslated": [...]
|
||
- "ignored": [...]
|
||
Diese Dateien können anschliessend mit --retranslate-json + --translate LANG
|
||
in das Multilang-Format konvertiert und übersetzt werden.
|
||
|
||
Args:
|
||
texts: Liste der akzeptierten Texte
|
||
ignored_texts: Liste der ignorierten Texte
|
||
output_path: Pfad zur Ausgabe-JSON-Datei
|
||
translation_dicts: Optional: Tuple für eine Sprache (für Excel/Text-Ausgabe, hier ignoriert)
|
||
translation_dicts_all: Optional: Dict mit {LANG: (multi, trigramme, bigramme, single)}
|
||
translation_languages: Optional: Liste der Sprachen in der Reihenfolge der Verarbeitung
|
||
"""
|
||
output_data = {}
|
||
|
||
# Hierarchische Struktur: immer wenn mindestens eine Übersetzungssprache angegeben
|
||
if translation_dicts_all and translation_languages:
|
||
|
||
# Lade bestehende Sprachblöcke aus vorhandener Datei (Merge-Unterstützung)
|
||
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]
|
||
output_data[f"[{lang}]"] = _build_lang_block(texts, multi, trigramme, bigramme, single)
|
||
|
||
# Ignorierte Texte am Ende (ohne Sprachzuordnung)
|
||
output_data["ignored"] = ignored_texts
|
||
|
||
# Flache Struktur: ohne Übersetzungssprachen
|
||
else:
|
||
if translation_dicts:
|
||
multi, trigramme, bigramme, single = translation_dicts
|
||
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"] = []
|
||
output_data["untranslated"] = [{"source": text, "target": ""} for text in texts]
|
||
|
||
output_data["ignored"] = ignored_texts
|
||
|
||
try:
|
||
with open(output_path, 'w', encoding='utf-8') as f:
|
||
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:
|
||
# Alle Sprachblöcke in der Ausgabedatei anzeigen (inkl. übernommene)
|
||
for key in output_data:
|
||
if key.startswith('[') and key.endswith(']') and len(key) > 2:
|
||
lang_data = output_data[key]
|
||
lang = key[1:-1]
|
||
marker = "" if lang in translation_languages else " (übernommen)"
|
||
print(f" [{lang}]{marker} Übersetzungen: {len(lang_data['translations'])}, "
|
||
f"Nicht übersetzt: {len(lang_data['untranslated'])}")
|
||
else:
|
||
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}")
|
||
sys.exit(1)
|
||
|
||
|
||
def write_texts_to_text(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):
|
||
"""
|
||
Schreibt die Texte in eine Textdatei mit drei Abschnitten.
|
||
|
||
Format:
|
||
--- translations:
|
||
|
||
source1 -> target1
|
||
source2 -> target2
|
||
|
||
--- untranslated:
|
||
|
||
text1
|
||
text2
|
||
|
||
--- ignored:
|
||
|
||
ignored1
|
||
ignored2
|
||
|
||
Args:
|
||
texts: Liste der akzeptierten Texte
|
||
ignored_texts: Liste der ignorierten Texte
|
||
output_path: Pfad zur Ausgabe-Textdatei
|
||
translation_dicts: Optional: Tuple mit (Multi, Trigramme, Bigramme, Single) für Übersetzungen
|
||
"""
|
||
try:
|
||
with open(output_path, 'w', encoding='utf-8') as f:
|
||
# Block 1: Übersetzte Texte
|
||
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((text, translated))
|
||
else:
|
||
untranslated.append(text)
|
||
|
||
# Schreibe translations Block
|
||
f.write("--- translations:\n\n")
|
||
for source, target in translations:
|
||
f.write(f"{source} -> {target}\n")
|
||
|
||
# Schreibe untranslated Block
|
||
f.write("\n--- untranslated:\n\n")
|
||
for text in untranslated:
|
||
f.write(f"{text}\n")
|
||
|
||
else:
|
||
# Ohne Übersetzungs-Config: alle Texte sind nicht übersetzt
|
||
f.write("--- translations:\n\n")
|
||
f.write("\n--- untranslated:\n\n")
|
||
for text in texts:
|
||
f.write(f"{text}\n")
|
||
|
||
# Block 3: Ignorierte Texte
|
||
f.write("\n--- ignored:\n\n")
|
||
for text in ignored_texts:
|
||
f.write(f"{text}\n")
|
||
|
||
print(f"Text-Datei erfolgreich erstellt: {output_path}")
|
||
if translation_dicts:
|
||
translations_count = len(translations)
|
||
untranslated_count = len(texts) - translations_count
|
||
print(f" Übersetzungen: {translations_count}")
|
||
print(f" Nicht übersetzte Texte: {untranslated_count}")
|
||
else:
|
||
print(f" Nicht übersetzte Texte: {len(texts)}")
|
||
print(f" Ignorierte Texte: {len(ignored_texts)}")
|
||
|
||
except Exception as e:
|
||
print(f"Fehler beim Schreiben der Text-Datei: {e}")
|
||
sys.exit(1)
|
||
|
||
|
||
def _dedup_source_list(
|
||
entry_list: list,
|
||
seen_from_other: set[str],
|
||
label: str,
|
||
list_name: str,
|
||
other_name: str) -> tuple[list, set[str]]:
|
||
"""
|
||
Bereinigt eine Liste von {source, target}-Einträgen um Duplikate.
|
||
Das erste Vorkommen je source wird behalten.
|
||
|
||
Zwei Fälle werden gemeldet:
|
||
- source bereits in seen_from_other → Cross-Duplikat mit der anderen Liste
|
||
- source bereits in dieser Liste → internes Duplikat
|
||
|
||
Args:
|
||
entry_list: Zu prüfende Liste
|
||
seen_from_other: Sources aus der Gegenliste (für Cross-Duplikat-Check)
|
||
label: Block-Bezeichnung für Warnmeldungen, z.B. "[CS]"
|
||
list_name: Name dieser Liste, z.B. "'translations'"
|
||
other_name: Name der Gegenliste, z.B. "'untranslated'"
|
||
|
||
Returns:
|
||
Tuple (bereinigte Liste, set der in dieser Liste gesehenen sources)
|
||
"""
|
||
seen: set[str] = set()
|
||
result = []
|
||
for entry in entry_list:
|
||
if not isinstance(entry, dict):
|
||
continue
|
||
source = entry.get('source', '')
|
||
if source in seen_from_other:
|
||
print(f" WARNUNG: {label} '{source}' aus {list_name} entfernt "
|
||
f"(bereits in {other_name})")
|
||
elif source in seen:
|
||
print(f" WARNUNG: {label} Duplikat in {list_name} entfernt: '{source}'")
|
||
else:
|
||
seen.add(source)
|
||
result.append(entry)
|
||
return result, seen
|
||
|
||
|
||
def _retranslate_language_block(
|
||
translations_list: list,
|
||
untranslated_list: list,
|
||
multi: dict[str, str],
|
||
trigramme: dict[str, str],
|
||
bigramme: dict[str, str],
|
||
single: dict[str, str],
|
||
label: str,
|
||
transfer_terms: set[str] | None = None) -> tuple[int, int, int, int]:
|
||
"""
|
||
Verarbeitet einen einzelnen Sprach-Block einer JSON-Übersetzungsdatei in-place.
|
||
|
||
Schritt 0 – Deduplizierung beider Listen (via _dedup_source_list):
|
||
Doppelte source-Einträge werden entfernt (erstes Vorkommen wird behalten).
|
||
Einträge, deren source bereits in translations_list vorkommt, werden aus
|
||
untranslated_list entfernt.
|
||
|
||
Schritt 1 – Re-übersetze bestehende Einträge in translations_list:
|
||
Jeder Eintrag wird mit der aktuellen Config neu übersetzt.
|
||
Wird keine Übersetzung mehr gefunden, wandert der Eintrag nach untranslated_list.
|
||
|
||
Schritt 2 – Auto-übersetze Einträge aus untranslated_list:
|
||
Für jeden Eintrag mit leerem target wird auto-übersetzt;
|
||
erfolgreiche Treffer wandern nach translations_list.
|
||
Einträge mit bereits gesetztem target (manuell befüllt) werden
|
||
ebenfalls direkt nach translations_list übernommen.
|
||
|
||
Schritt 3 – Transfer-Begriffe werden 1:1 übernommen (ohne Übersetzung).
|
||
|
||
Args:
|
||
translations_list: Liste der bereits übersetzten Einträge (in-place verändert)
|
||
untranslated_list: Liste der noch nicht übersetzten Einträge (in-place verändert)
|
||
multi: Multi-Phrasen-Dict
|
||
trigramme: Trigramme-Dict
|
||
bigramme: Bigramme-Dict
|
||
single: Einzelwörter-Dict
|
||
label: Bezeichnung für Warnmeldungen, z.B. "[CS]"
|
||
transfer_terms: Set mit Begriffen die 1:1 übernommen werden (aus [transfer] Sektion)
|
||
|
||
Returns:
|
||
Tuple (aktualisiert, unveraendert, neu_aus_untranslated, transferred)
|
||
"""
|
||
updated = 0
|
||
unchanged = 0
|
||
newly = 0
|
||
transferred = 0
|
||
|
||
# Schritt 0: Deduplizierung – erstes Vorkommen je source wird behalten
|
||
deduped_t, seen_t = _dedup_source_list(
|
||
translations_list, set(), label, "'translations'", "'untranslated'")
|
||
translations_list[:] = deduped_t
|
||
|
||
deduped_u, _ = _dedup_source_list(
|
||
untranslated_list, seen_t, label, "'untranslated'", "'translations'")
|
||
untranslated_list[:] = deduped_u
|
||
|
||
# Schritt 1: Re-übersetze bestehende translations-Einträge
|
||
for entry in translations_list[:]: # Kopie für sicheres Entfernen
|
||
if not isinstance(entry, dict) or 'source' not in entry:
|
||
continue
|
||
source = entry['source']
|
||
old_target = entry.get('target', '')
|
||
new_target = translate_text(source, multi, trigramme, bigramme, single, transfer_terms)
|
||
if new_target:
|
||
if new_target != old_target:
|
||
entry['target'] = new_target
|
||
updated += 1
|
||
else:
|
||
unchanged += 1
|
||
else:
|
||
# Übersetzung nicht mehr verfügbar → nach untranslated verschieben
|
||
untranslated_list.append({"source": source, "target": ""})
|
||
translations_list.remove(entry)
|
||
print(f" WARNUNG: {label} Übersetzung für '{source}' nicht mehr verfügbar, "
|
||
f"verschoben zu 'untranslated'")
|
||
|
||
# Schritt 2: Auto-übersetze Einträge aus untranslated_list
|
||
for entry in untranslated_list[:]: # Kopie für sicheres Entfernen
|
||
if not isinstance(entry, dict) or 'source' not in entry:
|
||
continue
|
||
if entry.get('target', ''):
|
||
# Manuell befüllter Eintrag → direkt nach translations verschieben
|
||
translations_list.append(entry)
|
||
untranslated_list.remove(entry)
|
||
newly += 1
|
||
continue
|
||
source = entry['source']
|
||
new_target = translate_text(source, multi, trigramme, bigramme, single, transfer_terms)
|
||
if new_target:
|
||
translations_list.append({"source": source, "target": new_target})
|
||
untranslated_list.remove(entry)
|
||
# Prüfe ob es ein Transfer-Begriff war
|
||
if transfer_terms and source in transfer_terms:
|
||
transferred += 1
|
||
else:
|
||
newly += 1
|
||
|
||
return updated, unchanged, newly, transferred
|
||
|
||
|
||
def _retranslate_legacy(data: dict,
|
||
translation_languages: list[str],
|
||
translation_dicts_all: dict[str, tuple],
|
||
transfer_terms_all: dict[str, set[str]] | None = None) -> 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), ...}
|
||
transfer_terms_all: Dict mit {LANG: set[transfer_terms], ...} (aus [transfer] Sektion)
|
||
|
||
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]
|
||
transfer_terms = transfer_terms_all.get(lang, set()) if transfer_terms_all else set()
|
||
|
||
updated, unchanged, newly, transferred = _retranslate_language_block(
|
||
data['translations'], data['untranslated'],
|
||
multi, trigramme, bigramme, single,
|
||
f"[{lang}]",
|
||
transfer_terms
|
||
)
|
||
|
||
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}")
|
||
if transferred:
|
||
print(f" Transfer-Begriffe (1:1): {transferred}")
|
||
|
||
# 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],
|
||
transfer_terms_all: dict[str, set[str]] | None = None) -> 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), ...}
|
||
transfer_terms_all: Dict mit {LANG: set[transfer_terms], ...} (aus [transfer] Sektion)
|
||
"""
|
||
print(f"Erkannte mehrsprachige Struktur mit Sprachen: {', '.join(existing_languages)}")
|
||
|
||
total_updated = 0
|
||
total_unchanged = 0
|
||
total_newly = 0
|
||
total_transferred = 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]
|
||
transfer_terms = transfer_terms_all.get(lang, set()) if transfer_terms_all else set()
|
||
updated, unchanged, newly, transferred = _retranslate_language_block(
|
||
lang_data['translations'], lang_data['untranslated'],
|
||
multi, trigramme, bigramme, single,
|
||
f"[{lang}]",
|
||
transfer_terms
|
||
)
|
||
total_updated += updated + newly
|
||
total_unchanged += unchanged
|
||
total_newly += newly
|
||
total_transferred += transferred
|
||
|
||
print(f" [{lang}] Aktualisiert: {updated}, Unverändert: {unchanged}", end="")
|
||
if newly:
|
||
print(f", Neu aus 'untranslated': {newly}", end="")
|
||
if transferred:
|
||
print(f", Transfer-Begriffe (1:1): {transferred}", end="")
|
||
print()
|
||
|
||
print(f"\nRe-Übersetzung abgeschlossen (mehrsprachige Struktur):")
|
||
print(f" Gesamt aktualisiert: {total_updated}")
|
||
print(f" Gesamt unverändert: {total_unchanged}")
|
||
if total_transferred:
|
||
print(f" Gesamt Transfer-Begriffe (1:1): {total_transferred}")
|
||
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],
|
||
transfer_terms_all: dict[str, set[str]] | None = None):
|
||
"""
|
||
Re-übersetzt eine JSON-Datei mit neuen Übersetzungs-Configs.
|
||
Überschreibt die ursprüngliche JSON-Datei mit den aktualisierten Übersetzungen.
|
||
|
||
Erkennt die Dateistruktur automatisch und delegiert die eigentliche
|
||
Übersetzungsarbeit an _retranslate_language_block():
|
||
- Alte Struktur: "translations"/ "untranslated" auf oberster Ebene
|
||
- Hierarchische Struktur: "[CS]": {"translations": ..., "untranslated": ...}
|
||
|
||
Args:
|
||
json_path: Pfad zur JSON-Datei
|
||
translation_dicts_all: Dict mit {LANG: (multi, trigramme, bigramme, single), ...}
|
||
translation_languages: Liste der Sprachen in der Reihenfolge der Verarbeitung
|
||
"""
|
||
try:
|
||
with open(json_path, 'r', encoding='utf-8') as f:
|
||
data = json.load(f)
|
||
|
||
if not isinstance(data, dict):
|
||
print(f"FEHLER: JSON-Datei hat ungültige Struktur (erwartet Object, "
|
||
f"bekam {type(data).__name__})")
|
||
sys.exit(1)
|
||
|
||
structure_type, existing_languages = detect_json_structure(data)
|
||
|
||
if structure_type == "legacy":
|
||
data = _retranslate_legacy(data, translation_languages, translation_dicts_all, transfer_terms_all)
|
||
elif structure_type == "multilang":
|
||
_retranslate_multilang(data, existing_languages, translation_languages, translation_dicts_all, transfer_terms_all)
|
||
else:
|
||
print(f"FEHLER: JSON-Datei hat unbekannte Struktur")
|
||
sys.exit(1)
|
||
|
||
with open(json_path, 'w', encoding='utf-8') as f:
|
||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||
|
||
print(f"\nJSON-Datei erfolgreich aktualisiert: {json_path}")
|
||
|
||
except json.JSONDecodeError as e:
|
||
print(f"Fehler beim Parsen der JSON-Datei: {e}")
|
||
sys.exit(1)
|
||
except Exception as e:
|
||
print(f"Fehler beim Re-Übersetzen der JSON-Datei: {e}")
|
||
sys.exit(1)
|
||
|
||
|
||
def resolve_input_file(name: str, work_dir: Path, label: str = "Datei") -> Path:
|
||
"""
|
||
Sucht eine Datei zuerst im Work-Ordner, dann als vollständigen/relativen Pfad.
|
||
Gibt den aufgelösten Path zurück oder beendet das Programm mit Fehlermeldung.
|
||
|
||
Verwendet intern check_file_in_work() aus utils für die Dateisuche.
|
||
|
||
Args:
|
||
name: Dateiname oder Pfad (wie vom Benutzer angegeben)
|
||
work_dir: Arbeitsverzeichnis, in dem zuerst gesucht wird
|
||
label: Bezeichnung der Datei für die Fehlermeldung (z.B. "DXF-Datei")
|
||
|
||
Returns:
|
||
Path zur gefundenen Datei
|
||
"""
|
||
path, found = check_file_in_work(work_dir, name)
|
||
if not found:
|
||
print(f"{label} nicht gefunden: {name}")
|
||
print(f" Geprüft in Work-Ordner: {work_dir / name}")
|
||
print(f" Geprüft als vollständiger Pfad: {Path(name)}")
|
||
sys.exit(1)
|
||
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,
|
||
transfer_terms_all: dict[str, set[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.
|
||
3. Transfer-Begriffe werden 1:1 übernommen (ohne Übersetzung).
|
||
|
||
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)
|
||
transfer_terms_all: Dict mit {LANG: set[transfer_terms], ...} (aus [transfer] Sektion)
|
||
"""
|
||
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]
|
||
transfer_terms = transfer_terms_all.get(lang, set()) if transfer_terms_all else set()
|
||
|
||
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, transfer_terms)
|
||
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
|
||
transferred = 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, transfer_terms)
|
||
if new_target:
|
||
new_translations.append((text, new_target))
|
||
# Prüfe ob es ein Transfer-Begriff war
|
||
if transfer_terms and text in transfer_terms:
|
||
transferred += 1
|
||
else:
|
||
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 transferred:
|
||
print(f" Transfer-Begriffe (1:1): {transferred}")
|
||
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,
|
||
transfer_terms_all: dict) -> 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, transfer_terms_all)
|
||
|
||
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(
|
||
description='Extrahiert TEXT/MTEXT aus DXF, filtert Texte aus TXT, verarbeitet JSON, oder übersetzt DXF direkt. Optional mit Übersetzungsfunktion.',
|
||
prog='translate'
|
||
)
|
||
|
||
# Mutually exclusive group: entweder DXF-Extraktion, TXT-Filterung, JSON-Reprocessing oder DXF-zu-DXF
|
||
input_group = parser.add_mutually_exclusive_group(required=True)
|
||
input_group.add_argument(
|
||
'-e', '--extract',
|
||
action='store_true',
|
||
help='Extrahiert Texte aus DXF/DWG Datei (benötigt --filename)'
|
||
)
|
||
input_group.add_argument(
|
||
'--fromtxt',
|
||
action='store',
|
||
metavar='FILE.txt',
|
||
help='Filtert Texte aus einer TXT Datei (ein Text pro Zeile)'
|
||
)
|
||
input_group.add_argument(
|
||
'--fromjson',
|
||
action='store',
|
||
metavar='FILE.json',
|
||
help='Liest untranslated Block aus JSON-Datei und übersetzt erneut (benötigt --translate)'
|
||
)
|
||
input_group.add_argument(
|
||
'--retranslate-json',
|
||
action='store',
|
||
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',
|
||
metavar='OUTPUT.dxf',
|
||
help='Übersetzt TEXT/MTEXT in DXF und schreibt neue DXF-Datei (benötigt --filename)'
|
||
)
|
||
|
||
parser.add_argument(
|
||
'-f', '--filename',
|
||
action='store',
|
||
help='DXF/DWG Datei (.dxf, .dwg) die verarbeitet werden soll (nur mit --extract)',
|
||
metavar='myfile.dxf'
|
||
)
|
||
parser.add_argument(
|
||
'-o', '--outname',
|
||
action='store',
|
||
help='Name der Ausgabedatei (ohne Pfad, mit oder ohne .xlsx/.txt Endung)',
|
||
metavar='output.xlsx'
|
||
)
|
||
parser.add_argument(
|
||
'-d', '--debug',
|
||
action='store_true',
|
||
help='Aktiviert Debug-Ausgabe für Pattern-Matching'
|
||
)
|
||
parser.add_argument(
|
||
'-t', '--export-type',
|
||
action='store',
|
||
default='json',
|
||
help='Exportformat(e): json (Standard), excel, text oder komma-separiert (z.B. excel,json,text)'
|
||
)
|
||
parser.add_argument(
|
||
'--translate',
|
||
action='store',
|
||
metavar='LANG',
|
||
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',
|
||
action='store',
|
||
default='DE',
|
||
metavar='LANG',
|
||
help='Quellsprache für Attribut in Blöcken (z.B. DE->TEXT-D, EN->TEXT-EN, CS->TEXT-CS). Standard: DE'
|
||
)
|
||
parser.add_argument(
|
||
'--translation-json',
|
||
action='store',
|
||
metavar='FILE.json',
|
||
help='JSON-Datei mit Übersetzungen für --todxf (Standard: <filename>_texts.json)'
|
||
)
|
||
return parser
|
||
|
||
|
||
def _run_retranslate(
|
||
args,
|
||
work_dir: Path,
|
||
translation_dicts_all: dict,
|
||
translation_languages: list,
|
||
transfer_terms_all: dict) -> None:
|
||
"""Workflow: Re-Übersetzung einer bestehenden JSON-Datei (--retranslate-json)."""
|
||
print("=== Re-Übersetzung JSON-Datei ===\n")
|
||
|
||
input_filename = args.retranslate_json
|
||
if not input_filename.lower().endswith('.json'):
|
||
print("Mit --retranslate-json nur .json Dateien erlaubt")
|
||
sys.exit(1)
|
||
|
||
json_file = resolve_input_file(input_filename, work_dir, "JSON-Datei")
|
||
|
||
print(f"JSON-Datei: {json_file}")
|
||
print(f"Sprachen: {', '.join(translation_languages)}")
|
||
|
||
retranslate_json_file(json_file, translation_dicts_all, translation_languages, transfer_terms_all)
|
||
|
||
print("\nVerarbeitung erfolgreich abgeschlossen.")
|
||
sys.exit(0)
|
||
|
||
|
||
def _run_todxf(
|
||
args,
|
||
work_dir: Path,
|
||
translation_languages: list,
|
||
wildcard_patterns: list,
|
||
regex_patterns: list,
|
||
source_attr_name: str,
|
||
target_attr_name: str,
|
||
target_layer_name: str | None) -> None:
|
||
"""Workflow: DXF-zu-DXF Übersetzung (--todxf)."""
|
||
print("=== DXF-zu-DXF Übersetzung ===\n")
|
||
|
||
input_filename = args.filename
|
||
if not input_filename.lower().endswith(('.dxf', '.dwg')):
|
||
print("Mit --todxf nur .dxf oder .dwg Dateien als Quelle erlaubt")
|
||
sys.exit(1)
|
||
|
||
source_dxf = resolve_input_file(input_filename, work_dir, "Quell-DXF")
|
||
print(f"Quell-DXF: {source_dxf}")
|
||
|
||
# Bestimme JSON-Datei mit Übersetzungen
|
||
if args.translation_json:
|
||
translation_json = resolve_input_file(args.translation_json, work_dir, "Übersetzungs-JSON")
|
||
else:
|
||
# Standard: <filename>_texts.json im Work-Ordner
|
||
translation_json = work_dir / (Path(input_filename).stem + "_texts.json")
|
||
if not translation_json.exists():
|
||
print(f"Standard-Übersetzungs-JSON nicht gefunden: {translation_json}")
|
||
print(f"Bitte erstellen Sie die Datei oder geben Sie eine alternative mit --translation-json an")
|
||
sys.exit(1)
|
||
|
||
print(f"Übersetzungs-JSON: {translation_json}")
|
||
|
||
target_lang = translation_languages[0] if translation_languages else None
|
||
translations = load_translations_from_json(translation_json, target_lang)
|
||
|
||
# Transfer-Begriffe aus der Sprachkonfiguration laden (nur für Symbol-Attribute)
|
||
transfer_terms: set[str] | None = None
|
||
if target_lang:
|
||
translation_dir = check_environment_var('PROJECT_TRANSLATION')
|
||
transfer_terms = load_transfer_terms(target_lang, translation_dir)
|
||
|
||
output_dxf_name = args.todxf
|
||
if not output_dxf_name.lower().endswith(('.dxf', '.dwg')):
|
||
output_dxf_name += '.dxf'
|
||
output_dxf = work_dir / output_dxf_name
|
||
|
||
print(f"Ziel-DXF: {output_dxf}")
|
||
|
||
translate_dxf_texts(
|
||
source_dxf,
|
||
output_dxf,
|
||
translations,
|
||
wildcard_patterns,
|
||
regex_patterns,
|
||
source_attr_name,
|
||
target_attr_name,
|
||
target_layer_name,
|
||
transfer_terms,
|
||
)
|
||
|
||
print("\nVerarbeitung erfolgreich abgeschlossen.")
|
||
sys.exit(0)
|
||
|
||
|
||
def _run_extract_or_import(
|
||
args,
|
||
work_dir: Path,
|
||
wildcard_patterns: list,
|
||
regex_patterns: list,
|
||
translation_dicts,
|
||
translation_dicts_all: dict,
|
||
translation_languages: list,
|
||
export_types: list,
|
||
source_attr_name: str) -> None:
|
||
"""Workflow: Texte extrahieren oder importieren (--extract, --fromtxt, --fromjson)."""
|
||
if args.extract:
|
||
input_filename = args.filename
|
||
if not input_filename.lower().endswith(('.dxf', '.dwg')):
|
||
print("Mit --extract nur .dxf oder .dwg Dateien erlaubt")
|
||
sys.exit(1)
|
||
filename = resolve_input_file(input_filename, work_dir, "DXF-Datei")
|
||
print(f"Lese DXF Datei: {filename}")
|
||
all_texts = extract_text_from_dxf(filename, source_attr_name)
|
||
|
||
elif args.fromtxt:
|
||
input_filename = args.fromtxt
|
||
if not input_filename.lower().endswith('.txt'):
|
||
print("Mit --fromtxt nur .txt Dateien erlaubt")
|
||
sys.exit(1)
|
||
filename = resolve_input_file(input_filename, work_dir, "Textdatei")
|
||
print(f"Lese Textdatei: {filename}")
|
||
all_texts = read_texts_from_file(filename)
|
||
|
||
else: # args.fromjson
|
||
input_filename = args.fromjson
|
||
if not input_filename.lower().endswith('.json'):
|
||
print("Mit --fromjson nur .json Dateien erlaubt")
|
||
sys.exit(1)
|
||
filename = resolve_input_file(input_filename, work_dir, "JSON-Datei")
|
||
print(f"Lese JSON-Datei: {filename}")
|
||
all_texts = read_texts_from_json(filename)
|
||
|
||
texts, ignored_texts = filter_texts(all_texts, wildcard_patterns, regex_patterns, args.debug)
|
||
|
||
extension_map = {
|
||
'excel': '.xlsx',
|
||
'json': '.json',
|
||
'text': '.txt'
|
||
}
|
||
|
||
print(f"\nExportiere in {len(export_types)} Format(e): {', '.join(export_types)}")
|
||
|
||
for export_type in export_types:
|
||
if args.outname:
|
||
base_name = args.outname
|
||
for ext in extension_map.values():
|
||
if base_name.endswith(ext):
|
||
base_name = base_name[:-len(ext)]
|
||
break
|
||
output_filename = f"{base_name}{extension_map[export_type]}"
|
||
else:
|
||
output_filename = f"{filename.stem}_texts{extension_map[export_type]}"
|
||
|
||
output_path = work_dir / output_filename
|
||
|
||
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, translation_dicts_all, translation_languages)
|
||
else: # text
|
||
write_texts_to_text(texts, ignored_texts, output_path, translation_dicts)
|
||
|
||
print(f" [OK] {export_type}: {output_path}")
|
||
|
||
print("\nVerarbeitung erfolgreich abgeschlossen.")
|
||
|
||
|
||
def main() -> None:
|
||
"""Einstiegspunkt für translate.py. Parst Argumente und delegiert an Workflow-Funktionen."""
|
||
parser = _build_arg_parser()
|
||
args = parser.parse_args()
|
||
|
||
# Parse und validiere Export-Typen (komma-separierte Liste)
|
||
valid_export_types = {'excel', 'json', 'text'}
|
||
export_types = [t.strip() for t in args.export_type.split(',')]
|
||
|
||
for export_type in export_types:
|
||
if export_type not in valid_export_types:
|
||
parser.error(f"Ungültiger Export-Typ: '{export_type}'. Erlaubt sind: {', '.join(sorted(valid_export_types))}")
|
||
|
||
seen: set = set()
|
||
export_types = [t for t in export_types if not (t in seen or seen.add(t))]
|
||
|
||
# Validierung der Argument-Kombinationen
|
||
if args.extract and not args.filename:
|
||
parser.error("--extract benötigt --filename")
|
||
if args.filename and not (args.extract or args.todxf):
|
||
parser.error("--filename kann nur mit --extract oder --todxf verwendet werden")
|
||
if args.fromjson and not args.translate:
|
||
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:
|
||
parser.error("--translation-json kann nur mit --todxf verwendet werden")
|
||
|
||
# Umgebungsvariablen und gemeinsame Konfiguration laden
|
||
work_dir = check_environment_var('PROJECT_WORK')
|
||
cfg_dir = check_environment_var('PROJECT_CFG')
|
||
|
||
wildcard_patterns, regex_patterns = load_ignore_patterns(cfg_dir)
|
||
symbol_attr_mapping = load_symbol_attribute_mapping(cfg_dir)
|
||
symbol_layer_mapping = load_symbol_layer_mapping(cfg_dir)
|
||
|
||
# Zielsprachen parsen (kommasepariert, dedupliziert)
|
||
translation_languages = []
|
||
if args.translate:
|
||
seen = set()
|
||
translation_languages = [
|
||
lang for lang in (l.strip().upper() for l in args.translate.split(','))
|
||
if lang and not (lang in seen or seen.add(lang))
|
||
]
|
||
|
||
# Attribut- und Layer-Namen bestimmen
|
||
source_attr_name = symbol_attr_mapping.get(args.lang_source, 'TEXT-D')
|
||
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 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 {translation_languages[0]}): {target_layer_name}")
|
||
else:
|
||
print(f" Ziel-Sprachen (--translate): {', '.join(translation_languages)}")
|
||
|
||
# Übersetzungs-Configs laden: {LANG: (multi, trigramme, bigramme, single), ...}
|
||
translation_dicts_all = {}
|
||
transfer_terms_all = {}
|
||
if translation_languages:
|
||
translation_dir = check_environment_var('PROJECT_TRANSLATION')
|
||
for lang in translation_languages:
|
||
translation_dicts_all[lang] = load_translation_config(lang, translation_dir)
|
||
transfer_terms_all[lang] = load_transfer_terms(lang, translation_dir)
|
||
|
||
# Für backwards compatibility: bei genau einer Sprache flaches Dict bereitstellen
|
||
translation_dicts = translation_dicts_all.get(translation_languages[0]) if len(translation_languages) == 1 else None
|
||
|
||
if args.debug:
|
||
print("DEBUG-Modus aktiviert")
|
||
|
||
# Workflow delegieren
|
||
if args.retranslate_json:
|
||
_run_retranslate(args, work_dir, translation_dicts_all, translation_languages, transfer_terms_all)
|
||
|
||
if args.retranslate_txt:
|
||
_run_retranslate_txt(args, work_dir, translation_dicts_all, translation_languages,
|
||
wildcard_patterns, regex_patterns, transfer_terms_all)
|
||
|
||
if args.todxf:
|
||
_run_todxf(args, work_dir, translation_languages, wildcard_patterns, regex_patterns,
|
||
source_attr_name, target_attr_name, target_layer_name)
|
||
|
||
_run_extract_or_import(args, work_dir, wildcard_patterns, regex_patterns,
|
||
translation_dicts, translation_dicts_all, translation_languages,
|
||
export_types, source_attr_name)
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|