1631 lines
61 KiB
Python
1631 lines
61 KiB
Python
import argparse
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
from configparser import RawConfigParser
|
|
from fnmatch import fnmatch
|
|
|
|
import ezdxf
|
|
from openpyxl import Workbook
|
|
|
|
|
|
"""
|
|
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 check_environment_var(env_str: str) -> Path:
|
|
"""Prüft ob eine Umgebungsvariable gesetzt ist und gibt den Pfad zurück."""
|
|
out_path = os.environ.get(env_str)
|
|
if out_path:
|
|
return Path(out_path)
|
|
else:
|
|
print(f"Umgebungsvariable {env_str} ist nicht gesetzt oder leer.")
|
|
sys.exit(1)
|
|
|
|
|
|
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)
|
|
|
|
# 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
|
|
|
|
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)}")
|
|
|
|
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]": {"translated": [...], ...}
|
|
|
|
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 translated Block
|
|
if 'translated' in lang_data:
|
|
translated = lang_data['translated']
|
|
if not isinstance(translated, list):
|
|
print(f"WARNUNG: [{lang}][translated] ist kein Array, überspringe")
|
|
continue
|
|
|
|
for idx, entry in enumerate(translated):
|
|
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]": {"translated": [...], "untranslated": [...]}
|
|
- "[FR]": {"translated": [...], "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}]"] = {
|
|
"translated": 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['translated'])}, 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)
|
|
|
|
|
|
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(
|
|
'--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: --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: 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.")
|