Step-Description Localization
The .i18n.json convention — how step descriptions surface in a tester’s own language, without touching the framework’s runtime.
← Back to overview · 🇩🇪 Deutsch · ← Custom Steps · Credential Management →
The problem this solves
Step descriptions — the text passed into Core.Step.numberedStep(...) / numberedStepBlock(...), or the description:/logTitle: field of a Core.defineExecutionStep/Core.defineTestStep call — surface directly to testers running a manual or semi-automated test, for example as a column in a test-case overview inside a test-management tool. That text is written once, in English, as part of the source code.
A tester who isn’t comfortable reading English step descriptions still needs to understand what a step does. This page documents the convention that lets a project ship translated step descriptions without touching the framework’s runtime or the source template at all — the framework itself, and its own console/log output, stay English; localization is a separate, additive, static-parseable layer.
💡 Why not just translate the template literal in the
.tsfile itself? Because the English text in the.tsfile is both the framework’s own runtime output and the source-of-truth that tooling reads — changing it in place would mean maintaining several language variants of your actual code. The catalog keeps your source code in one language while letting the display layer offer several.
The convention: a sibling catalog file
Any step file <StepFile>.ts under 2_Apps/<YourApp>/2_Steps/ may have a same-named catalog file directly next to it:
2_Apps/<YourApp>/2_Steps/TS_Main.ts
2_Apps/<YourApp>/2_Steps/TS_Main.i18n.json
The catalog is a plain JSON object. Each key is the exact name of an exported step function/constant from the sibling .ts file; each value is an object mapping a locale code to the translated description text:
{
"TS_Main_Button_Click": {
"en": "On Main, click button '${label}'",
"de": "Auf 'Main', Schaltfläche '${label}' klicken"
}
}
There is no fixed or required locale list — add as many locales as your project needs, one key per exported step.
Adding a new locale to a step file
Suppose TS_Main.ts contains:
// 2_Apps/<YourApp>/2_Steps/TS_Main.ts
export const TS_Main_Button_Click = Core.defineTestStep('<YourApp>', {
logTitle: (label: string) => `On Main, click button '${label}'`,
run: (label: string) => ControlsButton.click(label),
});
To add French alongside English and German, edit (or create) the sibling 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}'"
}
}
No code change, no rebuild, no framework configuration — the catalog file is the entire change.
💡 Which locale codes are valid? Any string works as a key — the convention itself doesn’t enforce a fixed list. Two-letter codes such as
en/de/frare a sensible default; whatever reads the catalog decides which codes it looks up.
The placeholder contract (critical)
Every locale entry for a given step key must use exactly the same ${paramName} placeholders as the source template — same names, same count. Only the surrounding wording, and the order the placeholders appear in the sentence, may change.
Valid — same two placeholders, different order and wording:
"TS_Main_Textfield_Fill": {
"en": "On Main, fill text field '${label}' with '${value}'",
"de": "Auf 'Main', Textfeld '${label}' mit '${value}' befüllen"
}
Invalid — ${value} is missing from the German entry:
"de": "Auf 'Main', Textfeld '${label}' befüllen"
A missing or extra placeholder isn’t cosmetic. Whatever renders the catalog substitutes each ${...} with the step’s actual runtime argument at display time — a dropped placeholder means information silently disappears from what the tester sees; an extra one means a substitution has nothing to bind to. Both are treated as errors by the validator (below), not warnings.
Fallback behavior
If a requested locale has no entry for a given step key — or the catalog file doesn’t exist at all — the display falls back to en. This is the normal, expected case, not an error: you can add catalog coverage incrementally, one step file or one locale at a time, without every step needing every locale from day one.
A step file with no sibling .i18n.json at all keeps working exactly as it always has — English-only, unaffected by any of this.
The validator
The templates package ships a standalone script that checks the KEYs referenced in every step file against their sibling .i18n.json catalog. Run it from inside pm/, once @meintest/cc-testframework-templates is installed there as a dev dependency:
node ./node_modules/@meintest/cc-testframework-templates/bin/validate-i18n.js <path>
For example, to validate every catalog under your project’s apps folder:
node ./node_modules/@meintest/cc-testframework-templates/bin/validate-i18n.js ./2_Apps/
The validator extracts every KEY referenced in code — via a Core.i18n.t('KEY', ...) call or a descriptionI18n: { key: 'KEY', ... } field — and checks:
| Check | Failure meaning |
|---|---|
The KEY has an en entry in the sibling .i18n.json catalog | The catalog is missing its source-of-truth entry for a key the code actually uses |
Every other locale’s placeholder set exactly matches the en entry’s | Placeholder contract violation (see above), checked against en as the canonical set |
A catalog key with no matching KEY reference anywhere in code | Warning only, not fatal — a translator may have staged a translation ahead of the code that uses it |
If a step supplies a fallback (see When to use fallback) and the catalog file doesn’t exist yet at all, the validator accepts the fallback as a stand-in for the missing en entry. Once a catalog exists, its en entry is authoritative regardless of any fallback still present in the code.
Exit code 0 means success — including the case where no catalogs exist at all (English-only is a fully supported, non-error state) and the case where every finding is a warning. Exit code 1 means at least one catalog key failed a failure-level check above; the script prints a file/key/locale-scoped error list so you can locate the fix quickly.
💡 This validator is also how the shipped example catalogs stay correct. The
.i18n.jsonfiles bundled under2_Apps/_Skeleton/2_Steps/are checked with this same script as part of the templates package’s own release process — so the examples you start from are always internally consistent. Wiring the same command into your own project’s CI (or a pre-commit hook) gives you the same guarantee for the catalogs you write yourself.
How a consumer renders localized text
Rendering happens entirely outside the framework’s runtime. A programmatic consumer — your own tooling, a CI report generator, an external test-management tool — that wants to show localized step descriptions typically does three things:
- Statically parses the
.tsfile to find each step’s exported name and theKEYit references. The validator’s primary check recognizes two such KEY-referencing shapes, and any custom tooling can do the same:Core.i18n.t('KEY', ...)call-sites anddescriptionI18n: { key: 'KEY', ... }fields (see Writing new steps with i18n support). A step still written in the olderdescription:/logTitle:style, or a bareCore.Step.numberedStep(…)/numberedStepBlock(…)call, carries its English text directly in the.tsfile instead of aKEY— see Backward compatibility. - Reads the sibling
.i18n.jsoncatalog. For aKEY-based step, the catalog’senentry is the source-of-truth English text — not the.tsfile — and every other locale is looked up the same way, falling back toenper the rule above. Afallback:string that may also appear in the code (see When to use fallback) only matters when no catalog exists yet at all. - Substitutes each
${paramName}placeholder in the chosen text with the step’s actual argument value at the moment it renders — the same values the framework itself would have substituted into the English template at runtime.
None of this requires executing the framework’s TypeScript. Steps 1 and 2 are pure static parsing; step 3 is plain string substitution.
💡 The framework’s own runtime now performs an equivalent render, at a different time. This section describes a static consumer reading the
.ts/.i18n.jsonpair without running any of the framework’s code — useful for a test-case overview generated ahead of time. Since the framework itself started reading these same catalogs too (see Runtime localization below), the two paths coexist: a static consumer can still pre-render a catalog for a test-case overview, while the framework separately renders the same catalog live, at the moment each step actually executes, into its own reports and logs.
Runtime localization
The framework’s own execution — not just a static consumer — now renders localized step titles directly into Playwright’s reports, the console, and the Self-Healing writeback report, from the same .i18n.json catalogs described above.
GlobalConfig.language
Add an optional language field to your project’s pm/2_Apps/1_Global/GlobalConfig.ts, next to the existing apps export:
// pm/2_Apps/1_Global/GlobalConfig.ts
export const apps = {
// ... your AUT entries
};
export const language = 'de'; // NEW — 'de' or 'en'. Default: 'en'.
There is no dedicated GlobalConfig type to import — language is read the same way apps already is: a plain named export from your own GlobalConfig.ts, picked up by the framework at runtime. The field is entirely optional; a project that doesn’t set it (including every project that predates this feature) keeps the default, 'en'.
CC_TESTFRAMEWORK_LOCALE environment variable
Set this environment variable to override GlobalConfig.language without editing the config file — useful for a CI job that needs to run the same suite once per locale:
CC_TESTFRAMEWORK_LOCALE=de npx playwright test
The effective locale is resolved through this priority chain, highest first:
| Priority | Source | Notes |
|---|---|---|
| 1 | Core.i18n.setLocale('de') | Explicit in-process override — e.g. from your own bootstrap code. Wins over everything below. |
| 2 | CC_TESTFRAMEWORK_LOCALE environment variable | Read live, not cached. |
| 3 | GlobalConfig.language | The field shown above. |
| 4 | 'en' | Framework default when nothing else applies. |
Core.i18n.getLocale() returns whichever locale this chain currently resolves to.
💡 Only
'de'and'en'are valid runtime locale values today. The.i18n.jsoncatalog format itself doesn’t limit how many locale keys you list (see Adding a new locale to a step file) — a catalog with an'fr'entry stays perfectly valid and readable by a static consumer. The framework’s own priority chain above, however, only ever resolves to'de'or'en'; it never selects a third locale on its own.
What gets localized
Anything the framework itself renders that surfaces a step’s title:
- The step name shown in Playwright’s HTML and JSON reports.
- The corresponding console/log line the framework prints as each step runs.
- Step-title entries inside a Self-Healing writeback report (see Self-Healing).
What stays English
- Dev-facing error messages and thrown exceptions.
- Stack traces.
- Framework-internal debug/diagnostic logging (for example, from the CLI tools).
These are aimed at whoever maintains the test code rather than at the tester reading a report, so they sit outside this localization layer.
Writing new steps with i18n support
Two ways to opt a step into runtime localization — both resolve the same sibling .i18n.json catalog described above, automatically, from the step’s own file location.
Factory-based (recommended for Core.defineTestStep / Core.defineExecutionStep) — add an optional descriptionI18n field alongside (or instead of) logTitle/description:
export const TS_MyStep = Core.defineTestStep('<YourApp>', {
descriptionI18n: {
key: 'TS_MyStep', // matches the catalog key
values: (label: string) => ({ label }), // placeholder values
},
run: (label: string) => ControlsButton.click(label),
});
key— the.i18n.jsonentry this step reads; itsenentry is the source-of-truth English text.values(...)— receives the same arguments asrun, returns the${placeholder}substitution map.
fallback is deliberately absent from this example — it’s optional, and for a factory-based step with a catalog it’s unnecessary. See When to use fallback below for the one case where it earns its place.
Direct t() call — for a hand-written Custom Step, or any raw Core.Step.numberedStep(...) call with no factory to attach descriptionI18n to:
Core.Step.numberedStep(
Core.i18n.t('TS_Custom_Example', { label }, {
fallback: `Do custom thing with '${label}'`,
}),
async () => { /* ... */ },
);
Both shapes locate the sibling <StepFile>.i18n.json catalog automatically — you never pass a catalog path yourself.
When to use fallback
The standard case — a factory-based step with a descriptionI18n.key and a matching .i18n.json catalog — never needs fallback. The catalog’s en entry is the source-of-truth; there’s no second place for the same English text to live, and the validator enforces that every referenced key actually has one.
fallback earns its place in exactly one situation: a Custom Step whose code is generated at the moment it’s authored — typically by the Authoring Agent — with no pre-existing catalog to read from. Until a translator (human or otherwise) adds a .i18n.json entry for that key, fallback is the only English text that exists for the step at all — it’s the runtime source-of-truth until (if ever) a catalog entry shows up. The direct t() call example above, and the shipped TS_Custom.ts template documented in Localizing Custom-Steps, are the canonical example of this pattern.
Once a catalog entry for that key does exist, the catalog wins — fallback only ever applies when no catalog is found at all, never as a way to override or bypass one that’s already there. (This is a different mechanism from the catalog-level Fallback behavior described earlier, which is about a single locale missing from an existing catalog, not about the catalog being absent entirely.)
Backward compatibility
Steps still using the older description: field (Core.defineExecutionStep) or logTitle: field (Core.defineTestStep) keep working exactly as before — no code change required, and they always render in English regardless of GlobalConfig.language or CC_TESTFRAMEWORK_LOCALE.
Migrating an existing step file to descriptionI18n: is entirely optional. Apps scaffolded from 2_Apps/_Skeleton/ already use descriptionI18n: for every generated step, so new projects are runtime-i18n-aware from the start. Existing step files can stay on description:/logTitle: until a locale requirement actually arises — the two forms mix freely within the same project, and even within the same step file.
A step’s descriptionI18n: block that still includes a fallback: field from before this convention flipped to catalog-first keeps working unchanged — fallback was always optional at the type level, and simply stops being read once its catalog entry exists. Removing a now-redundant fallback: field is a mechanical cleanup you can do at your own pace, not something this framework requires.
Where to go next
- Custom Steps — where the
numberedStep/description/logTitletemplates that this convention translates come from - API Reference — the
.i18n.jsonschema and validator quick-reference - Writing Your First TestCase — the
TS_<Type>_<Action>step-naming convention thekeys in these examples follow - FAQ — localizing descriptions and validator troubleshooting
📧 Questions? Contact: jens.szelag@itsbusiness.ch
itsbusiness AG · Bern · Switzerland