LocalePack
ChromeFirefoxEdgeOperaSafariSeznam v CWS
Vue.jsReact
Next.jsi18nextReact Native
Vývojářské návodyPříklady úspěchu
Home/Guides/Plasmo i18n
August 8, 2026

Plasmo i18n: the locales/ folder gotcha

Every Chrome extension tutorial tells you to put translations in _locales/ at the root of your project. In Plasmo that does nothing at all — the folder is ignored, the build succeeds, and your extension ships in one language. Here is where the files actually go.

Plasmo does not read _locales/ at the project root

Plasmo accepts locale files in exactly three locations:
  • 1.locales/{lang}/messages.json
  • 2.assets/_locales/{lang}/messages.json
  • 3.assets/locales/{lang}/messages.json

The documentation is explicit on both points: “Plasmo expects your locale files in any of these locations” and “You will need to pick one and stick to it!”

Note what is not in that list: a bare _locales/ at the project root. That path is where the built extension carries its catalogues — Plasmo generates it for you during the build. Putting source files there yourself puts them somewhere the build never looks.

✗_locales/en/messages.json
✓locales/en/messages.json

No leading underscore, and not at the root. This is the most common of the three choices.

✗src/_locales/en/messages.json
✓assets/_locales/en/messages.json

If you want to keep the underscore convention, it belongs under assets/, not src/.

Why unzipping a _locales ZIP at the root does nothing

Every messages.json translation service — LocalePack included — hands you a _locales.zip containing a _locales/ directory. That shape is correct for a plain extension, where you drop it next to manifest.json and are done. In Plasmo, extracting it at the project root produces a directory the bundler ignores.

What you get if you unzip at the root — nothing happens:

my-plasmo-extension/
├── _locales/              ← ignored by Plasmo
│   ├── de/
│   │   └── messages.json
│   ├── en/
│   │   └── messages.json
│   └── fr/
│       └── messages.json
├── package.json
├── popup.tsx
└── tsconfig.json

What it should look like — extract the ZIP’s contents into locales/:

my-plasmo-extension/
├── locales/               ← Plasmo reads this
│   ├── de/
│   │   └── messages.json
│   ├── en/
│   │   └── messages.json
│   └── fr/
│       └── messages.json
├── package.json
├── popup.tsx
└── tsconfig.json

Or, if you prefer the underscore convention, under assets/:

my-plasmo-extension/
├── assets/
│   └── _locales/          ← also read by Plasmo
│       ├── de/
│       │   └── messages.json
│       ├── en/
│       │   └── messages.json
│       └── fr/
│           └── messages.json
├── package.json
└── popup.tsx

The per-locale folder names are unchanged in every case — they are still Chrome _locales codes with underscores, such as pt_BR and zh_CN. Only the parent directory moves. See the _locales structure guide for the full list of valid codes.

Pick one of the three locations and use only that one. Splitting locales across, say, locales/ and assets/locales/ is exactly what the documentation warns against when it says to pick one and stick to it.

default_locale lives in package.json

The second thing that trips people up: there is no manifest.json to edit. Plasmo generates the manifest, and manifest fields are declared in package.json under the manifest key.

{
  "manifest": {
    "default_locale": "en"
  }
}

Everything you know about default_locale from plain extension development still applies to the generated manifest — it is the same field, just declared somewhere else. If you have not hit its validation rules before, they are catalogued in default_locale: rules and common errors.

The alphabetical-default trap

This one is genuinely dangerous, because it fails silently and only in production. The Plasmo documentation states it plainly:

“By default, Plasmo picks the first locale alphabetically available as default.”

So if you omit default_locale and your project ships Arabic and English, the default locale becomes ar — not because anyone chose it, but because ar sorts before en.

locales/
├── ar/                    ← becomes default_locale if you omit the field
│   └── messages.json
├── de/
│   └── messages.json
└── en/                    ← what you almost certainly wanted
    └── messages.json

You will not notice this in local development if your browser is set to English: Chrome finds _locales/en/ and never needs the fallback. The default locale only surfaces for users whose language you did not translate — who then see Arabic instead of English.

Adding a single new locale can change your default. A project with de and en defaults to de; add ar later and the default silently becomes ar. Declaring default_locale explicitly costs one line and removes the whole class of bug.

Runtime: plain chrome.i18n, no wrapper

Plasmo does not ship an i18n helper. You call the WebExtension API directly:

chrome.i18n.getMessage("popup")

Because there is no wrapper, every standard behaviour of the API applies unchanged:

  • •getMessage() is synchronous and returns a string. No await, no callback.
  • •A missing message is not an error. Verbatim from the Chrome documentation: “If the message is missing, this method returns an empty string (‘’).” That is why a misplaced locales/ folder shows up as blank UI rather than a thrown exception.
  • •getMessage() returns undefined if messageName is not a string, or if there are more than 9 substitutions.
  • •Unsupported locale folders are skipped rather than rejected. Verbatim: “If you use an unsupported locale, Google Chrome ignores it.”

