LocalePack
ChromeFirefoxEdgeOperaSafariCWS Listing
Vue.jsReact
Next.jsi18nextReact Native
Guides
Home/Guides/Chrome extension i18n
February 21, 2026

Chrome extension i18n: _locales structure and default_locale

Chrome’s i18n system is built around two things: the _locales folder and the default_locale field in manifest.json. Once both are in place, the chrome.i18n API activates and your extension becomes locale-aware. Here is exactly how to set it up from scratch.

Step 1: declare default_locale in manifest.json

Add default_locale to your manifest. Its value is the locale code of the language your source strings are written in — almost always "en":

{
  "manifest_version": 3,
  "name": "__MSG_appName__",
  "description": "__MSG_appDescription__",
  "version": "1.0.0",
  "default_locale": "en"
}
Required pairing: If you create a _locales directory, default_locale is mandatory. Conversely, if you declare default_locale without a _locales folder, Chrome will also error. Both must exist together.

Step 2: create the _locales folder structure

Each locale gets its own subfolder named after its locale code. Inside each folder is exactly one file: messages.json.

my-extension/
├── manifest.json
├── background.js
├── popup.html
└── _locales/
    ├── en/
    │   └── messages.json   ← matches default_locale
    ├── de/
    │   └── messages.json
    ├── fr/
    │   └── messages.json
    ├── ja/
    │   └── messages.json
    └── pt_BR/
        └── messages.json

The folder name must match the locale code exactly — including underscore casing (e.g. pt_BR, not pt-BR or ptBR). Chrome silently ignores folders with unrecognised names.

Chrome’s allowed locale codes

Chrome accepts a specific list of locale codes. These are the most commonly used:

arArabic
amAmharic
bgBulgarian
bnBengali
caCatalan
csCzech
daDanish
deGerman
elGreek
enEnglish
en_AUEnglish (Australia)
en_GBEnglish (UK)
en_USEnglish (US)
esSpanish
es_419Spanish (Latin America)
etEstonian
faPersian
fiFinnish
filFilipino
frFrench
guGujarati
heHebrew
hiHindi
hrCroatian
huHungarian
idIndonesian
itItalian
jaJapanese
knKannada
koKorean
ltLithuanian
lvLatvian
mlMalayalam
mrMarathi
msMalay
nlDutch
noNorwegian
plPolish
pt_BRPortuguese (Brazil)
pt_PTPortuguese (Portugal)
roRomanian
ruRussian
skSlovak
slSlovenian
srSerbian
svSwedish
swSwahili
taTamil
teTelugu
thThai
trTurkish
ukUkrainian
viVietnamese
zh_CNChinese (Simplified)
zh_TWChinese (Traditional)

The full list is available in the Chrome Extensions developer documentation under “Supported locales.” LocalePack generates folder names using the correct Chrome locale codes automatically.

Step 3: write your source messages.json

Create _locales/en/messages.json with your source strings:

{
  "appName": {
    "message": "My Extension",
    "description": "Name shown in the Chrome toolbar and Web Store."
  },
  "appDescription": {
    "message": "Boost your productivity with one click.",
    "description": "Short description shown in the Web Store listing."
  },
  "popupTitle": {
    "message": "Settings",
    "description": "Title of the extension popup window."
  },
  "enableButton": {
    "message": "Enable",
    "description": "Label on the main toggle button."
  }
}

Using __MSG_key__ in manifest.json

Manifest fields like name and description support a special substitution syntax: __MSG_key__. Chrome replaces these tokens with the matching entry from the active locale’s messages.json before the extension loads.

{
  "manifest_version": 3,
  "name": "__MSG_appName__",
  "description": "__MSG_appDescription__",
  "default_locale": "en"
}

When a German user installs the extension, Chrome reads _locales/de/messages.json and substitutes __MSG_appName__ with the German translation of appName. This is what makes the Chrome Web Store display your extension in the visitor’s language.

Only a subset of manifest fields support __MSG_key__: name, description, short_name, and a handful of others. Most extension logic uses chrome.i18n.getMessage() in JavaScript instead.

Step 4: call chrome.i18n.getMessage() in your code

