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 {
Geen gegevens gevonden.
} + @else {{{ emptyText() }}
} } @case ('Success') {