The practical consequence of the first two points together: there is no error anywhere in the pipeline that tells you the locale files are in the wrong place. The build succeeds, the API returns strings, and the strings happen to be empty.

The dev-server watching caveat

Locale files are watched by the dev server only if they existed before the dev server started. Create a new locale folder while plasmo dev is running and nothing will pick it up.

# Adding a locale while the dev server is running:
#   1. stop the dev server
#   2. create locales/fr/messages.json
#   3. start the dev server again

plasmo dev

This matters most right after you drop in a translated _locales ZIP: you have just created a dozen new directories at once. Restart the dev server before concluding the translations are broken.

Troubleshooting

My translations do not appear at all

Cause: The locale files are in _locales/ at the project root, which Plasmo ignores.

Fix: Move them to locales/{lang}/messages.json (or one of the two assets/ paths) and rebuild.

chrome.i18n.getMessage() returns an empty string

Cause: The key is not present in the catalogue Chrome resolved to — either the file is in an unread location, or the key is genuinely missing.

Fix: Confirm the file location first, then confirm the key exists in the default_locale catalogue. A missing message returns '' by design; it never throws.

The extension name and description are not localized

Cause: Manifest strings use __MSG_key__ substitution and are resolved from the default_locale catalogue only — and in Plasmo the manifest comes from package.json.

Fix: Declare the manifest fields under the manifest key in package.json using __MSG_ syntax, and make sure the referenced keys exist in the default locale.

Users in untranslated languages see the wrong language

Cause: default_locale was omitted, so Plasmo picked the first locale alphabetically.

Fix: Add "default_locale": "en" under the manifest key in package.json.

A locale I just added is not being served

Cause: The folder was created after plasmo dev started, so it is not being watched.

Fix: Restart the dev server.

For the __MSG_key__ mechanics behind the third row, see __MSG_key__ substitution in manifest.json.

Quick reference

ConcernIn Plasmo
Accepted path 1locales/{lang}/messages.json
Accepted path 2assets/_locales/{lang}/messages.json
Accepted path 3assets/locales/{lang}/messages.json
NOT read_locales/{lang}/messages.json (project root)
default_localepackage.json → "manifest": { "default_locale": "en" }
If default_locale is omittedfirst locale alphabetically wins
Runtime lookupchrome.i18n.getMessage("popup")
Missing messagereturns "" (empty string), never throws
Dev server watchingonly files that existed before startup
Locale folder namesunchanged Chrome codes, e.g. pt_BR, zh_CN

Frequently asked questions

Why are my Plasmo translations not appearing?

Almost always because the locale files are in _locales/ at the project root. Plasmo does not read that path. It expects locales/{lang}/messages.json, assets/_locales/{lang}/messages.json, or assets/locales/{lang}/messages.json. Move the folder to one of those three locations and rebuild.

Where do locale files go in a Plasmo extension?

One of exactly three places: locales/{lang}/messages.json, assets/_locales/{lang}/messages.json, or assets/locales/{lang}/messages.json. The Plasmo documentation states you should pick one and stick to it.

Can I unzip a _locales.zip into a Plasmo project?

Not at the project root. The ZIP contains a _locales/ directory, which is what Chrome loads from a built extension, but Plasmo generates that directory during the build. Unzip the contents into locales/ (or assets/_locales/) instead, and Plasmo will emit _locales/ in the build output for you.

Where does default_locale go in Plasmo?

In package.json under the manifest key: { "manifest": { "default_locale": "en" } }. Plasmo generates manifest.json from package.json, so there is no manifest.json file to edit.

What happens if I omit default_locale in Plasmo?

By default, Plasmo picks the first locale alphabetically available as default. So a project shipping ar and en will silently default to Arabic. Always declare default_locale explicitly.

Do I need a Plasmo-specific i18n API?

No. Runtime lookups are plain chrome.i18n.getMessage('popup') with no Plasmo wrapper, so all standard chrome.i18n behaviour applies, including the locale fallback chain and returning an empty string for a missing message.

Related guides

  • •The messages.json format explained — the file format itself, unchanged by Plasmo.
  • •Chrome extension _locales structure — valid folder names and the lookup fallback chain.
  • •default_locale: rules and common errors — the field Plasmo guesses for you if you omit it.
  • •WXT i18n: @wxt-dev/i18n and messages.json — how the other popular extension framework handles the same problem.

Get the catalogues, drop them in locales/

LocalePack takes your messages.json, translates it once for a single payment, and returns a _locales ZIP. For Plasmo, extract its contents into locales/ rather than the project root. Placeholders such as $PLACEHOLDER$ and $1 are preserved, and all 52 locale folder names are valid Chrome codes. No account, no subscription.

Translate your messages.json into 52 locales →
← Back to Guides
LocalePack
NávodyOchrana soukromíPodmínkyPodpora

© 2025 LocalePack. Všechna práva vyhrazena.

Tento projekt byl přeložen pomocí LocalePack logoLocalePack