Internationalizing a Firefox Extension: i18n Without a Library
Firefox extensions have a built-in i18n system that covers most use cases without any external library. Here's how to use it.
The _locales Directory Structure
extension/
├── manifest.json
├── _locales/
│ ├── en/
│ │ └── messages.json
│ ├── fr/
│ │ └── messages.json
│ ├── de/
│ │ └── messages.json
│ └── ja/
│ └── messages.json
└── newtab.html
messages.json Format
{"extensionName":{"message":"Weather & Clock Dashboard","description":"Name of the extension"},"extensionDescription":{"message":"Live weather, world clocks, and search for your new tab","description":"Extension description shown in AMO"},"searchPlaceholder":{"message":"Search or enter address","description":"Placeholder text for search input"},"temperatureUnit":{"message":"Temperature unit","description":"Label for the temperature unit setting"},"settingsTitle":{"message":"Settings"},"addClock":{"message":"Add clock"},"locationLabel":{"message":"Location: $LOCATION$","description":"Label showing current weather location","placeholders":{"location":{"content":"$1","example":"London, UK"}}}}
Using Strings in JavaScript
// Simple string
const name = browser.i18n.getMessage('extensionName');
// → "Weather & Clock Dashboard"
// String with placeholder
const label = browser.i18n.getMessage('locationLabel', ['London, UK']);
// → "Location: London, UK"
// Fallback if message not found
function t(key, substitutions) {
const msg = browser.i18n.getMessage(key, substitutions);
return msg || key; // Return key as fallback
}
Using Strings in HTML
For static HTML, you can use data attributes and apply translations at runtime:
<!-- HTML -->
<input type="search" data-i18n-placeholder="searchPlaceholder" />
<h2 data-i18n="settingsTitle"></h2>
<button data-i18n="addClock"></button>
// Apply all i18n strings on DOMContentLoaded
function applyI18n() {
// Text content
document.querySelectorAll('[data-i18n]').forEach(el => {
el.textContent = browser.i18n.getMessage(el.dataset.i18n);
});
// Placeholder text
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
el.placeholder = browser.i18n.getMessage(el.dataset.i18nPlaceholder);
});
// Title attributes
document.querySelectorAll('[data-i18n-title]').forEach(el => {
el.title = browser.i18n.getMessage(el.dataset.i18nTitle);
});
// Aria labels
document.querySelectorAll('[data-i18n-aria]').forEach(el => {
el.setAttribute('aria-label', browser.i18n.getMessage(el.dataset.i18nAria));
});
}
document.addEventListener('DOMContentLoaded', applyI18n);
manifest.json Localization
The name and description in manifest.json can also be localized:
{"name":"__MSG_extensionName__","description":"__MSG_extensionDescription__","default_locale":"en"}
The __MSG_*__ syntax references your messages.json keys directly. This is what appears in AMO and in the browser's add-ons page.
Getting the User's Language
// What Firefox thinks the user's language is
const language = browser.i18n.getUILanguage();
// → "en-US", "fr", "de", "ja-JP", etc.
// Accept-Language-style list (for API calls)
const acceptLanguages = await browser.i18n.getAcceptLanguages();
// → ["en-US", "en", "fr"]
Detecting RTL Languages
const RTL_LANGUAGES = ['ar', 'he', 'fa', 'ur', 'ps', 'ku'];
function isRTL() {
const lang = browser.i18n.getUILanguage().split('-')[0];
return RTL_LANGUAGES.includes(lang);
}
if (isRTL()) {
document.documentElement.setAttribute('dir', 'rtl');
}
Practical Tips
1. Keep English as your base. Always have a complete en/messages.json. Other locales only need to override what's different.
2. Avoid concatenation. Don't do:
// BAD: breaks in languages with different word order
const msg = browser.i18n.getMessage('weather') + '' + city;
Instead:
//messages.json{"weatherFor":{"message":"Weather for $CITY$","placeholders":{"city":{"content":"$1"}}}}
3. Test with pseudo-localization. Replace all characters with accented versions to find UI overflow before involving real translators:
function pseudoLocalize(str) {
const map = {'a':'à','e':'é','i':'î','o':'ö','u':'ü','n':'ñ'};
return '[!' + str.split('').map(c => map[c] || c).join('') + '!]';
}
4. AMO auto-detects locale. When you have _locales/fr/messages.json, Firefox and AMO will use it for French-speaking users automatically.
What the Built-In System Doesn't Cover
- Pluralization rules (
1 clockvs2 clocks) — roll your own or use a tiny library - Date/time formatting — use
Intl.DateTimeFormat(built into the browser) - Number formatting — use
Intl.NumberFormat
For most extensions, the built-in browser.i18n API covers 90% of needs without any external dependency.
Weather & Clock Dashboard — free Firefox new tab with weather, world clocks, search. MIT licensed.
For further actions, you may consider blocking this person and/or reporting abuse
