LocalePack
ChromeFirefoxEdgeOperaSafariCWS Listing
Vue.jsReact
Next.jsi18nextReact Native
Guides
Home/Guides/How to localize a Chrome extension
March 6, 2026

How to localize a Chrome extension (complete guide)

Chrome’s built-in i18n system is surprisingly capable — but easy to get wrong the first time. This guide covers everything: manifest setup, _locales folder structure, messages.json format, the chrome.i18n API, localizing your manifest strings, testing, and translating to every language the Chrome Web Store supports.

How Chrome extension i18n works

Chrome provides a first-party i18n API through the chrome.i18n namespace. When you enable it, Chrome automatically selects the right locale for each user based on their browser language, with a fallback to your default_locale if their language is not available.

The system has three moving parts:

  1. A default_locale field in manifest.json that tells Chrome which language your source strings are written in.
  2. A _locales folder containing one subfolder per language, each with a messages.json file.
  3. The chrome.i18n.getMessage() API call (in JS) and __MSG_key__ substitution tokens (in manifests and CSS) for reading strings at runtime.

All three must be in place before any strings will load. Let’s set them up one at a time.

Step 1: add default_locale to manifest.json

Open your manifest.json and add the default_locale field. 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",
  "action": {
    "default_title": "__MSG_actionTitle__",
    "default_popup": "popup.html"
  }
}
Notice that name, description, and default_title all use the __MSG_key__ pattern. This tells Chrome to substitute the localized string from messages.json at load time. The Chrome Web Store also reads these to display your extension name and description in the user’s language.
Required pairing: default_locale and the _locales folder must both be present or both absent. Chrome rejects the extension at load time if only one exists.

Step 2: create the _locales folder structure

At the root of your extension (next to manifest.json), create a _locales directory. Inside it, create one subfolder per language, each containing a single messages.json file:

my-extension/
├── manifest.json
├── background.js
├── popup.html
├── popup.js
└── _locales/
    ├── en/
    │   └── messages.json   ← default_locale (your source strings)
    ├── de/
    │   └── messages.json
    ├── fr/
    │   └── messages.json
    ├── es/
    │   └── messages.json
    └── pt_BR/
        └── messages.json

The folder names must match Chrome’s supported locale codes exactly — including underscore casing for regional variants (e.g. pt_BR not pt-BR). Chrome silently ignores folders with unrecognised names, so a typo means the locale never loads.

Commonly used locale codes: en, de, fr, es, it, pt_BR, pt_PT, ja, ko, zh_CN, zh_TW, ru, ar, hi. See the complete locale code list.

Step 3: write your source messages.json

Every string your extension displays lives in _locales/en/messages.json (your default locale). Each entry is a key with at minimum a message field. An optional description provides context for translators:

{
  "appName": {
    "message": "Tab Manager Pro",
    "description": "The extension's name as shown in the Chrome Web Store."
  },
  "appDescription": {
    "message": "Organize your browser tabs with one click.",
    "description": "Short description for the Chrome Web Store listing."
  },
  "actionTitle": {
    "message": "Open Tab Manager",
    "description": "Tooltip for the browser action button."
  },
  "welcomeMessage": {
    "message": "Welcome, $NAME$!",
    "description": "Greeting shown to the user on first open.",
    "placeholders": {
      "name": {
        "content": "$1",
        "example": "Alex"
      }
    }
  },
  "tabCount": {
    "message": "You have $COUNT$ tabs open.",
    "description": "Tab count label in the popup.",
    "placeholders": {
      "count": {
        "content": "$1",
        "example": "12"
      }
    }
  }
}

A few rules to know:

  • Keys are case-sensitive and must match what you pass to getMessage().
  • Placeholders like $NAME$ are substituted at runtime. The content value $1 means “the first argument passed to getMessage()”.
  • The description field is never shown to users — it’s purely a hint for translators. Write useful notes here, especially for short strings like button labels.
  • Every key that exists in your default_locale file must also exist in all other locale files. Missing keys fall back to the default locale silently, but it’s best to keep them in sync.

For a deeper reference on the format including all placeholder syntax options, see messages.json format explained.

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

In any JavaScript context (background service worker, popup, content script, options page), call chrome.i18n.getMessage(key) to retrieve a localized string:

// popup.js

// Simple string
document.getElementById("title").textContent =
  chrome.i18n.getMessage("appName");

// With placeholder substitution
const name = "Alex";
document.getElementById("greeting").textContent =
  chrome.i18n.getMessage("welcomeMessage", [name]);

// With multiple placeholders
const count = openTabs.length.toString();
document.getElementById("tabCount").textContent =
  chrome.i18n.getMessage("tabCount", [count]);
getMessage() returns an empty string "" if the key does not exist in any locale file — not an error. This can cause silent UI issues. Consider a wrapper that throws in development:
function t(key, subs) {
  const msg = chrome.i18n.getMessage(key, subs);
  if (!msg && process.env.NODE_ENV === "development") {
    throw new Error(`Missing i18n key: ${key}`);
  }
  return msg;
}

You can also use the API to detect the browser UI language for custom logic:

// Get the browser's display language, e.g. "en-US"
const uiLocale = chrome.i18n.getUILanguage();

// Get the user's accept-language list (returns an array)
chrome.i18n.getAcceptLanguages((languages) => {
  console.log(languages); // ["en-US", "fr", "de"]
});

