> ## Documentation Index
> Fetch the complete documentation index at: https://extension.js.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Locales and internationalization

> Add i18n to a browser extension with _locales and chrome.i18n. Extension.js discovers, validates, and emits locale JSON for each build.

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`

<img src="https://mintcdn.com/extensionjs/VCnDd7fX2Nza24SE/images/examples/action-locales/screenshot.png?fit=max&auto=format&n=VCnDd7fX2Nza24SE&q=85&s=a5bd1b1477de2d44b499d9a85e1426d3" alt="action-locales template screenshot" width="2400" height="1800" data-path="images/examples/action-locales/screenshot.png" />

See localized extension metadata and UI strings with `_locales` support.

<CodeGroup>
  ```bash npm theme={null}
  npx extension@latest create my-extension --template=action-locales
  ```

  ```bash pnpm theme={null}
  pnpx extension@latest create my-extension --template=action-locales
  ```

  ```bash yarn 2+ theme={null}
  yarn dlx extension@latest create my-extension --template=action-locales
  ```

  ```bash bun theme={null}
  bunx extension@latest create my-extension --template=action-locales
  ```

  ```bash deno theme={null}
  deno run -A npm:extension@latest create my-extension --template=action-locales
  ```
</CodeGroup>

Repository: [extension-js/examples/action-locales](https://github.com/extension-js/examples/tree/main/examples/action-locales)

## Locale capabilities

| Capability | What it gives you |
| - | - |
| Locale discovery | Detect `_locales/<locale>/messages.json` at the project root |
| Validation | Catch missing default locale files and unresolved `__MSG_*__` keys |
| Build output mapping | Emit locale files in the expected extension output structure |
| Dev watch support | Reload on locale file changes during development |

## Expected structure

```plaintext theme={null}
manifest.json
_locales/
  en/
    messages.json
  fr/
    messages.json
```

`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`:

```json theme={null}
{
  "manifest_version": 3,
  "name": "My Extension",
  "version": "1.0.0",
  "default_locale": "en",
  "description": "__MSG_extension_description__"
}
```

You would then include JSON files for each locale inside the `_locales` folder:

```plaintext theme={null}
_locales/
└── en/
    └── messages.json
```

## Sample `messages.json` file

Example `messages.json` file for translations:

```json theme={null}
{
  "extension_name": {
    "message": "My Extension"
  },
  "extension_description": {
    "message": "This is a localized description of my extension."
  }
}
```

## 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:

```js theme={null}
const title = chrome.i18n.getMessage("extension_name");
document.title = title;
```

Substitutions come from the second argument:

```json _locales/en/messages.json theme={null}
{
  "greeting": {
    "message": "Hello, $NAME$",
    "placeholders": {
      "name": {
        "content": "$1"
      }
    }
  }
}
```

```js theme={null}
chrome.i18n.getMessage("greeting", ["Ada"]);
```

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:

```html pages/popup.html theme={null}
<h1 data-i18n="extension_name"></h1>
```

```js theme={null}
for (const node of document.querySelectorAll("[data-i18n]")) {
  node.textContent = chrome.i18n.getMessage(node.dataset.i18n);
}
```

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](/docs/features/cross-browser-compatibility) for the details.

## Output path

Extension.js emits locale JSON files under:

```plaintext theme={null}
_locales/<locale>/messages.json
```

## 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`:

```json theme={null}
{
  "extension_description": {
    "message": "Localized 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

* Understand update outcomes in [dev update behavior](/docs/workflows/dev-update-behavior).
* Continue with [JSON in development](/docs/implementation-guide/json).
* Learn about [manifest development behavior](/docs/implementation-guide/manifest-json).

## Video walkthrough

<Frame>
  <iframe className="w-full aspect-video rounded-xl" src="https://www.youtube-nocookie.com/embed/bY0Mtv77bVc?rel=0" title="Extension.js: Action Locales template" loading="lazy" allow="encrypted-media; picture-in-picture; fullscreen" allowFullScreen />
</Frame>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.