Localization: integrate next-intl and migrate every English string in the UI into a message catalog #146

Closed
opened 2026-06-19 17:35:30 +00:00 by james · 0 comments
Owner

Every user-facing string in app/(app)/, app/login, app/register is currently hardcoded English. Self-hosters deploying Carol to a non-English speaker need locale support, and adopting an i18n library now means future strings start in the catalog instead of inline.

Scope

  • Library: next-intl. Works with the App Router (server + client components), supports compile-time-validated keys via TypeScript, light runtime. Capture the choice and the alternatives rejected (react-i18next, lingui) in an ADR.
  • Provider wiring: next-intl provider at the layout level. Locale resolution order: ?locale= query → locale cookie → Accept-LanguageDEFAULT_LOCALE. Persist the choice in the locale cookie when the user picks one.
  • Catalog convention: messages/en.json is the source of truth; new locales land as sibling files (messages/es.json, etc.). Document explicitly that en.json is the only catalog the codebase commits to keeping in sync; other locales accept gaps that fall through to en.
  • Migration: every English string in app/(app)/, app/login, app/register becomes a t('namespace.key') reference. Namespacing follows the route tree (profile.basics.name, account.signinMethods.title, etc.).
  • Configuration env vars (per the ADR-0014-derived "README is the env-var source of truth" convention from #114): DEFAULT_LOCALE=en, SUPPORTED_LOCALES=en (comma-separated allowlist). Both land in README.md Configuration → Operations.
  • Locale picker UI: sidebar footer alongside the theme switch (if the sidebar work has landed), or a settings dropdown otherwise. Smoke a second locale (messages/es.json — just a translated home/login pair) to confirm the switch round-trips.
  • CLAUDE.md gains a new bullet under Conventions: all new user-facing strings land in messages/en.json; never hardcode strings in JSX.

Acceptance criteria

  • next-intl installed; provider wired into app/(app)/layout.tsx and app/layout.tsx.
  • messages/en.json exists with the full set of current strings; every (app) / /login / /register surface renders via t(...).
  • A smoke messages/es.json (home + login translated) confirms ?locale=es and the cookie round-trip work.
  • Locale picker visible (sidebar footer or interim home).
  • README.md Configuration gains the DEFAULT_LOCALE / SUPPORTED_LOCALES rows; CLAUDE.md Conventions gains the no-hardcoded-strings bullet.
  • lavamoat.allowScripts review for next-intl install scripts done in the PR per ADR-0010.
  • ADR captured.

Out of scope

  • Real translations to any second language (the smoke es.json is just the mechanism check).
  • RTL layout — follow-up if/when we add an RTL locale.
  • Locale-aware date / number / currency formatting upgrades — separate ticket once a real localized surface needs them.
  • Internal surfaces (docs/ci.md, ADRs, API JSON error bodies) — stay English.

Dependency note

If the Carol's-voice copy-rewrite ticket lands first, this ticket migrates the already-voice-corrected strings. If this lands first, the voice rewrite edits messages/en.json instead of JSX. Either order works; the PR description must call out which.

Composes with

Design package (locale picker fits naturally next to the theme switch in the sidebar), CLAUDE.md Conventions, README Configuration table from #114, ADR-0008 (locale cookie follows the same path as the theme cookie), ADR-0010 (install scripts).

Every user-facing string in `app/(app)/`, `app/login`, `app/register` is currently hardcoded English. Self-hosters deploying Carol to a non-English speaker need locale support, and adopting an i18n library now means future strings start in the catalog instead of inline. ## Scope - **Library:** `next-intl`. Works with the App Router (server + client components), supports compile-time-validated keys via TypeScript, light runtime. Capture the choice and the alternatives rejected (`react-i18next`, `lingui`) in an ADR. - **Provider wiring:** `next-intl` provider at the layout level. Locale resolution order: `?locale=` query → `locale` cookie → `Accept-Language` → `DEFAULT_LOCALE`. Persist the choice in the `locale` cookie when the user picks one. - **Catalog convention:** `messages/en.json` is the source of truth; new locales land as sibling files (`messages/es.json`, etc.). Document explicitly that en.json is the only catalog the codebase commits to keeping in sync; other locales accept gaps that fall through to en. - **Migration:** every English string in `app/(app)/`, `app/login`, `app/register` becomes a `t('namespace.key')` reference. Namespacing follows the route tree (`profile.basics.name`, `account.signinMethods.title`, etc.). - **Configuration env vars** (per the ADR-0014-derived "README is the env-var source of truth" convention from #114): `DEFAULT_LOCALE=en`, `SUPPORTED_LOCALES=en` (comma-separated allowlist). Both land in `README.md` *Configuration → Operations*. - **Locale picker UI:** sidebar footer alongside the theme switch (if the sidebar work has landed), or a settings dropdown otherwise. Smoke a second locale (`messages/es.json` — just a translated home/login pair) to confirm the switch round-trips. - **CLAUDE.md** gains a new bullet under *Conventions*: all new user-facing strings land in `messages/en.json`; never hardcode strings in JSX. ## Acceptance criteria - [ ] `next-intl` installed; provider wired into `app/(app)/layout.tsx` and `app/layout.tsx`. - [ ] `messages/en.json` exists with the full set of current strings; every `(app)` / `/login` / `/register` surface renders via `t(...)`. - [ ] A smoke `messages/es.json` (home + login translated) confirms `?locale=es` and the cookie round-trip work. - [ ] Locale picker visible (sidebar footer or interim home). - [ ] `README.md` *Configuration* gains the `DEFAULT_LOCALE` / `SUPPORTED_LOCALES` rows; CLAUDE.md *Conventions* gains the no-hardcoded-strings bullet. - [ ] `lavamoat.allowScripts` review for `next-intl` install scripts done in the PR per ADR-0010. - [ ] ADR captured. ## Out of scope - Real translations to any second language (the smoke `es.json` is just the mechanism check). - RTL layout — follow-up if/when we add an RTL locale. - Locale-aware date / number / currency formatting upgrades — separate ticket once a real localized surface needs them. - Internal surfaces (`docs/ci.md`, ADRs, API JSON error bodies) — stay English. ## Dependency note If the Carol's-voice copy-rewrite ticket lands first, this ticket migrates the already-voice-corrected strings. If this lands first, the voice rewrite edits `messages/en.json` instead of JSX. Either order works; the PR description must call out which. ## Composes with Design package (locale picker fits naturally next to the theme switch in the sidebar), CLAUDE.md *Conventions*, README *Configuration* table from #114, ADR-0008 (locale cookie follows the same path as the theme cookie), ADR-0010 (install scripts).
james closed this issue 2026-06-20 01:00:43 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
james/carol#146
No description provided.