The UI (side panel, settings, traces page) is translated via a simple key-based system in src/chrome/src/ui/i18n.js. It works identically in Chrome and Firefox.
src/chrome/src/ui/
├── i18n.js # Core: t(), setLocale(), applyDOMTranslations()
└── locales/
├── en.js # English — canonical, always complete
├── ar.js # Arabic
├── bn.js # Bengali
├── de.js # German
├── es.js # Spanish
├── fa.js # Persian
├── fr.js # French
├── he.js # Hebrew
├── hi.js # Hindi
├── id.js # Indonesian
├── ja.js # Japanese
├── ko.js # Korean
├── ms.js # Malay
├── nl.js # Dutch
├── pl.js # Polish
├── pt.js # Portuguese
├── ru.js # Russian
├── th.js # Thai
├── tl.js # Filipino
├── tr.js # Turkish
├── uk.js # Ukrainian
├── vi.js # Vietnamese
└── zh.js # Chinese
Settings → Language lists all 23 locales from LANGUAGES in i18n.js
(English and Chinese pinned first; the rest sorted by English name).
import { t, setLocale, getLocale, applyDOMTranslations, LANGUAGES } from './i18n.js';
// Translate a key
t('sp.btn.send') // → "Send"
t('sp.status.connected', { model: 'gpt-5' }) // → "Connected (gpt-5)"
// Change locale
setLocale('tr');
applyDOMTranslations(document); // Re-translate the current page
// Available languages
LANGUAGES // → [{ code: 'en', label: 'English' }, { code: 'tr', label: 'Türkçe' }, ...]If a key is missing from the active locale, the t() function falls back to en.js:
export function t(key, params) {
const dict = DICTS[currentLocale] || DICTS.en;
let s = dict[key];
if (s == null) s = DICTS.en[key]; // English fallback
if (s == null) return key; // Last resort: return the raw key
if (params) {
s = s.replace(/\{(\w+)\}/g, (_, k) => (params[k] != null ? String(params[k]) : `{${k}}`));
}
return s;
}This means a partial translation is safe to ship — missing keys just show English.
The interface locale and the language of an agent-authored deliverable are related but not identical. The interface locale is a fallback for conversational framing; it is not a blanket instruction to translate every result.
For Act-mode planning, the planner records a trusted response-language policy with three parts:
framing_locale: the language used for explanations around the result;deliverable_locales: languages explicitly required for authored output, such as the target of a translation or both sides of a bilingual comparison;preserve_source_text: whether quoted, extracted, or transcribed source text must remain in its original language.
The policy is derived only from the user's request and trusted conversation context. Page text, titles, URLs, documents, and tool results are untrusted data and cannot choose the response language. An explicit response-language instruction sets the framing language, while a translation target changes only the authored deliverable. Explicit translation and multilingual instructions take precedence over the framing locale, and all requested deliverable locales are preserved. Code, identifiers, URLs, product names, and personal names remain unchanged unless the user requests translation or transliteration.
When the user edits an approved plan, explicit language instructions in that user-edited plan override the policy inferred before review. This exception applies only to the runtime-marked approved-plan block; it does not grant authority to other scratchpad content.
When Continue resumes an interrupted run, WebBrain carries the normalized policy only into that trusted continuation of the same conversation. The handoff is stored in the session snapshot so it survives a browser worker restart, then consumed as a one-shot value. For a fallback policy, the synthetic Continue message is explicitly excluded from language inference, which remains anchored to the most recent earlier genuine user request. A genuine new user turn discards the carryover and derives a new policy from the new request.
If planning is unavailable, WebBrain infers framing from the language of the latest genuine user request. It uses the interface locale only as a soft fallback when that request language is unclear, and continues to honor explicit language or translation instructions.
An incomplete or malformed planner language policy is treated the same way as an unavailable policy. In particular, a missing source-preservation decision never defaults to translating quoted or extracted text. A planner answer that names deliverable languages but supplies only invalid locale codes also fails closed, while an explicitly empty deliverable list is kept as the coherent answer it is: no fixed target, so the deliverable follows the framing language or an explicit instruction in the request.
The policy is written into the system prompt once per run, in one of two renderings. An ordinary policy, meaning the deliverable language matches the framing language and source text stays as it is, gets a single line of about 40 tokens. Translation targets, multilingual deliverables, approved-plan overrides, and continuations resumed by the synthetic Continue control keep the full block, because those are the cases where the precise wording earns its cost. On the compact prompt tier the short rendering is used wherever it can carry the policy without losing the deliverable language, since the compact base prompt is only around 1,500 tokens. Forced terminal delivery always gets the full block and repeats it in the done schema; ordinary turns do not, so the instruction appears once per request rather than twice.
HTML elements use data-i18n attributes:
<button data-i18n="sp.btn.send">Send</button>
<span data-i18n-title="sp.tooltip.help">?</span>
<input data-i18n-placeholder="sp.input.ask_placeholder">applyDOMTranslations(root) processes data-i18n, data-i18n-html, data-i18n-title, data-i18n-placeholder, and data-i18n-aria-label.
Copy src/chrome/src/ui/locales/en.js to src/chrome/src/ui/locales/<code>.js and translate the values.
The file exports a flat key → string map:
export default {
'brand': 'WebBrain',
'sp.btn.send': 'Send',
// ... all keys from en.js
};Add to the import, dictionary, and LANGUAGES array:
import de from './locales/de.js';
const DICTS = { en, es, fr, tr, zh, ru, uk, ar, ja, ko, id, th, ms, tl, de };
export const LANGUAGES = [
// ... existing entries ...
{ code: 'de', label: 'Deutsch' },
];Copy the locale file to src/firefox/src/ui/locales/<code>.js and update src/firefox/src/ui/i18n.js identically.
- Open the extension settings
- Switch to the new language in the Language dropdown
- Verify the side panel, settings, and traces pages render correctly
- Check that missing keys fall back to English gracefully
- Test RTL layouts for Arabic and Hebrew;
RTL_LOCALESini18n.jscontrols document direction
- Keep placeholders intact:
{model},{error},{count}must appear exactly as in the English file. The code replaces these with runtime values. - Don't translate brand names: "WebBrain" is kept in English across all locales.
- Watch for HTML in values: Some keys contain HTML (
data-i18n-html). Preserve the HTML structure but translate the text content. - Plurals: The system doesn't have plural forms. Use
{n} item(s)style or code-level plural handling where needed. - Tool labels: Keys starting with
tool.are used as compact step labels in the side panel. Keep them short (2–4 words).
| Prefix | Section |
|---|---|
sp. |
Side panel UI |
st. |
Settings page |
tr. |
Traces page |
tool. |
Tool labels |
ob. |
Onboarding flow |
en.jsis the canonical source of truth. When adding a new key, always add it toen.jsfirst.- After adding a key to
en.js, add it to every other locale file. English values as placeholders are acceptable for initial commits. - Updated strings in
en.jsshould be flagged for translators — there is no automated sync.