From 94ffcf3d415e08922b957a0514365ba6481d4b83 Mon Sep 17 00:00:00 2001 From: Edwin van den Houdt Date: Sat, 27 Jun 2026 13:42:12 +0200 Subject: [PATCH] Step 1: i18n foundation (@angular/localize) + data inventory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Domain reference data already lives in the backend (per ADR-0001); the only residual "move" was ~90 inlined Dutch UI strings with no i18n layer. - Wire @angular/localize ($localize) — Angular first-party, no third-party lib - Pin the pattern on shared/ui/async: Dutch fallbacks → language-agnostic input()s with localizable $localize defaults (English-shared-UI rule) - CLAUDE.md: drop i18n (now in scope) + stale "real backend"/"OpenAPI codegen" (both already shipped); document the $localize convention Bulk string sweep deferred to Step 2 to avoid touching ~36 files twice. Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 11 +- angular.json | 21 +--- package-lock.json | 119 +++++++++++++++++++++ package.json | 5 +- src/app/shared/ui/async/async.component.ts | 12 ++- src/main.ts | 5 +- tsconfig.app.json | 11 +- tsconfig.spec.json | 9 +- 8 files changed, 154 insertions(+), 39 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c430636..6f7cc3b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -122,6 +122,13 @@ not heavy component tests. - **Naming:** shared/reusable UI is **English** (language-agnostic: `button`, `wizard-shell`); domain contexts are **Dutch** (`registratie`, `herregistratie`, `*.machine.ts`). Pick the language by which side of the seam the code is on. +- **User-facing copy = `$localize`.** Every user-visible string is wrapped in Angular's + first-party `$localize` (no third-party i18n lib), with a stable custom id + (`` $localize`:@@context.key:Tekst` ``). Source locale is `nl`; a second locale is a + translation file, not a code change (the seam). Shared/English components must **not** + hardcode Dutch — expose copy as `input()`s with localizable defaults; the domain caller + supplies the text (see `shared/ui/async`). Format-validation messages in + `domain/value-objects/` stay co-located but are still `$localize`-wrapped. - **Forms = one idiom.** Any form with validation or submission uses a `*.machine.ts` (Model/Msg/reduce) + value objects + a `submit-*` command returning `Result` — the same shape as the wizards, whether it's one step or many. Don't hand-roll mutable @@ -148,5 +155,5 @@ dispatch messages). Worked example: the intake wizard (`herregistratie/`). ## Out of scope (POC, don't build unprompted) -Real auth/DigiD, real backend, i18n, NgRx, licensed Rijkshuisstijl font/logo, -runtime DTO validation on every endpoint, multi-tab session sync, OpenAPI codegen. +Real auth/DigiD, NgRx, licensed Rijkshuisstijl font/logo, +runtime DTO validation on every endpoint, multi-tab session sync. diff --git a/angular.json b/angular.json index aa0e737..d1eecd6 100644 --- a/angular.json +++ b/angular.json @@ -30,9 +30,8 @@ "input": "public" } ], - "styles": [ - "src/styles.scss" - ] + "styles": ["src/styles.scss"], + "polyfills": ["@angular/localize/init"] }, "configurations": { "production": { @@ -85,12 +84,7 @@ "configDir": ".storybook", "browserTarget": "atomic-design-poc:build", "compodoc": true, - "compodocArgs": [ - "-e", - "json", - "-d", - "." - ], + "compodocArgs": ["-e", "json", "-d", "."], "port": 6006 } }, @@ -100,16 +94,11 @@ "configDir": ".storybook", "browserTarget": "atomic-design-poc:build", "compodoc": true, - "compodocArgs": [ - "-e", - "json", - "-d", - "." - ], + "compodocArgs": ["-e", "json", "-d", "."], "outputDir": "storybook-static" } } } } } -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index 5d80276..7f8e350 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "@angular/build": "^22.0.4", "@angular/cli": "^22.0.4", "@angular/compiler-cli": "^22.0.0", + "@angular/localize": "^22.0.4", "@angular/platform-browser-dynamic": "^22.0.0", "@compodoc/compodoc": "^1.2.1", "@storybook/addon-a11y": "^10.4.6", @@ -781,6 +782,79 @@ "rxjs": "^6.5.3 || ^7.4.0" } }, + "node_modules/@angular/localize": { + "version": "22.0.4", + "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-22.0.4.tgz", + "integrity": "sha512-BJELtGDcTqPjNn10pUa5Ly0Zv+yOyhNOhMl7OA7izRwgK0bWCqgTRnCFh9OWuvn+KyTLfsd9HKVxSDTmtJYEJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "7.29.7", + "@types/babel__core": "7.20.5", + "tinyglobby": "^0.2.12", + "yargs": "^18.0.0" + }, + "bin": { + "localize-extract": "tools/bundles/src/extract/cli.js", + "localize-migrate": "tools/bundles/src/migrate/cli.js", + "localize-translate": "tools/bundles/src/translate/cli.js" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "@angular/compiler": "22.0.4", + "@angular/compiler-cli": "22.0.4" + } + }, + "node_modules/@angular/localize/node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@angular/localize/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@angular/localize/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@angular/platform-browser": { "version": "22.0.2", "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-22.0.2.tgz", @@ -8317,6 +8391,51 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", diff --git a/package.json b/package.json index 86e9168..7c04455 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@angular/build": "^22.0.4", "@angular/cli": "^22.0.4", "@angular/compiler-cli": "^22.0.0", + "@angular/localize": "^22.0.4", "@angular/platform-browser-dynamic": "^22.0.0", "@compodoc/compodoc": "^1.2.1", "@storybook/addon-a11y": "^10.4.6", @@ -58,6 +59,8 @@ "webpack-dev-server": "^5.2.5", "sockjs": "^0.3.24", "uuid": "^11.1.1", - "eslint": { "ajv": "^6.12.6" } + "eslint": { + "ajv": "^6.12.6" + } } } diff --git a/src/app/shared/ui/async/async.component.ts b/src/app/shared/ui/async/async.component.ts index 067421f..1d4f17c 100644 --- a/src/app/shared/ui/async/async.component.ts +++ b/src/app/shared/ui/async/async.component.ts @@ -46,15 +46,15 @@ export class AsyncErrorDirective { } @else { - Er ging iets mis bij het laden van de gegevens. + {{ errorText() }}
- Opnieuw proberen + {{ retryText() }}
} } @case ('Empty') { @if (emptyTpl()) { } - @else {

Geen gegevens gevonden.

} + @else {

{{ emptyText() }}

} } @case ('Success') { { data = input>(); isEmpty = input<(v: T) => boolean>(() => false); + // Shared/English component: copy lives behind language-agnostic inputs. Defaults are + // localizable via $localize (source = default locale, currently nl); callers may override. + errorText = input($localize`:@@async.error:Er ging iets mis bij het laden van de gegevens.`); + retryText = input($localize`:@@async.retry:Opnieuw proberen`); + emptyText = input($localize`:@@async.empty:Geen gegevens gevonden.`); + loadedTpl = contentChild.required(AsyncLoadedDirective); loadingTpl = contentChild(AsyncLoadingDirective); emptyTpl = contentChild(AsyncEmptyDirective); diff --git a/src/main.ts b/src/main.ts index 5df75f9..dc13603 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,7 @@ +/// + import { bootstrapApplication } from '@angular/platform-browser'; import { appConfig } from './app/app.config'; import { App } from './app/app'; -bootstrapApplication(App, appConfig) - .catch((err) => console.error(err)); +bootstrapApplication(App, appConfig).catch((err) => console.error(err)); diff --git a/tsconfig.app.json b/tsconfig.app.json index 98d0007..064a817 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -4,13 +4,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", - "types": [] + "types": ["@angular/localize"] }, - "include": [ - "src/**/*.ts" - ], - "exclude": [ - "src/**/*.spec.ts", - "src/**/*.stories.ts" - ] + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts", "src/**/*.stories.ts"] } diff --git a/tsconfig.spec.json b/tsconfig.spec.json index d383706..ce3255e 100644 --- a/tsconfig.spec.json +++ b/tsconfig.spec.json @@ -4,12 +4,7 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", - "types": [ - "vitest/globals" - ] + "types": ["vitest/globals", "@angular/localize"] }, - "include": [ - "src/**/*.d.ts", - "src/**/*.spec.ts" - ] + "include": ["src/**/*.d.ts", "src/**/*.spec.ts"] }