Step 2 (i18n): $localize sweep + JA_NEE dedup (M3, M4)

Wrap every user-facing Dutch string in Angular's first-party i18n — `i18n`/
`i18n-<attr>` in templates, `$localize` in TS (value-objects, machines, commands,
label constants, shared-component defaults). Source locale stays nl; a second
locale is now a translation file, not a code change.

- M3: ~145 strings localized with stable @@ ids across registratie,
  herregistratie, auth, shared/ui, shared/layout. Skipped: showcase, debug-state,
  scenario interceptor, generated client, specs/stories, raw status enum tags,
  internal parse* diagnostics.
- M4: single shared JA_NEE (localized labels) in radio-group; both wizard copies
  removed.

Gate green: lint, check:tokens, build, test 77/77.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-27 14:00:10 +02:00
parent 1c65025fef
commit 474c040410
36 changed files with 193 additions and 181 deletions

View File

@@ -9,7 +9,7 @@ export class DigidAdapter {
// Swap for a real OIDC redirect flow when there's a backend.
async authenticate(bsn: string): Promise<Result<string, Session>> {
const t = bsn.trim();
if (!/^\d{9}$/.test(t)) return err('Voer een geldig BSN van 9 cijfers in.');
if (!/^\d{9}$/.test(t)) return err($localize`:@@validation.bsn:Voer een geldig BSN van 9 cijfers in.`);
return ok({ bsn: t, naam: 'Dr. A. (Anna) de Vries' });
}
}

View File

@@ -10,15 +10,15 @@ import { ButtonComponent } from '@shared/ui/button/button.component';
imports: [FormsModule, FormFieldComponent, TextInputComponent, ButtonComponent],
template: `
<form (ngSubmit)="submitted.emit(bsn)" class="app-form app-form-panel">
<app-form-field label="BSN" fieldId="bsn" required description="9 cijfers (demo: vul iets in)">
<app-form-field i18n-label="@@login.bsnLabel" label="BSN" fieldId="bsn" required i18n-description="@@login.bsnDescription" description="9 cijfers (demo: vul iets in)">
<app-text-input inputId="bsn" [(ngModel)]="bsn" name="bsn" placeholder="123456789" />
</app-form-field>
<app-form-field label="Wachtwoord" fieldId="pw" required>
<app-form-field i18n-label="@@login.wachtwoordLabel" label="Wachtwoord" fieldId="pw" required>
<app-text-input inputId="pw" type="password" [(ngModel)]="password" name="pw" />
</app-form-field>
<app-button type="submit" variant="primary">Inloggen met DigiD</app-button>
<app-button type="submit" variant="primary" i18n="@@login.submit">Inloggen met DigiD</app-button>
</form>
`,
})

View File

