VOOZH about

URL: https://dev.to/weatherclockdash/internationalizing-a-firefox-extension-i18n-without-a-library-4ahc

⇱ Internationalizing a Firefox Extension: i18n Without a Library - DEV Community


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 clock vs 2 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.