> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hr-easy.nlead.ch/llms.txt
> Use this file to discover all available pages before exploring further.

# Internationalization

> next-intl message files, the parity gate, and the rule for applicant- and employee-facing 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`.

```tsx theme={null}
// client component
const t = useTranslations("feedback.cycle");

// server component
const t = await getTranslations("feedback.cycle");
```

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:

```bash theme={null}
npm run check:i18n-parity
```

<Warning>
  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.
</Warning>

## 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.

<Warning>
  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.
</Warning>

## 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

<Steps>
  <Step title="Pick the namespace">`module.section.key`, matching where the string is used.</Step>
  <Step title="Add it to all three files">Same path in `de.json`, `en.json`, `fr.json`.</Step>
  <Step title="Use it">`useTranslations` or `getTranslations`.</Step>

  <Step title="Verify">
    ```bash theme={null}
    npm run check:i18n-parity
    ```
  </Step>
</Steps>

## 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.
