#!/usr/bin/env python3 """Sammelt alle sichtbaren Oberflaechentexte und schlaegt i18n-Schluessel vor. Warum es das gibt: HiveGrid prueft mit `pnpm run verify:i18n`, dass keine Strings hartkodiert sind. Dieser Prototyp ist zu 100 % hartkodiertes Deutsch, weil er allein lauffaehig sein muss. Das Inventar nimmt dem Einbau die Sucharbeit ab. Erzeugt zwei Dateien: docs/port/i18n-inventar.json Schluessel, Text, Fundstelle docs/port/i18n-de.json derselbe Inhalt als Baum, einfuegbar nach apps/web/public/locales/de/translations.json Aufruf: npm run i18n:inventar Die Schluesselnamen sind maschinell aus dem deutschen Text gebildet und ein Vorschlag, kein Ergebnis. Wo HiveGrid schon einen Schluessel hat, gilt dessen Name; welche das sind, steht in docs/port/TEXTE.md Abschnitt 1. """ import json import pathlib import re import unicodedata # Props und Objektfelder, die im Prototyp sichtbaren Text tragen. PROPS = ("titel", "untertitel", "beschreibung", "label", "hinweis", "leerText", "zusatz", "aktion", "text", "detail", "titleKey", "absprungText", "trendLabel", "quelle", "lage", "anrede", "name", "leerHinweis") PROP_RE = re.compile(r'\b(' + "|".join(PROPS) + r')\s*[=:]\s*"([^"]{2,200})"') JSX_RE = re.compile(r'>\s*([A-ZÄÖÜ][^<>{}\n]{3,120}?)\s*<') # Nicht portiert: die Bausteingalerie. Keine Oberflaechentexte: die Attrappen. UEBERSPRINGEN = {"KomponentenSeite.tsx", "nutzer.ts", "seed.ts"} SEITE = { "UebersichtSeite": ("analytics", "uebersicht"), "FinanzenSeite": ("analytics", "finanzen"), "AuftraegeSeite": ("analytics", "auftraege"), "KundenSeite": ("analytics", "kunden"), "TeamSeite": ("analytics", "team"), "DashboardSeite": ("dashboard", "page"), "dashboard.vm": ("dashboard", "page"), "navigation": ("analytics", "nav"), "calculations": ("analytics", "alerts"), "AnalyticsToolbar": ("analytics", "filter"), "AppShell": ("analytics", "shell"), "PageHeader": ("analytics", "actions"), "DataTable": ("shared", "table"), "SeitenZustand": ("shared", "state"), "ChartLeer": ("shared", "chart"), "AlertList": ("shared", "alerts"), "KPICard": ("shared", "kpi"), "AreaVerlauf": ("shared", "chart"), "BalkenVertikal": ("shared", "chart"), "WidgetKachel": ("dashboard", "widget"), "chartTheme": ("shared", "chart"), } STOPP = {"der", "die", "das", "und", "oder", "im", "in", "den", "dem", "des", "zum", "zur", "für", "von", "mit", "auf", "aus", "bei", "nach", "als", "ein", "eine", "einen", "ist", "sind", "wird", "werden"} def slug(text, laenge=4): """Kurzform fuer den Schluesselnamen: die ersten Sinnwoerter in camelCase.""" t = (text.replace("ä", "ae").replace("ö", "oe").replace("ü", "ue") .replace("ß", "ss").replace("Ä", "Ae").replace("Ö", "Oe").replace("Ü", "Ue")) t = unicodedata.normalize("NFKD", t).encode("ascii", "ignore").decode() woerter = [w for w in re.findall(r"[A-Za-z0-9]+", t) if w.lower() not in STOPP][:laenge] if not woerter: woerter = ["text"] return woerter[0].lower() + "".join(w.capitalize() for w in woerter[1:]) def sammle(): dateien = sorted(pathlib.Path("features/dashboard").rglob("*.tsx")) + \ sorted(pathlib.Path("features/dashboard").rglob("*.ts")) treffer = {} for p in dateien: if p.name in UEBERSPRINGEN: continue for nr, zeile in enumerate(p.read_text().split("\n"), 1): if zeile.lstrip().startswith(("*", "//")): continue for m in PROP_RE.finditer(zeile): treffer.setdefault(m.group(2), []).append(f"{p}:{nr}") for m in JSX_RE.finditer(zeile): t = m.group(1).strip() if t and not t.startswith("{") and any(c.isalpha() for c in t): treffer.setdefault(t, []).append(f"{p}:{nr}") return dict(sorted(treffer.items())) def main(): texte = sammle() zeilen, baum, benutzt = [], {}, set() for text, orte in sorted(texte.items(), key=lambda kv: kv[1][0]): datei = pathlib.Path(orte[0].split(":")[0]).stem bereich, gruppe = SEITE.get(datei, ("shared", "misc")) basis = f"app.{bereich}.{gruppe}.{slug(text)}" schluessel, n = basis, 2 while schluessel in benutzt: schluessel, n = f"{basis}{n}", n + 1 benutzt.add(schluessel) zeilen.append((schluessel, text, orte[0])) knoten = baum for teil in schluessel.split(".")[:-1]: knoten = knoten.setdefault(teil, {}) knoten[schluessel.split(".")[-1]] = text ziel = pathlib.Path("docs/port") ziel.mkdir(parents=True, exist_ok=True) (ziel / "i18n-de.json").write_text(json.dumps(baum, ensure_ascii=False, indent=2) + "\n") (ziel / "i18n-inventar.json").write_text(json.dumps( [{"key": k, "de": t, "quelle": o} for k, t, o in zeilen], ensure_ascii=False, indent=1) + "\n") print(f"{len(zeilen)} Texte, {len(zeilen)} Schluessel -> docs/port/") if __name__ == "__main__": main()