Step-Description Localization
Die .i18n.json-Konvention — wie Step-Beschreibungen in der Sprache eines Testers erscheinen, ohne das Framework-Runtime anzufassen.
← Zurück zur Übersicht · 🇬🇧 English · ← Custom Steps · Credential Management →
Das Problem, das das löst
Step-Beschreibungen — der Text, der an Core.Step.numberedStep(...) / numberedStepBlock(...) übergeben wird, oder das description:/logTitle:-Feld eines Core.defineExecutionStep/Core.defineTestStep-Aufrufs — erscheinen direkt für Tester, die einen manuellen oder halbautomatisierten Testlauf ausführen, zum Beispiel als Spalte in einer Testfall-Übersicht in einem Test-Management-Tool. Dieser Text wird einmal geschrieben, auf Englisch, als Teil des Quellcodes.
Ein Tester, der englische Step-Beschreibungen nicht flüssig liest, muss trotzdem verstehen, was ein Step tut. Diese Seite dokumentiert die Konvention, die es einem Projekt erlaubt, übersetzte Step-Beschreibungen auszuliefern — ohne das Framework-Runtime oder das Quell-Template selbst anzufassen. Das Framework selbst, und dessen eigene Konsolen-/Log-Ausgabe, bleiben englisch; die Lokalisierung passiert als separate, additive, statisch parsbare Schicht.
💡 Warum nicht einfach das Template-Literal in der
.ts-Datei selbst übersetzen? Weil der englische Text in der.ts-Datei sowohl die eigene Runtime-Ausgabe des Frameworks als auch die Quelle der Wahrheit ist, die Tooling liest — ihn direkt zu ändern würde bedeuten, mehrere Sprachvarianten des eigentlichen Codes zu pflegen. Der Katalog hält den Quellcode in einer Sprache, während die Anzeige-Schicht mehrere anbietet.
Die Konvention: eine Sibling-Katalog-Datei
Jede Step-Datei <StepFile>.ts unter 2_Apps/<DeineApp>/2_Steps/ kann eine gleichnamige Katalog-Datei direkt daneben haben:
2_Apps/<DeineApp>/2_Steps/TS_Main.ts
2_Apps/<DeineApp>/2_Steps/TS_Main.i18n.json
Der Katalog ist ein einfaches JSON-Objekt. Jeder Schlüssel ist exakt der Name einer exportierten Step-Funktion/-Konstante aus der Sibling-.ts-Datei; jeder Wert ist ein Objekt, das ein Locale-Kürzel auf den übersetzten Beschreibungstext abbildet:
{
"TS_Main_Button_Click": {
"en": "On Main, click button '${label}'",
"de": "Auf 'Main', Schaltfläche '${label}' klicken"
}
}
Es gibt keine feste oder vorgeschriebene Locale-Liste — füg so viele Locales hinzu, wie dein Projekt braucht, je einen Schlüssel pro exportiertem Step.
Eine neue Locale zu einer Step-Datei hinzufügen
Angenommen, TS_Main.ts enthält:
// 2_Apps/<DeineApp>/2_Steps/TS_Main.ts
export const TS_Main_Button_Click = Core.defineTestStep('<DeineApp>', {
logTitle: (label: string) => `On Main, click button '${label}'`,
run: (label: string) => ControlsButton.click(label),
});
Um Französisch neben Englisch und Deutsch zu ergänzen, bearbeite (oder erstelle) die Sibling-Datei TS_Main.i18n.json:
{
"TS_Main_Button_Click": {
"en": "On Main, click button '${label}'",
"de": "Auf 'Main', Schaltfläche '${label}' klicken",
"fr": "Sur 'Main', cliquer sur le bouton '${label}'"
}
}
Keine Code-Änderung, kein Rebuild, keine Framework-Konfiguration — die Katalog-Datei ist die gesamte Änderung.
💡 Welche Locale-Kürzel sind gültig? Jeder String funktioniert als Schlüssel — die Konvention selbst schreibt keine feste Liste vor. Zwei-Buchstaben-Kürzel wie
en/de/frsind ein sinnvoller Standard; was auch immer den Katalog liest, entscheidet, welche Kürzel es nachschlägt.
Der Platzhalter-Vertrag (kritisch)
Jeder Locale-Eintrag für einen bestimmten Step-Schlüssel muss exakt dieselben ${paramName}-Platzhalter enthalten wie das Quell-Template — gleiche Namen, gleiche Anzahl. Nur die Umgebung im Satz und die Reihenfolge der Platzhalter darf sich ändern.
Korrekt — dieselben zwei Platzhalter, andere Reihenfolge und Formulierung:
"TS_Main_Textfield_Fill": {
"en": "On Main, fill text field '${label}' with '${value}'",
"de": "Auf 'Main', Textfeld '${label}' mit '${value}' befüllen"
}
Falsch — ${value} fehlt im deutschen Eintrag:
"de": "Auf 'Main', Textfeld '${label}' befüllen"
Ein fehlender oder zusätzlicher Platzhalter ist keine Kosmetik. Was auch immer den Katalog rendert, ersetzt jedes ${...} zur Anzeigezeit mit dem tatsächlichen Laufzeit-Argument des Steps — ein weggelassener Platzhalter bedeutet, dass Information für den Tester stillschweigend verschwindet; ein zusätzlicher bedeutet, dass eine Ersetzung nichts zum Binden hat. Beides behandelt der Validator (unten) als Fehler, nicht als Warnung.
Fallback-Verhalten
Fehlt für eine angeforderte Locale ein Eintrag zu einem bestimmten Step-Schlüssel — oder existiert die Katalog-Datei gar nicht — fällt die Anzeige auf en zurück. Das ist der normale, erwartete Fall, kein Fehler: du kannst Katalog-Abdeckung schrittweise ergänzen, eine Step-Datei oder eine Locale nach der anderen, ohne dass von Anfang an jeder Step jede Locale braucht.
Eine Step-Datei ganz ohne Sibling-.i18n.json funktioniert weiterhin genau wie bisher — nur Englisch, von alldem unberührt.
Der Validator
Das Templates-Paket liefert ein eigenständiges Skript, das die in jeder Step-Datei referenzierten KEYs gegen ihren Sibling-.i18n.json-Katalog prüft. Führe es aus pm/ heraus aus, sobald @meintest/cc-testframework-templates dort als Dev-Dependency installiert ist:
node ./node_modules/@meintest/cc-testframework-templates/bin/validate-i18n.js <Pfad>
Zum Beispiel, um jeden Katalog unter dem Apps-Ordner deines Projekts zu prüfen:
node ./node_modules/@meintest/cc-testframework-templates/bin/validate-i18n.js ./2_Apps/
Der Validator extrahiert jeden im Code referenzierten KEY — über einen Core.i18n.t('KEY', ...)-Aufruf oder ein descriptionI18n: { key: 'KEY', ... }-Feld — und prüft:
| Check | Fehler-Bedeutung |
|---|---|
Der KEY hat einen en-Eintrag im Sibling-.i18n.json-Katalog | Dem Katalog fehlt der Quelle-der-Wahrheit-Eintrag für einen Schlüssel, den der Code tatsächlich nutzt |
Jede andere Locale’s Platzhalter-Menge stimmt exakt mit der des en-Eintrags überein | Verstoß gegen den Platzhalter-Vertrag (siehe oben), geprüft gegen en als kanonische Menge |
Ein Katalog-Schlüssel ohne passende KEY-Referenz irgendwo im Code | Nur Warnung, nicht fatal — ein Übersetzer könnte eine Übersetzung vorbereitet haben, bevor der Code sie nutzt |
Liefert ein Step einen fallback (siehe Wann fallback verwenden) und existiert die Katalog-Datei noch gar nicht, akzeptiert der Validator den fallback als Ersatz für den fehlenden en-Eintrag. Sobald ein Katalog existiert, ist dessen en-Eintrag maßgeblich, unabhängig von einem im Code noch vorhandenen fallback.
Exit-Code 0 bedeutet Erfolg — auch dann, wenn gar keine Kataloge existieren (nur-Englisch ist ein vollständig unterstützter Nicht-Fehler-Zustand) und wenn jeder Befund nur eine Warnung ist. Exit-Code 1 bedeutet, dass mindestens ein Katalog-Schlüssel einen der obigen Fehler-Checks nicht bestanden hat; das Skript druckt eine nach Datei/Schlüssel/Locale aufgeschlüsselte Fehlerliste, damit du die Stelle schnell findest.
💡 Dieser Validator sorgt auch dafür, dass die mitgelieferten Beispiel-Kataloge korrekt bleiben. Die
.i18n.json-Dateien unter2_Apps/_Skeleton/2_Steps/werden mit demselben Skript als Teil des eigenen Release-Prozesses des Templates-Pakets geprüft — die Beispiele, von denen du startest, sind also immer intern konsistent. Denselben Befehl in die CI deines eigenen Projekts (oder einen Pre-Commit-Hook) einzubauen, gibt dir dieselbe Garantie für die Kataloge, die du selbst schreibst.
Wie ein Consumer lokalisierten Text rendert
Das Rendern passiert vollständig außerhalb des Framework-Runtimes. Ein programmatischer Consumer — dein eigenes Tooling, ein CI-Report-Generator, ein externes Test-Management-Tool — der lokalisierte Step-Beschreibungen anzeigen will, tut typischerweise drei Dinge:
- Parst die
.ts-Datei statisch, um den exportierten Namen jedes Steps und denKEYzu finden, den er referenziert. Der primäre Check des Validators erkennt zwei solche KEY-referenzierenden Formen, und jedes eigene Tooling kann dasselbe tun:Core.i18n.t('KEY', ...)-Aufrufstellen unddescriptionI18n: { key: 'KEY', ... }-Felder (siehe Neue Steps mit i18n ausstatten). Ein Step, der noch im älterendescription:/logTitle:-Stil geschrieben ist, oder ein roherCore.Step.numberedStep(…)/numberedStepBlock(…)-Aufruf, trägt seinen englischen Text direkt in der.ts-Datei statt einesKEY— siehe Abschnitt “Abwärtskompatibilität” weiter unten. - Liest den Sibling-
.i18n.json-Katalog. Für einenKEY-basierten Step ist deren-Eintrag des Katalogs die Quelle der Wahrheit für den englischen Text — nicht die.ts-Datei —, und jede andere Locale wird genauso nachgeschlagen, mit Fallback aufengemäß obiger Regel. Einfallback:-String, der zusätzlich im Code stehen kann (siehe Wann fallback verwenden), spielt nur eine Rolle, wenn noch gar kein Katalog existiert. - Ersetzt jeden
${paramName}-Platzhalter im gewählten Text mit dem tatsächlichen Argument-Wert des Steps im Moment des Renderns — dieselben Werte, die das Framework selbst zur Laufzeit ins englische Template eingesetzt hätte.
Nichts davon erfordert, das TypeScript des Frameworks auszuführen. Schritte 1 und 2 sind reines statisches Parsen; Schritt 3 ist einfache String-Ersetzung.
💡 Das Framework-Runtime selbst führt inzwischen ein gleichwertiges Rendering aus, zu einem anderen Zeitpunkt. Dieser Abschnitt beschreibt einen statischen Consumer, der das
.ts/.i18n.json-Paar liest, ohne irgendeinen Code des Frameworks auszuführen — nützlich für eine im Voraus generierte Testfall-Übersicht. Seit das Framework selbst dieselben Kataloge auch liest (siehe Laufzeit-Lokalisierung unten), existieren beide Pfade nebeneinander: ein statischer Consumer kann weiterhin einen Katalog für eine Testfall-Übersicht im Voraus rendern, während das Framework denselben Katalog separat live rendert, im Moment, in dem jeder Step tatsächlich ausgeführt wird, in seine eigenen Reports und Logs.
Laufzeit-Lokalisierung
Die Ausführung des Frameworks selbst — nicht nur ein statischer Consumer — rendert jetzt lokalisierte Step-Titel direkt in Playwrights Reports, die Konsole und den Self-Healing-Writeback-Report, aus denselben oben beschriebenen .i18n.json-Katalogen.
GlobalConfig.language
Füge deinem Projekt in pm/2_Apps/1_Global/GlobalConfig.ts ein optionales language-Feld hinzu, neben dem bestehenden apps-Export:
// pm/2_Apps/1_Global/GlobalConfig.ts
export const apps = {
// ... deine AUT-Einträge
};
export const language = 'de'; // NEU — 'de' oder 'en'. Standard: 'en'.
Es gibt keinen eigenen GlobalConfig-Typ zum Importieren — language wird genauso gelesen wie bereits apps: ein einfacher benannter Export aus deiner eigenen GlobalConfig.ts, den das Framework zur Laufzeit aufgreift. Das Feld ist vollständig optional; ein Projekt, das es nicht setzt (inklusive jedes Projekts, das älter als dieses Feature ist), behält den Standardwert 'en'.
Umgebungsvariable CC_TESTFRAMEWORK_LOCALE
Setze diese Umgebungsvariable, um GlobalConfig.language zu überschreiben, ohne die Config-Datei zu bearbeiten — nützlich für einen CI-Job, der dieselbe Suite einmal je Locale laufen lassen muss:
CC_TESTFRAMEWORK_LOCALE=de npx playwright test
Die effektive Locale wird über diese Prioritätskette aufgelöst, höchste Priorität zuerst:
| Priorität | Quelle | Hinweise |
|---|---|---|
| 1 | Core.i18n.setLocale('de') | Expliziter In-Process-Override — z.B. aus eigenem Bootstrap-Code. Gewinnt gegen alles darunter. |
| 2 | Umgebungsvariable CC_TESTFRAMEWORK_LOCALE | Live gelesen, nicht zwischengespeichert. |
| 3 | GlobalConfig.language | Das oben gezeigte Feld. |
| 4 | 'en' | Framework-Standard, wenn nichts anderes zutrifft. |
Core.i18n.getLocale() gibt zurück, auf welche Locale diese Kette aktuell auflöst.
💡 Nur
'de'und'en'sind heute gültige Laufzeit-Locale-Werte. Das.i18n.json-Katalogformat selbst begrenzt nicht, wie viele Locale-Schlüssel du auflistest (siehe Abschnitt „Eine neue Locale zu einer Step-Datei hinzufügen“ weiter oben) — ein Katalog mit einem'fr'-Eintrag bleibt für einen statischen Consumer, der die Datei direkt liest, vollkommen gültig und lesbar. Die eigene Prioritätskette des Frameworks oben löst jedoch immer nur zu'de'oder'en'auf; sie wählt nie selbstständig eine dritte Locale.
Was lokalisiert wird
Alles, was das Framework selbst rendert und dabei den Titel eines Steps zeigt:
- Der Step-Name in Playwrights HTML- und JSON-Reports.
- Die entsprechende Konsolen-/Log-Zeile, die das Framework beim Ausführen jedes Steps druckt.
- Step-Titel-Einträge innerhalb eines Self-Healing-Writeback-Reports (siehe Self-Healing).
Was auf Englisch bleibt
- An Entwickler gerichtete Fehlermeldungen und geworfene Exceptions.
- Stack-Traces.
- Framework-internes Debug-/Diagnose-Logging (zum Beispiel aus den CLI-Tools).
Diese richten sich an, wer den Testcode wartet, nicht an den Tester, der einen Report liest — deshalb liegen sie außerhalb dieser Lokalisierungs-Schicht.
Neue Steps mit i18n ausstatten
Zwei Wege, einen Step in die Laufzeit-Lokalisierung einzubinden — beide lösen automatisch, aus dem eigenen Dateipfad des Steps, denselben oben beschriebenen Sibling-.i18n.json-Katalog auf.
Factory-basiert (empfohlen für Core.defineTestStep / Core.defineExecutionStep) — füge ein optionales descriptionI18n-Feld neben (oder anstelle von) logTitle/description hinzu:
export const TS_MyStep = Core.defineTestStep('<DeineApp>', {
descriptionI18n: {
key: 'TS_MyStep', // entspricht dem Katalog-Schlüssel
values: (label: string) => ({ label }), // Platzhalter-Werte
},
run: (label: string) => ControlsButton.click(label),
});
key— der.i18n.json-Eintrag, den dieser Step liest; dessenen-Eintrag ist die englische Quelle der Wahrheit.values(...)— bekommt dieselben Argumente wierun, liefert die${placeholder}-Ersetzungswerte.
fallback fehlt in diesem Beispiel bewusst — es ist optional, und für einen Factory-basierten Step mit Katalog unnötig. Siehe Wann fallback verwenden weiter unten für den einen Fall, in dem es seinen Platz verdient.
Direkter t()-Aufruf — für einen handgeschriebenen Custom Step, oder jeden rohen Core.Step.numberedStep(...)-Aufruf ohne Factory, an die man descriptionI18n hängen könnte:
Core.Step.numberedStep(
Core.i18n.t('TS_Custom_Example', { label }, {
fallback: `Custom-Aktion mit '${label}' ausführen`,
}),
async () => { /* ... */ },
);
Beide Formen finden den Sibling-<StepFile>.i18n.json-Katalog automatisch — du übergibst nie selbst einen Katalog-Pfad.
Wann fallback verwenden
Der Standardfall — ein Factory-basierter Step mit einem descriptionI18n.key und einem passenden .i18n.json-Katalog — braucht nie fallback. Der en-Eintrag des Katalogs ist die Quelle der Wahrheit; es gibt keinen zweiten Ort, an dem derselbe englische Text leben würde, und der Validator erzwingt, dass jeder referenzierte Schlüssel tatsächlich einen solchen Eintrag hat.
fallback verdient sich seinen Platz in genau einer Situation: einem Custom Step, dessen Code im Moment seiner Erstellung generiert wird — typischerweise vom Authoring-Agent — ohne einen bereits existierenden Katalog, aus dem gelesen werden könnte. Bis ein Übersetzer (Mensch oder Tool) einen .i18n.json-Eintrag für diesen Schlüssel ergänzt, ist fallback der einzige englische Text, der für den Step überhaupt existiert — er ist die Quelle der Wahrheit zur Laufzeit, bis (falls überhaupt) ein Katalog-Eintrag dazukommt. Das Beispiel Direkter t()-Aufruf oben, und das ausgelieferte TS_Custom.ts-Template, dokumentiert in Custom-Steps lokalisieren, sind das kanonische Beispiel für dieses Muster.
Existiert erst einmal ein Katalog-Eintrag für diesen Schlüssel, gewinnt der Katalog — fallback greift nur, wenn gar kein Katalog gefunden wird, nie um einen bereits vorhandenen zu überschreiben oder zu umgehen. (Das ist ein anderer Mechanismus als das weiter oben beschriebene katalog-interne Fallback-Verhalten, bei dem einer existierenden Katalog-Datei eine einzelne Locale fehlt — nicht der ganze Katalog.)
Abwärtskompatibilität
Steps, die weiterhin das ältere description:-Feld (Core.defineExecutionStep) oder logTitle:-Feld (Core.defineTestStep) nutzen, funktionieren exakt wie bisher weiter — keine Code-Änderung nötig, und sie rendern immer auf Englisch, unabhängig von GlobalConfig.language oder CC_TESTFRAMEWORK_LOCALE.
Eine bestehende Step-Datei auf descriptionI18n: zu migrieren ist vollständig optional. Apps, die aus 2_Apps/_Skeleton/ generiert werden, nutzen bereits descriptionI18n: für jeden generierten Step — neue Projekte sind damit von Anfang an laufzeit-i18n-fähig. Bestehende Step-Dateien können bei description:/logTitle: bleiben, bis tatsächlich ein Locale-Bedarf entsteht — beide Formen lassen sich innerhalb desselben Projekts, sogar innerhalb derselben Step-Datei, frei mischen.
Ein descriptionI18n:-Block eines Steps, der noch ein fallback:-Feld aus der Zeit vor dieser Umstellung auf Katalog-first enthält, funktioniert unverändert weiter — fallback war auf Typ-Ebene schon immer optional und wird schlicht nicht mehr gelesen, sobald der zugehörige Katalog-Eintrag existiert. Ein jetzt überflüssiges fallback:-Feld zu entfernen ist ein mechanisches Aufräumen in eigenem Tempo, keine vom Framework geforderte Pflicht.
Wo es weitergeht
- Custom Steps — woher die
numberedStep/description/logTitle-Templates kommen, die diese Konvention übersetzt - API-Referenz — das
.i18n.json-Schema und die Validator-Kurzreferenz - Deinen ersten TestCase schreiben — die
TS_<Type>_<Action>-Step-Naming-Konvention, der diekeys in diesen Beispielen folgen - FAQ — Beschreibungen lokalisieren und Validator-Troubleshooting
📧 Fragen? Kontakt: jens.szelag@itsbusiness.ch
itsbusiness AG · Bern · Schweiz