__MSG_key__ in manifest.json explained
Chrome and Firefox let you embed locale-aware strings directly in manifest.json using the __MSG_key__ token syntax. These tokens are substituted before your extension loads — which means your extension name, description, and action tooltip can all display in the user’s language without any JavaScript. Here is exactly how it works, which fields support it, and every pitfall that causes silent failures.
What __MSG_key__ does
When Chrome or Firefox reads manifest.json, it scans string values for tokens matching the pattern __MSG_key__ — two underscores, the word MSG, an underscore, the key name, then two more underscores. For each token it finds, it looks up key in the active locale’s messages.json file and replaces the token with the message value. This happens at manifest parse time — before any JavaScript runs.
// manifest.json
{
"manifest_version": 3,
"name": "__MSG_appName__",
"description": "__MSG_appDescription__",
"default_locale": "en",
"version": "1.0.0",
"action": {
"default_title": "__MSG_actionTitle__",
"default_popup": "popup.html"
}
}
// _locales/en/messages.json
{
"appName": { "message": "Tab Manager Pro" },
"appDescription": { "message": "Organize your browser tabs with one click." },
"actionTitle": { "message": "Open Tab Manager" }
}
// _locales/de/messages.json
{
"appName": { "message": "Tab Manager Pro" },
"appDescription": { "message": "Organisiere deine Browser-Tabs mit einem Klick." },
"actionTitle": { "message": "Tab-Manager öffnen" }
}For a German-language user, Chrome reads _locales/de/messages.json and resolves the manifest as if it were written:
{
"name": "Tab Manager Pro",
"description": "Organisiere deine Browser-Tabs mit einem Klick.",
"action": { "default_title": "Tab-Manager öffnen" }
}chrome.runtime.getManifest().name returns the already-substituted string.Which manifest fields support __MSG_key__
Not every manifest field accepts the token. Chrome only substitutes __MSG_key__ in fields that it treats as user-visible strings. Using the token in an unsupported field causes it to be treated as a literal string — it will appear exactly as __MSG_key__ in the UI.
| Manifest field | Supported | Notes |
|---|---|---|
| name | ✓ Yes | Extension name in toolbar, store listing, and chrome://extensions |
| short_name | ✓ Yes | Shorter name used in space-constrained UI contexts |
| description | ✓ Yes | Store listing short description (max 132 chars on Chrome) |
| action.default_title | ✓ Yes | Tooltip when hovering over the extension's toolbar button |
| commands[*].description | ✓ Yes | Keyboard shortcut description shown in chrome://extensions/shortcuts |
| omnibox.keyword | ✓ Yes | Address bar trigger keyword |
| version | ✗ No | Must be a literal semver string — substitution causes validation failure |
| permissions | ✗ No | Permission identifiers are not localizable |
| content_scripts[*].matches | ✗ No | URL match patterns — not localizable |
| background.service_worker | ✗ No | Script file path — not localizable |
| icons | ✗ No | Icon file paths — not localizable |
In practice, most developers only use __MSG_ for name and description — the two fields that appear in both the browser toolbar and the store listing.
How Chrome resolves the active locale
Chrome determines which messages.json to use by checking the user’s browser UI language against your available _locales folders, following a three-step fallback:
- 1Look for the exact locale match:
_locales/de_AT/messages.jsonfor an Austrian German user. - 2If not found, try the parent language:
_locales/de/messages.json. - 3If still not found, fall back to
_locales/{default_locale}/messages.json.
This fallback applies to __MSG_key__ tokens in the manifest the same way it applies to chrome.i18n.getMessage() calls in JavaScript. See the complete Chrome extension localization guide for the full setup walkthrough.
The connection to the Chrome Web Store listing
This is one of the most valuable but least understood aspects of __MSG_key__: the Chrome Web Store reads your manifest and uses the resolved name and description values to display your extension to store visitors in their language.
Concretely: if you have a _locales/de/messages.json with a German appDescription, a German-speaking user browsing the Chrome Web Store sees your extension description in German — automatically, without you entering anything in the Chrome Developer Dashboard.
name and description fields (max 132 characters for description). The long-form store page description, screenshots, and promotional tiles are entered separately in the Chrome Developer Dashboard — they are not read from messages.json. If you upload a per-locale description in the dashboard, that text takes priority over the __MSG_ substitution.Firefox AMO works the same way: it resolves __MSG_appName__ and __MSG_appDescription__ from your messages.json files and displays the localized text on the add-on listing page for visitors in matching locales.
Common pitfalls
Key referenced in manifest but missing from default_locale
upload rejected__MSG_appName__ is in your manifest but appName is absent from your default_locale messages.json, the Chrome Web Store rejects your upload with a validation error. Locally, Chrome displays an empty string for the extension name. Always validate that every __MSG_key__ reference has a corresponding key in your source file before uploading.// ✗ manifest references appName, but it's missing from the source file
// _locales/en/messages.json
{ "appDescription": { "message": "..." } } // ← appName is missing
// ✓ add the missing key
{ "appName": { "message": "My Extension" }, "appDescription": { "message": "..." } }Typo in the token name
silent failure__MSG_appame__ (missing the ‘N’) causes Chrome to look for a key named appame, find nothing, and display an empty string for the extension name. The extension still loads — you just won’t see a name in the toolbar or store listing.// ✗ typo — token is __MSG_appame__ (missing N)
"name": "__MSG_appame__"
// ✓ matches the key exactly
"name": "__MSG_appName__"Using __MSG_key__ in an unsupported field
renders literallyversion, permissions, and content_scripts do not support __MSG_key__. Chrome treats the token as a literal string value — your manifest ends up with "version": "__MSG_version__", which fails validation entirely.// ✗ version does not support __MSG_ substitution
"version": "__MSG_version__" ← fails manifest validation
// ✓ version must be a literal semver string
"version": "1.0.0"Using __MSG_key__ in HTML or JavaScript
renders literally__MSG_key__ substitution only happens when Chrome parses manifest.json and CSS files loaded via content_scripts. In HTML files, the token renders as the literal string. In JavaScript, it is not evaluated at all. Use chrome.i18n.getMessage() in JS and set textContent from a script in HTML.<!-- ✗ does NOT work in HTML — renders as literal text -->
<h1>__MSG_appName__</h1>
<!-- ✓ set text content from JS instead -->
<h1 id="title"></h1>
<script>
document.getElementById("title").textContent =
chrome.i18n.getMessage("appName");
</script>Hardcoding strings in manifest instead of using __MSG_
missed localization"name": "Tab Manager Pro" as a literal string, Chrome always displays the English name — even if you have perfectly good translations in all your messages.json files. The Chrome Web Store also always shows the English name to all users. Switching to __MSG_appName__ costs nothing and immediately improves store discoverability in non-English markets.// ✗ always shows English, ignores translations
"name": "Tab Manager Pro"
// ✓ resolves from messages.json for each user's locale
"name": "__MSG_appName__"Minimal working example
Here is the smallest possible extension that uses __MSG_key__ correctly, with English and German locales:
// manifest.json
{
"manifest_version": 3,
"name": "__MSG_appName__",
"description": "__MSG_appDescription__",
"default_locale": "en",
"version": "1.0.0"
}
// _locales/en/messages.json
{
"appName": {
"message": "My Extension",
"description": "The extension name shown in the toolbar and Chrome Web Store."
},
"appDescription": {
"message": "A short description of what my extension does.",
"description": "Short store listing description. Max 132 characters."
}
}
// _locales/de/messages.json
{
"appName": {
"message": "Meine Erweiterung",
"description": "The extension name shown in the toolbar and Chrome Web Store."
},
"appDescription": {
"message": "Eine kurze Beschreibung meiner Erweiterung.",
"description": "Short store listing description. Max 132 characters."
}
}With this setup, German-language users see “Meine Erweiterung” in the Chrome toolbar, in the extension management page at chrome://extensions, and in the Chrome Web Store listing — all from those two keys in messages.json.
Validating __MSG_key__ references before uploading
The Chrome Web Store validates all __MSG_key__ references on upload and rejects any ZIP where a referenced key is missing from the default_locale file. You can catch this locally before uploading with a short script — the same one from the messages.json validation guide:
// validate-manifest-keys.mjs
import { readFileSync } from "node:fs";
import { join } from "node:path";
const manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
const defaultLocale = manifest.default_locale;
if (!defaultLocale) process.exit(0);
const sourceKeys = new Set(
Object.keys(JSON.parse(readFileSync(join("_locales", defaultLocale, "messages.json"), "utf8")))
);
const manifestStr = JSON.stringify(manifest);
const MSG_RE = /__MSG_([A-Za-z0-9_@]+)__/g;
let hasErrors = false;
for (const [, key] of manifestStr.matchAll(MSG_RE)) {
if (!sourceKeys.has(key)) {
console.error(`manifest.json: __MSG_${key}__ not found in ${defaultLocale}/messages.json`);
hasErrors = true;
}
}
if (hasErrors) process.exit(1);
else console.log("All manifest __MSG_ references valid ✓");See default_locale rules and common errors for the full list of manifest validation errors Chrome and the Web Store enforce.
Quick reference
Translate appName and appDescription into 52 languages
Once your manifest uses __MSG_appName__ and __MSG_appDescription__, LocalePack translates those keys — along with all your other in-extension strings — into up to 52 languages. The output ZIP has correctly named _locales folders, validated JSON, and preserved placeholders. Upload once, pay once, download.