Die Uebergabe geht an einen Dev, der den Einbau mit einem Coding-Agenten macht. Dafuer reicht "hier ist das Repo" nicht. Vier neue Artefakte, alle gegen das Zielsystem geprueft, nicht aus dem Gedaechtnis geschrieben. **EINBAU.md** ist der Einstieg. Der wichtigste Satz darin steht ganz oben: Das Dashboard in HiveGrid ist kein leeres Blatt. Route, Widget-Registry, Rollen-Layouts, Drilldown, Export, Live-Updates und Layout-Speicherung sind gebaut. Wer dort eine neue Seite anlegt, wirft funktionierende Software weg. Das ist nicht meine Einschaetzung: Der Brief P30 im Zielsystem sagt woertlich "Delta-Phase, kein Neubau ... Dashboard/Widget-Registry voll gebaut". Analytics dagegen gibt es noch nicht und wird nach dem Muster von leads.analytics.tsx neu gebaut. Dazu eine Tabelle, welche Datei wohin wandert, und eine zweite, was ausdruecklich nicht mitwandert und warum. **docs/port/TICKET-ANALYTICS.md und TICKET-DASHBOARD.md** sind im Format von .planning/ticket-docs/ geschrieben, mit Header-Tabelle, Abhaengigkeiten, Definition of Done, Technischen Bausteinen, Quellen und Dev-Nachzug. Sie koennen dorthin kopiert werden; dann greift der bestehende Ablauf, statt dass wir einen zweiten danebenstellen. **docs/port/i18n-*.json plus TEXTE.md.** pnpm run verify:i18n ist in HiveGrid ein Gate: keine hartkodierten Strings. Dieser Prototyp ist zu 100 % hartkodiertes Deutsch. 185 Texte mit Schluesselvorschlag und Fundstelle, dazu derselbe Inhalt als einfuegbarer Baum. Erzeugt von scripts/i18n-inventar.py (npm run i18n:inventar), damit das Inventar nicht auseinanderlaeuft, wenn sich am Prototyp noch etwas aendert. Dabei gefunden: app.dashboard.widgets.* enthaelt bereits 17 Widgets, darunter genau fuenf, die dieser Prototyp gestaltet (receivables, orderIntake, daysToPayment, winRate, stageDistribution) und genau die drei, die er als reserviert markiert (myTasks, scheduleToday, hrRequests). Neue Schluessel dafuer waeren Dubletten; die acht sind in TEXTE.md einzeln aufgefuehrt. Die Anrede stimmt ebenfalls schon ueberein, HiveGrid duzt. **CLAUDE.md** als Einstiegspunkt, den Claude Code beim Oeffnen des Repos von selbst liest: was dieses Repo ist, die drei Dinge, an denen ein Einbau sonst scheitert, und wo was steht. Jeder in den Dokumenten genannte Pfad wurde in beiden Repos nachgeprueft. Ein Fehler dabei gefunden und behoben: der Designer-Brief heisst P11-suche-dashboard.md, nicht P11.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPeZNwSeU7qG3RRCwaxEZX
113 lines
5.0 KiB
Python
113 lines
5.0 KiB
Python
#!/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()
|