@@ -9,8 +9,8 @@ import { SessionStore } from '@auth/application/session.store';
selector: 'app-login-page',
imports: [PageShellComponent, AlertComponent, LoginFormComponent],
template: `
<app-page-shell heading="Inloggen" width="narrow"
intro="Log in op uw persoonlijke BIG-register omgeving.">
<app-page-shell i18n-heading="@@login.heading" heading="Inloggen" width="narrow"
i18n-intro="@@login.intro" intro="Log in op uw persoonlijke BIG-register omgeving.">
@if (error()) { <app-alert type="error">{{ error() }}</app-alert> }
<app-login-form (submitted)="login($event)" />
</app-page-shell>

View File

@@ -73,9 +73,9 @@ function validateStep(step: StepId, a: Answers, scholingThreshold: number): Resu
const errors: Errors = {};
switch (step) {
case 'buitenland':
if (!a.buitenlandGewerkt) errors.buitenlandGewerkt = 'Maak een keuze.';
if (!a.buitenlandGewerkt) errors.buitenlandGewerkt = $localize`:@@validation.maakKeuze:Maak een keuze.`;
else if (a.buitenlandGewerkt === 'ja') {
if (!a.land || a.land.trim() === '') errors.land = 'Vul een land in.';
if (!a.land || a.land.trim() === '') errors.land = $localize`:@@validation.land:Vul een land in.`;
const u = parseUren(a.buitenlandseUren ?? '');
if (!u.ok) errors.buitenlandseUren = u.error;
}
@@ -83,7 +83,7 @@ function validateStep(step: StepId, a: Answers, scholingThreshold: number): Resu
case 'werk': {
const u = parseUren(a.uren ?? '');
if (!u.ok) errors.uren = u.error;
if (lageUren(a, scholingThreshold) && !a.scholingGevolgd) errors.scholingGevolgd = 'Maak een keuze.';
if (lageUren(a, scholingThreshold) && !a.scholingGevolgd) errors.scholingGevolgd = $localize`:@@validation.maakKeuze:Maak een keuze.`;
// Nascholingspunten are only asked (and required) when scholing was followed.
if (a.scholingGevolgd === 'ja') {
const p = parseUren(a.punten ?? '');

View File

@@ -36,23 +36,23 @@ import { ApiClient } from '@shared/infrastructure/api-client';
(retry)="onRetry()">
@if (step() === 1) {
<app-form-field label="Gewerkte uren (afgelopen 5 jaar)" fieldId="uren" required [error]="errUren()">
<app-form-field i18n-label="@@herregWizard.urenLabel" label="Gewerkte uren (afgelopen 5 jaar)" fieldId="uren" required [error]="errUren()">
<app-text-input inputId="uren" [ngModel]="draft().uren" (ngModelChange)="dispatch({ tag: 'SetField', key: 'uren', value: $event })"
name="uren" [invalid]="!!errUren()" placeholder="bijv. 4160" />
name="uren" [invalid]="!!errUren()" i18n-placeholder="@@herregWizard.urenPlaceholder" placeholder="bijv. 4160" />
</app-form-field>
<app-form-field label="Aantal jaren werkzaam" fieldId="jaren" required [error]="errJaren()">
<app-form-field i18n-label="@@herregWizard.jarenLabel" label="Aantal jaren werkzaam" fieldId="jaren" required [error]="errJaren()">
<app-text-input inputId="jaren" [ngModel]="draft().jaren" (ngModelChange)="dispatch({ tag: 'SetField', key: 'jaren', value: $event })"
name="jaren" [invalid]="!!errJaren()" placeholder="bijv. 5" />
name="jaren" [invalid]="!!errJaren()" i18n-placeholder="@@herregWizard.jarenPlaceholder" placeholder="bijv. 5" />
</app-form-field>
} @else {
<app-form-field label="Behaalde nascholingspunten" fieldId="punten" required [error]="errPunten()">
<app-form-field i18n-label="@@herregWizard.puntenLabel" label="Behaalde nascholingspunten" fieldId="punten" required [error]="errPunten()">
<app-text-input inputId="punten" [ngModel]="draft().punten" (ngModelChange)="dispatch({ tag: 'SetField', key: 'punten', value: $event })"
name="punten" [invalid]="!!errPunten()" placeholder="bijv. 200" />
name="punten" [invalid]="!!errPunten()" i18n-placeholder="@@herregWizard.puntenPlaceholder" placeholder="bijv. 200" />
</app-form-field>
}
<div wizardSuccess>
<app-alert type="ok">Uw aanvraag tot herregistratie is ontvangen.</app-alert>
<app-alert type="ok" i18n="@@herregWizard.success">Uw aanvraag tot herregistratie is ontvangen.</app-alert>
</div>
</app-wizard-shell>
`,
@@ -69,8 +69,8 @@ export class HerregistratieWizardComponent {
protected dispatch = this.store.dispatch;
// Stepper labels + per-step heading titles (presentational only).
readonly stepLabels = ['Werkervaring', 'Nascholing'];
private stepTitles = ['Werkervaring (afgelopen 5 jaar)', 'Nascholing'];
readonly stepLabels = [$localize`:@@herregWizard.step.werkervaring:Werkervaring`, $localize`:@@herregWizard.step.nascholing:Nascholing`];
private stepTitles = [$localize`:@@herregWizard.title.werkervaring:Werkervaring (afgelopen 5 jaar)`, $localize`:@@herregWizard.title.nascholing:Nascholing`];
private editing = computed(() => whenTag(this.state(), 'Editing'));
protected step = computed(() => this.editing()?.step ?? 1);
@@ -82,8 +82,8 @@ export class HerregistratieWizardComponent {
// --- Presentational wiring for the shared wizard shell ---------------------
protected stepTitle = computed(() => this.stepTitles[this.step() - 1]);
protected primaryLabel = computed(() => (this.step() === 1 ? 'Volgende' : 'Herregistratie aanvragen'));
protected errorMessage = computed(() => `Indienen mislukt: ${this.failedError()}`);
protected primaryLabel = computed(() => (this.step() === 1 ? $localize`:@@wizard.volgende:Volgende` : $localize`:@@herregWizard.indienen:Herregistratie aanvragen`));
protected errorMessage = computed(() => $localize`:@@wizard.indienenMislukt:Indienen mislukt:` + ` ${this.failedError()}`);
protected shellStatus = computed<WizardStatus>(() => {
switch (this.state().tag) {
case 'Editing': return 'editing';

View File

@@ -13,18 +13,18 @@ import { HerregistratieWizardComponent } from '@herregistratie/ui/herregistratie
selector: 'app-herregistratie-page',
imports: [PageShellComponent, AlertComponent, ...ASYNC, HerregistratieWizardComponent],
template: `
<app-page-shell heading="Herregistratie aanvragen" backLink="/dashboard">
<app-page-shell i18n-heading="@@herregistratie.heading" heading="Herregistratie aanvragen" backLink="/dashboard">
<app-async [data]="eligibility()">
<ng-template appAsyncLoaded let-eligible>
@if (eligible) {
<app-alert type="info">
<app-alert type="info" i18n="@@herregistratie.eligible">
Uw huidige registratie verloopt binnenkort. Vraag tijdig herregistratie aan.
</app-alert>
<div class="app-section">
<app-herregistratie-wizard />
</div>
} @else {
<app-alert type="warning">
<app-alert type="warning" i18n="@@herregistratie.notEligible">
Voor uw huidige registratiestatus is herregistratie niet mogelijk.
</app-alert>
}

View File

@@ -2,7 +2,7 @@ import { Component, computed, effect, inject, input, untracked } from '@angular/
import { FormsModule } from '@angular/forms';
import { FormFieldComponent } from '@shared/ui/form-field/form-field.component';
import { TextInputComponent } from '@shared/ui/text-input/text-input.component';
import { RadioGroupComponent } from '@shared/ui/radio-group/radio-group.component';
import { RadioGroupComponent, JA_NEE } from '@shared/ui/radio-group/radio-group.component';
import { ButtonComponent } from '@shared/ui/button/button.component';
import { AlertComponent } from '@shared/ui/alert/alert.component';
import { DataRowComponent } from '@shared/ui/data-row/data-row.component';
@@ -26,7 +26,6 @@ import { ApiClient } from '@shared/infrastructure/api-client';
import { IntakePolicyAdapter } from '@herregistratie/infrastructure/intake-policy.adapter';
const STORAGE_KEY = 'intake-v3'; // ponytail: bump the suffix if the persisted state shape changes; no migration.
const JA_NEE = [{ value: 'ja', label: 'Ja' }, { value: 'nee', label: 'Nee' }];
/** Organism: a BRANCHING intake questionnaire. All state lives in one signal
driven by the pure `reduce` (intake.machine.ts). Which step renders is derived
@@ -53,58 +52,58 @@ const JA_NEE = [{ value: 'ja', label: 'Ja' }, { value: 'nee', label: 'Nee' }];
@switch (step()) {
@case ('buitenland') {
<app-form-field label="Heeft u de afgelopen 5 jaar buiten Nederland gewerkt?" fieldId="buitenlandGewerkt" required [error]="err('buitenlandGewerkt')">
<app-form-field i18n-label="@@intake.q.buitenland" label="Heeft u de afgelopen 5 jaar buiten Nederland gewerkt?" fieldId="buitenlandGewerkt" required [error]="err('buitenlandGewerkt')">
<app-radio-group name="buitenlandGewerkt" [options]="jaNee"
[ngModel]="answers().buitenlandGewerkt ?? ''" (ngModelChange)="set('buitenlandGewerkt', $event)" />
</app-form-field>
@if (answers().buitenlandGewerkt === 'ja') {
<app-form-field label="In welk land?" fieldId="land" required [error]="err('land')">
<app-text-input inputId="land" [ngModel]="answers().land ?? ''" (ngModelChange)="set('land', $event)" name="land" placeholder="bijv. België" />
<app-form-field i18n-label="@@intake.q.land" label="In welk land?" fieldId="land" required [error]="err('land')">
<app-text-input inputId="land" [ngModel]="answers().land ?? ''" (ngModelChange)="set('land', $event)" name="land" i18n-placeholder="@@intake.q.landPlaceholder" placeholder="bijv. België" />
</app-form-field>
<app-form-field label="Hoeveel uur heeft u daar gewerkt?" fieldId="buitenlandseUren" required [error]="err('buitenlandseUren')">
<app-text-input inputId="buitenlandseUren" [ngModel]="answers().buitenlandseUren ?? ''" (ngModelChange)="set('buitenlandseUren', $event)" name="buitenlandseUren" placeholder="bijv. 800" />
<app-form-field i18n-label="@@intake.q.buitenlandseUren" label="Hoeveel uur heeft u daar gewerkt?" fieldId="buitenlandseUren" required [error]="err('buitenlandseUren')">
<app-text-input inputId="buitenlandseUren" [ngModel]="answers().buitenlandseUren ?? ''" (ngModelChange)="set('buitenlandseUren', $event)" name="buitenlandseUren" i18n-placeholder="@@intake.q.buitenlandseUrenPlaceholder" placeholder="bijv. 800" />
</app-form-field>
}
}
@case ('werk') {
<app-form-field label="Gewerkte uren in Nederland (afgelopen 5 jaar)" fieldId="uren" required [error]="err('uren')">
<app-text-input inputId="uren" [ngModel]="answers().uren ?? ''" (ngModelChange)="set('uren', $event)" name="uren" placeholder="bijv. 4160" />
<app-form-field i18n-label="@@intake.q.urenNl" label="Gewerkte uren in Nederland (afgelopen 5 jaar)" fieldId="uren" required [error]="err('uren')">
<app-text-input inputId="uren" [ngModel]="answers().uren ?? ''" (ngModelChange)="set('uren', $event)" name="uren" i18n-placeholder="@@intake.q.urenNlPlaceholder" placeholder="bijv. 4160" />
</app-form-field>
@if (scholingZichtbaar()) {
<app-form-field label="U werkte relatief weinig uren. Heeft u aanvullende scholing gevolgd?" fieldId="scholingGevolgd" required [error]="err('scholingGevolgd')">
<app-form-field i18n-label="@@intake.q.scholing" label="U werkte relatief weinig uren. Heeft u aanvullende scholing gevolgd?" fieldId="scholingGevolgd" required [error]="err('scholingGevolgd')">
<app-radio-group name="scholingGevolgd" [options]="jaNee"
[ngModel]="answers().scholingGevolgd ?? ''" (ngModelChange)="set('scholingGevolgd', $event)" />
</app-form-field>
}
@if (answers().scholingGevolgd === 'ja') {
<app-form-field label="Behaalde nascholingspunten" fieldId="punten" required [error]="err('punten')">
<app-text-input inputId="punten" [ngModel]="answers().punten ?? ''" (ngModelChange)="set('punten', $event)" name="punten" placeholder="bijv. 200" />
<app-form-field i18n-label="@@intake.q.punten" label="Behaalde nascholingspunten" fieldId="punten" required [error]="err('punten')">
<app-text-input inputId="punten" [ngModel]="answers().punten ?? ''" (ngModelChange)="set('punten', $event)" name="punten" i18n-placeholder="@@intake.q.puntenPlaceholder" placeholder="bijv. 200" />
</app-form-field>
}
}
@case ('review') {
<app-alert type="info">Controleer uw antwoorden en dien de aanvraag in.</app-alert>
<app-alert type="info" i18n="@@intake.review.controleer">Controleer uw antwoorden en dien de aanvraag in.</app-alert>
<dl class="rhc-data-summary rhc-data-summary--row app-section">
<app-data-row key="Buiten NL gewerkt" [value]="answers().buitenlandGewerkt ?? '—'" />
<app-data-row i18n-key="@@intake.review.buitenNl" key="Buiten NL gewerkt" [value]="answers().buitenlandGewerkt ?? '—'" />
@if (answers().buitenlandGewerkt === 'ja') {
<app-data-row key="Land" [value]="answers().land ?? ''" />
<app-data-row key="Buitenlandse uren" [value]="answers().buitenlandseUren ?? ''" />
<app-data-row i18n-key="@@intake.review.land" key="Land" [value]="answers().land ?? ''" />
<app-data-row i18n-key="@@intake.review.buitenlandseUren" key="Buitenlandse uren" [value]="answers().buitenlandseUren ?? ''" />
}
<app-data-row key="Uren NL" [value]="answers().uren ?? ''" />
<app-data-row i18n-key="@@intake.review.urenNl" key="Uren NL" [value]="answers().uren ?? ''" />
@if (scholingZichtbaar()) {
<app-data-row key="Aanvullende scholing" [value]="answers().scholingGevolgd ?? ''" />
<app-data-row i18n-key="@@intake.review.scholing" key="Aanvullende scholing" [value]="answers().scholingGevolgd ?? ''" />
}
@if (answers().scholingGevolgd === 'ja') {
<app-data-row key="Nascholingspunten" [value]="answers().punten ?? ''" />
<app-data-row i18n-key="@@intake.review.punten" key="Nascholingspunten" [value]="answers().punten ?? ''" />
}
</dl>
}
}
<div wizardSuccess>
<app-alert type="ok">Uw aanvraag tot herregistratie is ontvangen.</app-alert>
<app-alert type="ok" i18n="@@intake.success">Uw aanvraag tot herregistratie is ontvangen.</app-alert>
<div class="app-section">
<app-button variant="secondary" (click)="restart()">Opnieuw beginnen</app-button>
<app-button variant="secondary" (click)="restart()" i18n="@@intake.opnieuw">Opnieuw beginnen</app-button>
</div>
</div>
</app-wizard-shell>
@@ -140,15 +139,15 @@ export class IntakeWizardComponent {
protected failedError = computed(() => whenTag(this.state(), 'Failed')?.error ?? '');
// --- Presentational wiring for the shared wizard shell ---------------------
readonly stepLabels = ['Buitenland', 'Werk', 'Controle'];
readonly stepLabels = [$localize`:@@intake.step.buitenland:Buitenland`, $localize`:@@intake.step.werk:Werk`, $localize`:@@intake.step.controle:Controle`];
private stepTitles: Record<StepId, string> = {
buitenland: 'Werken in het buitenland',
werk: 'Werkervaring in Nederland',
review: 'Controleren en indienen',
buitenland: $localize`:@@intake.title.buitenland:Werken in het buitenland`,
werk: $localize`:@@intake.title.werk:Werkervaring in Nederland`,
review: $localize`:@@intake.title.review:Controleren en indienen`,
};
protected stepTitle = computed(() => this.stepTitles[this.step()]);
protected primaryLabel = computed(() => (this.step() === 'review' ? 'Aanvraag indienen' : 'Volgende'));
protected errorMessage = computed(() => `Indienen mislukt: ${this.failedError()}`);
protected primaryLabel = computed(() => (this.step() === 'review' ? $localize`:@@intake.indienen:Aanvraag indienen` : $localize`:@@wizard.volgende:Volgende`));
protected errorMessage = computed(() => $localize`:@@wizard.indienenMislukt:Indienen mislukt:` + ` ${this.failedError()}`);
protected shellStatus = computed<WizardStatus>(() => {
switch (this.state().tag) {
case 'Answering': return 'editing';

View File

@@ -9,8 +9,8 @@ import { IntakeWizardComponent } from '@herregistratie/ui/intake-wizard/intake-w
selector: 'app-intake-page',
imports: [PageShellComponent, AlertComponent, IntakeWizardComponent],
template: `
<app-page-shell heading="Herregistratie — intake" backLink="/dashboard">
<app-alert type="info">
<app-page-shell i18n-heading="@@intake.heading" heading="Herregistratie — intake" backLink="/dashboard">
<app-alert type="info" i18n="@@intake.intro">
Een paar vragen bepalen welke gegevens we nodig hebben. Afhankelijk van uw
antwoorden verschijnen er extra vragen. Uw antwoorden blijven bewaard als u
de pagina herlaadt.

View File

@@ -40,7 +40,7 @@ function validate(draft: Draft): Result<Errors, Valid> {
const straat = draft.straat.trim();
const postcode = parsePostcode(draft.postcode);
const errors: Errors = {};
if (!straat) errors.straat = 'Vul straat en huisnummer in.';
if (!straat) errors.straat = $localize`:@@validation.straat:Vul straat en huisnummer in.`;
if (!postcode.ok) errors.postcode = postcode.error;
if (straat && postcode.ok) {
return { ok: true, value: { straat, postcode: postcode.value, woonplaats: draft.woonplaats.trim() } };

View File

@@ -85,11 +85,11 @@ function validateStep(step: StepId, d: Draft): Result<Errors, void> {
const errors: Errors = {};
switch (step) {
case 'adres': {
if (!d.straat || d.straat.trim() === '') errors.straat = 'Vul een straat en huisnummer in.';
if (!d.straat || d.straat.trim() === '') errors.straat = $localize`:@@validation.straat2:Vul een straat en huisnummer in.`;
const pc = parsePostcode(d.postcode ?? '');
if (!pc.ok) errors.postcode = pc.error;
if (!d.woonplaats || d.woonplaats.trim() === '') errors.woonplaats = 'Vul een woonplaats in.';
if (!d.correspondentie) errors.correspondentie = 'Maak een keuze.';
if (!d.woonplaats || d.woonplaats.trim() === '') errors.woonplaats = $localize`:@@validation.woonplaats:Vul een woonplaats in.`;
if (!d.correspondentie) errors.correspondentie = $localize`:@@validation.maakKeuze:Maak een keuze.`;
// E-mail is only required when 'email' is the chosen channel.
if (d.correspondentie === 'email') {
const e = parseEmail(d.email ?? '');
@@ -100,7 +100,7 @@ function validateStep(step: StepId, d: Draft): Result<Errors, void> {
case 'beroep': {
// A diploma must be chosen (or declared manually); its beroep is then known.
if (!d.diplomaId || !d.beroep) {
errors.diploma = 'Kies het diploma waarmee u zich wilt registreren, of voer het handmatig in.';
errors.diploma = $localize`:@@validation.diploma:Kies het diploma waarmee u zich wilt registreren, of voer het handmatig in.`;
break;
}
// Every policy question the chosen diploma raised must be answered. Which
@@ -108,7 +108,7 @@ function validateStep(step: StepId, d: Draft): Result<Errors, void> {
// they're answered.
const open: Record<string, string> = {};
for (const id of d.vraagIds ?? []) {
if (!(d.antwoorden[id] ?? '').trim()) open[id] = 'Beantwoord deze vraag.';
if (!(d.antwoorden[id] ?? '').trim()) open[id] = $localize`:@@validation.beantwoordVraag:Beantwoord deze vraag.`;
}
if (Object.keys(open).length > 0) errors.antwoorden = open;
break;

View File

@@ -28,30 +28,30 @@ export function tasksFromProfile(reg: Registration, eligibleForHerregistratie: b
if (eligibleForHerregistratie) {
const deadline = herregistratieDeadline(reg);
tasks.push({
title: 'Vraag uw herregistratie aan',
title: $localize`:@@task.herregistratie.title:Vraag uw herregistratie aan`,
description: deadline
? `Verleng uw registratie vóór ${formatNL(deadline)}.`
: 'U kunt nu uw herregistratie aanvragen.',
? $localize`:@@task.herregistratie.deadline:Verleng uw registratie vóór ${formatNL(deadline)}:deadline:.`
: $localize`:@@task.herregistratie.nodeadline:U kunt nu uw herregistratie aanvragen.`,
to: '/herregistratie',
actionLabel: 'Herregistratie aanvragen',
actionLabel: $localize`:@@task.herregistratie.action:Herregistratie aanvragen`,
});
}
if (reg.status.tag === 'Geschorst') {
tasks.push({
title: 'Uw registratie is geschorst',
title: $localize`:@@task.geschorst.title:Uw registratie is geschorst`,
description: reg.status.reden,
to: '/registratie',
actionLabel: 'Bekijk uw gegevens',
actionLabel: $localize`:@@task.bekijkGegevens.action:Bekijk uw gegevens`,
});
}
if (reg.status.tag === 'Doorgehaald') {
tasks.push({
title: 'Uw registratie is doorgehaald',
title: $localize`:@@task.doorgehaald.title:Uw registratie is doorgehaald`,
description: reg.status.reden,
to: '/registratie',
actionLabel: 'Bekijk uw gegevens',
actionLabel: $localize`:@@task.bekijkGegevens.action:Bekijk uw gegevens`,
});
}

View File

@@ -5,5 +5,5 @@ export type BigNummer = Brand<string, 'BigNummer'>;
export function parseBigNummer(raw: string): Result<string, BigNummer> {
const t = raw.trim();
return /^\d{11}$/.test(t) ? ok(t as BigNummer) : err('Een BIG-nummer bestaat uit 11 cijfers.');
return /^\d{11}$/.test(t) ? ok(t as BigNummer) : err($localize`:@@validation.bigNummer:Een BIG-nummer bestaat uit 11 cijfers.`);
}

View File

@@ -13,7 +13,7 @@ export function parseEmail(raw: string): Result<string, Email> {
// Deliberately lax: a single @ with non-empty, dot-bearing parts. Good enough
// for instant feedback; the server re-validates.
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(t)) {
return err('Voer een geldig e-mailadres in, bijv. naam@voorbeeld.nl.');
return err($localize`:@@validation.email:Voer een geldig e-mailadres in, bijv. naam@voorbeeld.nl.`);
}
return ok(t as Email);
}

View File

@@ -10,7 +10,7 @@ export type Postcode = Brand<string, 'Postcode'>;
export function parsePostcode(raw: string): Result<string, Postcode> {
const t = raw.trim().toUpperCase();
if (!/^[1-9]\d{3}\s?[A-Z]{2}$/.test(t)) {
return err('Voer een geldige postcode in, bijv. 1234 AB.');
return err($localize`:@@validation.postcode:Voer een geldige postcode in, bijv. 1234 AB.`);
}
// Normalise to "1234 AB" — the parser also cleans up.
return ok(t.replace(/^(\d{4})\s?([A-Z]{2})$/, '$1 $2') as Postcode);

View File

@@ -8,7 +8,7 @@ export function parseUren(raw: string): Result<string, Uren> {
const n = Number(t);
// Number('') is 0 — guard the empty string explicitly.
if (t === '' || !Number.isInteger(n) || n < 0) {
return err('Vul een geheel aantal in (0 of meer).');
return err($localize`:@@validation.uren:Vul een geheel aantal in (0 of meer).`);
}
return ok(n as Uren);
}

View File

@@ -27,17 +27,17 @@ export type AdresErrors = Partial<Record<keyof AdresValue, string>>;
template: `
<fieldset class="utrecht-form-fieldset utrecht-form-fieldset--html-fieldset">
<legend class="utrecht-form-fieldset__legend utrecht-form-fieldset__legend--html-legend">{{ legend() }}</legend>
<app-form-field label="Straat en huisnummer" [fieldId]="idPrefix() + '-straat'" required [error]="errors().straat">
<app-form-field i18n-label="@@address.straat" label="Straat en huisnummer" [fieldId]="idPrefix() + '-straat'" required [error]="errors().straat">
<app-text-input [inputId]="idPrefix() + '-straat'" [invalid]="!!errors().straat"
[ngModel]="value().straat" (ngModelChange)="fieldChange.emit({ key: 'straat', value: $event })"
name="straat" [ngModelOptions]="{ standalone: true }" />
</app-form-field>
<app-form-field label="Postcode" [fieldId]="idPrefix() + '-postcode'" required [error]="errors().postcode">
<app-form-field i18n-label="@@address.postcode" label="Postcode" [fieldId]="idPrefix() + '-postcode'" required [error]="errors().postcode">
<app-text-input [inputId]="idPrefix() + '-postcode'" [invalid]="!!errors().postcode"
[ngModel]="value().postcode" (ngModelChange)="fieldChange.emit({ key: 'postcode', value: $event })"
name="postcode" placeholder="1234 AB" [ngModelOptions]="{ standalone: true }" />
name="postcode" i18n-placeholder="@@address.postcodePlaceholder" placeholder="1234 AB" [ngModelOptions]="{ standalone: true }" />
</app-form-field>
<app-form-field label="Woonplaats" [fieldId]="idPrefix() + '-woonplaats'" required [error]="errors().woonplaats">
<app-form-field i18n-label="@@address.woonplaats" label="Woonplaats" [fieldId]="idPrefix() + '-woonplaats'" required [error]="errors().woonplaats">
<app-text-input [inputId]="idPrefix() + '-woonplaats'" [invalid]="!!errors().woonplaats"
[ngModel]="value().woonplaats" (ngModelChange)="fieldChange.emit({ key: 'woonplaats', value: $event })"
name="woonplaats" [ngModelOptions]="{ standalone: true }" />
@@ -50,6 +50,6 @@ export class AddressFieldsComponent {
errors = input<AdresErrors>({});
/** Prefix for field ids/labels — keeps them unique if two blocks ever co-exist. */
idPrefix = input('adres');
legend = input('Adres');
legend = input($localize`:@@address.legend:Adres`);
fieldChange = output<{ key: keyof AdresValue; value: string }>();
}

View File

@@ -20,14 +20,14 @@ import { ApiClient } from '@shared/infrastructure/api-client';
imports: [ButtonComponent, HeadingComponent, AlertComponent, AddressFieldsComponent],
template: `
@if (state().tag === 'Submitted') {
<app-alert type="ok">
<app-alert type="ok" i18n="@@changeRequest.success">
Uw adreswijziging is ontvangen (referentie {{ referentie() }}). U ontvangt binnen 5 werkdagen bericht.
</app-alert>
<div class="app-section">
<app-button variant="secondary" (click)="dispatch({ tag: 'Reset' })">Nieuwe wijziging doorgeven</app-button>
<app-button variant="secondary" (click)="dispatch({ tag: 'Reset' })" i18n="@@changeRequest.nieuwe">Nieuwe wijziging doorgeven</app-button>
</div>
} @else {
<app-heading [level]="2">Adreswijziging doorgeven</app-heading>
<app-heading [level]="2" i18n="@@changeRequest.heading">Adreswijziging doorgeven</app-heading>
<form (ngSubmit)="onSubmit()" class="app-form app-form-panel app-section">
<app-address-fields
idPrefix="cr"
@@ -36,11 +36,11 @@ import { ApiClient } from '@shared/infrastructure/api-client';
(fieldChange)="dispatch({ tag: 'SetField', key: $event.key, value: $event.value })" />
@if (failedError()) {
<app-alert type="error">Het indienen is niet gelukt: {{ failedError() }}</app-alert>
<app-alert type="error"><ng-container i18n="@@changeRequest.failed">Het indienen is niet gelukt:</ng-container> {{ failedError() }}</app-alert>
}
<app-button type="submit" variant="primary" [disabled]="state().tag === 'Submitting'">
{{ state().tag === 'Submitting' ? 'Bezig met indienen…' : 'Wijziging indienen' }}
{{ state().tag === 'Submitting' ? submitBezigLabel : submitLabel }}
</app-button>
</form>
}
@@ -56,6 +56,9 @@ export class ChangeRequestFormComponent {
readonly state = this.store.model;
protected dispatch = this.store.dispatch;
protected readonly submitLabel = $localize`:@@changeRequest.submit:Wijziging indienen`;
protected readonly submitBezigLabel = $localize`:@@changeRequest.submitBezig:Bezig met indienen…`;
private editing = computed(() => whenTag(this.state(), 'Editing'));
protected errors = computed<AdresErrors>(() => this.editing()?.errors ?? {});
protected failedError = computed(() => whenTag(this.state(), 'Failed')?.error ?? '');

View File

@@ -25,13 +25,13 @@ import { tasksFromProfile } from '@registratie/domain/tasks';
RegistrationSummaryComponent, RegistrationTableComponent,
],
template: `
<app-page-shell heading="Mijn overzicht" intro="Welkom in uw persoonlijke omgeving van het BIG-register. Hier ziet u uw registratie en regelt u uw zaken.">
<app-page-shell i18n-heading="@@dashboard.heading" heading="Mijn overzicht" i18n-intro="@@dashboard.intro" intro="Welkom in uw persoonlijke omgeving van het BIG-register. Hier ziet u uw registratie en regelt u uw zaken.">
<div class="app-overview">
<app-side-nav [items]="nav" />
<div class="app-stack">
@if (store.pendingHerregistratie()) {
<app-alert type="info">Uw herregistratie-aanvraag is in behandeling.</app-alert>
<app-alert type="info" i18n="@@dashboard.pendingHerregistratie">Uw herregistratie-aanvraag is in behandeling.</app-alert>
}
<app-async [data]="store.profile()">
@@ -39,24 +39,24 @@ import { tasksFromProfile } from '@registratie/domain/tasks';
@let tasks = tasksFor($any(p).registration);
<section>
<app-heading [level]="2">Wat moet ik regelen</app-heading>
<app-heading [level]="2" i18n="@@dashboard.watMoetIkRegelen">Wat moet ik regelen</app-heading>
@if (tasks.length) {
<app-task-list class="app-section" [tasks]="tasks" />
} @else {
<p class="rhc-paragraph app-text-subtle">U heeft op dit moment niets openstaan.</p>
<p class="rhc-paragraph app-text-subtle" i18n="@@dashboard.nietsOpenstaan">U heeft op dit moment niets openstaan.</p>
}
</section>
<section>
<app-heading [level]="2">Mijn registratie</app-heading>
<app-heading [level]="2" i18n="@@dashboard.mijnRegistratie">Mijn registratie</app-heading>
<div class="app-section">
<app-registration-summary [reg]="$any(p).registration" />
</div>
<app-card class="app-section" heading="Persoonsgegevens (BRP)">
<app-card class="app-section" i18n-heading="@@dashboard.persoonsgegevens" heading="Persoonsgegevens (BRP)">
<dl class="rhc-data-summary rhc-data-summary--row">
<app-data-row key="Straat" [value]="$any(p).person.adres.straat" />
<app-data-row key="Postcode" [value]="$any(p).person.adres.postcode" />
<app-data-row key="Woonplaats" [value]="$any(p).person.adres.woonplaats" />
<app-data-row i18n-key="@@dashboard.straat" key="Straat" [value]="$any(p).person.adres.straat" />
<app-data-row i18n-key="@@dashboard.postcode" key="Postcode" [value]="$any(p).person.adres.postcode" />
<app-data-row i18n-key="@@dashboard.woonplaats" key="Woonplaats" [value]="$any(p).person.adres.woonplaats" />
</dl>
</app-card>
</section>
@@ -67,7 +67,7 @@ import { tasksFromProfile } from '@registratie/domain/tasks';
</app-async>
<section>
<app-heading [level]="2">Specialismen en aantekeningen</app-heading>
<app-heading [level]="2" i18n="@@dashboard.specialismen">Specialismen en aantekeningen</app-heading>
<div class="app-section">
<app-async [data]="store.aantekeningen()">
<ng-template appAsyncLoaded let-r>
@@ -77,14 +77,14 @@ import { tasksFromProfile } from '@registratie/domain/tasks';
<app-skeleton height="2.5rem" [count]="3" />
</ng-template>
<ng-template appAsyncEmpty>
<p class="rhc-paragraph app-text-subtle">U heeft nog geen specialismen of aantekeningen.</p>
<p class="rhc-paragraph app-text-subtle" i18n="@@dashboard.geenSpecialismen">U heeft nog geen specialismen of aantekeningen.</p>
</ng-template>
</app-async>
</div>
</section>
<section>
<app-heading [level]="2">Wat wilt u doen?</app-heading>
<app-heading [level]="2" i18n="@@dashboard.watWiltUDoen">Wat wilt u doen?</app-heading>
<ul class="app-card-grid app-section">
@for (a of acties; track a.to) {
<li>
@@ -116,16 +116,16 @@ export class DashboardPage {
/** Portal sections (navigation, not actions). */
protected readonly nav: NavItem[] = [
{ label: 'Overzicht', to: '/dashboard' },
{ label: 'Mijn gegevens', to: '/registratie' },
{ label: 'Herregistratie', to: '/herregistratie' },
{ label: 'Inschrijven', to: '/registreren' },
{ label: $localize`:@@dashboard.nav.overzicht:Overzicht`, to: '/dashboard' },
{ label: $localize`:@@dashboard.nav.gegevens:Mijn gegevens`, to: '/registratie' },
{ label: $localize`:@@dashboard.nav.herregistratie:Herregistratie`, to: '/herregistratie' },
{ label: $localize`:@@dashboard.nav.inschrijven:Inschrijven`, to: '/registreren' },
];
/** Primary transactional actions, as a card grid. */
protected readonly acties = [
{ to: '/registreren', titel: 'Inschrijven', tekst: 'Schrijf u in in het BIG-register via de registratiewizard.', actie: 'Start inschrijving' },
{ to: '/herregistratie', titel: 'Herregistratie aanvragen', tekst: 'Verleng uw registratie voor de komende periode.', actie: 'Vraag aan' },
{ to: '/registratie', titel: 'Gegevens wijzigen', tekst: 'Bekijk uw gegevens of geef een wijziging door.', actie: 'Bekijk gegevens' },
{ to: '/registreren', titel: $localize`:@@dashboard.actie.inschrijven.titel:Inschrijven`, tekst: $localize`:@@dashboard.actie.inschrijven.tekst:Schrijf u in in het BIG-register via de registratiewizard.`, actie: $localize`:@@dashboard.actie.inschrijven.actie:Start inschrijving` },
{ to: '/herregistratie', titel: $localize`:@@dashboard.actie.herregistratie.titel:Herregistratie aanvragen`, tekst: $localize`:@@dashboard.actie.herregistratie.tekst:Verleng uw registratie voor de komende periode.`, actie: $localize`:@@dashboard.actie.herregistratie.actie:Vraag aan` },
{ to: '/registratie', titel: $localize`:@@dashboard.actie.wijzigen.titel:Gegevens wijzigen`, tekst: $localize`:@@dashboard.actie.wijzigen.tekst:Bekijk uw gegevens of geef een wijziging door.`, actie: $localize`:@@dashboard.actie.wijzigen.actie:Bekijk gegevens` },
];
}

View File

@@ -2,7 +2,7 @@ import { Component, computed, effect, inject, input, untracked } from '@angular/
import { FormsModule } from '@angular/forms';
import { FormFieldComponent } from '@shared/ui/form-field/form-field.component';
import { TextInputComponent } from '@shared/ui/text-input/text-input.component';
import { RadioGroupComponent } from '@shared/ui/radio-group/radio-group.component';
import { RadioGroupComponent, JA_NEE } from '@shared/ui/radio-group/radio-group.component';
import { ButtonComponent } from '@shared/ui/button/button.component';
import { AlertComponent } from '@shared/ui/alert/alert.component';
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.component';
@@ -31,8 +31,10 @@ import { submitRegistratie } from '@registratie/application/submit-registratie';
import { ApiClient } from '@shared/infrastructure/api-client';
const STORAGE_KEY = 'registratie-v1'; // ponytail: bump the suffix if the persisted shape changes; no migration.
const KANALEN = [{ value: 'email', label: 'E-mail' }, { value: 'post', label: 'Post' }];
const JA_NEE = [{ value: 'ja', label: 'Ja' }, { value: 'nee', label: 'Nee' }];
const KANALEN = [
{ value: 'email', label: $localize`:@@registratie.kanaalEmail:E-mail` },
{ value: 'post', label: $localize`:@@registratie.kanaalPost:Post` },
];
const HANDMATIG = '__handmatig__'; // sentinel option: "my diploma isn't listed"
/** Organism: the BIG-registration wizard. All state lives in one signal driven by
@@ -58,7 +60,7 @@ const HANDMATIG = '__handmatig__'; // sentinel option: "my diploma isn't listed"
[canGoBack]="cursor() > 0"
[errors]="errorList()"
[errorMessage]="errorMessage()"
submittingLabel="Uw registratie wordt verwerkt…"
i18n-submittingLabel="@@regWizard.submitting" submittingLabel="Uw registratie wordt verwerkt…"
(primary)="onPrimary()"
(back)="dispatch({ tag: 'Back' })"
(cancel)="restart()"
@@ -71,26 +73,26 @@ const HANDMATIG = '__handmatig__'; // sentinel option: "my diploma isn't listed"
} @else {
@switch (adresStatus()) {
@case ('gevonden') {
<app-alert type="info">Vooraf ingevuld op basis van de BRP. Controleer en pas zo nodig aan.</app-alert>
<app-alert type="info" i18n="@@regWizard.brpGevonden">Vooraf ingevuld op basis van de BRP. Controleer en pas zo nodig aan.</app-alert>
}
@case ('geen') {
<app-alert type="warning">We vonden geen adres in de BRP. Vul uw adres hieronder handmatig in.</app-alert>
<app-alert type="warning" i18n="@@regWizard.brpGeen">We vonden geen adres in de BRP. Vul uw adres hieronder handmatig in.</app-alert>
}
@case ('fout') {
<app-alert type="warning">We konden de BRP nu niet bereiken. Vul uw adres hieronder handmatig in.</app-alert>
<app-alert type="warning" i18n="@@regWizard.brpFout">We konden de BRP nu niet bereiken. Vul uw adres hieronder handmatig in.</app-alert>
}
}
<app-address-fields
[value]="{ straat: draft().straat ?? '', postcode: draft().postcode ?? '', woonplaats: draft().woonplaats ?? '' }"
[errors]="{ straat: err('straat'), postcode: err('postcode'), woonplaats: err('woonplaats') }"
(fieldChange)="set($event.key, $event.value)" />
<app-form-field label="Hoe wilt u correspondentie ontvangen?" fieldId="correspondentie" required [error]="err('correspondentie')">
<app-form-field i18n-label="@@regWizard.correspondentieLabel" label="Hoe wilt u correspondentie ontvangen?" fieldId="correspondentie" required [error]="err('correspondentie')">
<app-radio-group name="correspondentie" [options]="kanalen" [invalid]="!!err('correspondentie')"
[ngModel]="draft().correspondentie ?? ''" (ngModelChange)="setKanaal($event)" />
</app-form-field>
@if (draft().correspondentie === 'email') {
<app-form-field label="E-mailadres" fieldId="email" required [error]="err('email')">
<app-text-input inputId="email" type="email" [invalid]="!!err('email')" [ngModel]="draft().email ?? ''" (ngModelChange)="set('email', $event)" name="email" placeholder="naam@voorbeeld.nl" />
<app-form-field i18n-label="@@regWizard.emailLabel" label="E-mailadres" fieldId="email" required [error]="err('email')">
<app-text-input inputId="email" type="email" [invalid]="!!err('email')" [ngModel]="draft().email ?? ''" (ngModelChange)="set('email', $event)" name="email" i18n-placeholder="@@regWizard.emailPlaceholder" placeholder="naam@voorbeeld.nl" />
</app-form-field>
}
}
@@ -98,20 +100,20 @@ const HANDMATIG = '__handmatig__'; // sentinel option: "my diploma isn't listed"
@case ('beroep') {
<app-async [data]="lookupRd()">
<ng-template appAsyncLoaded let-data>
<app-form-field label="Kies het diploma waarmee u zich wilt registreren" fieldId="diploma" required [error]="err('diploma')">
<app-form-field i18n-label="@@regWizard.diplomaLabel" label="Kies het diploma waarmee u zich wilt registreren" fieldId="diploma" required [error]="err('diploma')">
<app-radio-group name="diploma" [options]="diplomaOptions($any(data))" [invalid]="!!err('diploma')"
[ngModel]="diplomaKeuze()" (ngModelChange)="onDiplomaKeuze($any(data), $event)" />
</app-form-field>
@if (handmatigActief()) {
<app-alert type="warning">Een handmatig ingevoerd diploma kan niet automatisch worden geverifieerd. Kies uw beroep en beantwoord de aanvullende vragen; uw aanvraag wordt daarna handmatig beoordeeld.</app-alert>
<app-form-field label="Voor welk beroep wilt u zich registreren?" fieldId="hm-beroep" [error]="err('diploma')">
<app-alert type="warning" i18n="@@regWizard.handmatigWaarschuwing">Een handmatig ingevoerd diploma kan niet automatisch worden geverifieerd. Kies uw beroep en beantwoord de aanvullende vragen; uw aanvraag wordt daarna handmatig beoordeeld.</app-alert>
<app-form-field i18n-label="@@regWizard.beroepLabel" label="Voor welk beroep wilt u zich registreren?" fieldId="hm-beroep" [error]="err('diploma')">
<app-radio-group name="hm-beroep" [options]="beroepOptions($any(data))" [invalid]="!!err('diploma')"
[ngModel]="draft().beroep ?? ''" (ngModelChange)="dispatch({ tag: 'DeclareerBeroep', beroep: $event })" />
</app-form-field>
} @else if (draft().beroep) {
<dl class="rhc-data-summary rhc-data-summary--row app-section">
<app-data-row key="Beroep (afgeleid uit diploma)" [value]="draft().beroep ?? ''" />
<app-data-row i18n-key="@@regWizard.beroepAfgeleid" key="Beroep (afgeleid uit diploma)" [value]="draft().beroep ?? ''" />
</dl>
}
@@ -133,31 +135,31 @@ const HANDMATIG = '__handmatig__'; // sentinel option: "my diploma isn't listed"
</app-async>
}
@case ('controle') {
<app-alert type="info">Controleer uw gegevens en dien de registratie in.</app-alert>
<app-alert type="info" i18n="@@regWizard.controleer">Controleer uw gegevens en dien de registratie in.</app-alert>
<dl class="rhc-data-summary rhc-data-summary--row app-section">
<app-data-row key="Adres" [value]="adresSamenvatting()" />
<app-data-row key="Herkomst adres" [value]="adresHerkomstLabel()" />
<app-data-row key="Correspondentie" [value]="correspondentieLabel()" />
<app-data-row i18n-key="@@regWizard.summary.adres" key="Adres" [value]="adresSamenvatting()" />
<app-data-row i18n-key="@@regWizard.summary.herkomstAdres" key="Herkomst adres" [value]="adresHerkomstLabel()" />
<app-data-row i18n-key="@@regWizard.summary.correspondentie" key="Correspondentie" [value]="correspondentieLabel()" />
@if (draft().correspondentie === 'email') {
<app-data-row key="E-mailadres" [value]="draft().email ?? ''" />
<app-data-row i18n-key="@@regWizard.summary.email" key="E-mailadres" [value]="draft().email ?? ''" />
}
<app-data-row key="Beroep" [value]="draft().beroep ?? ''" />
<app-data-row key="Herkomst diploma" [value]="diplomaHerkomstLabel()" />
<app-data-row i18n-key="@@regWizard.summary.beroep" key="Beroep" [value]="draft().beroep ?? ''" />
<app-data-row i18n-key="@@regWizard.summary.herkomstDiploma" key="Herkomst diploma" [value]="diplomaHerkomstLabel()" />
@for (item of samenvattingVragen(); track item.vraag) {
<app-data-row [key]="item.vraag" [value]="item.antwoord" />
}
</dl>
<div class="app-button-row app-section">
<app-button type="button" variant="subtle" (click)="dispatch({ tag: 'GaNaarStap', cursor: 0 })">Adres wijzigen</app-button>
<app-button type="button" variant="subtle" (click)="dispatch({ tag: 'GaNaarStap', cursor: 1 })">Diploma wijzigen</app-button>
<app-button type="button" variant="subtle" (click)="dispatch({ tag: 'GaNaarStap', cursor: 0 })" i18n="@@regWizard.adresWijzigen">Adres wijzigen</app-button>
<app-button type="button" variant="subtle" (click)="dispatch({ tag: 'GaNaarStap', cursor: 1 })" i18n="@@regWizard.diplomaWijzigen">Diploma wijzigen</app-button>
</div>
}
}
<div wizardSuccess>
<app-alert type="ok">Uw registratie is ontvangen. Uw referentienummer is {{ referentie() }}. Bewaar dit nummer voor uw administratie.</app-alert>
<app-alert type="ok" i18n="@@regWizard.success">Uw registratie is ontvangen. Uw referentienummer is {{ referentie() }}. Bewaar dit nummer voor uw administratie.</app-alert>
<div class="app-section">
<app-button variant="secondary" (click)="restart()">Nieuwe registratie starten</app-button>
<app-button variant="secondary" (click)="restart()" i18n="@@regWizard.nieuweRegistratie">Nieuwe registratie starten</app-button>
</div>
</div>
</app-wizard-shell>
@@ -176,8 +178,8 @@ export class RegistratieWizardComponent {
seed = input<RegistratieState>(initial);
readonly kanalen = KANALEN;
readonly stepLabels = ['Adres', 'Beroep', 'Controle']; // short labels for the stepper
private stepTitles = ['Adres en correspondentievoorkeur', 'Beroep op basis van uw diploma', 'Controleren en indienen'];
readonly stepLabels = [$localize`:@@regWizard.step.adres:Adres`, $localize`:@@regWizard.step.beroep:Beroep`, $localize`:@@regWizard.step.controle:Controle`]; // short labels for the stepper
private stepTitles = [$localize`:@@regWizard.title.adres:Adres en correspondentievoorkeur`, $localize`:@@regWizard.title.beroep:Beroep op basis van uw diploma`, $localize`:@@regWizard.title.controle:Controleren en indienen`];
readonly state = this.store.model;
readonly dispatch = this.store.dispatch;
@@ -190,8 +192,8 @@ export class RegistratieWizardComponent {
protected failedError = computed(() => whenTag(this.state(), 'Mislukt')?.error ?? '');
// --- Presentational wiring for the shared wizard shell ---------------------
protected primaryLabel = computed(() => (this.step() === 'controle' ? 'Registratie indienen' : 'Volgende'));
protected errorMessage = computed(() => `Het indienen is niet gelukt: ${this.failedError()}`);
protected primaryLabel = computed(() => (this.step() === 'controle' ? $localize`:@@regWizard.indienen:Registratie indienen` : $localize`:@@wizard.volgende:Volgende`));
protected errorMessage = computed(() => $localize`:@@regWizard.indienenMislukt:Het indienen is niet gelukt:` + ` ${this.failedError()}`);
protected shellStatus = computed<WizardStatus>(() => {
switch (this.state().tag) {
case 'Invullen': return 'editing';
@@ -217,9 +219,9 @@ export class RegistratieWizardComponent {
return [d.straat, [d.postcode, d.woonplaats].filter(Boolean).join(' ')].filter(Boolean).join(', ');
});
// Readable labels for the controle summary (instead of raw enum values).
protected adresHerkomstLabel = computed(() => ({ brp: 'Automatisch uit de BRP', handmatig: 'Handmatig ingevoerd' }[this.draft().adresHerkomst ?? 'handmatig']));
protected correspondentieLabel = computed(() => ({ email: 'Per e-mail', post: 'Per post' }[this.draft().correspondentie ?? 'post']));
protected diplomaHerkomstLabel = computed(() => ({ duo: 'Geverifieerd via DUO', handmatig: 'Handmatig ingevoerd (wordt beoordeeld)' }[this.draft().diplomaHerkomst ?? 'handmatig']));
protected adresHerkomstLabel = computed(() => ({ brp: $localize`:@@regWizard.herkomst.adresBrp:Automatisch uit de BRP`, handmatig: $localize`:@@regWizard.herkomst.adresHandmatig:Handmatig ingevoerd` }[this.draft().adresHerkomst ?? 'handmatig']));
protected correspondentieLabel = computed(() => ({ email: $localize`:@@regWizard.corr.email:Per e-mail`, post: $localize`:@@regWizard.corr.post:Per post` }[this.draft().correspondentie ?? 'post']));
protected diplomaHerkomstLabel = computed(() => ({ duo: $localize`:@@regWizard.herkomst.diplomaDuo:Geverifieerd via DUO`, handmatig: $localize`:@@regWizard.herkomst.diplomaHandmatig:Handmatig ingevoerd (wordt beoordeeld)` }[this.draft().diplomaHerkomst ?? 'handmatig']));
/** BRP lookup outcome. A failure or "geen adres" never blocks the wizard — both
fall back to manual entry (PRD §7). 'geen' covers both no-address-found and a
@@ -262,7 +264,7 @@ export class RegistratieWizardComponent {
protected diplomaOptions = (data: DuoLookupDto) => [
...data.diplomas.map((d) => ({ value: d.id, label: `${d.naam}${d.instelling} (${d.jaar})` })),
{ value: HANDMATIG, label: 'Mijn diploma staat er niet bij' },
{ value: HANDMATIG, label: $localize`:@@regWizard.diplomaNietBij:Mijn diploma staat er niet bij` },
];
protected beroepOptions = (data: DuoLookupDto) => data.handmatig.beroepen.map((b) => ({ value: b, label: b }));

View File

@@ -10,9 +10,10 @@ import { RegistratieWizardComponent } from '@registratie/ui/registratie-wizard/r
imports: [PageShellComponent, AlertComponent, RegistratieWizardComponent],
template: `
<app-page-shell
i18n-heading="@@registratie.heading"
heading="Inschrijven in het BIG-register"
backLink="/dashboard">
<app-alert type="info">
<app-alert type="info" i18n="@@registratie.intro">
In drie stappen schrijft u zich in: uw adres en correspondentievoorkeur, het
diploma waarmee u zich registreert, en een controle. Uw gegevens blijven bewaard
als u de pagina herlaadt.

View File

@@ -13,7 +13,7 @@ import { BigProfileStore } from '@registratie/application/big-profile.store';
RegistrationSummaryComponent, ChangeRequestFormComponent,
],
template: `
<app-page-shell heading="Mijn gegevens" backLink="/dashboard">
<app-page-shell i18n-heading="@@registratieDetail.heading" heading="Mijn gegevens" backLink="/dashboard">
<app-async [data]="store.profile()">
<ng-template appAsyncLoaded let-p>
<app-registration-summary [reg]="$any(p).registration" />

View File

@@ -13,25 +13,25 @@ import { CardComponent } from '@shared/ui/card/card.component';
template: `
<app-card>
<dl class="rhc-data-summary rhc-data-summary--row">
<app-data-row key="BIG-nummer" [value]="reg().bigNummer" />
<app-data-row key="Naam" [value]="reg().naam" />
<app-data-row key="Beroep" [value]="reg().beroep" />
<app-data-row key="Status">
<app-data-row i18n-key="@@summary.bigNummer" key="BIG-nummer" [value]="reg().bigNummer" />
<app-data-row i18n-key="@@summary.naam" key="Naam" [value]="reg().naam" />
<app-data-row i18n-key="@@summary.beroep" key="Beroep" [value]="reg().beroep" />
<app-data-row i18n-key="@@summary.status" key="Status">
<app-status-badge [label]="label()" [color]="color()" />
</app-data-row>
<app-data-row key="Registratiedatum" [value]="reg().registratiedatum | date:'longDate'" />
<app-data-row i18n-key="@@summary.registratiedatum" key="Registratiedatum" [value]="reg().registratiedatum | date:'longDate'" />
<!-- Each status variant renders only the row its own data supports. -->
@switch (reg().status.tag) {
@case ('Geregistreerd') {
<app-data-row key="Uiterste herregistratie" [value]="$any(reg().status).herregistratieDatum | date:'longDate'" />
<app-data-row i18n-key="@@summary.uiterste" key="Uiterste herregistratie" [value]="$any(reg().status).herregistratieDatum | date:'longDate'" />
}
@case ('Geschorst') {
<app-data-row key="Geschorst tot" [value]="$any(reg().status).geschorstTot | date:'longDate'" />
<app-data-row key="Reden" [value]="$any(reg().status).reden" />
<app-data-row i18n-key="@@summary.geschorstTot" key="Geschorst tot" [value]="$any(reg().status).geschorstTot | date:'longDate'" />
<app-data-row i18n-key="@@summary.reden" key="Reden" [value]="$any(reg().status).reden" />
}
@case ('Doorgehaald') {
<app-data-row key="Doorgehaald op" [value]="$any(reg().status).doorgehaaldOp | date:'longDate'" />
<app-data-row key="Reden" [value]="$any(reg().status).reden" />
<app-data-row i18n-key="@@summary.doorgehaaldOp" key="Doorgehaald op" [value]="$any(reg().status).doorgehaaldOp | date:'longDate'" />
<app-data-row i18n-key="@@summary.reden" key="Reden" [value]="$any(reg().status).reden" />
}
}
</dl>

View File

@@ -11,9 +11,9 @@ import { Aantekening } from '@registratie/domain/registration';
<table class="utrecht-table utrecht-table--html-table utrecht-table--alternate-row-color">
<thead>
<tr>
<th class="utrecht-table__header-cell">Type</th>
<th class="utrecht-table__header-cell">Omschrijving</th>
<th class="utrecht-table__header-cell">Datum</th>
<th class="utrecht-table__header-cell" i18n="@@table.type">Type</th>
<th class="utrecht-table__header-cell" i18n="@@table.omschrijving">Omschrijving</th>
<th class="utrecht-table__header-cell" i18n="@@table.datum">Datum</th>
</tr>
</thead>
<tbody>

View File

@@ -15,6 +15,6 @@ export async function runSubmit<T>(fn: () => Promise<T>, fallback: string): Prom
}
}
// ponytail: i18n in Step 3/M3 wraps this in $localize with a stable @@id, which
// dedupes it at the translation layer; for now it's the single shared default.
export const SUBMIT_FAILED = 'Het indienen is niet gelukt. Probeer het later opnieuw.';
// Single shared default for a failed submit; the @@id dedupes it at the
// translation layer.
export const SUBMIT_FAILED = $localize`:@@submit.failed:Het indienen is niet gelukt. Probeer het later opnieuw.`;

View File

@@ -6,12 +6,12 @@ import { BreadcrumbItem } from './breadcrumb.component';
interface Crumb { label: string; parent?: string }
const ROUTES: Record<string, Crumb> = {
'/dashboard': { label: 'Mijn overzicht' },
'/registratie': { label: 'Mijn gegevens', parent: '/dashboard' },
'/registreren': { label: 'Inschrijven', parent: '/dashboard' },
'/herregistratie': { label: 'Herregistratie', parent: '/dashboard' },
'/intake': { label: 'Herregistratie-intake', parent: '/dashboard' },
'/concepts': { label: 'Functionele patronen', parent: '/dashboard' },
'/dashboard': { label: $localize`:@@crumb.dashboard:Mijn overzicht` },
'/registratie': { label: $localize`:@@crumb.registratie:Mijn gegevens`, parent: '/dashboard' },
'/registreren': { label: $localize`:@@crumb.registreren:Inschrijven`, parent: '/dashboard' },
'/herregistratie': { label: $localize`:@@crumb.herregistratie:Herregistratie`, parent: '/dashboard' },
'/intake': { label: $localize`:@@crumb.intake:Herregistratie-intake`, parent: '/dashboard' },
'/concepts': { label: $localize`:@@crumb.concepts:Functionele patronen`, parent: '/dashboard' },
};
/** Build the breadcrumb trail for a router url (query/fragment stripped).

View File

@@ -26,7 +26,7 @@ export interface BreadcrumbItem {
:host(.inverse) .sep { color: var(--rhc-color-foreground-on-primary); }
`],
template: `
<nav class="rhc-breadcrumb-nav utrecht-breadcrumb-nav" aria-label="Kruimelpad">
<nav class="rhc-breadcrumb-nav utrecht-breadcrumb-nav" i18n-aria-label="@@breadcrumb.aria" aria-label="Kruimelpad">
<ol class="utrecht-breadcrumb-nav__list list">
@for (item of items(); track item.label; let last = $last; let first = $first) {
<li class="utrecht-breadcrumb-nav__item crumb">

View File

@@ -32,6 +32,6 @@ export class PageShellComponent {
heading = input.required<string>();
intro = input<string>();
backLink = input<string>();
backLabel = input('Terug naar overzicht');
backLabel = input($localize`:@@pageShell.backLabel:Terug naar overzicht`);
width = input<'default' | 'narrow'>('default');
}

View File

@@ -17,7 +17,7 @@ import { DebugStateComponent } from '@shared/ui/debug-state/debug-state.componen
.content{max-inline-size:var(--app-content-max);margin-inline:auto;padding:var(--rhc-space-max-3xl) var(--rhc-space-max-2xl);box-sizing:border-box}
`],
template: `
<a href="#main" class="rhc-skip-link skip">Naar de inhoud</a>
<a href="#main" class="rhc-skip-link skip" i18n="@@shell.skipLink">Naar de inhoud</a>
<div class="utrecht-page-layout utrecht-page-layout--stretch layout">
<app-site-header />
<main id="main" class="utrecht-page-content main">

View File

@@ -46,5 +46,5 @@ export interface NavItem {
})
export class SideNavComponent {
items = input.required<NavItem[]>();
ariaLabel = input('Mijn omgeving');
ariaLabel = input($localize`:@@sideNav.aria:Mijn omgeving`);
}

View File

@@ -22,18 +22,18 @@ import { Component } from '@angular/core';
<footer class="utrecht-page-footer bar">
<div class="inner">
<div>
<div class="tagline">De Rijksoverheid. Voor Nederland.</div>
<div class="ministry">CIBG — Ministerie van Volksgezondheid, Welzijn en Sport</div>
<div class="tagline" i18n="@@footer.tagline">De Rijksoverheid. Voor Nederland.</div>
<div class="ministry" i18n="@@footer.ministry">CIBG — Ministerie van Volksgezondheid, Welzijn en Sport</div>
</div>
<nav class="col" aria-label="Over deze site">
<h2>Over deze site</h2>
<nav class="col" i18n-aria-label="@@footer.overSiteAria" aria-label="Over deze site">
<h2 i18n="@@footer.overSite">Over deze site</h2>
<ul class="links">
<li><a href="https://www.rijksoverheid.nl/privacy" rel="noopener" target="_blank">Privacy</a></li>
<li><a href="https://www.rijksoverheid.nl/cookies" rel="noopener" target="_blank">Cookies</a></li>
<li><a href="https://www.rijksoverheid.nl/toegankelijkheid" rel="noopener" target="_blank">Toegankelijkheid</a></li>
<li><a href="https://www.rijksoverheid.nl/privacy" rel="noopener" target="_blank" i18n="@@footer.privacy">Privacy</a></li>
<li><a href="https://www.rijksoverheid.nl/cookies" rel="noopener" target="_blank" i18n="@@footer.cookies">Cookies</a></li>
<li><a href="https://www.rijksoverheid.nl/toegankelijkheid" rel="noopener" target="_blank" i18n="@@footer.toegankelijkheid">Toegankelijkheid</a></li>
</ul>
</nav>
<div class="meta">Demo / POC — geen echte gegevens.</div>
<div class="meta" i18n="@@footer.demo">Demo / POC — geen echte gegevens.</div>
</div>
</footer>
`,

View File

@@ -35,13 +35,13 @@ import { trailFor } from '@shared/layout/breadcrumb/breadcrumb-trail';
<div class="inner">
<a routerLink="/dashboard" class="brand">
<span aria-hidden="true" class="mark"></span>
<span class="name">Rijksoverheid<br><span class="sub">BIG-register</span></span>
<span class="name" i18n="@@header.brand">Rijksoverheid<br><span class="sub">BIG-register</span></span>
</a>
<span class="portal sub">{{ subtitle() }}</span>
@if (session(); as s) {
<span class="session">
<span class="user">Ingelogd als {{ s.naam }}</span>
<button type="button" class="logout" (click)="logout()">Uitloggen</button>
<span class="user"><ng-container i18n="@@header.ingelogdAls">Ingelogd als</ng-container> {{ s.naam }}</span>
<button type="button" class="logout" (click)="logout()" i18n="@@header.uitloggen">Uitloggen</button>
</span>
}
</div>
@@ -56,7 +56,7 @@ import { trailFor } from '@shared/layout/breadcrumb/breadcrumb-trail';
`,
})
export class SiteHeaderComponent {
subtitle = input('Mijn omgeving');
subtitle = input($localize`:@@header.subtitle:Mijn omgeving`);
private router = inject(Router);
private sessionPort = inject(SESSION_PORT, { optional: true });

View File

@@ -39,7 +39,7 @@ export type WizardStatus = 'editing' | 'submitting' | 'submitted' | 'failed';
@if (errors().length) {
<div #errorSummary tabindex="-1" role="alert" aria-labelledby="wizard-error-title" class="app-section">
<app-alert type="error">
<h3 id="wizard-error-title" class="rhc-heading nl-heading--level-3 es-title">Er ging iets mis met uw invoer</h3>
<h3 id="wizard-error-title" class="rhc-heading nl-heading--level-3 es-title" i18n="@@wizard.errorTitle">Er ging iets mis met uw invoer</h3>
<ul class="es-list">
@for (e of errors(); track e.id) {
<li><a [href]="'#' + e.id" (click)="goToField($event, e.id)">{{ e.message }}</a></li>
@@ -52,10 +52,10 @@ export type WizardStatus = 'editing' | 'submitting' | 'submitted' | 'failed';
<ng-content />
<div class="app-button-row app-button-row--spaced">
@if (canGoBack()) {
<app-button type="button" variant="secondary" (click)="back.emit()">Vorige</app-button>
<app-button type="button" variant="secondary" (click)="back.emit()" i18n="@@wizard.vorige">Vorige</app-button>
}
<app-button type="submit" variant="primary">{{ primaryLabel() }}</app-button>
<app-button type="button" variant="subtle" (click)="cancel.emit()">Annuleren</app-button>
<app-button type="button" variant="subtle" (click)="cancel.emit()" i18n="@@wizard.annuleren">Annuleren</app-button>
</div>
</form>
}
@@ -68,7 +68,7 @@ export type WizardStatus = 'editing' | 'submitting' | 'submitted' | 'failed';
@case ('failed') {
<app-alert type="error">{{ errorMessage() }}</app-alert>
<div class="app-section">
<app-button variant="secondary" (click)="retry.emit()">Opnieuw proberen</app-button>
<app-button variant="secondary" (click)="retry.emit()" i18n="@@wizard.opnieuwProberen">Opnieuw proberen</app-button>
</div>
}
}
@@ -83,7 +83,7 @@ export class WizardShellComponent {
canGoBack = input(false);
errors = input<readonly WizardError[]>([]);
errorMessage = input('');
submittingLabel = input('Aanvraag wordt verwerkt…');
submittingLabel = input($localize`:@@wizard.submitting:Aanvraag wordt verwerkt…`);
primary = output<void>();
back = output<void>();

View File

@@ -14,7 +14,7 @@ import { Component, booleanAttribute, input } from '@angular/core';
template: `
<div class="rhc-form-field utrecht-form-field" [class.utrecht-form-field--invalid]="!!error()">
<label class="utrecht-form-label label" [id]="fieldId() + '-label'" [for]="fieldId()">
{{ label() }}@if (required()) { <span class="req">(verplicht)</span> }
{{ label() }}@if (required()) { <span class="req" i18n="@@formField.verplicht">(verplicht)</span> }
</label>
@if (description()) {
<div class="utrecht-form-field-description desc" [id]="fieldId() + '-desc'">{{ description() }}</div>

View File

@@ -6,6 +6,13 @@ export interface RadioOption {
label: string;
}
/** The ubiquitous yes/no option pair. Language-agnostic now the labels are
localized, so it lives next to RadioOption and both wizards import it. */
export const JA_NEE: RadioOption[] = [
{ value: 'ja', label: $localize`:@@common.ja:Ja` },
{ value: 'nee', label: $localize`:@@common.nee:Nee` },
];
/** Atom: a radio group. Thin wrapper over the Utrecht/RHC radio CSS, wired as a
form control so it works with ngModel just like the text-input atom. */
@Component({

View File

@@ -14,7 +14,7 @@ import { Component, OnDestroy, OnInit, input, signal } from '@angular/core';
`],
template: `
@if (visible()) {
<div class="sp" role="status" aria-label="Bezig met laden"></div>
<div class="sp" role="status" i18n-aria-label="@@spinner.aria" aria-label="Bezig met laden"></div>
}
`,
})

View File

@@ -38,8 +38,8 @@ import { Component, input } from '@angular/core';
.step--current .label{font-weight:var(--rhc-text-font-weight-semi-bold)}
`],
template: `
<nav aria-label="Voortgang">
<p class="sr-only">Stap {{ current() + 1 }} van {{ steps().length }}: {{ steps()[current()] }}</p>
<nav i18n-aria-label="@@stepper.aria" aria-label="Voortgang">
<p class="sr-only" i18n="@@stepper.stapVan">Stap {{ current() + 1 }} van {{ steps().length }}: {{ steps()[current()] }}</p>
<ol class="steps">
@for (label of steps(); track label; let i = $index) {
<li