LocalePack
ChromeFirefoxEdge오페라사파리CWS 등록 페이지
Vue.jsReact
Next.jsi18nextReact Native
개발자 가이드성공 사례
Home/Guides/chrome.i18n
August 8, 2026

chrome.i18n: the complete API reference

chrome.i18n has four methods. Two are synchronous and two return promises, which is the single most common source of confusion when using it. This page documents each one — its signature, its exact return type, and what it does when it fails — so you can stop guessing which calls need an await.

The whole API at a glance

The entire surface is four methods. Read the middle column first: it is the part that trips people up.

MethodTimingReturns
getMessage(messageName, substitutions?)Synchronousstring
getUILanguage()Synchronousstring
getAcceptLanguages()AsynchronousPromise<LanguageCode[]>
detectLanguage(text)AsynchronousPromise<object>
The split is not arbitrary. The two synchronous methods read values Chrome already has resolved when your extension starts — the message catalogue and the UI language. The two asynchronous ones do work: reading a user preference list, and running language detection over text you supply.

Everything below expands on one row of that table.

Prerequisites, briefly

Three of the four methods work with no setup at all. getMessage() is the exception: it needs message catalogues on disk and a default_locale in the manifest.

my-extension/
├── manifest.json          "default_locale": "en"
└── _locales/
    ├── en/
    │   └── messages.json  ← the default_locale catalogue
    └── de/
        └── messages.json

Those two pieces are a matched set — declare one without the other and Chrome refuses to load the extension. We cover the rules and the exact error strings in default_locale: rules and common errors, and the directory layout in the _locales structure guide. The rest of this page assumes both are in place.

getMessage()

The method you will call thousands of times. It is synchronous and returns a string. There is no promise, no callback, and no loading state — Chrome resolves the catalogue before your code runs.

chrome.i18n.getMessage(messageName, substitutions?)  // → string

// Simple lookup
chrome.i18n.getMessage("appName")
// → "Tab Sorter"

// One substitution
chrome.i18n.getMessage("greeting", ["Ada"])
// → "Hello, Ada!"

// Several substitutions, in order
chrome.i18n.getMessage("summary", ["Ada", "12"])
// → "Ada, you have 12 tabs open."

// A single substitution may be passed as a bare string
chrome.i18n.getMessage("greeting", "Ada")

Because it is synchronous, it composes directly into templates and JSX with no ceremony:

// No await, no useEffect, no loading branch
document.title = chrome.i18n.getMessage("appName")

// React
<h1>{chrome.i18n.getMessage("appName")}</h1>

Substitutions and $1–$9

The second argument fills numbered slots in the message string. The values are positional: $1 is the first array element, $2 the second, up to $9.

The recommended form uses a named placeholders block:

// _locales/en/messages.json
{
  "summary": {
    "message": "$USER$, you have $COUNT$ tabs open.",
    "description": "Shown at the top of the popup",
    "placeholders": {
      "user": {
        "content": "$1",
        "example": "Ada"
      },
      "count": {
        "content": "$2",
        "example": "12"
      }
    }
  }
}

The named form is worth the extra lines. Translators see $USER$ rather than $1, so they know what the value is, and the example field tells them what it looks like. Crucially, it also lets them reorder the placeholders — many languages will not put the name first — without ever touching the numbered indices, which stay bound to your argument order.

Nine is a hard ceiling, and it is enforced by the return value rather than by an error. Passing a tenth substitution makes the call return undefined. If you need more than nine dynamic values in one string, the string is doing too much — split it.

The two documented failure returns

This is the part competitors leave out, and it is the part that costs debugging time. getMessage() does not throw. It fails by returning a value, and the two failure values mean different things.

"" — The message is missing

Verbatim from the Chrome documentation: “If the message is missing, this method returns an empty string (‘’).” The key does not exist in the resolved catalogue — a typo, a key that never made it into the default locale, or catalogues that were never copied into the build output.

undefined — messageName is not a string, or there are more than 9 substitutions

A different failure entirely: the call itself was malformed. Passing a number, null, or an object where a key was expected lands here, as does a substitutions array with ten or more entries.

