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. // Swap for a real OIDC redirect flow when there's a backend.
async authenticate(bsn: string): Promise<Result<string, Session>> { async authenticate(bsn: string): Promise<Result<string, Session>> {
const t = bsn.trim(); 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' }); 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], imports: [FormsModule, FormFieldComponent, TextInputComponent, ButtonComponent],
template: ` template: `
<form (ngSubmit)="submitted.emit(bsn)" class="app-form app-form-panel"> <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-text-input inputId="bsn" [(ngModel)]="bsn" name="bsn" placeholder="123456789" />
</app-form-field> </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-text-input inputId="pw" type="password" [(ngModel)]="password" name="pw" />
</app-form-field> </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> </form>
`, `,
}) })

View File

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

View File

@@ -73,9 +73,9 @@ function validateStep(step: StepId, a: Answers, scholingThreshold: number): Resu
const errors: Errors = {}; const errors: Errors = {};
switch (step) { switch (step) {
case 'buitenland': 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') { 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 ?? ''); const u = parseUren(a.buitenlandseUren ?? '');
if (!u.ok) errors.buitenlandseUren = u.error; if (!u.ok) errors.buitenlandseUren = u.error;
} }
@@ -83,7 +83,7 @@ function validateStep(step: StepId, a: Answers, scholingThreshold: number): Resu
case 'werk': { case 'werk': {
const u = parseUren(a.uren ?? ''); const u = parseUren(a.uren ?? '');
if (!u.ok) errors.uren = u.error; 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. // Nascholingspunten are only asked (and required) when scholing was followed.
if (a.scholingGevolgd === 'ja') { if (a.scholingGevolgd === 'ja') {
const p = parseUren(a.punten ?? ''); const p = parseUren(a.punten ?? '');

View File

@@ -36,23 +36,23 @@ import { ApiClient } from '@shared/infrastructure/api-client';
(retry)="onRetry()"> (retry)="onRetry()">
@if (step() === 1) { @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 })" <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>
<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 })" <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> </app-form-field>
} @else { } @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 })" <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> </app-form-field>
} }
<div wizardSuccess> <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> </div>
</app-wizard-shell> </app-wizard-shell>
`, `,
@@ -69,8 +69,8 @@ export class HerregistratieWizardComponent {
protected dispatch = this.store.dispatch; protected dispatch = this.store.dispatch;
// Stepper labels + per-step heading titles (presentational only). // Stepper labels + per-step heading titles (presentational only).
readonly stepLabels = ['Werkervaring', 'Nascholing']; readonly stepLabels = [$localize`:@@herregWizard.step.werkervaring:Werkervaring`, $localize`:@@herregWizard.step.nascholing:Nascholing`];
private stepTitles = ['Werkervaring (afgelopen 5 jaar)', 'Nascholing']; private stepTitles = [$localize`:@@herregWizard.title.werkervaring:Werkervaring (afgelopen 5 jaar)`, $localize`:@@herregWizard.title.nascholing:Nascholing`];
private editing = computed(() => whenTag(this.state(), 'Editing')); private editing = computed(() => whenTag(this.state(), 'Editing'));
protected step = computed(() => this.editing()?.step ?? 1); protected step = computed(() => this.editing()?.step ?? 1);
@@ -82,8 +82,8 @@ export class HerregistratieWizardComponent {
// --- Presentational wiring for the shared wizard shell --------------------- // --- Presentational wiring for the shared wizard shell ---------------------
protected stepTitle = computed(() => this.stepTitles[this.step() - 1]); protected stepTitle = computed(() => this.stepTitles[this.step() - 1]);
protected primaryLabel = computed(() => (this.step() === 1 ? 'Volgende' : 'Herregistratie aanvragen')); protected primaryLabel = computed(() => (this.step() === 1 ? $localize`:@@wizard.volgende:Volgende` : $localize`:@@herregWizard.indienen:Herregistratie aanvragen`));
protected errorMessage = computed(() => `Indienen mislukt: ${this.failedError()}`); protected errorMessage = computed(() => $localize`:@@wizard.indienenMislukt:Indienen mislukt:` + ` ${this.failedError()}`);
protected shellStatus = computed<WizardStatus>(() => { protected shellStatus = computed<WizardStatus>(() => {
switch (this.state().tag) { switch (this.state().tag) {
case 'Editing': return 'editing'; case 'Editing': return 'editing';

View File

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

View File

@@ -2,7 +2,7 @@ import { Component, computed, effect, inject, input, untracked } from '@angular/
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { FormFieldComponent } from '@shared/ui/form-field/form-field.component'; import { FormFieldComponent } from '@shared/ui/form-field/form-field.component';
import { TextInputComponent } from '@shared/ui/text-input/text-input.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 { ButtonComponent } from '@shared/ui/button/button.component';
import { AlertComponent } from '@shared/ui/alert/alert.component'; import { AlertComponent } from '@shared/ui/alert/alert.component';
import { DataRowComponent } from '@shared/ui/data-row/data-row.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'; 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 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 /** Organism: a BRANCHING intake questionnaire. All state lives in one signal
driven by the pure `reduce` (intake.machine.ts). Which step renders is derived 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()) { @switch (step()) {
@case ('buitenland') { @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" <app-radio-group name="buitenlandGewerkt" [options]="jaNee"
[ngModel]="answers().buitenlandGewerkt ?? ''" (ngModelChange)="set('buitenlandGewerkt', $event)" /> [ngModel]="answers().buitenlandGewerkt ?? ''" (ngModelChange)="set('buitenlandGewerkt', $event)" />
</app-form-field> </app-form-field>
@if (answers().buitenlandGewerkt === 'ja') { @if (answers().buitenlandGewerkt === 'ja') {
<app-form-field label="In welk land?" fieldId="land" required [error]="err('land')"> <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" placeholder="bijv. België" /> <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>
<app-form-field label="Hoeveel uur heeft u daar gewerkt?" fieldId="buitenlandseUren" required [error]="err('buitenlandseUren')"> <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" placeholder="bijv. 800" /> <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> </app-form-field>
} }
} }
@case ('werk') { @case ('werk') {
<app-form-field label="Gewerkte uren in Nederland (afgelopen 5 jaar)" fieldId="uren" required [error]="err('uren')"> <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" placeholder="bijv. 4160" /> <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> </app-form-field>
@if (scholingZichtbaar()) { @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" <app-radio-group name="scholingGevolgd" [options]="jaNee"
[ngModel]="answers().scholingGevolgd ?? ''" (ngModelChange)="set('scholingGevolgd', $event)" /> [ngModel]="answers().scholingGevolgd ?? ''" (ngModelChange)="set('scholingGevolgd', $event)" />
</app-form-field> </app-form-field>
} }
@if (answers().scholingGevolgd === 'ja') { @if (answers().scholingGevolgd === 'ja') {
<app-form-field label="Behaalde nascholingspunten" fieldId="punten" required [error]="err('punten')"> <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" placeholder="bijv. 200" /> <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> </app-form-field>
} }
} }
@case ('review') { @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"> <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') { @if (answers().buitenlandGewerkt === 'ja') {
<app-data-row key="Land" [value]="answers().land ?? ''" /> <app-data-row i18n-key="@@intake.review.land" key="Land" [value]="answers().land ?? ''" />
<app-data-row key="Buitenlandse uren" [value]="answers().buitenlandseUren ?? ''" /> <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()) { @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') { @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> </dl>
} }
} }
<div wizardSuccess> <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"> <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>
</div> </div>
</app-wizard-shell> </app-wizard-shell>
@@ -140,15 +139,15 @@ export class IntakeWizardComponent {
protected failedError = computed(() => whenTag(this.state(), 'Failed')?.error ?? ''); protected failedError = computed(() => whenTag(this.state(), 'Failed')?.error ?? '');
// --- Presentational wiring for the shared wizard shell --------------------- // --- 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> = { private stepTitles: Record<StepId, string> = {
buitenland: 'Werken in het buitenland', buitenland: $localize`:@@intake.title.buitenland:Werken in het buitenland`,
werk: 'Werkervaring in Nederland', werk: $localize`:@@intake.title.werk:Werkervaring in Nederland`,
review: 'Controleren en indienen', review: $localize`:@@intake.title.review:Controleren en indienen`,
}; };
protected stepTitle = computed(() => this.stepTitles[this.step()]); protected stepTitle = computed(() => this.stepTitles[this.step()]);
protected primaryLabel = computed(() => (this.step() === 'review' ? 'Aanvraag indienen' : 'Volgende')); protected primaryLabel = computed(() => (this.step() === 'review' ? $localize`:@@intake.indienen:Aanvraag indienen` : $localize`:@@wizard.volgende:Volgende`));
protected errorMessage = computed(() => `Indienen mislukt: ${this.failedError()}`); protected errorMessage = computed(() => $localize`:@@wizard.indienenMislukt:Indienen mislukt:` + ` ${this.failedError()}`);
protected shellStatus = computed<WizardStatus>(() => { protected shellStatus = computed<WizardStatus>(() => {
switch (this.state().tag) { switch (this.state().tag) {
case 'Answering': return 'editing'; case 'Answering': return 'editing';

View File

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

View File

@@ -40,7 +40,7 @@ function validate(draft: Draft): Result<Errors, Valid> {
const straat = draft.straat.trim(); const straat = draft.straat.trim();
const postcode = parsePostcode(draft.postcode); const postcode = parsePostcode(draft.postcode);
const errors: Errors = {}; 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 (!postcode.ok) errors.postcode = postcode.error;
if (straat && postcode.ok) { if (straat && postcode.ok) {
return { ok: true, value: { straat, postcode: postcode.value, woonplaats: draft.woonplaats.trim() } }; 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 = {}; const errors: Errors = {};
switch (step) { switch (step) {
case 'adres': { 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 ?? ''); const pc = parsePostcode(d.postcode ?? '');
if (!pc.ok) errors.postcode = pc.error; if (!pc.ok) errors.postcode = pc.error;
if (!d.woonplaats || d.woonplaats.trim() === '') errors.woonplaats = 'Vul een woonplaats in.'; if (!d.woonplaats || d.woonplaats.trim() === '') errors.woonplaats = $localize`:@@validation.woonplaats:Vul een woonplaats in.`;
if (!d.correspondentie) errors.correspondentie = 'Maak een keuze.'; if (!d.correspondentie) errors.correspondentie = $localize`:@@validation.maakKeuze:Maak een keuze.`;
// E-mail is only required when 'email' is the chosen channel. // E-mail is only required when 'email' is the chosen channel.
if (d.correspondentie === 'email') { if (d.correspondentie === 'email') {
const e = parseEmail(d.email ?? ''); const e = parseEmail(d.email ?? '');
@@ -100,7 +100,7 @@ function validateStep(step: StepId, d: Draft): Result<Errors, void> {
case 'beroep': { case 'beroep': {
// A diploma must be chosen (or declared manually); its beroep is then known. // A diploma must be chosen (or declared manually); its beroep is then known.
if (!d.diplomaId || !d.beroep) { 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; break;
} }
// Every policy question the chosen diploma raised must be answered. Which // 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. // they're answered.
const open: Record<string, string> = {}; const open: Record<string, string> = {};
for (const id of d.vraagIds ?? []) { 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; if (Object.keys(open).length > 0) errors.antwoorden = open;
break; break;

View File

@@ -28,30 +28,30 @@ export function tasksFromProfile(reg: Registration, eligibleForHerregistratie: b
if (eligibleForHerregistratie) { if (eligibleForHerregistratie) {
const deadline = herregistratieDeadline(reg); const deadline = herregistratieDeadline(reg);
tasks.push({ tasks.push({
title: 'Vraag uw herregistratie aan', title: $localize`:@@task.herregistratie.title:Vraag uw herregistratie aan`,
description: deadline description: deadline
? `Verleng uw registratie vóór ${formatNL(deadline)}.` ? $localize`:@@task.herregistratie.deadline:Verleng uw registratie vóór ${formatNL(deadline)}:deadline:.`
: 'U kunt nu uw herregistratie aanvragen.', : $localize`:@@task.herregistratie.nodeadline:U kunt nu uw herregistratie aanvragen.`,
to: '/herregistratie', to: '/herregistratie',
actionLabel: 'Herregistratie aanvragen', actionLabel: $localize`:@@task.herregistratie.action:Herregistratie aanvragen`,
}); });
} }
if (reg.status.tag === 'Geschorst') { if (reg.status.tag === 'Geschorst') {
tasks.push({ tasks.push({
title: 'Uw registratie is geschorst', title: $localize`:@@task.geschorst.title:Uw registratie is geschorst`,
description: reg.status.reden, description: reg.status.reden,
to: '/registratie', to: '/registratie',
actionLabel: 'Bekijk uw gegevens', actionLabel: $localize`:@@task.bekijkGegevens.action:Bekijk uw gegevens`,
}); });
} }
if (reg.status.tag === 'Doorgehaald') { if (reg.status.tag === 'Doorgehaald') {
tasks.push({ tasks.push({
title: 'Uw registratie is doorgehaald', title: $localize`:@@task.doorgehaald.title:Uw registratie is doorgehaald`,
description: reg.status.reden, description: reg.status.reden,
to: '/registratie', 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> { export function parseBigNummer(raw: string): Result<string, BigNummer> {
const t = raw.trim(); 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 // Deliberately lax: a single @ with non-empty, dot-bearing parts. Good enough
// for instant feedback; the server re-validates. // for instant feedback; the server re-validates.
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(t)) { 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); 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> { export function parsePostcode(raw: string): Result<string, Postcode> {
const t = raw.trim().toUpperCase(); const t = raw.trim().toUpperCase();
if (!/^[1-9]\d{3}\s?[A-Z]{2}$/.test(t)) { 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. // Normalise to "1234 AB" — the parser also cleans up.
return ok(t.replace(/^(\d{4})\s?([A-Z]{2})$/, '$1 $2') as Postcode); 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); const n = Number(t);
// Number('') is 0 — guard the empty string explicitly. // Number('') is 0 — guard the empty string explicitly.
if (t === '' || !Number.isInteger(n) || n < 0) { 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); return ok(n as Uren);
} }

View File

@@ -27,17 +27,17 @@ export type AdresErrors = Partial<Record<keyof AdresValue, string>>;
template: ` template: `
<fieldset class="utrecht-form-fieldset utrecht-form-fieldset--html-fieldset"> <fieldset class="utrecht-form-fieldset utrecht-form-fieldset--html-fieldset">
<legend class="utrecht-form-fieldset__legend utrecht-form-fieldset__legend--html-legend">{{ legend() }}</legend> <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" <app-text-input [inputId]="idPrefix() + '-straat'" [invalid]="!!errors().straat"
[ngModel]="value().straat" (ngModelChange)="fieldChange.emit({ key: 'straat', value: $event })" [ngModel]="value().straat" (ngModelChange)="fieldChange.emit({ key: 'straat', value: $event })"
name="straat" [ngModelOptions]="{ standalone: true }" /> name="straat" [ngModelOptions]="{ standalone: true }" />
</app-form-field> </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" <app-text-input [inputId]="idPrefix() + '-postcode'" [invalid]="!!errors().postcode"
[ngModel]="value().postcode" (ngModelChange)="fieldChange.emit({ key: 'postcode', value: $event })" [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>
<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" <app-text-input [inputId]="idPrefix() + '-woonplaats'" [invalid]="!!errors().woonplaats"
[ngModel]="value().woonplaats" (ngModelChange)="fieldChange.emit({ key: 'woonplaats', value: $event })" [ngModel]="value().woonplaats" (ngModelChange)="fieldChange.emit({ key: 'woonplaats', value: $event })"
name="woonplaats" [ngModelOptions]="{ standalone: true }" /> name="woonplaats" [ngModelOptions]="{ standalone: true }" />
@@ -50,6 +50,6 @@ export class AddressFieldsComponent {
errors = input<AdresErrors>({}); errors = input<AdresErrors>({});
/** Prefix for field ids/labels — keeps them unique if two blocks ever co-exist. */ /** Prefix for field ids/labels — keeps them unique if two blocks ever co-exist. */
idPrefix = input('adres'); idPrefix = input('adres');
legend = input('Adres'); legend = input($localize`:@@address.legend:Adres`);
fieldChange = output<{ key: keyof AdresValue; value: string }>(); 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], imports: [ButtonComponent, HeadingComponent, AlertComponent, AddressFieldsComponent],
template: ` template: `
@if (state().tag === 'Submitted') { @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. Uw adreswijziging is ontvangen (referentie {{ referentie() }}). U ontvangt binnen 5 werkdagen bericht.
</app-alert> </app-alert>
<div class="app-section"> <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> </div>
} @else { } @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"> <form (ngSubmit)="onSubmit()" class="app-form app-form-panel app-section">
<app-address-fields <app-address-fields
idPrefix="cr" idPrefix="cr"
@@ -36,11 +36,11 @@ import { ApiClient } from '@shared/infrastructure/api-client';
(fieldChange)="dispatch({ tag: 'SetField', key: $event.key, value: $event.value })" /> (fieldChange)="dispatch({ tag: 'SetField', key: $event.key, value: $event.value })" />
@if (failedError()) { @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'"> <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> </app-button>
</form> </form>
} }
@@ -56,6 +56,9 @@ export class ChangeRequestFormComponent {
readonly state = this.store.model; readonly state = this.store.model;
protected dispatch = this.store.dispatch; 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')); private editing = computed(() => whenTag(this.state(), 'Editing'));
protected errors = computed<AdresErrors>(() => this.editing()?.errors ?? {}); protected errors = computed<AdresErrors>(() => this.editing()?.errors ?? {});
protected failedError = computed(() => whenTag(this.state(), 'Failed')?.error ?? ''); protected failedError = computed(() => whenTag(this.state(), 'Failed')?.error ?? '');

View File

@@ -25,13 +25,13 @@ import { tasksFromProfile } from '@registratie/domain/tasks';
RegistrationSummaryComponent, RegistrationTableComponent, RegistrationSummaryComponent, RegistrationTableComponent,
], ],
template: ` 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"> <div class="app-overview">
<app-side-nav [items]="nav" /> <app-side-nav [items]="nav" />
<div class="app-stack"> <div class="app-stack">
@if (store.pendingHerregistratie()) { @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()"> <app-async [data]="store.profile()">
@@ -39,24 +39,24 @@ import { tasksFromProfile } from '@registratie/domain/tasks';
@let tasks = tasksFor($any(p).registration); @let tasks = tasksFor($any(p).registration);
<section> <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) { @if (tasks.length) {
<app-task-list class="app-section" [tasks]="tasks" /> <app-task-list class="app-section" [tasks]="tasks" />
} @else { } @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>
<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"> <div class="app-section">
<app-registration-summary [reg]="$any(p).registration" /> <app-registration-summary [reg]="$any(p).registration" />
</div> </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"> <dl class="rhc-data-summary rhc-data-summary--row">
<app-data-row key="Straat" [value]="$any(p).person.adres.straat" /> <app-data-row i18n-key="@@dashboard.straat" key="Straat" [value]="$any(p).person.adres.straat" />
<app-data-row key="Postcode" [value]="$any(p).person.adres.postcode" /> <app-data-row i18n-key="@@dashboard.postcode" 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.woonplaats" key="Woonplaats" [value]="$any(p).person.adres.woonplaats" />
</dl> </dl>
</app-card> </app-card>
</section> </section>
@@ -67,7 +67,7 @@ import { tasksFromProfile } from '@registratie/domain/tasks';
</app-async> </app-async>
<section> <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"> <div class="app-section">
<app-async [data]="store.aantekeningen()"> <app-async [data]="store.aantekeningen()">
<ng-template appAsyncLoaded let-r> <ng-template appAsyncLoaded let-r>
@@ -77,14 +77,14 @@ import { tasksFromProfile } from '@registratie/domain/tasks';
<app-skeleton height="2.5rem" [count]="3" /> <app-skeleton height="2.5rem" [count]="3" />
</ng-template> </ng-template>
<ng-template appAsyncEmpty> <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> </ng-template>
</app-async> </app-async>
</div> </div>
</section> </section>
<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"> <ul class="app-card-grid app-section">
@for (a of acties; track a.to) { @for (a of acties; track a.to) {
<li> <li>
@@ -116,16 +116,16 @@ export class DashboardPage {
/** Portal sections (navigation, not actions). */ /** Portal sections (navigation, not actions). */
protected readonly nav: NavItem[] = [ protected readonly nav: NavItem[] = [
{ label: 'Overzicht', to: '/dashboard' }, { label: $localize`:@@dashboard.nav.overzicht:Overzicht`, to: '/dashboard' },
{ label: 'Mijn gegevens', to: '/registratie' }, { label: $localize`:@@dashboard.nav.gegevens:Mijn gegevens`, to: '/registratie' },
{ label: 'Herregistratie', to: '/herregistratie' }, { label: $localize`:@@dashboard.nav.herregistratie:Herregistratie`, to: '/herregistratie' },
{ label: 'Inschrijven', to: '/registreren' }, { label: $localize`:@@dashboard.nav.inschrijven:Inschrijven`, to: '/registreren' },
]; ];
/** Primary transactional actions, as a card grid. */ /** Primary transactional actions, as a card grid. */
protected readonly acties = [ protected readonly acties = [
{ to: '/registreren', titel: 'Inschrijven', tekst: 'Schrijf u in in het BIG-register via de registratiewizard.', actie: 'Start inschrijving' }, { 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: 'Herregistratie aanvragen', tekst: 'Verleng uw registratie voor de komende periode.', actie: 'Vraag aan' }, { 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: 'Gegevens wijzigen', tekst: 'Bekijk uw gegevens of geef een wijziging door.', actie: 'Bekijk gegevens' }, { 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 { FormsModule } from '@angular/forms';
import { FormFieldComponent } from '@shared/ui/form-field/form-field.component'; import { FormFieldComponent } from '@shared/ui/form-field/form-field.component';
import { TextInputComponent } from '@shared/ui/text-input/text-input.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 { ButtonComponent } from '@shared/ui/button/button.component';
import { AlertComponent } from '@shared/ui/alert/alert.component'; import { AlertComponent } from '@shared/ui/alert/alert.component';
import { SkeletonComponent } from '@shared/ui/skeleton/skeleton.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'; import { ApiClient } from '@shared/infrastructure/api-client';
const STORAGE_KEY = 'registratie-v1'; // ponytail: bump the suffix if the persisted shape changes; no migration. 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 KANALEN = [
const JA_NEE = [{ value: 'ja', label: 'Ja' }, { value: 'nee', label: 'Nee' }]; { 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" const HANDMATIG = '__handmatig__'; // sentinel option: "my diploma isn't listed"
/** Organism: the BIG-registration wizard. All state lives in one signal driven by /** 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" [canGoBack]="cursor() > 0"
[errors]="errorList()" [errors]="errorList()"
[errorMessage]="errorMessage()" [errorMessage]="errorMessage()"
submittingLabel="Uw registratie wordt verwerkt…" i18n-submittingLabel="@@regWizard.submitting" submittingLabel="Uw registratie wordt verwerkt…"
(primary)="onPrimary()" (primary)="onPrimary()"
(back)="dispatch({ tag: 'Back' })" (back)="dispatch({ tag: 'Back' })"
(cancel)="restart()" (cancel)="restart()"
@@ -71,26 +73,26 @@ const HANDMATIG = '__handmatig__'; // sentinel option: "my diploma isn't listed"
} @else { } @else {
@switch (adresStatus()) { @switch (adresStatus()) {
@case ('gevonden') { @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') { @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') { @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 <app-address-fields
[value]="{ straat: draft().straat ?? '', postcode: draft().postcode ?? '', woonplaats: draft().woonplaats ?? '' }" [value]="{ straat: draft().straat ?? '', postcode: draft().postcode ?? '', woonplaats: draft().woonplaats ?? '' }"
[errors]="{ straat: err('straat'), postcode: err('postcode'), woonplaats: err('woonplaats') }" [errors]="{ straat: err('straat'), postcode: err('postcode'), woonplaats: err('woonplaats') }"
(fieldChange)="set($event.key, $event.value)" /> (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')" <app-radio-group name="correspondentie" [options]="kanalen" [invalid]="!!err('correspondentie')"
[ngModel]="draft().correspondentie ?? ''" (ngModelChange)="setKanaal($event)" /> [ngModel]="draft().correspondentie ?? ''" (ngModelChange)="setKanaal($event)" />
</app-form-field> </app-form-field>
@if (draft().correspondentie === 'email') { @if (draft().correspondentie === 'email') {
<app-form-field label="E-mailadres" fieldId="email" required [error]="err('email')"> <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" placeholder="naam@voorbeeld.nl" /> <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> </app-form-field>
} }
} }
@@ -98,20 +100,20 @@ const HANDMATIG = '__handmatig__'; // sentinel option: "my diploma isn't listed"
@case ('beroep') { @case ('beroep') {
<app-async [data]="lookupRd()"> <app-async [data]="lookupRd()">
<ng-template appAsyncLoaded let-data> <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')" <app-radio-group name="diploma" [options]="diplomaOptions($any(data))" [invalid]="!!err('diploma')"
[ngModel]="diplomaKeuze()" (ngModelChange)="onDiplomaKeuze($any(data), $event)" /> [ngModel]="diplomaKeuze()" (ngModelChange)="onDiplomaKeuze($any(data), $event)" />
</app-form-field> </app-form-field>
@if (handmatigActief()) { @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-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 label="Voor welk beroep wilt u zich registreren?" fieldId="hm-beroep" [error]="err('diploma')"> <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')" <app-radio-group name="hm-beroep" [options]="beroepOptions($any(data))" [invalid]="!!err('diploma')"
[ngModel]="draft().beroep ?? ''" (ngModelChange)="dispatch({ tag: 'DeclareerBeroep', beroep: $event })" /> [ngModel]="draft().beroep ?? ''" (ngModelChange)="dispatch({ tag: 'DeclareerBeroep', beroep: $event })" />
</app-form-field> </app-form-field>
} @else if (draft().beroep) { } @else if (draft().beroep) {
<dl class="rhc-data-summary rhc-data-summary--row app-section"> <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> </dl>
} }
@@ -133,31 +135,31 @@ const HANDMATIG = '__handmatig__'; // sentinel option: "my diploma isn't listed"
</app-async> </app-async>
} }
@case ('controle') { @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"> <dl class="rhc-data-summary rhc-data-summary--row app-section">
<app-data-row key="Adres" [value]="adresSamenvatting()" /> <app-data-row i18n-key="@@regWizard.summary.adres" key="Adres" [value]="adresSamenvatting()" />
<app-data-row key="Herkomst adres" [value]="adresHerkomstLabel()" /> <app-data-row i18n-key="@@regWizard.summary.herkomstAdres" key="Herkomst adres" [value]="adresHerkomstLabel()" />
<app-data-row key="Correspondentie" [value]="correspondentieLabel()" /> <app-data-row i18n-key="@@regWizard.summary.correspondentie" key="Correspondentie" [value]="correspondentieLabel()" />
@if (draft().correspondentie === 'email') { @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 i18n-key="@@regWizard.summary.beroep" key="Beroep" [value]="draft().beroep ?? ''" />
<app-data-row key="Herkomst diploma" [value]="diplomaHerkomstLabel()" /> <app-data-row i18n-key="@@regWizard.summary.herkomstDiploma" key="Herkomst diploma" [value]="diplomaHerkomstLabel()" />
@for (item of samenvattingVragen(); track item.vraag) { @for (item of samenvattingVragen(); track item.vraag) {
<app-data-row [key]="item.vraag" [value]="item.antwoord" /> <app-data-row [key]="item.vraag" [value]="item.antwoord" />
} }
</dl> </dl>
<div class="app-button-row app-section"> <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: 0 })" i18n="@@regWizard.adresWijzigen">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: 1 })" i18n="@@regWizard.diplomaWijzigen">Diploma wijzigen</app-button>
</div> </div>
} }
} }
<div wizardSuccess> <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"> <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>
</div> </div>
</app-wizard-shell> </app-wizard-shell>
@@ -176,8 +178,8 @@ export class RegistratieWizardComponent {
seed = input<RegistratieState>(initial); seed = input<RegistratieState>(initial);
readonly kanalen = KANALEN; readonly kanalen = KANALEN;
readonly stepLabels = ['Adres', 'Beroep', 'Controle']; // short labels for the stepper readonly stepLabels = [$localize`:@@regWizard.step.adres:Adres`, $localize`:@@regWizard.step.beroep:Beroep`, $localize`:@@regWizard.step.controle:Controle`]; // short labels for the stepper
private stepTitles = ['Adres en correspondentievoorkeur', 'Beroep op basis van uw diploma', 'Controleren en indienen']; 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 state = this.store.model;
readonly dispatch = this.store.dispatch; readonly dispatch = this.store.dispatch;
@@ -190,8 +192,8 @@ export class RegistratieWizardComponent {
protected failedError = computed(() => whenTag(this.state(), 'Mislukt')?.error ?? ''); protected failedError = computed(() => whenTag(this.state(), 'Mislukt')?.error ?? '');
// --- Presentational wiring for the shared wizard shell --------------------- // --- Presentational wiring for the shared wizard shell ---------------------
protected primaryLabel = computed(() => (this.step() === 'controle' ? 'Registratie indienen' : 'Volgende')); protected primaryLabel = computed(() => (this.step() === 'controle' ? $localize`:@@regWizard.indienen:Registratie indienen` : $localize`:@@wizard.volgende:Volgende`));
protected errorMessage = computed(() => `Het indienen is niet gelukt: ${this.failedError()}`); protected errorMessage = computed(() => $localize`:@@regWizard.indienenMislukt:Het indienen is niet gelukt:` + ` ${this.failedError()}`);
protected shellStatus = computed<WizardStatus>(() => { protected shellStatus = computed<WizardStatus>(() => {
switch (this.state().tag) { switch (this.state().tag) {
case 'Invullen': return 'editing'; case 'Invullen': return 'editing';
@@ -217,9 +219,9 @@ export class RegistratieWizardComponent {
return [d.straat, [d.postcode, d.woonplaats].filter(Boolean).join(' ')].filter(Boolean).join(', '); return [d.straat, [d.postcode, d.woonplaats].filter(Boolean).join(' ')].filter(Boolean).join(', ');
}); });
// Readable labels for the controle summary (instead of raw enum values). // 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 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: 'Per e-mail', post: 'Per post' }[this.draft().correspondentie ?? 'post'])); 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: 'Geverifieerd via DUO', handmatig: 'Handmatig ingevoerd (wordt beoordeeld)' }[this.draft().diplomaHerkomst ?? 'handmatig'])); 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 /** 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 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) => [ protected diplomaOptions = (data: DuoLookupDto) => [
...data.diplomas.map((d) => ({ value: d.id, label: `${d.naam}${d.instelling} (${d.jaar})` })), ...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 })); 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], imports: [PageShellComponent, AlertComponent, RegistratieWizardComponent],
template: ` template: `
<app-page-shell <app-page-shell
i18n-heading="@@registratie.heading"
heading="Inschrijven in het BIG-register" heading="Inschrijven in het BIG-register"
backLink="/dashboard"> 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 In drie stappen schrijft u zich in: uw adres en correspondentievoorkeur, het
diploma waarmee u zich registreert, en een controle. Uw gegevens blijven bewaard diploma waarmee u zich registreert, en een controle. Uw gegevens blijven bewaard
als u de pagina herlaadt. als u de pagina herlaadt.

View File

@@ -13,7 +13,7 @@ import { BigProfileStore } from '@registratie/application/big-profile.store';
RegistrationSummaryComponent, ChangeRequestFormComponent, RegistrationSummaryComponent, ChangeRequestFormComponent,
], ],
template: ` 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()"> <app-async [data]="store.profile()">
<ng-template appAsyncLoaded let-p> <ng-template appAsyncLoaded let-p>
<app-registration-summary [reg]="$any(p).registration" /> <app-registration-summary [reg]="$any(p).registration" />

View File

@@ -13,25 +13,25 @@ import { CardComponent } from '@shared/ui/card/card.component';
template: ` template: `
<app-card> <app-card>
<dl class="rhc-data-summary rhc-data-summary--row"> <dl class="rhc-data-summary rhc-data-summary--row">
<app-data-row key="BIG-nummer" [value]="reg().bigNummer" /> <app-data-row i18n-key="@@summary.bigNummer" key="BIG-nummer" [value]="reg().bigNummer" />
<app-data-row key="Naam" [value]="reg().naam" /> <app-data-row i18n-key="@@summary.naam" key="Naam" [value]="reg().naam" />
<app-data-row key="Beroep" [value]="reg().beroep" /> <app-data-row i18n-key="@@summary.beroep" key="Beroep" [value]="reg().beroep" />
<app-data-row key="Status"> <app-data-row i18n-key="@@summary.status" key="Status">
<app-status-badge [label]="label()" [color]="color()" /> <app-status-badge [label]="label()" [color]="color()" />
</app-data-row> </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. --> <!-- Each status variant renders only the row its own data supports. -->
@switch (reg().status.tag) { @switch (reg().status.tag) {
@case ('Geregistreerd') { @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') { @case ('Geschorst') {
<app-data-row key="Geschorst tot" [value]="$any(reg().status).geschorstTot | date:'longDate'" /> <app-data-row i18n-key="@@summary.geschorstTot" 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.reden" key="Reden" [value]="$any(reg().status).reden" />
} }
@case ('Doorgehaald') { @case ('Doorgehaald') {
<app-data-row key="Doorgehaald op" [value]="$any(reg().status).doorgehaaldOp | date:'longDate'" /> <app-data-row i18n-key="@@summary.doorgehaaldOp" 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.reden" key="Reden" [value]="$any(reg().status).reden" />
} }
} }
</dl> </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"> <table class="utrecht-table utrecht-table--html-table utrecht-table--alternate-row-color">
<thead> <thead>
<tr> <tr>
<th class="utrecht-table__header-cell">Type</th> <th class="utrecht-table__header-cell" i18n="@@table.type">Type</th>
<th class="utrecht-table__header-cell">Omschrijving</th> <th class="utrecht-table__header-cell" i18n="@@table.omschrijving">Omschrijving</th>
<th class="utrecht-table__header-cell">Datum</th> <th class="utrecht-table__header-cell" i18n="@@table.datum">Datum</th>
</tr> </tr>
</thead> </thead>
<tbody> <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 // Single shared default for a failed submit; the @@id dedupes it at the
// dedupes it at the translation layer; for now it's the single shared default. // translation layer.
export const SUBMIT_FAILED = 'Het indienen is niet gelukt. Probeer het later opnieuw.'; 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 } interface Crumb { label: string; parent?: string }
const ROUTES: Record<string, Crumb> = { const ROUTES: Record<string, Crumb> = {
'/dashboard': { label: 'Mijn overzicht' }, '/dashboard': { label: $localize`:@@crumb.dashboard:Mijn overzicht` },
'/registratie': { label: 'Mijn gegevens', parent: '/dashboard' }, '/registratie': { label: $localize`:@@crumb.registratie:Mijn gegevens`, parent: '/dashboard' },
'/registreren': { label: 'Inschrijven', parent: '/dashboard' }, '/registreren': { label: $localize`:@@crumb.registreren:Inschrijven`, parent: '/dashboard' },
'/herregistratie': { label: 'Herregistratie', parent: '/dashboard' }, '/herregistratie': { label: $localize`:@@crumb.herregistratie:Herregistratie`, parent: '/dashboard' },
'/intake': { label: 'Herregistratie-intake', parent: '/dashboard' }, '/intake': { label: $localize`:@@crumb.intake:Herregistratie-intake`, parent: '/dashboard' },
'/concepts': { label: 'Functionele patronen', parent: '/dashboard' }, '/concepts': { label: $localize`:@@crumb.concepts:Functionele patronen`, parent: '/dashboard' },
}; };
/** Build the breadcrumb trail for a router url (query/fragment stripped). /** 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); } :host(.inverse) .sep { color: var(--rhc-color-foreground-on-primary); }
`], `],
template: ` 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"> <ol class="utrecht-breadcrumb-nav__list list">
@for (item of items(); track item.label; let last = $last; let first = $first) { @for (item of items(); track item.label; let last = $last; let first = $first) {
<li class="utrecht-breadcrumb-nav__item crumb"> <li class="utrecht-breadcrumb-nav__item crumb">

View File

@@ -32,6 +32,6 @@ export class PageShellComponent {
heading = input.required<string>(); heading = input.required<string>();
intro = input<string>(); intro = input<string>();
backLink = input<string>(); backLink = input<string>();
backLabel = input('Terug naar overzicht'); backLabel = input($localize`:@@pageShell.backLabel:Terug naar overzicht`);
width = input<'default' | 'narrow'>('default'); 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} .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: ` 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"> <div class="utrecht-page-layout utrecht-page-layout--stretch layout">
<app-site-header /> <app-site-header />
<main id="main" class="utrecht-page-content main"> <main id="main" class="utrecht-page-content main">

View File

@@ -46,5 +46,5 @@ export interface NavItem {
}) })
export class SideNavComponent { export class SideNavComponent {
items = input.required<NavItem[]>(); 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"> <footer class="utrecht-page-footer bar">
<div class="inner"> <div class="inner">
<div> <div>
<div class="tagline">De Rijksoverheid. Voor Nederland.</div> <div class="tagline" i18n="@@footer.tagline">De Rijksoverheid. Voor Nederland.</div>
<div class="ministry">CIBG — Ministerie van Volksgezondheid, Welzijn en Sport</div> <div class="ministry" i18n="@@footer.ministry">CIBG — Ministerie van Volksgezondheid, Welzijn en Sport</div>
</div> </div>
<nav class="col" aria-label="Over deze site"> <nav class="col" i18n-aria-label="@@footer.overSiteAria" aria-label="Over deze site">
<h2>Over deze site</h2> <h2 i18n="@@footer.overSite">Over deze site</h2>
<ul class="links"> <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/privacy" rel="noopener" target="_blank" i18n="@@footer.privacy">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/cookies" rel="noopener" target="_blank" i18n="@@footer.cookies">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/toegankelijkheid" rel="noopener" target="_blank" i18n="@@footer.toegankelijkheid">Toegankelijkheid</a></li>
</ul> </ul>
</nav> </nav>
<div class="meta">Demo / POC — geen echte gegevens.</div> <div class="meta" i18n="@@footer.demo">Demo / POC — geen echte gegevens.</div>
</div> </div>
</footer> </footer>
`, `,

View File

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

View File

@@ -39,7 +39,7 @@ export type WizardStatus = 'editing' | 'submitting' | 'submitted' | 'failed';
@if (errors().length) { @if (errors().length) {
<div #errorSummary tabindex="-1" role="alert" aria-labelledby="wizard-error-title" class="app-section"> <div #errorSummary tabindex="-1" role="alert" aria-labelledby="wizard-error-title" class="app-section">
<app-alert type="error"> <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"> <ul class="es-list">
@for (e of errors(); track e.id) { @for (e of errors(); track e.id) {
<li><a [href]="'#' + e.id" (click)="goToField($event, e.id)">{{ e.message }}</a></li> <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 /> <ng-content />
<div class="app-button-row app-button-row--spaced"> <div class="app-button-row app-button-row--spaced">
@if (canGoBack()) { @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="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> </div>
</form> </form>
} }
@@ -68,7 +68,7 @@ export type WizardStatus = 'editing' | 'submitting' | 'submitted' | 'failed';
@case ('failed') { @case ('failed') {
<app-alert type="error">{{ errorMessage() }}</app-alert> <app-alert type="error">{{ errorMessage() }}</app-alert>
<div class="app-section"> <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> </div>
} }
} }
@@ -83,7 +83,7 @@ export class WizardShellComponent {
canGoBack = input(false); canGoBack = input(false);
errors = input<readonly WizardError[]>([]); errors = input<readonly WizardError[]>([]);
errorMessage = input(''); errorMessage = input('');
submittingLabel = input('Aanvraag wordt verwerkt…'); submittingLabel = input($localize`:@@wizard.submitting:Aanvraag wordt verwerkt…`);
primary = output<void>(); primary = output<void>();
back = output<void>(); back = output<void>();

View File

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

View File

@@ -6,6 +6,13 @@ export interface RadioOption {
label: string; 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 /** 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. */ form control so it works with ngModel just like the text-input atom. */
@Component({ @Component({

View File

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