Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions apps/maple-agent/docs/i18n.md
Original file line number Diff line number Diff line change
@@ -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.
Loading