diff --git a/apps/maple-agent/docs/i18n.md b/apps/maple-agent/docs/i18n.md new file mode 100644 index 000000000..124f3f4f8 --- /dev/null +++ b/apps/maple-agent/docs/i18n.md @@ -0,0 +1,113 @@ +# Agent localization (i18n) + +Design and plan for localizing the Maple Agent GPUI application. Status: +proposal for review; no code has been extracted yet. + +## Current state + +The app is English-only with no localization infrastructure: + +- No i18n dependencies (`fluent`, `gettext`, `rust-i18n`, or similar) anywhere + in `apps/maple-agent`. +- No language or locale preference in `app/src/settings.rs`. +- User-facing strings are hardcoded English literals inline in the GPUI UI + modules, roughly 377 `text(...)`/string call sites across `app/src/ui/` and + its subdirectories (`chat/`, `settings/`, `remote/`, `markdown/`, + `store_gate/`, ...). Some are not user-facing (markdown code-language + labels, debug strings), so the real translatable surface is somewhat + smaller. + +## Decision: Fluent + +Localization will use the Mozilla Fluent stack (`fluent` + +`fluent-templates`) rather than `rust-i18n` or gettext. + +The deciding factor is scope: we target roughly twenty languages, not two or +three. At that scale Fluent's strengths are decisive: + +- **Correct plural and grammar rules.** Fluent applies CLDR plural + categories per language. Languages in our target set with three or more + plural categories (for example Russian, Polish, Arabic) cannot be handled + by `rust-i18n`'s two-form `_one`/`_other` convention without hand-rolled + logic per message. Retrofitting grammar later means reworking every + pluralized message, which is the expensive direction. +- **Translator workflow.** `.ftl` files carry comments and per-message + metadata, are edited without touching Rust, and have an existing tooling + ecosystem (checkers, editor support) built for localization teams. +- **Runtime locale switching is first-class** rather than a bolted-on + global. + +Trade-offs we accept: + +- More integration code than `rust-i18n`: we own bundle loading, locale + negotiation, and fallback chains (section 3 makes this a single small + module). +- No compile-time key checking in either candidate; both fail soft at + runtime, so we add the catalog completeness test in section 5. +- Slightly higher dependency and build cost. + +Gettext was rejected for the `.po` build-time workflow overhead and weaker +fit for GPUI's inline string construction. + +## Architecture + +### Catalogs + +- `apps/maple-agent/app/locales/{locale}/app.ftl`, one file per locale with + identical key sets. `en` is the fallback and the source of truth. +- Start with `en.ftl` only. Other locales fill in incrementally and fall + back to `en` at runtime; the app never ships blank strings. +- Keys are namespaced per module: `login-title`, `settings-privacy-header`, + `remote-pairing-cancelled`, ... + +### Lookup layer + +Widgets do not touch `FluentBundle` directly. A new `app/src/ui/i18n.rs` +provides: + +- `tr("key") -> SharedString` and `tr_args("key", ...) -> SharedString` for + parameterized messages, so call sites stay one-liners inside GPUI layout + expressions. +- A swap-once bundle handle so changing locale is cheap and GPUI re-renders + naturally (text resolves at render time). +- Fallback: active locale → `en` → the key itself, with a log line on every + fallback so misses surface in development instead of showing blank UI. + +### Locale resolution + +- Default from the OS locale at startup. +- Overridable in Settings, stored next to existing preferences in + `app/src/settings.rs`. + +## Extraction plan + +Incremental, per module; the app stays shippable after every step. + +1. **Infrastructure PR (this plan's first code change):** `i18n.rs` lookup + layer, `en.ftl` skeleton, catalog completeness test, and one module + converted end to end as the proof. +2. **Module-by-module extraction,** roughly in order: `settings/`, + `login.rs`, `chat/`, `remote/`, `store_gate/`, remaining `ui/` modules. + Each PR moves its module's literals into `en.ftl` and nothing else. +3. **Locale enablement:** add target locales as translations arrive; the + fallback chain means partial catalogs are safe to ship. + +## Guardrails + +- **Catalog completeness test:** parse `en.ftl`, assert every other locale's + key set matches (missing key fails, extra key warns). +- **Fluent syntax check** in CI so a broken placeholder in a translation + fails the lane rather than a user's screen. +- **Hardcoded-string gate:** a test or CI check flagging new user-facing + string literals in `app/src/ui/` to prevent drift. Imperfect by design; + it catches the common case. + +## Open questions + +- **Tray and native menus** (`app/src/tray.rs`, `app/src/ui/menus.rs`) go + through platform APIs rather than GPUI text. Do they count as in-scope for + the first pass? +- **Non-GPUI surfaces:** `hosted_oauth_complete.html` is a standalone HTML + page; it needs its own mechanism or an explicit out-of-scope decision. +- **Catalog location:** app crate (assumed here) versus shared resources, if + other components ever consume the same catalogs.