In your extension’s JavaScript, HTML, or CSS files, use chrome.i18n.getMessage() to retrieve strings at runtime:

// background.js / popup.js / content.js
const title = chrome.i18n.getMessage("popupTitle");
// → "Settings" (en) or "Einstellungen" (de), etc.

const btnLabel = chrome.i18n.getMessage("enableButton");
document.getElementById("toggle").textContent = btnLabel;

In HTML files you cannot call JavaScript inline, so a common pattern is to set text content from a script that runs on DOMContentLoaded:

<!-- popup.html -->
<button id="toggle"></button>
<script src="popup.js"></script>

// popup.js
document.addEventListener("DOMContentLoaded", () => {
  document.getElementById("toggle").textContent =
    chrome.i18n.getMessage("enableButton");
});
chrome.i18n.getMessage() returns an empty string if the key doesn’t exist in the active locale and the default locale. Always test with your default locale first to catch missing keys early.

How Chrome picks the right messages.json

Chrome uses a two-step fallback when resolving locale strings:

  1. 1Look for _locales/{user's locale}/messages.json. If found and the key exists, use it.
  2. 2If not found, fall back to _locales/{default_locale}/messages.json.

For example, if default_locale is "en" and a user has Chrome set to Swiss German (de_CH), Chrome first checks for _locales/de_CH/messages.json. If that folder doesn’t exist, it tries _locales/de/messages.json. If that also doesn’t exist, it falls back to _locales/en/messages.json.

Individual keys also fall back independently. If a key is present in the user’s locale file but another key is missing, Chrome fetches the missing key from default_locale without failing. This means partially-translated locale files still work.

Testing i18n locally

Chrome does not provide a built-in locale switcher for unpacked extensions, but you can test different locales using the --lang flag when launching Chrome from the command line:

# macOS
/Applications/Google Chrome.app/Contents/MacOS/Google Chrome --lang=de

# Windows
"C:\Program Files\Google\Chrome\Application\chrome.exe" --lang=de

# Linux
google-chrome --lang=de

After launching with --lang=de, load your unpacked extension and all chrome.i18n.getMessage() calls will resolve from _locales/de/messages.json.

Common mistakes

Hyphen instead of underscore in locale folder names

A folder named pt-BR is silently ignored. The correct name is pt_BR. Chrome only recognises underscored locale codes from its approved list.

_locales without default_locale (or vice versa)

Both must exist together. Creating _locales/ without default_locale in manifest.json causes a manifest validation error and the extension fails to load.

Forgetting to include the default locale folder

If default_locale is "en" but there is no _locales/en/ folder, the extension will fail to load. The default locale folder must always be present.

Using __MSG_key__ outside supported manifest fields

Only specific manifest fields support the __MSG_key__ syntax. Using it in unsupported fields (like permissions) will cause it to be treated as a literal string, not a translation lookup.

Missing key in default_locale messages.json

If a key is missing from the default locale file, getMessage() returns an empty string for users of any locale. Always treat the default locale file as the authoritative source and keep it complete.

What the finished structure looks like

After adding a few translations, your extension root should look like this:

my-extension/
├── manifest.json           ← includes "default_locale": "en"
├── background.js
├── popup.html
├── popup.js
├── icons/
│   └── icon128.png
└── _locales/
    ├── en/
    │   └── messages.json   ← source strings (complete)
    ├── de/
    │   └── messages.json   ← German translation
    ├── fr/
    │   └── messages.json   ← French translation
    ├── es/
    │   └── messages.json   ← Spanish translation
    ├── ja/
    │   └── messages.json   ← Japanese translation
    └── pt_BR/
        └── messages.json   ← Brazilian Portuguese translation

This is exactly the ZIP structure LocalePack generates for you — with every locale folder named correctly and every messages.json file validated against Chrome’s format.

Skip the manual work: generate your _locales ZIP instantly

Upload your source messages.json, choose your target languages, and LocalePack generates a ready-to-extract _locales ZIP with correctly named folders and validated messages.json files. Placeholders preserved. Pay once, no subscription.

Generate my _locales ZIP →
← Back to Guides
LocalePack
GuidesPrivacyTermsSupport

© 2025 LocalePack. All rights reserved.

This project was translated with LocalePack logoLocalePack