Files
atomic-design-poc/docs/backlog/WP-20-second-locale.md
Edwin van den Houdt e276629107 feat(fp): WP-20 — second locale proof (nl/en build seam)
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>
2026-07-04 18:16:11 +02:00

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" convention
  • angular.json (current build config — no i18n section)
  • A handful of $localize call 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/localize extraction 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 i18n merge, 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 en file 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 — add i18n.sourceLocale: "nl" and i18n.locales.en pointing at the new translation file; add an en configuration under build/serve that merges it (standard Angular CLI i18n scaffolding, ng add @angular/localize if the schematic isn't already fully wired).
  • New src/locale/messages.en.xlf (or .json, whichever ng extract-i18n defaults 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 the frontend job (or add a step) to build both locales: ng build --localize (builds all configured locales in one pass) or two explicit ng build --configuration=production,en invocations — 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 the en locale).

Steps

  1. Run ng extract-i18n once to generate the master translation file from every $localize/i18n="@@id" call site; commit it as the nl reference (or the tool's default source-language artifact, per Angular's convention).
  2. Copy it to messages.en.xlf, fill in English text for every <trans-unit> (or your chosen placeholder strategy per the Decisions above).
  3. Wire angular.json's i18n block + an en build configuration.
  4. ng build --localize (or the two-configuration equivalent) — confirm two output bundles (dist/.../nl/, dist/.../en/) each serve correctly with ng serve --configuration=en or a static server against the en output.
  5. Wire CI to build both locales as part of the existing build step (or a parallel step) so a broken translation file fails CI, not just a local build.
  6. Spot-check the en build 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-i18n runs clean (no missing/duplicate @@ids).
  • messages.en.xlf exists with a translation for every extracted unit.
  • ng build --localize (or equivalent) produces both an nl and an en output bundle in CI, and CI fails if the en file is missing a unit the source gains.
  • Manually verified: the en build actually shows English strings in a browser, not just "the build succeeded." (login.submit: nl bundle ships "Inloggen met DigiD", en bundle 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.