Files
kabellaengen/lib/translate.py
T

2029 lines
78 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
"""
Dieses Programm:
- liest eine DXF Datei und sammelt alle TEXT und MTEXT Objekte
ODER liest Texte aus einer TXT Datei (ein Text pro Zeile)
ODER liest den 'untranslated' Block aus einer JSON Datei
ODER übersetzt TEXT/MTEXT direkt in einer DXF und erstellt neue übersetzte DXF
- filtert die Texte basierend auf Mustern in translator.cfg
- übersetzt optional Texte mit Trigrammen, Bigrammen und Einzelwörtern aus translation/<LANG>.cfg
- schreibt die Ergebnisse in eine Excel-, JSON- oder Text-Datei
- speichert die Ausgabedatei im PROJECT_WORK Verzeichnis
"""
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
config = RawConfigParser()
# Überschreibe optionxform um Groß-/Kleinschreibung zu erhalten
config.optionxform = str
try:
config.read(config_file, encoding='utf-8')
except Exception as e:
print(f"Fehler beim Lesen der Übersetzungs-Konfiguration: {e}")
sys.exit(1)
multi = {}
trigramme = {}
bigramme = {}
single = {}
# Lade Multi (4+ Wörter)
if config.has_section('multi'):
for key in config['multi']:
original = key
translation = config['multi'][key]
if translation and not translation.startswith('#'):
multi[original] = translation
# Lade Trigramme
if config.has_section('trigramme'):
for key in config['trigramme']:
original = key
translation = config['trigramme'][key]
if translation and not translation.startswith('#'):
trigramme[original] = translation
# Lade Bigramme
if config.has_section('bigramme'):
for key in config['bigramme']:
original = key
translation = config['bigramme'][key]
if translation and not translation.startswith('#'):
bigramme[original] = translation
# Lade Single
if config.has_section('single'):
for key in config['single']:
original = key
translation = config['single'][key]
if translation and not translation.startswith('#'):
single[original] = translation
# 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_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_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)
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_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)
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)
"""
config_file = cfg_path / "translator.cfg"
if not config_file.exists():
print(f"FEHLER: Konfigurationsdatei nicht gefunden: {config_file}")
print(f"Bitte erstellen Sie die Datei 'translator.cfg' im Verzeichnis: {cfg_path}")
sys.exit(1)
# RawConfigParser verhindert dass Backslashes in Regex-Mustern escaped werden
config = RawConfigParser()
# Überschreibe optionxform um Groß-/Kleinschreibung zu erhalten
config.optionxform = str
try:
config.read(config_file, encoding='utf-8')
except Exception as e:
print(f"Fehler beim Lesen der Konfigurationsdatei: {e}")
sys.exit(1)
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]) -> str:
"""
Übersetzt einen Text durch Ersetzen von Multi-Phrasen, Trigrammen, Bigrammen und Einzelwörtern.
Ersetzungen erfolgen in dieser Reihenfolge: 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.
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
Returns:
Übersetzter Text (leer wenn keine Übersetzung gefunden wurde)
"""
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
# Ignoriere leere Texte
if not text_d_value:
empty_count += 1
continue
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) -> tuple[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])
Returns:
Tuple mit (translated_count, skipped_count, ignored_count) für diesen Block
"""
if not entity.has_attrib:
return 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
original_text = text_source_attrib.dxf.text.strip() if text_source_attrib.dxf.text else None
if not original_text:
return 0, 0, 0
# Prüfe ob Text ignoriert werden soll
if should_ignore_text(original_text, wildcard_patterns, regex_patterns):
return 0, 0, 1
# Suche Übersetzung
if original_text not in translations:
return 0, 1, 0
# Schreibe Übersetzung in Ziel-Attribut
if text_target_attrib:
# Ziel-Attribut existiert bereits, aktualisiere es
text_target_attrib.dxf.text = translations[original_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=translations[original_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
return 1, 0, 0
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):
"""
Ü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)
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"
"""
# 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 = 0
text_skipped = 0
text_ignored = 0
mtext_translated = 0
mtext_skipped = 0
mtext_ignored = 0
block_translated = 0
block_skipped = 0
block_ignored = 0
# Übersetze TEXT Objekte
for entity in msp.query('TEXT'):
if entity.dxf.text:
original_text = entity.dxf.text.strip()
# Prüfe ob Text ignoriert werden soll
if should_ignore_text(original_text, wildcard_patterns, regex_patterns):
text_ignored += 1
continue
# Suche Übersetzung
if original_text in translations:
entity.dxf.text = translations[original_text]
text_translated += 1
else:
text_skipped += 1
# Übersetze MTEXT Objekte
for entity in msp.query('MTEXT'):
if entity.text:
original_text = entity.text.strip()
# Prüfe ob Text ignoriert werden soll
if should_ignore_text(original_text, wildcard_patterns, regex_patterns):
mtext_ignored += 1
continue
# Suche Übersetzung
if original_text in translations:
entity.text = translations[original_text]
mtext_translated += 1
else:
mtext_skipped += 1
# Übersetze Text-Attribute in Blöcken
for entity in msp.query('INSERT'):
translated, skipped, ignored = translate_block_attribute(
entity,
doc,
translations,
wildcard_patterns,
regex_patterns,
required_attributes,
text_source_attr,
text_target_attr,
target_layer,
)
block_translated += translated
block_skipped += skipped
block_ignored += ignored
# 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}")
print(f" Gesamt: {translated_count} übersetzt, {skipped_count} übersprungen, {ignored_count} ignoriert")
print(f" Hinweis: Übersetzungen in Blöcken wurden in {text_target_attr} geschrieben, TEXT-D 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 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.
Bei einer Sprache (alte Struktur):
- "translations": Liste von {source, target} für übersetzte Texte
- "untranslated": Liste von {source, target: ""} für nicht übersetzte Texte
- "ignored": Liste von Strings für gefilterte/ignorierte Texte
Bei mehreren Sprachen (hierarchische Struktur):
- "[EN]": {"translations": [...], "untranslated": [...]}
- "[FR]": {"translations": [...], "untranslated": [...]}
- "ignored": Liste von Strings für gefilterte/ignorierte Texte
Args:
texts: Liste der akzeptierten Texte
ignored_texts: Liste der ignorierten Texte
output_path: Pfad zur Ausgabe-JSON-Datei
translation_dicts: Optional: Tuple mit (Multi, Trigramme, Bigramme, Single) für eine Sprache (backwards compatibility)
translation_dicts_all: Optional: Dict mit {LANG: (multi, trigramme, bigramme, single)} für mehrere Sprachen
translation_languages: Optional: Liste der Sprachen in der Reihenfolge der Verarbeitung
"""
output_data = {}
# Mehrsprachige hierarchische Struktur
if translation_dicts_all and translation_languages and len(translation_languages) > 1:
for lang in translation_languages:
if lang not in translation_dicts_all:
continue
multi, trigramme, bigramme, single = translation_dicts_all[lang]
translations = []
untranslated = []
for text in texts:
translated = translate_text(text, multi, trigramme, bigramme, single)
if translated: # Übersetzung vorhanden
translations.append({
"source": text,
"target": translated
})
else: # Keine Übersetzung vorhanden
untranslated.append({
"source": text,
"target": ""
})
# Hierarchische Struktur: [LANG] als Schlüssel
output_data[f"[{lang}]"] = {
"translations": translations,
"untranslated": untranslated
}
# Ignorierte Texte am Ende (ohne Sprachzuordnung)
output_data["ignored"] = ignored_texts
# Alte Struktur (eine Sprache oder keine Übersetzung)
else:
# Block 1: Übersetzte Texte (als Liste von Dicts mit source/target)
# Block 2: Nicht übersetzte Texte (als Liste von Dicts mit source und leerem target)
if translation_dicts:
multi, trigramme, bigramme, single = translation_dicts
translations = []
untranslated = []
for text in texts:
translated = translate_text(text, multi, trigramme, bigramme, single)
if translated: # Übersetzung vorhanden
translations.append({
"source": text,
"target": translated
})
else: # Keine Übersetzung vorhanden
untranslated.append({
"source": text,
"target": ""
})
output_data["translations"] = translations
output_data["untranslated"] = untranslated
else:
# Ohne Übersetzungs-Config: alle akzeptierten Texte sind nicht übersetzt
output_data["translations"] = []
output_data["untranslated"] = [{"source": text, "target": ""} for text in texts]
# Block 3: Ignorierte Texte (als Liste)
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 and len(translation_languages) > 1:
# Mehrsprachige Struktur
for lang in translation_languages:
if f"[{lang}]" in output_data:
lang_data = output_data[f"[{lang}]"]
print(f" [{lang}] Übersetzungen: {len(lang_data['translations'])}, Nicht übersetzt: {len(lang_data['untranslated'])}")
else:
# Alte Struktur
if translation_dicts:
print(f" Übersetzungen: {len(output_data['translations'])}")
print(f" Nicht übersetzte Texte: {len(output_data['untranslated'])}")
else:
print(f" Nicht übersetzte Texte: {len(output_data['untranslated'])}")
print(f" Ignorierte Texte: {len(ignored_texts)}")
except Exception as e:
print(f"Fehler beim Schreiben der JSON-Datei: {e}")
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 = sum(1 for text in texts if translate_text(text, multi, trigramme, bigramme, single))
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 retranslate_json_file(json_path: Path,
translation_dicts_all: dict[str, tuple],
translation_languages: list[str]):
"""
Re-übersetzt eine JSON-Datei mit neuen Übersetzungs-Configs.
Liest alle "source" Texte aus dem "translations" Block und übersetzt sie neu.
Überschreibt die ursprüngliche JSON-Datei mit den aktualisierten Übersetzungen.
Unterstützt beide Strukturen:
- Alte Struktur: "translations": [{"source": "...", "target": "..."}, ...]
- Hierarchische Struktur: "[LANG]": {"translations": [...], ...}
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:
# Lade bestehende JSON-Datei
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, bekam {type(data).__name__})")
sys.exit(1)
# Erkenne Struktur
structure_type, existing_languages = detect_json_structure(data)
updated_count = 0
unchanged_count = 0
new_count = 0
if structure_type == "legacy":
# Alte Struktur: ein Sprach-Block
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)
# Verwende erste Sprache für Re-Übersetzung
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)
multi, trigramme, bigramme, single = translation_dicts_all[translation_languages[0]]
# Re-übersetze alle Einträge
for entry in translations_list:
if not isinstance(entry, dict) or 'source' not in entry:
continue
source = entry['source']
old_target = entry.get('target', '')
# Neue Übersetzung berechnen
new_target = translate_text(source, multi, trigramme, bigramme, single)
if new_target:
if new_target != old_target:
entry['target'] = new_target
updated_count += 1
else:
unchanged_count += 1
else:
# Keine Übersetzung mehr verfügbar -> in untranslated verschieben
if 'untranslated' not in data:
data['untranslated'] = []
data['untranslated'].append({
"source": source,
"target": ""
})
translations_list.remove(entry)
print(f" WARNUNG: Übersetzung für '{source}' nicht mehr verfügbar, verschoben zu 'untranslated'")
print(f"\nRe-Übersetzung abgeschlossen (alte Struktur, Sprache: {translation_languages[0]}):")
print(f" Aktualisiert: {updated_count}")
print(f" Unverändert: {unchanged_count}")
elif structure_type == "multilang":
# Hierarchische mehrsprachige Struktur
print(f"Erkannte mehrsprachige Struktur mit Sprachen: {', '.join(existing_languages)}")
# Re-übersetze für alle angegebenen Sprachen
for lang in translation_languages:
lang_key = f"[{lang}]"
# Wenn Sprache nicht in JSON existiert, erstelle neuen Block
if lang_key not in data:
print(f" Erstelle neuen Sprach-Block: [{lang}]")
data[lang_key] = {
"translations": [],
"untranslated": []
}
new_count += 1
lang_data = data[lang_key]
if not isinstance(lang_data, dict):
print(f"WARNUNG: [{lang}] Block ist kein Object, überspringe")
continue
# Stelle sicher dass translations Block existiert
if 'translations' not in lang_data:
lang_data['translations'] = []
if 'untranslated' not in lang_data:
lang_data['untranslated'] = []
translations_list = lang_data['translations']
if not isinstance(translations_list, list):
print(f"WARNUNG: [{lang}][translations] ist kein Array, überspringe")
continue
# Hole Übersetzungs-Config für diese Sprache
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]
lang_updated = 0
lang_unchanged = 0
# Re-übersetze alle Einträge für diese Sprache
for entry in translations_list[:]: # Kopie der Liste für sicheres Entfernen
if not isinstance(entry, dict) or 'source' not in entry:
continue
source = entry['source']
old_target = entry.get('target', '')
# Neue Übersetzung berechnen
new_target = translate_text(source, multi, trigramme, bigramme, single)
if new_target:
if new_target != old_target:
entry['target'] = new_target
lang_updated += 1
else:
lang_unchanged += 1
else:
# Keine Übersetzung mehr verfügbar -> in untranslated verschieben
lang_data['untranslated'].append({
"source": source,
"target": ""
})
translations_list.remove(entry)
print(f" WARNUNG: [{lang}] Übersetzung für '{source}' nicht mehr verfügbar, verschoben zu 'untranslated'")
updated_count += lang_updated
unchanged_count += lang_unchanged
print(f" [{lang}] Aktualisiert: {lang_updated}, Unverändert: {lang_unchanged}")
print(f"\nRe-Übersetzung abgeschlossen (mehrsprachige Struktur):")
print(f" Gesamt aktualisiert: {updated_count}")
print(f" Gesamt unverändert: {unchanged_count}")
if new_count > 0:
print(f" Neue Sprach-Blöcke erstellt: {new_count}")
else:
print(f"FEHLER: JSON-Datei hat unbekannte Struktur")
sys.exit(1)
# Schreibe aktualisierte JSON-Datei zurück
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)
if __name__ == '__main__':
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(
'--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)'
)
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(',')]
# Validiere jeden Export-Typ
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))}")
# Entferne Duplikate und behalte Reihenfolge
seen = set()
export_types = [t for t in export_types if not (t in seen or seen.add(t))]
# Validierung: --extract benötigt --filename
if args.extract and not args.filename:
parser.error("--extract benötigt --filename")
# Validierung: --filename nur mit --extract oder --todxf
if args.filename and not (args.extract or args.todxf):
parser.error("--filename kann nur mit --extract oder --todxf verwendet werden")
# Validierung: --fromjson benötigt --translate
if args.fromjson and not args.translate:
parser.error("--fromjson benötigt --translate (Übersetzung ist erforderlich)")
# Validierung: --retranslate-json benötigt --translate
if args.retranslate_json and not args.translate:
parser.error("--retranslate-json benötigt --translate (Übersetzung ist erforderlich)")
# Validierung: --todxf benötigt --filename
if args.todxf and not args.filename:
parser.error("--todxf benötigt --filename (Quell-DXF muss angegeben werden)")
# Validierung: --translation-json nur mit --todxf
if args.translation_json and not args.todxf:
parser.error("--translation-json kann nur mit --todxf verwendet werden")
# Prüfe Umgebungsvariablen
work_dir = check_environment_var('PROJECT_WORK')
cfg_dir = check_environment_var('PROJECT_CFG')
# Lade Ignore-Muster aus Konfigurationsdatei
wildcard_patterns, regex_patterns = load_ignore_patterns(cfg_dir)
# Lade Symbol-Attribut- und Layer-Mapping
symbol_attr_mapping = load_symbol_attribute_mapping(cfg_dir)
symbol_layer_mapping = load_symbol_layer_mapping(cfg_dir)
# Parse Sprachen aus --translate Parameter (kommaseparierte Liste)
translation_languages = []
if args.translate:
translation_languages = [lang.strip().upper() for lang in args.translate.split(',')]
# Entferne Duplikate und behalte Reihenfolge
seen = set()
translation_languages = [lang for lang in translation_languages if lang and not (lang in seen or seen.add(lang))]
# Bestimme Quell-Attributnamen basierend auf Sprachcode
source_attr_name = symbol_attr_mapping.get(args.lang_source, 'TEXT-D')
# Für --todxf: verwende nur die erste Sprache als Ziel (backwards compatibility)
target_attr_name = symbol_attr_mapping.get(translation_languages[0], 'TEXT-CS') if translation_languages else 'TEXT-CS'
target_layer_name = symbol_layer_mapping.get(translation_languages[0]) if translation_languages else None
print(f"\nVerwendete Attribut-Namen:")
print(f" Quell-Attribut (--lang_source {args.lang_source}): {source_attr_name}")
if 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)}")
# Optional: Lade Übersetzungs-Configs für alle Sprachen
# translation_dicts_all ist ein Dict: {LANG: (multi, trigramme, bigramme, single), ...}
translation_dicts_all = {}
if translation_languages:
translation_dir = check_environment_var('PROJECT_TRANSLATION')
for lang in translation_languages:
translation_dicts_all[lang] = load_translation_config(lang, translation_dir)
# Für backwards compatibility: wenn nur eine Sprache, setze translation_dicts wie früher
translation_dicts = translation_dicts_all.get(translation_languages[0]) if len(translation_languages) == 1 else None
if args.debug:
print("DEBUG-Modus aktiviert")
# Spezieller Workflow: Re-Übersetzung einer JSON-Datei
if args.retranslate_json:
print("=== Re-Übersetzung JSON-Datei ===\n")
# Finde JSON-Datei
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 = work_dir / input_filename
if not json_file.exists():
json_file = Path(input_filename)
if not json_file.exists():
print(f"JSON-Datei nicht gefunden: {input_filename}")
print(f" Geprüft in Work-Ordner: {work_dir / input_filename}")
print(f" Geprüft als vollständiger Pfad: {json_file}")
sys.exit(1)
print(f"JSON-Datei: {json_file}")
print(f"Sprachen: {', '.join(translation_languages)}")
# Re-übersetze JSON-Datei
retranslate_json_file(json_file, translation_dicts_all, translation_languages)
print("\nVerarbeitung erfolgreich abgeschlossen.")
sys.exit(0)
# Spezieller Workflow: DXF-zu-DXF Übersetzung
if args.todxf:
print("=== DXF-zu-DXF Übersetzung ===\n")
# Finde Quell-DXF
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 = work_dir / input_filename
if not source_dxf.exists():
source_dxf = Path(input_filename)
if not source_dxf.exists():
print(f"Quell-DXF nicht gefunden: {input_filename}")
print(f" Geprüft in Work-Ordner: {work_dir / input_filename}")
print(f" Geprüft als vollständiger Pfad: {source_dxf}")
sys.exit(1)
print(f"Quell-DXF: {source_dxf}")
# Bestimme JSON-Datei mit Übersetzungen
if args.translation_json:
translation_json = work_dir / args.translation_json
if not translation_json.exists():
translation_json = Path(args.translation_json)
if not translation_json.exists():
print(f"Übersetzungs-JSON nicht gefunden: {args.translation_json}")
print(f" Geprüft in Work-Ordner: {work_dir / args.translation_json}")
print(f" Geprüft als vollständiger Pfad: {translation_json}")
sys.exit(1)
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}")
# Lade Übersetzungen (verwende erste Sprache als Ziel-Sprache)
target_lang = translation_languages[0] if translation_languages else None
translations = load_translations_from_json(translation_json, target_lang)
# Bestimme Ausgabe-DXF
output_dxf_name = args.todxf
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}")
# Übersetze DXF (inkl. ggf. neuer TEXT-Attribute und Layer)
translate_dxf_texts(
source_dxf,
output_dxf,
translations,
wildcard_patterns,
regex_patterns,
source_attr_name,
target_attr_name,
target_layer_name,
)
print("\nVerarbeitung erfolgreich abgeschlossen.")
sys.exit(0)
# Bestimme Input-Modus und Datei
if args.extract:
# DXF/DWG-Modus
input_filename = args.filename
file_extension = input_filename.lower()
if not file_extension.endswith(('.dxf', '.dwg')):
print("Mit --extract nur .dxf oder .dwg Dateien erlaubt")
sys.exit(1)
# Versuche zuerst Datei im Work-Ordner zu finden
filename = work_dir / input_filename
if not filename.exists():
# Falls nicht gefunden, prüfe ob es ein vollständiger Pfad ist
filename = Path(input_filename)
if not filename.exists():
print(f"Datei nicht gefunden: {input_filename}")
print(f" Geprüft in Work-Ordner: {work_dir / input_filename}")
print(f" Geprüft als vollständiger Pfad: {filename}")
sys.exit(1)
print(f"Lese DXF Datei: {filename}")
all_texts = extract_text_from_dxf(filename, source_attr_name)
elif args.fromtxt:
# TXT-Modus
input_filename = args.fromtxt
if not input_filename.lower().endswith('.txt'):
print("Mit --fromtxt nur .txt Dateien erlaubt")
sys.exit(1)
# Versuche zuerst Datei im Work-Ordner zu finden
filename = work_dir / input_filename
if not filename.exists():
# Falls nicht gefunden, prüfe ob es ein vollständiger Pfad ist
filename = Path(input_filename)
if not filename.exists():
print(f"Datei nicht gefunden: {input_filename}")
print(f" Geprüft in Work-Ordner: {work_dir / input_filename}")
print(f" Geprüft als vollständiger Pfad: {filename}")
sys.exit(1)
print(f"Lese Textdatei: {filename}")
all_texts = read_texts_from_file(filename)
else: # args.fromjson
# JSON-Modus
input_filename = args.fromjson
if not input_filename.lower().endswith('.json'):
print("Mit --fromjson nur .json Dateien erlaubt")
sys.exit(1)
# Versuche zuerst Datei im Work-Ordner zu finden
filename = work_dir / input_filename
if not filename.exists():
# Falls nicht gefunden, prüfe ob es ein vollständiger Pfad ist
filename = Path(input_filename)
if not filename.exists():
print(f"Datei nicht gefunden: {input_filename}")
print(f" Geprüft in Work-Ordner: {work_dir / input_filename}")
print(f" Geprüft als vollständiger Pfad: {filename}")
sys.exit(1)
print(f"Lese JSON-Datei: {filename}")
all_texts = read_texts_from_json(filename)
# Schritt 2: Texte filtern
texts, ignored_texts = filter_texts(all_texts, wildcard_patterns, regex_patterns, args.debug)
# Mapping für Dateiendungen
extension_map = {
'excel': '.xlsx',
'json': '.json',
'text': '.txt'
}
# Export für jeden angegebenen Typ durchführen
print(f"\nExportiere in {len(export_types)} Format(e): {', '.join(export_types)}")
for export_type in export_types:
# Erstelle Output-Dateinamen basierend auf Export-Typ
if args.outname:
# Verwende benutzerdefinierten Namen
base_name = args.outname
# Entferne existierende Endung falls vorhanden
for ext in extension_map.values():
if base_name.endswith(ext):
base_name = base_name[:-len(ext)]
break
# Füge korrekte Endung hinzu
output_filename = f"{base_name}{extension_map[export_type]}"
else:
# Standardname basierend auf Eingabedatei
output_filename = f"{filename.stem}_texts{extension_map[export_type]}"
output_path = work_dir / output_filename
# Schreibe Texte in gewähltem Format
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.")