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

104 lines
5.6 KiB
Markdown

# 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
- [x] `ng extract-i18n` runs clean (no missing/duplicate `@@id`s).
- [x] `messages.en.xlf` exists with a translation for every extracted unit.
- [x] `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.
- [x] 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
`@@id`s 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.