Die Browser-Abnahme hat drei uebersehene Stellen gefunden — "Aenderungen speichern",
"Oeffnen" und einen LDAP-Hinweis — und damit die Luecke im Waechter aufgedeckt, durch
die sie geschluepft sind: sein Verdachtsmuster war case-sensitiv. Beides mit 85d2d77
behoben.
In der SUMMARY festgehalten, wie geprueft wurde: eine unabhaengige Analyse aller
Tokens in de.json findet keine Ersatzschreibung mehr, der Schluesselsatz ist
unveraendert bei 787, und die Modulbeschreibung in der bestehenden Datenbank ist beim
API-Neustart per Seed-Upsert mitgewandert — ohne Migration, wie geplant.
Ausserdem vermerkt, dass eine erste Sichtpruefung per fetch aus der laufenden Seite
faelschlich Entwarnung gab. Dieselbe Methode hatte in dieser Sitzung schon einmal ein
falsches Ergebnis geliefert; die berichteten Zahlen stammen aus echten
Seitenaufrufen.
Backlog-Punkt zu den Umlauten nach completed verschoben.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K5jtbGzC5Sf9npJ3JCjKhq
22 KiB
quick_id, title, type, wave, depends_on, autonomous, files_modified, estimate, must_haves
| quick_id | title | type | wave | depends_on | autonomous | files_modified | estimate | must_haves | |||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 260907-hyi | Umlaute in der deutschen Oberflaeche korrigieren | execute | 1 | false |
|
|
|
Purpose: Ein Produkt, das an externe Kunden verkauft werden soll, kann keine Oberflaeche ohne Umlaute haben.
Output: Korrigierte de.json, korrigierte API-Texte, ein Woerterbuch-Modul als einzige
Wahrheitsquelle und ein Vitest-Waechter, der neue Ersatzschreibweisen blockiert.
Ausdruecklich NICHT Teil dieses Plans: Umformulierungen. Es geht ausschliesslich um
Schreibweise — gleicher Wortlaut, richtige Zeichen. Deutsche Code-Kommentare
(z. B. in tessera-logo.tsx) bleiben unberuehrt, sie sind nicht nutzersichtbar.
- Task 1 erledigt (Commit
3d351c6):umlaut-dictionary.tsangelegt, 67 Tokens / 140 Fundstellen inde.jsonkorrigiert, 787 Schluessel unveraendert. - Task 2 erledigt, noch nicht committet:
umlaut-guard.spec.tsliegt untracked vor.pnpm --filter @tessera/web test -- src/messages/laeuft gruen (2 Dateien, 6 Tests). - Task 3 offen:
mail.service.tsunddomaincheck.seed.tssind unveraendert. - Task 4 offen: Sichtpruefung im Browser und in der Datenbank.
Die beiden Korrekturen, die aus der Planvalidierung stammen (Bestaetigen→Bestätigen sowie
die korrigierten Formen auf der Positivliste), sind im ausgefuehrten Stand bereits enthalten
— nachgeprueft: kein Bestaetigen mehr in de.json, Waechter gruen.
<execution_context> @~/.claude/gsd-core/workflows/execute-plan.md </execution_context>
@CLAUDE.md @.planning/todos/pending/2026-09-07-umlaute-in-der-oberflaeche.md @apps/web/src/messages/tenderRadar-parity.spec.ts @apps/api/src/mail/mail.service.ts @apps/api/src/domaincheck/domaincheck.seed.ts Diese Punkte wurden beim Planen am Code verifiziert und ersparen dem Executor die Suche:-
de.jsonist bereits gemischt. 86 Zeilen tragen heute schon korrekte Umlaute (Verschlüsselung,müssen,Feuerlösch-,Mineralölerzeugnisse) — direkt neben den falschen Varianten (Verschluesselung,muessen). Eine Datei-weite Umschaltung ist also weder noetig noch moeglich; nur die falschen Tokens werden angefasst. -
92 verschiedene Tokens in den
de.json-Werten enthaltenae/oe/ue, davon sind 25 bereits korrekt und duerfen nicht angefasst werden. 43 Tokens enthaltenss, davon sind 40 korrekt. -
seedModule()ist einupsertMITupdate-Zweig (apps/api/src/module-registry/module-registry.service.ts:167) und laeuft inonModuleInit(domaincheck.module.ts:30). Derupdate-Zweig schreibtdescriptionbei jedem Start neu. Es ist also keine Migration und kein Backfill noetig — die Korrektur der Quellzeichenkette repariert bestehende Zeilen beim naechsten API-Start genauso wie frische Installationen. -
Die anderen drei Modul-Seeds sind sauber.
cert-manager,dkv,tenderstragen keine Ersatzschreibweisen in ihren Beschreibungen — geprueft, nichts zu tun. -
de.jsonunden.jsonhaben heute identische Schluesselsaetze (je 787). Dieser Plan aendert ausschliesslich Werte inde.json, nie Schluessel. -
Nur zwei API-Dateien betroffen. Ein Scan ueber
apps/api/srcfindet Ersatzschreibweisen ausschliesslich inmail.service.tsunddomaincheck.seed.ts.
Gefahr 2 — Teilwort-Ueberlappung.
hinzuzufuegen enthaelt hinzufuegen? Nein — aber Verfuegbarkeit enthaelt Verfuegbar,
und ueberpruefen enthaelt pruefen. Deshalb: Ersetzung auf ganzen Tokens, nicht per
Teilzeichenkette. Der Tokenizer (/[A-Za-zÄÖÜäöüß]+/g) trennt an Bindestrich und @,
sodass Export-Empfaenger als Export + Empfaenger zerfaellt.
Gefahr 3 — die E-Mail-Platzhalteradresse.
de.json enthaelt den Wert "empfaenger@example.com" (Schluessel testToPlaceholder).
Das ist eine Beispiel-E-Mail-Adresse, kein deutscher Fliesstext. Ein Umlaut darin wuerde
suggerieren, dass Umlaut-Adressen das erwartete Format sind. Dieser Wert bleibt
unveraendert — Regel: Werte, die ein @ enthalten, werden uebersprungen.
Gefahr 4 — der Waechter darf sich nicht selbst ausloesen.
umlaut-dictionary.ts enthaelt die falschen Schreibweisen zwangslaeufig als Map-Schluessel
('fuer', 'loeschen', …). Ein repo-weites grep nach diesen Zeichenketten wuerde
deshalb immer anschlagen. Alle Pruefungen lesen ausschliesslich die Werte aus de.json
(via JSON-Parse), niemals repo-weit ueber Quelldateien.
| falsch | richtig |
|---|---|
Groesse |
Größe |
Schliessen |
Schließen |
ausschliessen |
ausschließen |
Dazu in mail.service.ts: Gruessen → Grüßen.
Alle anderen 40 ss-Tokens sind korrekt (kurzer Vokal davor) und stehen auf der
Positivliste: Passwort, Adresse, muss, musste, lassen, vergessen, stattdessen,
Wasser, Abwasser, Hessen, Erzeugnisse, Ausschreibung*, erfasst, Erfassung,
Ergebnisse, gedrosselt, ausgeschlossen, Ausschlussliste, Informationssysteme,
Zeitmessung, Tessera, Passen, Passt, Fasst.
UMLAUT_REPLACEMENTS — Record<string, string>, Schluessel ist das falsche Token, Wert das
richtige. Die vollstaendige Liste, aus den Werten von de.json erhoben und Eintrag fuer
Eintrag geprueft:
loeschen→löschen, Loeschen→Löschen, geloescht→gelöscht, hinzufuegen→hinzufügen, Hinzufuegen→Hinzufügen, hinzuzufuegen→hinzuzufügen, einfuegen→einfügen, fuege→füge, Fuege→Füge, Fuegen→Fügen, fuer→für, pruefen→prüfen, Pruefen→Prüfen, Pruefe→Prüfe, Pruefung→Prüfung, geprueft→geprüft, ueberpruefen→überprüfen, Ueberpruefe→Überprüfe, Zertifikatspruefung→Zertifikatsprüfung, aendern→ändern, geaendert→geändert, Moechten→Möchten, moeglich→möglich, moegliche→mögliche, koennen→können, verfuegbar→verfügbar, Verfuegbar→Verfügbar, Verfuegbare→Verfügbare, Verfuegbarkeit→Verfügbarkeit, Zurueck→Zurück, Zuruecksetzen→Zurücksetzen, zuruecksetzen→zurücksetzen, zurueckgesetzt→zurückgesetzt, rueckgaengig→rückgängig, Zusammenfuehren→Zusammenführen, zusammenfuehren→zusammenführen, bestaetigen→bestätigen, Bestaetigen→Bestätigen, Passwoerter→Passwörter, spaeter→später, ueberein→überein, uebersprungen→übersprungen, ueber→über, ungueltig→ungültig, Verschluesselung→Verschlüsselung, entschluesselt→entschlüsselt, ausgewaehlt→ausgewählt, Ausgewaehlt→Ausgewählt, ausgewaehlte→ausgewählte, Ausgewaehlte→Ausgewählte, waehle→wähle, benoetigt→benötigt, noetig→nötig, muessen→müssen, Domaene→Domäne, Einschraenkung→Einschränkung, Empfaenger→Empfänger, Oberflaeche→Oberfläche, Postfaecher→Postfächer, Praefix→Präfix, Sicherheitsgruenden→Sicherheitsgründen, unterstuetzt→unterstützt, vertrauenswuerdigen→vertrauenswürdigen, zusaetzlich→zusätzlich, zusaetzliche→zusätzliche, Groesse→Größe, Schliessen→Schließen, ausschliessen→ausschließen
UMLAUT_ALLOWLIST — readonly string[] der Tokens, die ae/oe/ue/ss enthalten und
bereits korrekt sind, also niemals ersetzt werden duerfen:
manuell, Manuell, Quelle, Quellen, Quell, Quellportal, Kalenderquelle, Kalenderquellen,
Energiequellen, neue, neuen, Neue, Neues, aktuelle, Aktuell, Aktuelle, Aktuelles,
zuerst, dauerhaft, Schauen, Taktfrequenz, Feuerlösch, Bergbauerzeugnisse, true, guest,
Passwort, Adresse, Absenderadresse, muss, musste, müssen, lassen, vergessen, stattdessen,
Wasser, Abwasser, Hessen, Erzeugnisse, Druckerzeugnisse, Gummierzeugnisse,
Mineralölerzeugnisse, Ausschreibung, Ausschreibungen, Ausschreibungs, Ausschreibungsdetails,
erfasst, erfasster, Erfassung, Ergebnisse, gedrosselt, ausgeschlossen, Ausgeschlossen,
Ausschlussliste, Informationssysteme, Zeitmessung, Tessera, Passen, Passt, Fasst,
Verschlüsselung
Dazu zwingend die korrigierten Formen, die nach der Ersetzung immer noch ein ss oder
ue enthalten — sonst schlaegt der Waechter aus Task 2 sofort auf der frisch korrigierten
Datei an: Passwörter (ss), entschlüsselt (ss), ausschließen (ss),
vertrauenswürdigen (ue in "vertra-ue-ns"), Verschlüsselung (ss), müssen (ss).
Das ist der unintuitive Teil der Positivliste: sie beschreibt den Zustand nach dem Fix,
nicht davor.
empfaenger steht bewusst nicht auf der Positivliste: der einzige Fundort ist die
Beispiel-Adresse empfaenger@example.com, und die wird bereits ueber die @-Regel
uebersprungen. Ein Positivlisten-Eintrag waere nicht nur ueberfluessig, er wuerde auch
kuenftigen deutschen Fliesstext mit empfaenger unbemerkt durchlassen.
Kommentiere im Dateikopf, warum die Liste explizit ist (eine Zeichenregel zerstoert Quelle,
manuell, neuen) und dass die @-Regel die Beispiel-Adresse schuetzt.
Wende die Ersetzung dann per Wegwerf-Node-Skript auf apps/web/src/messages/de.json an:
Datei JSON-parsen, rekursiv ueber alle Blattwerte laufen, Werte mit @ ueberspringen,
in den uebrigen Werten jedes Token per \b-begrenztem Ganzwort-Treffer gegen
UMLAUT_REPLACEMENTS ersetzen, mit 2 Leerzeichen Einrueckung und abschliessendem
Zeilenumbruch zurueckschreiben. Nur Werte anfassen, Schluessel und Reihenfolge nie.
Das Skript ist ein Hilfsmittel und wird nicht eingecheckt.
node -e 'const WRONG="loeschen Loeschen geloescht hinzufuegen Hinzufuegen hinzuzufuegen einfuegen fuege Fuege Fuegen fuer pruefen Pruefen Pruefe Pruefung geprueft ueberpruefen Ueberpruefe Zertifikatspruefung aendern geaendert Moechten moeglich moegliche koennen verfuegbar Verfuegbar Verfuegbare Verfuegbarkeit Zurueck Zuruecksetzen zuruecksetzen zurueckgesetzt rueckgaengig Zusammenfuehren zusammenfuehren bestaetigen Passwoerter spaeter ueberein uebersprungen ueber ungueltig Verschluesselung entschluesselt ausgewaehlt Ausgewaehlt ausgewaehlte Ausgewaehlte waehle benoetigt noetig muessen Domaene Einschraenkung Empfaenger Oberflaeche Postfaecher Praefix Sicherheitsgruenden unterstuetzt vertrauenswuerdigen zusaetzlich zusaetzliche Groesse Schliessen ausschliessen".split(" ");const S=new Set(WRONG);const d=require("./apps/web/src/messages/de.json");const hits=[];const walk=(o,p)=>{for(const k of Object.keys(o)){const v=o[k],q=p?p+"."+k:k;if(typeof v==="string"){if(v.includes("@"))continue;for(const w of v.match(/[A-Za-zÄÖÜäöüß]+/g)||[])if(S.has(w))hits.push(q+": "+w)}else if(v&&typeof v==="object")walk(v,q)}};walk(d,"");if(hits.length){console.error("noch falsch:\n"+hits.join("\n"));process.exit(1)}console.log("de.json ok - keine Ersatzschreibweise mehr")'
node -e 'const a=require("./apps/web/src/messages/de.json"),b=require("./apps/web/src/messages/en.json");const K=(o,p="")=>Object.entries(o).flatMap(([k,v])=>v&&typeof v==="object"?K(v,p+k+"."):[p+k]);const A=K(a).sort(),B=K(b).sort();if(A.length!==787||JSON.stringify(A)!==JSON.stringify(B)){console.error("KEY DRIFT");process.exit(1)}console.log("keys ok",A.length)'
de.json enthaelt keinen Schluessel aus UMLAUT_REPLACEMENTS mehr. Jedes verbliebene Token
mit ae/oe/ue/ss steht auf UMLAUT_ALLOWLIST. de.json und en.json haben weiterhin
je 787 identische Schluessel. Der Wert empfaenger@example.com ist unveraendert.
Die Spec importiert de.json sowie UMLAUT_REPLACEMENTS und UMLAUT_ALLOWLIST aus
./umlaut-dictionary. Sie laeuft rekursiv ueber alle Blattwerte, ueberspringt Werte mit @,
tokenisiert mit /[A-Za-zÄÖÜäöüß]+/g und prueft drei Dinge:
Erstens: kein Token ist ein Schluessel aus UMLAUT_REPLACEMENTS. Die Fehlermeldung nennt das
gefundene Token, die richtige Schreibweise und den Schluesselpfad.
Zweitens: jedes Token, das ae, oe, ue oder ss enthaelt, steht auf UMLAUT_ALLOWLIST.
Die Fehlermeldung sagt, dass ein neues Wort aufgetaucht ist, und fordert auf, es entweder in
UMLAUT_REPLACEMENTS (falls Ersatzschreibweise) oder in UMLAUT_ALLOWLIST (falls korrektes
Deutsch) einzutragen. Genau dieser zweite Test ist der eigentliche Waechter: er faellt bei
jeder NEU eingefuehrten Ersatzschreibweise, ohne heutige Texte festzunageln — er pinnt
Vokabular, nicht Wortlaut.
Drittens: de.json und en.json tragen denselben rekursiv abgeflachten Schluesselsatz.
Wichtig: Die Spec liest ausschliesslich das geparste JSON. Sie darf niemals repo-weit ueber Quelldateien greppen, sonst schlaegt sie am Woerterbuch selbst an (siehe Gefahr 4).
Halte den Waechter fuer verlaesslich: Falsch-Positive sind auf neue Woerter beschraenkt, und
die Behebung ist ein Ein-Zeilen-Eintrag mit klarer Anleitung in der Fehlermeldung. Das ist
billiger als eine unbemerkte Regression.
pnpm --filter @tessera/web test -- src/messages/umlaut-guard.spec.ts
pnpm --filter @tessera/web run type-check
umlaut-guard.spec.ts laeuft gruen. Ein probeweise in einen de.json-Wert eingefuegtes
fuer laesst die Spec fehlschlagen mit einer Meldung, die für und den Schluesselpfad nennt
(danach zuruecknehmen). type-check bleibt sauber.
Im Betreff (Z. 34) zuruecksetzen → zurücksetzen. Im Fliesstext der Passwort-Mail
(Z. 41, 43, 46, 48, 50): Passwortzuruecksetzung → Passwortzurücksetzung, fuer → für,
zurueckzusetzen → zurückzusetzen (Template-Literal), gueltig → gültig,
koennen → können, Gruessen → Grüßen. In der Willkommens-Mail (Z. 104, 106):
koennen → können (Template-Literal), Gruessen → Grüßen.
Die englischen Zweige bleiben unberuehrt.
Korrigiere in apps/api/src/domaincheck/domaincheck.seed.ts Zeile 20 die deutsche
Beschreibung: 'Domain-Verfuegbarkeit pruefen' → 'Domain-Verfügbarkeit prüfen'.
Der en-Wert bleibt unberuehrt.
Keine Migration, kein Backfill, kein manueller Schritt: seedModule() ist ein upsert,
dessen update-Zweig description bei jedem Start neu schreibt, und der Seed laeuft in
onModuleInit. Ein API-Neustart bringt bestehende wie frische Installationen auf denselben
Stand. Die anderen drei Modul-Seeds sind bereits korrekt und werden nicht angefasst.
Nodemailer kodiert text selbsttaetig als UTF-8; am Mail-Versand ist nichts umzustellen.
node -e 'const s=require("fs").readFileSync("apps/api/src/mail/mail.service.ts","utf8");const bad=s.match(/[A-Za-z](zuruecksetz|zurueckzusetz|Passwortzuruecksetzung|gueltig|koennen|Gruessen)[a-z]|\bfuer\b/g)||[];if(bad.length){console.error("noch falsch:",[...new Set(bad)].join(" "));process.exit(1)}for(const need of ["zurücksetzen","zurückzusetzen","Passwortzurücksetzung","für","gültig","können","Grüßen"])if(!s.includes(need)){console.error("Korrektur fehlt:",need);process.exit(1)}if((s.match(/Grüßen/g)||[]).length!==2){console.error("Grüßen muss in beiden Mails stehen");process.exit(1)}console.log("mail ok")'
node -e 'const s=require("fs").readFileSync("apps/api/src/domaincheck/domaincheck.seed.ts","utf8");if(!/Domain-Verfügbarkeit prüfen/.test(s)){console.error("seed nicht korrigiert");process.exit(1)}console.log("seed ok")'
pnpm --filter @tessera/api run build
Beide deutschen Mail-Texte und die domaincheck-Beschreibung tragen echte Umlaute, die
englischen Texte sind unveraendert, und die API baut fehlerfrei.
Beide Container muessen vorher neu gebaut werden, weil Web und API aus gebauten Images laufen:
docker compose build web api && docker compose up -d --force-recreate web api
Danach als admin/admin123 auf http://localhost:3000 anmelden und die fuenf Stellen
ansehen, an denen die falsche Schreibweise heute sichtbar ist:
- Anmeldeseite, Slogan: muss
Modulare Workflow-Plattform für Ihr Unternehmenlauten. - Leeres Dashboard:
Klicken Sie auf Bearbeiten, um Widgets hinzuzufügen. - Administration, Rueck-Link:
Zurück zum Dashboard. - Benutzerverwaltung, Schaltflaeche:
Löschen. - Marktplatz:
Verfügbarsowie die ModulbeschreibungDomain-Verfügbarkeit prüfen.
Punkt 5 belegt zugleich, dass der Seed die bestehende Datenbankzeile ueberschrieben hat. Zur Gegenprobe direkt in der Datenbank:
docker compose exec db psql -U tessera -d tessera -c \
"select slug, description->>'de' from \"Module\" where slug='domaincheck';"
Erwartet: Domain-Verfügbarkeit prüfen.
Zusaetzlich stichprobenartig bestaetigen, dass korrektes Deutsch unbeschaedigt blieb —
in der Benutzerverwaltung stehen weiterhin Passwort, Adresse, manuell, und im
Ausschreibungs-Modul Quellen und Ausschreibungen (nicht Qülle, nicht Erzügnisse).
Alle fuenf Bildschirme zeigen echte Umlaute, die Datenbankzeile ebenfalls, und kein korrektes Wort wurde beschaedigt.
Der Nutzer bestaetigt die Schreibweise auf allen fuenf Bildschirmen und in der Datenbankzeile.
<success_criteria>
Die deutsche Oberflaeche, die Passwort-Mail und die Marktplatz-Beschreibung schreiben Umlaute
als Umlaute. Kein zuvor korrektes Wort wurde beschaedigt. Ein Vitest-Waechter blockiert neue
Ersatzschreibweisen in de.json. Weder Wortlaut noch Schluessel wurden veraendert.
</success_criteria>
<backlog_notes> Beim Planen aufgefallen, aber ausdruecklich NICHT Teil dieses Plans (Wortlaut, nicht Schreibweise) — gehoert ins Backlog:
- Der Loeschdialog schreibt
1 Mitgliederstatt1 Mitglied(fehlende Pluralregel). - Der Aktivierungsdialog duzt, waehrend die uebrige Oberflaeche siezt.
Beide stammen aus .planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-UAT-2026-09-07.md
und sind bereits dort vermerkt.
</backlog_notes>