# Internationalization (i18n) — Goose Desktop UI This document describes the i18n infrastructure for the Goose Desktop UI (`ui/desktop/`). ## Overview The i18n system is built on [react-intl](https://formatjs.io/docs/react-intl/) (part of the FormatJS suite). It uses the **ICU MessageFormat** standard for translations, which provides full support for pluralization, gender/select, number/date formatting, and nested messages — all governed by CLDR rules. **Key design decisions:** - English strings live in source code as `defaultMessage` values — no duplication between code and catalog. - The `@formatjs/cli` tool extracts messages automatically from source into translation catalogs. - Date, time, and number formatting use the same locale as text translations (single source of truth via `IntlProvider`). - No build pipeline changes required — react-intl is a pure runtime library. ## Marking strings for translation ### In React components ```tsx import { defineMessages, useIntl } from 'react-intl'; const messages = defineMessages({ greeting: { id: 'myComponent.greeting', defaultMessage: 'Hello, {name}!', }, itemCount: { id: 'myComponent.itemCount', defaultMessage: '{count, plural, one {# item} other {# items}}', }, }); function MyComponent({ name, count }: { name: string; count: number }) { const intl = useIntl(); return (

{intl.formatMessage(messages.greeting, { name })}

{intl.formatMessage(messages.itemCount, { count })}

); } ``` ### Message ID conventions Use dot-separated, hierarchical IDs that reflect the component location: ``` settings.appearance.title sessions.delete.confirmMessage launcher.placeholder searchBar.caseSensitive ``` ### ICU MessageFormat syntax | Feature | Syntax | Example | |---|---|---| | Interpolation | `{variable}` | `Hello, {name}!` | | Plural | `{var, plural, one {…} other {…}}` | `{count, plural, one {# file} other {# files}}` | | Select | `{var, select, male {…} female {…} other {…}}` | `{gender, select, male {He} female {She} other {They}}` | | Number | `{var, number}` | `{price, number, ::currency/USD}` | | Date | `{var, date, medium}` | `{when, date, long}` | The `#` symbol inside plural/selectordinal is replaced with the formatted number. For full syntax details, see the [ICU MessageFormat specification](https://unicode-org.github.io/icu/userguide/format_parse/messages/). ## Extracting messages After adding or modifying `defineMessages` calls, regenerate the English catalog: ```bash cd ui/desktop pnpm i18n:extract ``` This scans all `src/**/*.{ts,tsx}` files and writes the canonical English catalog to `src/i18n/messages/en.json`. Commit this file — it serves as the reference for translators. ### Keeping en.json in sync (automated check) The `lint:check` script includes `i18n:check`, which re-runs extraction and verifies the output matches what's committed: ```bash pnpm i18n:check ``` This runs as part of `pnpm lint:check` (and therefore CI). If a developer changes a `defaultMessage` in source but forgets to run `pnpm i18n:extract`, the check fails with a diff showing exactly what's out of date. To compile messages into an optimized AST format (optional, for production performance): ```bash pnpm i18n:compile ``` Compiled files go to `src/i18n/compiled/` (gitignored). ## Locale detection The locale is resolved at startup in the following order: 1. **`GOOSE_LOCALE`** — explicit override (set on the `window` object or via env) 2. **`navigator.language`** — the browser/OS locale 3. **`"en"`** — fallback default The resolved locale is used for both text translations and all Intl formatting (dates, numbers, relative times). ## Date and number formatting ### Inside React components Use `intl.formatDate()`, `intl.formatNumber()`, `intl.formatRelativeTime()` from the `useIntl()` hook. These automatically use the same locale as text translations: ```tsx const intl = useIntl(); intl.formatDate(new Date(), { month: 'long', day: 'numeric' }); intl.formatNumber(1234.5, { style: 'currency', currency: 'USD' }); ``` ### Outside React context For utility functions that don't have access to the React tree (e.g., `timeUtils.ts`), import the resolved locale directly: ```ts import { currentLocale } from '../i18n'; new Intl.DateTimeFormat(currentLocale, { ... }).format(date); ``` This ensures date/number formatting uses the same locale as the rest of the UI. ## Adding a new language 1. Copy `src/i18n/messages/en.json` to a new file, e.g., `src/i18n/messages/ja.json`. 2. Translate the `defaultMessage` values. Keep ICU syntax intact (e.g., `{count, plural, ...}`). 3. Add the locale code to `SUPPORTED_LOCALES` in `src/i18n/index.ts`. 4. Optionally run `pnpm i18n:compile` to pre-compile. No other code changes are needed — `loadMessages()` dynamically imports the correct catalog at runtime. ## Testing ### Wrapping test renders with IntlProvider Any component that uses `useIntl()` must be rendered inside an `IntlProvider`. Use the test helper: ```tsx import { IntlTestWrapper } from '../i18n/test-utils'; render(, { wrapper: IntlTestWrapper }); ``` ### i18n-specific tests Unit tests for locale detection and message loading live in `src/i18n/i18n.test.ts`. Run them with: ```bash cd ui/desktop pnpm test:run -- src/i18n/i18n.test.ts ``` ## Architecture summary ``` src/i18n/ ├── index.ts # Locale detection, loadMessages(), re-exports ├── messages/ │ └── en.json # Extracted English catalog (committed) ├── compiled/ # Compiled catalogs (gitignored) ├── test-utils.tsx # IntlTestWrapper for tests └── i18n.test.ts # Unit tests src/renderer.tsx # IntlProvider wraps the entire app tree ```