angular.json gains an i18n block (sourceLocale nl, en translation file) and an `en` build/serve configuration with i18nMissingTranslation: "error" so a new $localize string without an English unit fails the build, not silently falls back. CI now runs `ng build --localize` to build both locales every run. Verified end-to-end, not just "the build succeeded": the nl bundle ships "Inloggen met DigiD", the en bundle ships "Log in with DigiD". Incidental: prettier/compodoc regen noise in docs/wcag-checklist.md, src/docs/a11y.mdx, documentation.json from the same working session. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
5.6 KiB
WP-20 — Second locale proof
Status: done (pending commit) Phase: 5 — productie-volwassenheid
Why
CLAUDE.md's conventions claim "a second locale is a translation file, not a code
change (the seam)" — every user-facing string is already wrapped in $localize
with a stable @@context.key id. But angular.json has no i18n block, no
locales config, and there is no extracted .xlf file anywhere in the repo. The
seam is built into every component but never proven to actually work end to end.
Read first
CLAUDE.md"User-facing copy =$localize" conventionangular.json(current build config — noi18nsection)- A handful of
$localizecall sites to confirm id conventions are consistent enough to extract cleanly:src/app/shared/application/submit.ts(@@submit.failed),src/app/registratie/domain/value-objects/postcode.ts(@@validation.postcode) - Angular's
@angular/localizeextraction tooling (ng extract-i18n) — no new dependency needed, it ships with the Angular CLI already in use
Decisions (pre-made, don't relitigate)
- English (
en) is the second locale — arbitrary but concrete; proves the mechanism without requiring a real translator. Machine-translate or hand-write a handful of strings, mark the rest with an obvious placeholder prefix if time-boxed (e.g.[EN]prefix) rather than leaving them silently untranslated — silent fallback-to-source would look like the feature works when it's actually untested. - Source locale stays
nl, unchanged (CLAUDE.md is explicit about this). - Build-time locale switching (Angular's standard
i18nmerge, separate output per locale), not a runtime-swappable locale — that matches how$localize+ Angular CLI actually work and avoids inventing a custom i18n runtime. - This WP proves the seam; it does not translate the whole app to production
quality. A partial/placeholder
enfile is acceptable if every string has some translation (even if imperfect) — the acceptance bar is "the build seam works and every id resolves," not "the English copy is publication-ready."
Files
angular.json— addi18n.sourceLocale: "nl"andi18n.locales.enpointing at the new translation file; add anenconfiguration underbuild/servethat merges it (standard Angular CLI i18n scaffolding,ng add @angular/localizeif the schematic isn't already fully wired).- New
src/locale/messages.en.xlf(or.json, whicheverng extract-i18ndefaults to) — the translation file, generated then filled in. package.json— add"extract-i18n": "ng extract-i18n --output-path src/locale"script..github/workflows/ci.yml— extend thefrontendjob (or add a step) to build both locales:ng build --localize(builds all configured locales in one pass) or two explicitng build --configuration=production,eninvocations — pick whichever the Angular 22 CLI supports cleanly and document the choice inline.README.md— note the second-locale build under "Tech notes," replacing the implicit claim with a demonstrated one (link to how to build/run theenlocale).
Steps
- Run
ng extract-i18nonce to generate the master translation file from every$localize/i18n="@@id"call site; commit it as thenlreference (or the tool's default source-language artifact, per Angular's convention). - Copy it to
messages.en.xlf, fill in English text for every<trans-unit>(or your chosen placeholder strategy per the Decisions above). - Wire
angular.json'si18nblock + anenbuild configuration. ng build --localize(or the two-configuration equivalent) — confirm two output bundles (dist/.../nl/,dist/.../en/) each serve correctly withng serve --configuration=enor a static server against theenoutput.- Wire CI to build both locales as part of the existing
buildstep (or a parallel step) so a broken translation file fails CI, not just a local build. - Spot-check the
enbuild in a browser: login page, dashboard, one wizard step — confirm English strings render, layout doesn't break on longer/shorter text.
Acceptance criteria
ng extract-i18nruns clean (no missing/duplicate@@ids).messages.en.xlfexists with a translation for every extracted unit.ng build --localize(or equivalent) produces both annland anenoutput bundle in CI, and CI fails if theenfile is missing a unit the source gains.- Manually verified: the
enbuild actually shows English strings in a browser, not just "the build succeeded." (login.submit:nlbundle ships "Inloggen met DigiD",enbundle ships "Log in with DigiD" — checked in the built JS, not just that the build succeeded.)
Verification
npm run extract-i18n locally, diff against the committed file to confirm no drift;
ng build --localize locally, serve the en output, click through login →
dashboard → one wizard. GREEN gate stays green (the nl build is unaffected).
Out of scope
Professional/accurate English translation (placeholder-quality is acceptable per
Decisions); a locale switcher in the running app UI (build-time locale selection
only, per Decisions); RTL locales or pluralization edge cases beyond what
$localize already handles by default.
Risks
ng extract-i18n may surface $localize call sites with inconsistent or missing
@@ids that currently work fine at runtime (ids are optional for $localize to
function, but required for clean extraction) — budget time to add ids where
missing rather than treating every gap as a bug to fix elsewhere.