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"
}_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.jsonThe 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:
arArabicamAmharicbgBulgarianbnBengalicaCatalancsCzechdaDanishdeGermanelGreekenEnglishen_AUEnglish (Australia)en_GBEnglish (UK)en_USEnglish (US)esSpanishes_419Spanish (Latin America)etEstonianfaPersianfiFinnishfilFilipinofrFrenchguGujaratiheHebrewhiHindihrCroatianhuHungarianidIndonesianitItalianjaJapaneseknKannadakoKoreanltLithuanianlvLatvianmlMalayalammrMarathimsMalaynlDutchnoNorwegianplPolishpt_BRPortuguese (Brazil)pt_PTPortuguese (Portugal)roRomanianruRussianskSlovakslSloveniansrSerbiansvSwedishswSwahilitaTamilteTeluguthThaitrTurkishukUkrainianviVietnamesezh_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.
__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:
- 1Look for
_locales/{user's locale}/messages.json. If found and the key exists, use it. - 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.
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=deAfter 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 translationThis 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.