Skip to content

agent: localization design and plan (Fluent) - #1370

Closed
w3irdrobot wants to merge 1 commit into
masterfrom
agent-i18n-plan
Closed

w3irdrobot wants to merge 1 commit into
masterfrom
agent-i18n-plan

Conversation

@w3irdrobot

@w3irdrobot w3irdrobot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Design and plan for localizing the Maple Agent GPUI app. Docs only; no code changes yet.

  • Documents the current state: English-only, ~377 hardcoded string call sites across app/src/ui/.
  • Chooses the Fluent stack (fluent + fluent-templates) over rust-i18n and gettext, with the rationale: at a ~20-language scope, Fluent's CLDR plural/grammar handling and translator workflow are decisive, and retrofitting plural handling later would mean reworking every pluralized message.
  • Architecture: per-locale app.ftl catalogs with en as fallback, a thin app/src/ui/i18n.rs lookup layer (tr/tr_args) so widgets never touch FluentBundle, OS-locale default with a Settings override.
  • Incremental per-module extraction plan (infrastructure PR with one converted module first, then settings → login → chat → remote → store_gate → rest), with the app shippable at every step.
  • Guardrails: catalog completeness test, Fluent syntax check in CI, and a hardcoded-string drift gate.
  • Open questions: tray/native menu scope, the standalone OAuth completion HTML page, and catalog location.

Validation

Docs-only change; no runtime behavior affected. just lane repo is the applicable lane for documentation changes and can be run before un-drafting.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

Proposes the Fluent stack for Agent localization, with the rationale for
choosing it over rust-i18n and gettext at a ~20-language scope, the
catalog/lookup-layer architecture, the incremental extraction plan, and
the guardrails against catalog drift.
@AnthonyRonning

Copy link
Copy Markdown
Contributor

Friendly second-opinion pass. This isn't a blocker or a final call, just what I found when I checked the plan against current master (6546e30c). Fluent looks like a sound choice to me, and the plan's incremental, fallback-to-en approach fits the codebase well. Most of what follows is about scope the doc may be underestimating.

What fits well

  • Compile-time catalogs match how the app already ships resources: fonts use include_bytes! (assets.rs:73) and the spell dictionary uses include_str! (spell.rs:93). app/locales/ is already inside the Nix source fileset (flake.nix:84), so packaging shouldn't need to change. It may be worth saying explicitly in the doc that catalogs are embedded, not loaded from disk.
  • The completeness and syntax checks can be one cargo test that parses every bundle, so they run in the existing agent-test lane without a new CI lane.
  • A locale field fits the host-synced settings, which already use #[serde(default)] for forward and backward compatibility.

Things the plan may be underestimating

  1. String count. My count is a rough heuristic (English-looking literals, test files excluded, some false positives), but it finds about 1,100 in app/src/ui and about 2,400 across app/src, against the doc's ~377. Some big chunks sit outside ui/:

    • About 240 shortcut names and categories in the static slot table in keymap.rs, shown in Settings.
    • User-facing sentences in host.rs, backend/, remote/, update.rs and desktop.rs.
  2. Errors are English Strings that cross process boundaries. There are roughly 500+ Result<_, String> signatures in app/src. Host messages such as CompletionNotice.message (host.rs:737) come from the separate host process, which ACP shares, and the UI shows them as-is through error: Option<String> fields. Localizing them means either telling the host the UI locale or moving to typed error kinds with args that the UI formats. That's probably the largest single piece, and it may deserve its own section, or an explicit "errors stay English in phase 1" decision.

  3. Some display text is persisted. store/projector.rs writes "Agent notice", "Context compacted" and "Branch summary" into SQLite timeline rows. Making those translatable means storing a semantic kind and bumping TIMELINE_FORMAT, per the component AGENTS.md. Separately, the runtime crate mixes model-facing text (tool descriptions, prompt notices) with user-facing text. A clear "model-facing strings stay English" rule would keep extraction PRs from translating prompts.

  4. Runtime locale switching. "GPUI re-renders naturally" isn't quite true in this app:

    • Panels embedded with Entity::cached (the sidebar) don't redraw when the screen updates.
    • Sidebar sections are precomputed.
    • Native menus are set once (ui/menus.rs:16) and so is the tray menu (tray.rs:16).
    • AGENTS.md discourages Window::refresh.

    "Language change applies on restart" might be the simplest correct v1.

  5. Render-path cost. AGENTS.md treats per-frame work in render_* as a bug. Calling tr() from render would run Fluent formatting every frame. The lookup layer could cache resolved no-argument messages per locale as SharedString, and format messages with arguments when state changes.

Smaller notes

  • Dates, numbers and money. Dates use chrono with English month names (settings/account.rs:807, chat/transcript.rs:468). fluent-rs has no DATETIME, and its NUMBER doesn't apply locale separators, so dates and billing amounts probably need ICU4X or similar alongside Fluent.
  • Bidi isolation marks. Fluent wraps placeables in U+2068/U+2069 by default. It's worth deciding use_isolating up front, both for GPUI rendering and for tests that compare strings.
  • RTL and scripts. Arabic is mentioned in the target set, but the pinned GPUI revision has no RTL layout or mirroring that I could find. The bundled Manrope font has no CJK or Arabic glyphs, so those rely on per-platform system fallback, which is worth validating on Linux in particular.
  • Spell check is en_US only, so users typing in their own language would see most words flagged. UI locale and content language are probably separate settings.
  • Tests. About 200 assertions compare English text. With the OS locale as the default, the test harness should pin en so results don't depend on the machine.
  • macOS bundle localizations. Packaging ships only Resources/en.lproj/InfoPlist.strings (checked by release-info.py, verify-release.sh and the packaging tests). macOS chooses the language of system-provided UI (file dialogs, standard menu items) from the localizations the app declares. Each shipped locale probably needs its own .lproj (or CFBundleLocalizations), and those scripts would need to follow.
  • Doc placement. The other files in apps/maple-agent/docs/ describe current behaviour and contracts, and root AGENTS.md says to prefer current source over historical plans. It might fit better to keep the decision and plan with our internal specs, and land a short contract doc with the infrastructure PR (how to add a string, tr()/tr_args(), what not to translate).
  • Agent guidance. Much of this code is written by agents, so adding a rule to apps/maple-agent/AGENTS.md and $develop-maple-agent in the infrastructure PR would likely do more than the hardcoded-string gate alone. With ~1,000+ existing literals, that gate probably needs to be a shrinking allowlist.

Rough sizing (my read)

Piece Size
Infrastructure PR (i18n.rs, en.ftl, locale detection, setting, completeness test, one module) Small to medium
Pure UI literals across ~68 files Large but mechanical; ~8–12 PRs, with merge churn in hot files like chat/mod.rs
Host, backend and remote errors as typed kinds Large; effectively its own project
Persisted timeline text and the model-facing vs user-facing split Medium
Dates, numbers and currency Medium
RTL languages Blocked on GPUI

Overall it's medium to start and large to finish. One defensible phase 1: UI text through the catalog, language changes applied on restart, left-to-right locales only, and errors and persisted text as explicitly tracked follow-ups. Happy to talk through any of this.

@w3irdrobot

w3irdrobot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor Author

Closing this superseded docs-only PR: the design and second-opinion review have moved to internal docs.

@w3irdrobot w3irdrobot closed this Oct 30, 2026
@w3irdrobot
w3irdrobot deleted the agent-i18n-plan branch October 30, 2026 16:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants