Firefox WebExtensions i18n: messages.json reference
Firefox add-ons use the exact same _locales/ / messages.json format as Chrome. But the runtime API is browser.i18n instead of chrome.i18n, and AMO (addons.mozilla.org) has its own review and display behaviour. This article covers everything you need to know.
The format is identical to Chrome
Firefox implements the WebExtension standard, which uses the same messages.json format Chrome pioneered. The folder structure, key schema, placeholder syntax, and default_locale rule are all identical:
my-addon/
├── manifest.json ← "default_locale": "en" required
└── _locales/
├── en/
│ └── messages.json
├── de/
│ └── messages.json
└── fr/
└── messages.jsonA messages.json file that works in Chrome will work in Firefox without modification. The only differences are in the runtime API name and a handful of AMO-specific considerations.
browser.i18n vs chrome.i18n
Firefox exposes the i18n API as browser.i18n. Chrome exposes it as chrome.i18n. The method signatures are identical — only the namespace differs:
// Chrome / Edge / Opera
chrome.i18n.getMessage("appName");
chrome.i18n.getMessage("welcomeMessage", ["Alice"]);
chrome.i18n.getUILanguage();
// Firefox / Safari
browser.i18n.getMessage("appName");
browser.i18n.getMessage("welcomeMessage", ["Alice"]);
browser.i18n.getUILanguage();chrome.* namespace as an alias for backwards compatibility. Code written for Chrome using chrome.i18n will run in Firefox without changes. The browser.i18n namespace is preferred in Firefox-first code because it returns Promises where applicable and is the canonical WebExtension API name.If you want a single codebase that works across Chrome and Firefox, the safest approach is to use chrome.i18n everywhere — Firefox supports it, and it avoids a runtime polyfill. Alternatively, use a compatibility shim:
// i18n.js — cross-browser shim
const i18n = (typeof browser !== "undefined" ? browser : chrome).i18n;
export function t(key, substitutions) {
return i18n.getMessage(key, substitutions);
}messages.json format reference
Firefox supports all three fields of the WebExtension message schema:
{
"appName": {
"message": "My Add-on",
"description": "Name of the add-on shown in Firefox toolbar and AMO listing."
},
"notificationText": {
"message": "New message from $SENDER$",
"description": "Push notification text shown when a new message arrives.",
"placeholders": {
"sender": {
"content": "$1",
"example": "Alice"
}
}
},
"itemCount": {
"message": "$COUNT$ items saved",
"description": "Status bar label showing how many items are saved.",
"placeholders": {
"count": {
"content": "$1",
"example": "12"
}
}
}
}messagerequiredThe translated string. In Firefox, getMessage() returns this value. Placeholder tokens like $SENDER$ are replaced at runtime with substitution values.
descriptionoptionalTranslator context. Firefox itself ignores this field at runtime, but AMO reviewer tools and AI translation services (including LocalePack) read it to improve accuracy.
placeholdersoptionalNamed dynamic values. Each placeholder key maps a $NAME$ token in message to a positional substitution ($1–$9) or a literal string. Firefox preserves these identically to Chrome.
Using __MSG_key__ in manifest.json
The __MSG_key__ substitution syntax works the same way in Firefox as in Chrome. Firefox reads the active locale’s messages.json and substitutes the token before the manifest is evaluated:
{
"manifest_version": 2,
"name": "__MSG_appName__",
"description": "__MSG_appDescription__",
"default_locale": "en",
"version": "1.0.0"
}AMO localization behaviour
When you submit an add-on to AMO, the store uses your _locales/ folder in two ways:
Store listing display
_locales/. If not, it falls back to your default_locale strings. This is separate from the store listing text you enter in the AMO developer hub — more on that below.Two distinct sources of text on AMO
- 1.In-extension strings — from
_locales/*/messages.json. Displayed in the browser toolbar, popups, and options pages. - 2.AMO listing text — entered manually in the AMO developer hub under “Manage Versions › Localize.” This is what appears on the public store page and search results. It is not read from
messages.json.
Reviewer experience
Firefox locale codes
Firefox uses the same locale code format as Chrome — lowercase language code, optional underscore-separated region code — but the approved list is slightly different. The most important differences:
Both Chrome and Firefox
endefresjakozh_CNzh_TWpt_BRruarplnlFirefox accepts (Chrome may not)
nb_NOnn_NOhy_AMkaazbemksqbscyeuglIn practice, if you are targeting the major languages (top 20–30 locales), the codes are identical between Chrome and Firefox. Differences only appear for smaller regional languages.
Testing Firefox add-on i18n locally
The cleanest way to test a different locale in Firefox is web-ext, Mozilla’s official CLI tool for add-on development. Pass --start-url along with a Firefox profile set to the target locale, or use the --pref flag:
# Install web-ext globally
npm install -g web-ext
# Run with German locale
web-ext run --pref intl.locale.requested=de
# Run with Japanese locale
web-ext run --pref intl.locale.requested=jaAlternatively, change the locale in Firefox directly:
# 1. Go to about:config in Firefox
# 2. Search for: intl.locale.requested
# 3. Set the value to your target locale code, e.g. "de" or "ja"
# 4. Restart Firefox
# 5. Reload your extension from about:debuggingCommon Firefox-specific pitfalls
Using Promises with browser.i18n.getMessage()
Unlike most browser.* APIs, browser.i18n.getMessage() is synchronous and returns a plain string, not a Promise. Do not await it or chain .then() — it will silently return the Promise object rather than the translated string.
Forgetting default_locale causes AMO submission rejection
AMO runs the same manifest validation as Firefox itself. A _locales/ directory without default_locale in manifest.json will cause an automatic validation failure during upload. The error appears immediately in the AMO developer hub before human review even begins.
Confusing in-extension strings with AMO listing text
messages.json controls what users see inside the extension (toolbar badge, popup text, options page labels). The AMO store page description, screenshots, and summary are entered separately in the developer hub. Translating messages.json does not update your AMO listing — you must enter listing translations manually.
Invalid JSON in any locale file blocks the whole add-on
Firefox parses every messages.json file on install. A single syntax error in any locale — trailing comma, unescaped quote, missing brace — prevents the entire add-on from loading, even if that locale is never used. Always validate all locale files before packaging.
Placeholder name case mismatch
In the message string, $SENDER$ references the placeholder defined by the key sender (lowercase) in the placeholders object. The token in message is always uppercase with dollar signs; the object key is always lowercase. Firefox matches them case-insensitively, but Chrome does not — so always follow the convention to keep the add-on cross-browser.
Cross-browser compatibility summary
| Feature | Chrome | Firefox |
|---|---|---|
| File format | messages.json | messages.json (identical) |
| Folder structure | _locales/{locale}/ | _locales/{locale}/ (identical) |
| Runtime API | chrome.i18n | browser.i18n (chrome.i18n also works) |
| getMessage() | Synchronous, returns string | Synchronous, returns string |
| Substitution args | Array of strings | Array of strings (identical) |
| $PLACEHOLDER$ syntax | Supported | Supported (identical) |
| __MSG_key__ in manifest | Supported | Supported (identical) |
| default_locale required | Yes (when _locales/ exists) | Yes (when _locales/ exists) |
| Manifest version | MV3 only (MV2 deprecated) | MV2 and MV3 both supported |
| Per-key fallback | Yes (falls back to default_locale) | Yes (identical) |
Translate your Firefox add-on into 52 languages
LocalePack generates a _locales ZIP compatible with both Firefox and Chrome — correct locale codes, preserved $PLACEHOLDER$ tokens, validated JSON in every file. Upload your source messages.json, pay once, and download.