Step 5: use __MSG_key__ in manifest.json and CSS

Chrome supports a special substitution syntax outside of JavaScript — the __MSG_key__ pattern. Chrome replaces these tokens at extension load time (not at runtime).

In manifest.json: Use it for any string field that Chrome reads before your JS runs — name, description, action titles, and context menu items:

{
  "name": "__MSG_appName__",
  "description": "__MSG_appDescription__",
  "action": {
    "default_title": "__MSG_actionTitle__"
  },
  "commands": {
    "toggle": {
      "description": "__MSG_commandToggleDescription__"
    }
  }
}

In CSS files (loaded via content_scripts), you can use the same pattern in content properties:

/* content.css */
.my-extension-tooltip::after {
  content: "__MSG_tooltipText__";
}
__MSG_key__ substitution does not work in HTML files or in JavaScript. In HTML, use JS to set textContent after load. In JS, use chrome.i18n.getMessage() directly.

Step 6: test your localization

Chrome does not switch languages mid-session — it reads the locale at extension load time. To test a specific locale:

  1. Open Chrome settings ( chrome://settings/languages) and move your target language to the top of the list (or set it as the display language).
  2. Restart Chrome so the new UI language takes effect.
  3. Load your unpacked extension from chrome://extensions. Chrome will now load strings from the matching _locales subfolder.
  4. If no matching locale folder exists, Chrome falls back to your default_locale strings.
For rapid iteration, a faster approach is to use the --lang flag when launching Chrome from the terminal:
# macOS
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --lang=de

# Linux
google-chrome --lang=ja

# Windows (PowerShell)
& "C:\Program Files\Google\Chrome\Application\chrome.exe" --lang=fr
This overrides the display language for that Chrome instance without changing your system settings.

Step 7: translate to other languages

Once your source _locales/en/messages.json is final, you need a messages.json for each target language with the same keys and translated message values. Placeholder tokens ($NAME$) must be preserved exactly — they are not translatable.

A correctly translated _locales/de/messages.json for the example above would look like:

{
  "appName": {
    "message": "Tab Manager Pro",
    "description": "The extension's name as shown in the Chrome Web Store."
  },
  "appDescription": {
    "message": "Organisiere deine Browser-Tabs mit einem Klick.",
    "description": "Short description for the Chrome Web Store listing."
  },
  "actionTitle": {
    "message": "Tab-Manager öffnen",
    "description": "Tooltip for the browser action button."
  },
  "welcomeMessage": {
    "message": "Willkommen, $NAME$!",
    "description": "Greeting shown to the user on first open.",
    "placeholders": {
      "name": {
        "content": "$1",
        "example": "Alex"
      }
    }
  },
  "tabCount": {
    "message": "Du hast $COUNT$ Tabs geöffnet.",
    "description": "Tab count label in the popup.",
    "placeholders": {
      "count": {
        "content": "$1",
        "example": "12"
      }
    }
  }
}

Three things to verify in every translated file:

  • All keys from the source file are present (no missing keys).
  • All placeholder tokens ($NAME$, $COUNT$) are present in the translated message string.
  • The placeholders object is copied verbatim from the source — its contents are never translated.

Common mistakes

_locales exists but default_locale is missing

Chrome refuses to load the extension with: “Manifest file is invalid”. Both must exist together.

Wrong locale folder name casing

pt-BR or ptBR instead of pt_BR. Chrome silently ignores the folder — the locale never loads and users see your default language.

Translated file is missing keys from the source

Missing keys fall back to the default_locale silently. The extension still loads, but some strings will appear in the wrong language — hard to catch without testing each locale.

Placeholder token dropped or renamed in translation

If a translator removes $NAME$ from the translated message string, the substitution silently disappears from the UI. Always verify placeholders are present in every translated message.

Using __MSG_key__ in HTML or JS

The token substitution only works in manifest.json and CSS files. In HTML, it renders literally as the string __MSG_appName__.

Invalid JSON in a translated file

A single syntax error (missing comma, trailing comma, unescaped quote) in any messages.json file causes Chrome to fail loading that locale silently. Validate JSON before shipping — use JSON.parse() in the browser console or a CI JSON lint step.

What a complete localized extension looks like

After translating to five languages, your extension folder looks like:

my-extension/
├── manifest.json
├── background.js
├── popup.html
├── popup.js
├── options.html
├── icons/
│   └── icon128.png
└── _locales/
    ├── en/
    │   └── messages.json   ← source (English)
    ├── de/
    │   └── messages.json
    ├── fr/
    │   └── messages.json
    ├── es/
    │   └── messages.json
    ├── ja/
    │   └── messages.json
    └── pt_BR/
        └── messages.json

When you submit to the Chrome Web Store, include the entire _locales folder in your ZIP. The Store automatically displays your extension name and description in the visitor’s language, improving discoverability for non-English speakers.

Skip the manual translation step

Once your source messages.json is ready, LocalePack translates it into up to 52 languages, preserves every $PLACEHOLDER$ token, and delivers a ready-to-extract _locales ZIP with correctly named folders. Upload, pay once, download — no subscription, no platform setup.

Translate my extension →
← Back to Guides
LocalePack
GuidesPrivacyTermsSupport

© 2025 LocalePack. All rights reserved.

This project was translated with LocalePack logoLocalePack