Telling them apart is the fastest way to narrow a bug. An empty string means look at your catalogues; an undefined means look at your call site. The empty-string case has its own dedicated walkthrough in chrome.i18n.getMessage() returns an empty string.

> chrome.i18n.getMessage("appNmae")     // typo in the key
< ""

> chrome.i18n.getMessage(42)             // not a string
< undefined
Neither failure logs anything. In a popup with twenty labels, one bad key renders as one blank element in an otherwise correct UI, and nothing in the console points at it. Validate your catalogues in CI rather than hoping to notice at runtime — see validating messages.json.

getUILanguage() vs getAcceptLanguages()

These two look interchangeable and are not. They answer different questions, have different return types, and one of them is asynchronous.

chrome.i18n.getUILanguage()      // → string          (synchronous)
chrome.i18n.getAcceptLanguages() // → Promise<LanguageCode[]>  (asynchronous)

getUILanguage()

What language is the browser itself in?

Returns the browser's UI language as a plain string, synchronously. This is the value that decides which _locales folder your extension actually loads, so it is the right choice whenever you need to reason about, log, or display the locale your own strings came from.

getAcceptLanguages()

What languages has the user said they can read?

Returns a promise resolving to the user's ordered list of accepted languages — the same preference list the browser sends when requesting web content. Use it when you are choosing content for the user rather than chrome for the browser: which language to fetch an article in, or which of several available translations to offer first.

Because one is a promise, the calling code differs:

// Synchronous — use the value immediately
const ui = chrome.i18n.getUILanguage()
console.log("Extension strings are coming from:", ui)

// Asynchronous — await it
const accepted = await chrome.i18n.getAcceptLanguages()
console.log("User reads:", accepted)   // e.g. ["en-US", "en", "de"]
The two can disagree, and that is normal rather than a bug. Someone running Chrome in English while reading mostly German will report an English UI language and a list with German high in it. If you pick your extension’s strings from the accept-languages list, you will show German text inside an English-looking browser — which is exactly what getUILanguage() exists to prevent.

One practical note on formats: locale identifiers you get back from the browser are not necessarily spelled the way _locales folders are. Chrome folder names use underscores and a fixed accepted list, so do not feed a value straight from these methods into a path without checking it against the supported locale codes.

detectLanguage()

The odd one out. It has nothing to do with your extension’s own localization — it tells you what language a piece of arbitrary text appears to be written in.

chrome.i18n.detectLanguage(text)   // → Promise<object>  (asynchronous)

const result = await chrome.i18n.detectLanguage(
  "Der Text, dessen Sprache erkannt werden soll."
)

It is asynchronous and resolves to an object describing the detected languages. Consult the official chrome.i18n reference for that object’s exact fields before depending on its shape — detection results include confidence information, and treating a low-confidence guess as fact is how this API produces bad user experiences.

✓Deciding whether to offer a translation of page content a content script has read
✗Deciding which language to render your own popup in
✓Tagging user-submitted notes or clippings by language
✗Choosing which _locales folder to load

For the second column, the answer is always getUILanguage() — or, better, nothing at all, because Chrome already picked the catalogue for you before getMessage() ran.

browser.i18n vs chrome.i18n

Same API, different namespace. Which one exists depends on the browser your code is running in, not on what the API does.

BrowserNamespace
Chromechrome.i18n
Firefoxbrowser.i18n (chrome.i18n also works)
Safaribrowser.i18n (chrome.i18n alias)
Any, via polyfillbrowser.i18n (webextension-polyfill)

Because Firefox and Safari both accept chrome.i18n while Chrome does not provide browser, the simplest portable choice is to write chrome.i18n everywhere and ship the same code to all three stores.

If you prefer the browser namespace — usually because the rest of your extension uses promise-based WebExtension APIs — load Mozilla’s webextension-polyfill, which provides a browser object in Chrome:

import browser from 'webextension-polyfill'

// Works identically in Chrome, Firefox and Safari
browser.i18n.getMessage("appName")
The polyfill converts callback-style APIs to promises. That does not change getMessage() or getUILanguage(), which are synchronous in every namespace and stay that way. Do not add an await in front of them expecting the polyfill to have made them async — you will get a promise wrapping a string only where the underlying API already returned one.

