Files
marceloandClaude Opus 5.5 af1b7668e2 feat(i18n): add Brazilian Portuguese to the web and CLI
Add embedded Fluent catalogs for `en` and `pt-BR`, covering the web
interface, CLI help, prompts, status lines and command errors.

The CLI language comes from `--language`, `AI_MEMORY_LANGUAGE` or the
top-level `language` config key and defaults to English. The web interface
negotiates `Accept-Language` per request and has a selector that saves a
cookie. There is no global language state.

CLI errors keep their English format strings in the source. They are
translated at the display boundary by matching the rendered text against
catalog templates, and unknown text passes through unchanged. A drift test
fails when a CLI diagnostic has no catalog entry.

JSON, MCP payloads, logs, stored content and generated configuration keep
their existing representation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tg6C9GhYwn5XBPrm1Hu5Zh
2026-10-31 18:09:13 -03:00

7.3 KiB

Interface languages

The built-in web interface and human CLI output support English (en) and Brazilian Portuguese (pt-BR). English remains the CLI default.

ai-memory --language pt-BR status
ai-memory --language pt-BR --help
AI_MEMORY_LANGUAGE=pt-BR ai-memory doctor

For a persistent terminal preference, set the top-level language = "pt-BR" in config.toml. Precedence is --language, then AI_MEMORY_LANGUAGE, then the config file, then English. Help and argument-validation errors work before configuration is loaded, so they use the flag or environment preference. A --language after the harness in ai-memory run belongs to that harness, just like its other native arguments.

The web interface negotiates Accept-Language on each request. The language selector saves an ai_memory_language preference cookie scoped to the installation's URL prefix. It takes precedence over the browser header. Two operators can use different languages on the same server. HTML responses carry Content-Language and vary by language header and cookie; the JSON API does not. The one-shot setup --web wizard uses the launching CLI's language and passes it to its setup child.

Translation boundary

Localize application-owned interface text, accessible labels, human reports, confirmation questions, help, and explanatory error messages. Keep command names, flags, identifiers, paths and copied commands intact. Confirmation prompts in Portuguese accept s/sim and y/yes. A prompt that must recognize a refusal, such as the --yolo warning, also declines não/nao next to n/no.

Do not translate JSON keys or values, MCP payloads, stored pages, conversation content, generated agent instructions, generated configuration files, log event identities, or diagnostic details supplied by an OS, provider or third-party tool. Those remain usable by automation and retain their original evidence. Known fixed authentication errors are localized at the display boundary; their wire representation stays unchanged. Unknown diagnostic text is preserved. Bundled product guides and release notes keep their source language.

Known gaps

These strings still appear in English under pt-BR:

  • Text assembled at runtime and passed along by value, so it never reaches the catalog as a known template. The --jail summary and the web's invalid-URL errors, such as invalid session id, are examples. The drift test only sees format-string literals, so it does not catch this kind of message.
  • Log output (tracing), by design.

Catalogs and ownership

Fluent resources live under crates/ai-memory-core/locales/<language>/:

  • common.ftl: elapsed times, status labels and fixed protocol errors.
  • cli.ftl: human command output and interactive prompts.
  • cli-help.ftl: clap command and argument descriptions.
  • web.ftl: HTML, progressive enhancement and setup wizard text.
  • diagnostics.ftl: CLI errors and status lines whose source stays in English, plus fixed web labels defined in Rust, such as feature states and guide titles.

Resources are embedded with include_str!. Cargo builds the complete binary; there is no runtime catalog download, JavaScript package or additional build step. The core Localizer owns concurrent Fluent bundles behind Arc, so cloning a context is cheap. Configuration resolves the CLI context once; web middleware selects a request-local context from router-owned catalogs. There is no mutable global language.

The English catalog is the reference and the fallback. Keep message IDs stable when editing wording. Use descriptive names for new messages and named parameters for values that translators may need to reorder:

let text = i18n.format("cli-http-server-response", &[
    ("method", method.to_string()),
    ("path", path.to_owned()),
    ("status", status.to_string()),
    ("detail", detail.to_owned()),
]);

CLI errors and status lines deep in a command keep their English format string in the source, where tests and logs rely on it. Localizer::diagnostic recognizes the rendered text at the terminal boundary and returns the translation, keeping the values it captured. The failure printed when a command exits, and each cause in its chain, go through it automatically. Text it does not recognize stays as written, so OS, library and provider details are preserved. Each English value in diagnostics.ftl must equal the source format string. Adding a bail!, anyhow!, .context or diagnostic message means adding it to both catalogs; the every_cli_diagnostic_has_a_catalog_entry test lists anything missing. Web templates use label, the same lookup, for fixed text that Rust passes to them. Hook commands run before configuration loads, so they follow only AI_MEMORY_LANGUAGE. Commented configuration snippets printed for pasting stay in English, like other generated configuration.

Use message for typed Fluent values and count for numeric plural selectors. Translate the complete inflected phrase; do not concatenate English suffixes. one, two and three are positional conveniences for askama, whose pinned version does not support constructing argument arrays in templates. Their parameters are value0, value1 and value2. Keep a sentence in one message with its values as parameters; do not assemble it from fragments in a template.

Askama escapes ordinary translated text and substitutions. Never mark a translation as safe HTML, with one exception: rich and rich_one escape the whole message first and then turn its `backtick` spans into <code>, so a sentence can carry a code token and their output may use the safe filter. Inside a script, use js, js_one, js_two, or errors_js: they produce JSON and escape HTML script delimiters and Unicode line separators. Only these already-encoded values may use askama's safe filter. Progressive enhancement must use textContent for displayed values.

Adding a language or message

Copy all five English resources into a new BCP 47 language directory, translate them, then register the tag, accepted aliases and embedded resources in ai-memory-core::i18n. Add the language to the browser selector. Preserve the parameters in each message; use Fluent selectors for the language's plural forms. Keep technical tokens inside translated prose unchanged.

A new message belongs in every shipped language in the same change. Help IDs follow the command/argument path, such as cli-help-root-status-json-help. The CLI metadata test compares the reference catalog with clap, so editing a help description or adding a flag also requires updating its catalog entries. The resources are ordinary UTF-8 text; contributors can edit them directly.

Validation

Run the repository's normal formatting, clippy, test and dependency gates. Focused checks during translation work are:

cargo test -p ai-memory-core i18n::tests
cargo test -p ai-memory-cli --lib localization::tests
cargo test -p ai-memory-web localization --all-features

Tests validate Fluent syntax, duplicate IDs, language parity, parameter parity, CLI help drift, CLI diagnostic coverage, language negotiation and isolation, script escaping, unchanged API responses, and the administrative gate with Portuguese HTML. When changing browser messages, inspect rendered pages in both languages and check keyboard navigation, labels, long text and the small-screen layout.