DEV Community

Weather Clock Dash
Weather Clock Dash

Posted on

Internationalizing a Firefox Extension: i18n Without a Library

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
Enter fullscreen mode Exit fullscreen mode

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"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
// 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);
Enter fullscreen mode Exit fullscreen mode

manifest.json Localization

The name and description in manifest.json can also be localized:

{
  "name": "__MSG_extensionName__",
  "description": "__MSG_extensionDescription__",
  "default_locale": "en"
}
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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');
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Instead:

// messages.json
{
  "weatherFor": {
    "message": "Weather for $CITY$",
    "placeholders": { "city": { "content": "$1" } }
  }
}
Enter fullscreen mode Exit fullscreen mode

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('') + '!]';
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)