Files
atomic-design-poc/docs/backlog/WP-20-second-locale.md
Edwin van den Houdt cbb8ae548c
Some checks failed
CI / storybook-a11y (push) Successful in 4m12s
CI / backend (push) Successful in 1m6s
CI / api-client-drift (push) Successful in 1m37s
CI / frontend (push) Successful in 1m31s
CI / codeql (csharp) (push) Failing after 1m51s
CI / codeql (javascript-typescript) (push) Failing after 1m24s
docs(backlog): add WP-18..22 (productie-volwassenheid phase)
Gap analysis found the POC's designed-but-unbuilt strategic gaps: ABAC
authorization (ADR-0002/PRD-0002 phase P1), no e2e coverage, unproven
i18n second-locale seam, thin resilience seams (correlation-id,
idempotency, retry), and in-memory-only persistence. Each WP is grounded
in the current code (file paths + line numbers), not just the analysis.

Also corrects PRD-0001's stale 'Proposed' status header — the Mijn
aanvragen vertical is fully built.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 20:07:05 +02:00

5.4 KiB

WP-20 — Second locale proof

Status: todo 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."

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.