de, fr, en — and two distinct mechanisms.
UI chrome: next-intl message files
Message files live insrc/messages/{de,fr,en}.json.
module.section.key.
All three files, together
A key added tode.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:
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.
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 insrc/lib/formatting/ handle the locale mapping.
Number separators matter on payroll exports, where a mis-parsed separator is a
mis-stated amount.