Internationalization (i18n)¶
Audience: Developers adding or editing user-facing copy.
Scope: How
next-intlis wired in the LMS, locale parity rules, message-key conventions, and the relationship between LMS chrome and CMS-authored content.
The LMS ships six locales: en, es, fr, de, pt, zh. The default locale is en. Locale negotiation runs in src/proxy.ts (Next.js 16 — not middleware.ts).
Library¶
next-intlis the only translation library. Do not introduce another (react-intl,i18next, custom hooks).- Configuration:
src/i18n/. - Messages:
messages/{en,es,fr,de,pt,zh}.json.
Server vs Client¶
| Surface | Helper | Notes |
|---|---|---|
| Server components | getTranslations() / getLocale() |
Async; resolves the active locale per request. |
| Server actions | getTranslations({ locale }) |
Pass an explicit locale when sending email, etc. |
| Client components | useTranslations() / useLocale() |
Inside NextIntlClientProvider. |
| Email templates | getTranslations({ locale: user.locale }) |
Use the recipient's users.locale. |
Always use the helper above; never construct locale-specific strings inline.
Key Conventions¶
- One namespace per top-level surface, e.g.
auth.signIn.title,dashboard.empty.message,admin.users.deactivate.confirm. - Keys are dot-separated, lowercase, kebab-cased segments allowed (
reset-password.success). - Reuse keys across locales — every key present in
en.jsonMUST exist in all five other locale files. CI enforces this via the monorepo scriptscripts/check-i18n.mjs(npm run check:i18n, also run as part of rootnpm run check). - Enum values from the database (roles, statuses) are mapped to translation keys via helpers in
src/lib/i18n/keys.ts. Never render raw enum strings to the UI.
Coverage Rules¶
- All visible text — labels, placeholders, button copy, headings, empty states, error toasts, validation messages, ARIA labels, page titles — must be translation-keyed.
- The same applies to
aria-label,aria-describedby, andplaceholderattributes used as visual hints. - Marketing copy not sourced from the CMS (CTAs, footer links,
404/500fallbacks) is translation-keyed. - CMS-authored body content (Portable Text, widget payloads) is rendered as authored. Locale support for CMS content is the CMS's responsibility and is out of scope for the LMS.
Locale Switching¶
- Anonymous users:
LocaleSwitcher(header dropdown) navigates to/{locale}/...and sets theNEXT_LOCALEcookie. The URL and cookie drive the rendered UI. - Signed-in users:
users.locale(profile field on the account page) is the source of truth for UI language.src/proxy.tsreadstoken.localefrom the Auth.js session and redirects any request whose URL locale differs from the profile locale (respectinglocalePrefix: "as-needed"). - Signed-in switcher: When a session is present,
LocaleSwitcherpersists the chosen locale viasetLocalePreferenceAction(updatesusers.locale, refreshes the JWT withunstable_update, then navigates). The account profile form uses the same persistence path and navigates immediately after a locale change. - Emails: Email subjects and bodies always use the recipient's
users.locale, independent of the active page locale.
CI Checks¶
npm run check:i18n (from the repo root; also part of npm run check) runs scripts/check-i18n.mjs. Canonical behavior and flags are documented in Root Scripts Reference.
Vitest tests/unit/i18n/messages.parity.test.ts and tests/unit/i18n/messages.leftovers.test.ts mirror parity and leftover rules.
The Playwright i18n regression suite (Phase 11) visits each top-level route on es and asserts no English-locale-only string from en.json appears in the rendered HTML.
Adding a Locale¶
Promoting a seventh locale is a follow-up tracked in roadmap.md. When the time comes:
- Add
messages/<locale>.jsonmirroringen.jsonkeys exactly. - Update the locale list in
src/i18n/. - Add the locale to
LocaleSwitcher. - Add the locale to the i18n smoke and regression Playwright projects.
- Update
reference/tech-stack.md.
Related References¶
reference/tech-stack.md— i18n tooling and fallback rules.- Root rule 100 — Documentation — broader docs and i18n expectations.