The catalogue format is identical across all three browsers, so the same _locales tree ships everywhere. Store-level differences are covered in the Firefox and Safari guides.

Unsupported locales fail silently

Chrome accepts a fixed list of locale codes for _locales folder names. What happens to a code outside that list is stated plainly in the documentation:

“If you use an unsupported locale, Google Chrome ignores it.”

Ignores — not rejects. There is no build error, no load error, and no console warning. The folder sits in your extension, contributes nothing, and every user who should have received it falls through to your default_locale instead. From the outside this is indistinguishable from a working extension, which is why it survives code review and ships.

✗_locales/pt-BR/
✓_locales/pt_BR/

Underscore, not hyphen. The hyphenated folder is ignored outright.

✗_locales/zh/
✓_locales/zh_CN/ or _locales/zh_TW/

Chrome's accepted list names the script variants explicitly.

✗_locales/en_CA/
✓_locales/en/

Only some regional English variants are on the list. Anything else is ignored.

The full accepted list — and which of your folders will quietly do nothing — is in Chrome extension locale codes.

Quick reference

QuestionAnswer
Which methods need await?getAcceptLanguages(), detectLanguage()
Which methods return a plain value?getMessage(), getUILanguage()
Key does not exist"" (empty string)
messageName is not a stringundefined
More than 9 substitutionsundefined
Maximum substitutions per message9 ($1 through $9)
Locale code Chrome does not supportIgnored, no error
Required manifest field for getMessage()default_locale

Frequently asked questions

Is chrome.i18n.getMessage() synchronous or asynchronous?

Synchronous. It returns a string directly, not a promise, so you can call it inline in a render function with no await and no callback. This is unusual for extension APIs — getAcceptLanguages() and detectLanguage() in the same namespace are both promise-based.

What does chrome.i18n.getMessage() return if the key does not exist?

An empty string. The Chrome documentation states: 'If the message is missing, this method returns an empty string ('').' It does not throw and it does not return the key name, so a typo produces a blank label with no console error.

When does chrome.i18n.getMessage() return undefined?

In two documented cases: when messageName is not a string, and when you pass more than 9 substitutions. Both are distinct from the missing-key case, which returns an empty string instead. If you are seeing undefined rather than '', check the type of your key and the length of your substitutions array.

What is the difference between getUILanguage() and getAcceptLanguages()?

getUILanguage() returns the browser's own UI language as a synchronous string — the language Chrome's menus are displayed in, and the one that decides which _locales folder your extension loads. getAcceptLanguages() is asynchronous and resolves to the user's ordered list of accepted languages, which is a preference list for content rather than for the browser chrome.

Is browser.i18n the same as chrome.i18n?

It is the same API under a different namespace. Firefox exposes it as browser.i18n and also accepts chrome.i18n; Safari web extensions use browser.i18n with chrome.i18n available as an alias. Chrome itself only provides chrome.i18n natively, so cross-browser code either writes chrome.i18n everywhere or loads webextension-polyfill to get a browser object in Chrome.

What happens if I use a locale code chrome.i18n does not support?

Nothing visible. The Chrome documentation states: 'If you use an unsupported locale, Google Chrome ignores it.' The folder is skipped, no error is raised, and users fall through to your default_locale — which looks identical to a working extension unless you specifically test that locale.

The API is the easy half

Four methods take an afternoon. Producing catalogues for every locale you want to support is the part that stalls. LocalePack takes your source messages.json and returns a _locales ZIP covering 52 locales, every one of them a valid Chrome folder name — so none of them are the kind Chrome silently ignores. Placeholders such as $PLACEHOLDER$ and $1 come back untouched. Pay once — no account, no subscription.

Translate your messages.json into 52 locales →
← Back to Guides
LocalePack
가이드개인정보처리방침이용약관지원

© 2025 LocalePack. 모든 권리 보유.

이 프로젝트는 다음으로 번역되었습니다: LocalePack logoLocalePack