Files
schalli bf7f250c81
Tessera CI/CD / Lint & Type Check (push) Successful in 44s
Tessera CI/CD / Tests (push) Successful in 49s
Tessera CI/CD / Build & Publish Images (push) Successful in 3m51s
docs(quick-260907-hyi): Abnahme dokumentiert, Umlaut-Korrektur abgeschlossen
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
2026-09-07 13:20:36 +02:00

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
apps/web/src/messages/umlaut-dictionary.ts
apps/web/src/messages/de.json
apps/web/src/messages/umlaut-guard.spec.ts
apps/api/src/mail/mail.service.ts
apps/api/src/domaincheck/domaincheck.seed.ts
tokens raw_tokens tasks confidence
58000 29000 3 low
truths artifacts key_links
Die Anmeldeseite zeigt 'Modulare Workflow-Plattform für Ihr Unternehmen'.
Das leere Dashboard zeigt 'Klicken Sie auf Bearbeiten, um Widgets hinzuzufügen'.
Der Marktplatz zeigt 'Verfügbar' und 'Domain-Verfügbarkeit prüfen'.
Die Passwort-Reset-Mail schreibt 'zurücksetzen', 'gültig' und 'Mit freundlichen Grüßen'.
Bereits korrekte Woerter wie Passwort, Adresse, Quelle, manuell, Ausschreibung bleiben unveraendert.
de.json und en.json tragen weiterhin denselben Schluesselsatz (787 Schluessel).
apps/web/src/messages/umlaut-dictionary.ts
apps/web/src/messages/umlaut-guard.spec.ts
umlaut-dictionary.ts ist einzige Quelle sowohl fuer die einmalige Ersetzung als auch fuer den Regressionswaechter.
seedModule() ist ein upsert MIT update-Zweig -> API-Neustart repariert bestehende Module-Zeilen ohne Migration.
Die deutsche Oberflaeche schreibt Umlaute als Buchstabenpaare aus ("fuer" statt "für", "Loeschen" statt "Löschen"). Das sieht jeder Nutzer auf jeder Seite. Dieser Plan korrigiert die Schreibweise an allen drei betroffenen Stellen — Oberflaechentexte, Passwort-Mail und Marktplatz-Beschreibung — und sichert das Ergebnis gegen Rueckfall ab.

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.

Waehrend der Planvalidierung wurde bereits parallel ausgefuehrt — Stand bei Planabgabe:
  • Task 1 erledigt (Commit 3d351c6): umlaut-dictionary.ts angelegt, 67 Tokens / 140 Fundstellen in de.json korrigiert, 787 Schluessel unveraendert.
  • Task 2 erledigt, noch nicht committet: umlaut-guard.spec.ts liegt untracked vor. pnpm --filter @tessera/web test -- src/messages/ laeuft gruen (2 Dateien, 6 Tests).
  • Task 3 offen: mail.service.ts und domaincheck.seed.ts sind 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:
  1. de.json ist 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.

  2. 92 verschiedene Tokens in den de.json-Werten enthalten ae/oe/ue, davon sind 25 bereits korrekt und duerfen nicht angefasst werden. 43 Tokens enthalten ss, davon sind 40 korrekt.

  3. seedModule() ist ein upsert MIT update-Zweig (apps/api/src/module-registry/module-registry.service.ts:167) und laeuft in onModuleInit (domaincheck.module.ts:30). Der update-Zweig schreibt description bei 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.

  4. Die anderen drei Modul-Seeds sind sauber. cert-manager, dkv, tenders tragen keine Ersatzschreibweisen in ihren Beschreibungen — geprueft, nichts zu tun.

  5. de.json und en.json haben heute identische Schluesselsaetze (je 787). Dieser Plan aendert ausschliesslich Werte in de.json, nie Schluessel.

  6. Nur zwei API-Dateien betroffen. Ein Scan ueber apps/api/src findet Ersatzschreibweisen ausschliesslich in mail.service.ts und domaincheck.seed.ts.

**Gefahr 1 — blinde Ersetzung zerstoert korrektes Deutsch.** Eine allgemeine `ue`→`ü`-Regel macht aus `Passwort` … nichts, aber aus `Quelle`→`Qülle`, `manuell`→`manüll`, `neuen`→`nün`, `Erzeugnisse`→`Erzügnisse`. Deshalb: **ausschliesslich Ersetzung ueber eine explizite Token-Liste**, nie ueber eine Zeichenregel.

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.

Die `ss`/`ß`-Frage wird **mitbehandelt, nicht ausgeklammert** — sie ist derselbe Defekt und betrifft nur drei eindeutige Woerter in `de.json`:
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.

Task 1: Woerterbuch anlegen und de.json korrigieren apps/web/src/messages/umlaut-dictionary.ts, apps/web/src/messages/de.json Lege `apps/web/src/messages/umlaut-dictionary.ts` an. Es exportiert zwei Konstanten und ist die einzige Wahrheitsquelle sowohl fuer die einmalige Korrektur als auch fuer den Waechter aus Task 2.

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.

Task 2: Regressionswaechter als Vitest-Spec apps/web/src/messages/umlaut-guard.spec.ts Lege `apps/web/src/messages/umlaut-guard.spec.ts` an, im Stil der bestehenden `tenderRadar-parity.spec.ts` daneben (gleicher Ordner, gleiche Import-Form, Kopfkommentar mit Zweck).

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.

Task 3: API-Texte — Passwort-Mail und Modul-Beschreibung apps/api/src/mail/mail.service.ts, apps/api/src/domaincheck/domaincheck.seed.ts Korrigiere in `apps/api/src/mail/mail.service.ts` ausschliesslich die deutschen Zeichenketten — Wortlaut identisch, nur Zeichen richtig. Betroffen sind beide Mails, nicht nur die Passwort-Mail:

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.

Task 4: Sichtpruefung im Browser und in der Datenbank Der Orchestrator fuehrt diese Pruefung durch — nicht der Executor.

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:

  1. Anmeldeseite, Slogan: muss Modulare Workflow-Plattform für Ihr Unternehmen lauten.
  2. Leeres Dashboard: Klicken Sie auf Bearbeiten, um Widgets hinzuzufügen.
  3. Administration, Rueck-Link: Zurück zum Dashboard.
  4. Benutzerverwaltung, Schaltflaeche: Löschen.
  5. Marktplatz: Verfügbar sowie die Modulbeschreibung Domain-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.

- `pnpm --filter @tessera/web test` laeuft vollstaendig gruen (inkl. neuem Waechter und bestehender `tenderRadar-parity.spec.ts`). - `pnpm --filter @tessera/web run type-check` ist sauber. - `pnpm --filter @tessera/api run build` ist sauber. - `de.json` und `en.json` tragen je 787 identische Schluessel. - Kein Token aus `UMLAUT_REPLACEMENTS` steht mehr in einem `de.json`-Wert. - Sichtpruefung aus Task 4 bestaetigt.

<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 Mitglieder statt 1 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>

Quick-Task — kein SUMMARY noetig. Commits atomar pro Task: - `fix(i18n): Umlaute in de.json korrigieren` - `test(i18n): Waechter gegen neue Umlaut-Ersatzschreibweisen` - `fix(api): Umlaute in Passwort-Mail und Modulbeschreibung`