Skip to main content
Three locales — de, fr, en — and two distinct mechanisms.

UI chrome: next-intl message files

Message files live in src/messages/{de,fr,en}.json.
Namespace keys as module.section.key.

All three files, together

A key added to de.json without en.json and fr.json is a runtime error for non-German users. The parity gate compares the full nested key tree of all three files and fails CI on a mismatch:
Never hardcode a German, French or English string in component JSX or in a service return value that reaches the UI. That includes error messages, button labels, table headers and placeholder text.

Authored content: per-locale fields

Anything a person reads that your customer wrote — e-mail templates, portal messages and notifications, job ads, pipeline stage instructions, offer letters, generated documents, survey and feedback questions. These are stored in per-locale fields on the record: field, fieldEn, fieldFr. Every such surface must ship with the shared TranslateButton (@/components/translate-button), which drafts the other locales from the one the user wrote for review before publication.
A single-locale communication surface is not acceptable. If a feature sends an applicant or an employee something they read, it must be authorable in all three languages.

Recipient locale wins

Outbound communication renders in the recipient’s locale, not the sender’s. The notification dispatcher resolves the recipient’s locale before rendering. Where a dispatch is recorded — probation questionnaires, payslip mails — the wording actually used is snapshotted on the dispatch row, so the record shows what the person received rather than what the template says today.

Testing

Component tests use a stable intl mock, checked by its own guard (npm run check:stable-intl-mock) so a test cannot accidentally depend on a non-deterministic translation shim. Assert on the translation key or on a stable test id, not on translated prose — a test that breaks when a comma moves in de.json is testing the wrong thing.

Adding a key

1

Pick the namespace

module.section.key, matching where the string is used.
2

Add it to all three files

Same path in de.json, en.json, fr.json.
3

Use it

useTranslations or getTranslations.
4

Verify

Formats

Locale affects dates, number separators and currency formatting. CHF is the currency throughout; the formatting helpers in src/lib/formatting/ handle the locale mapping. Number separators matter on payroll exports, where a mis-parsed separator is a mis-stated amount.