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.
Top comments (0)