Skip to main content
Internationalization, usually shortened to i18n, works through two pieces: a _locales folder that holds your translations, and the chrome.i18n API that reads them at runtime. Extension.js reads the _locales folder at your project root and validates that each declared locale has a messages.json. It emits locale JSON assets into the browser-specific build. During development, it picks up edits to any locale file without a full restart.

Template example

action-locales

action-locales template screenshot See localized extension metadata and UI strings with _locales support.
Repository: extension-js/examples/action-locales

Locale capabilities

Expected structure

default_locale in manifest.json should map to an existing _locales/<default>/messages.json.

Legacy layout: _locales next to the manifest

The project root is the canonical place for _locales, even when your manifest lives in a subfolder such as src/. A _locales folder next to a nested manifest still builds. The compiler emits a LocalesLayoutWarning that asks you to move it to the root. Browsers read locales from the extension root, so the root placement matches what ships.

Sample locales declaration in manifest.json

Here is how to declare locales in manifest.json:
You would then include JSON files for each locale inside the _locales folder:

Sample messages.json file

Example messages.json file for translations:

Read strings at runtime with chrome.i18n

__MSG_*__ placeholders are a manifest feature. The browser expands them in manifest.json and in CSS files that the manifest declares. Nowhere else. For every string in your own code, call the API:
Substitutions come from the second argument:
_locales/en/messages.json
Two more calls are useful:
  • chrome.i18n.getUILanguage() returns the browser’s language.
  • chrome.i18n.getAcceptLanguages() returns the user’s accepted languages.

Extension.js does not substitute placeholders for you

Extension.js reads __MSG_*__ references. It never rewrites them.
  • In manifest.json, it checks each reference against the default locale and reports the ones that are missing.
  • At packaging time, it resolves a __MSG_*__ name so the zip filename carries the translated name.
  • In HTML, JavaScript, and JSON files, it leaves the text untouched.
So a placeholder that you write into an HTML file ships as literal text. Set the string from script instead:
pages/popup.html
The same limit applies to CSS that a content script inserts as <style> text. __MSG_@@extension_id__ does not expand there. Extension.js already rewrites url() references in that CSS to the extension root, so you do not need the placeholder.

The browser.i18n spelling

Firefox and Safari ship browser.i18n natively. On Chromium, Extension.js provides the browser namespace through webextension-polyfill, so browser.i18n.getMessage works on every target. Read Cross-browser compatibility for the details.

Output path

Extension.js emits locale JSON files under:

Development behavior

  • Extension.js adds locale JSON files to compilation dependencies and watches them.
  • Locale changes trigger extension reload behavior (hard reload), not component-style hot module replacement (HMR).
  • Extension.js fails validation with actionable diagnostics when required locale files are missing or invalid.

Validation behavior

Extension.js validates:
  • A _locales folder without default_locale in the manifest fails the build, because browsers reject that combination
  • Existence of _locales/<default> and its messages.json
  • JSON validity for every locale’s messages.json
  • __MSG_*__ references in the manifest against default locale keys
Two details of the __MSG_*__ scan:
  • Predefined @@ messages, such as __MSG_@@ui_locale__, are exempt because the browser provides them.
  • The @ character is allowed inside message keys, matching Chrome’s grammar for message names.

Troubleshooting missing locale keys

If your manifest uses __MSG_extension_description__, ensure the default locale file contains extension_description:
If the default locale does not define the key, Extension.js surfaces a diagnostic explaining the mismatch.

Packaging behavior

When you build with --zip or --zip-source, Extension.js checks the default locale again at zip time. A manifest that declares default_locale without a matching messages.json produces a warning, because stores reject packages without their default locale. Zip filenames come from the manifest name. A __MSG_*__ name resolves against the default locale’s messages.json, so the archive carries the translated name, not the placeholder.

Best practices

  • Keep messages.json keys consistent across locales.
  • Update default locale first, then propagate keys to other locales.
  • Validate locale JSON in continuous integration (CI) to catch malformed files before packaging.
  • Keep _locales at the project root, the placement that browsers and the packaging step read.

Next steps

Video walkthrough