LocalePack
ChromeFirefoxEdgeOperaSafariCWS Listing
Vue.jsReact
Next.jsi18nextReact Native
Guides
Home/Guides/messages.json format
February 21, 2026

messages.json format explained (with placeholders)

The messages.json file is the core of browser extension i18n. Every Chrome, Firefox, Edge, Safari, and Opera extension that supports multiple languages uses this format. Here is exactly how it works — including the parts that trip up most developers.

File location

Each locale gets its own messages.json file inside a _locales folder in the extension root:

my-extension/
├── manifest.json
├── _locales/
│   ├── en/
│   │   └── messages.json   ← default locale
│   ├── de/
│   │   └── messages.json
│   ├── fr/
│   │   └── messages.json
│   └── ja/
│       └── messages.json
└── ...

The default_locale field in manifest.json tells the browser which folder is the fallback when a user’s locale is not available:

{
  "manifest_version": 3,
  "name": "__MSG_appName__",
  "default_locale": "en",
  ...
}
Rule: If a _locales directory exists, you must declare default_locale. Omitting it causes the extension to be rejected by the Chrome Web Store and Firefox AMO.

The three fields of a message entry

Each key in messages.json maps to an object with up to three fields:

{
  "appName": {
    "message": "My Extension",
    "description": "The name of the extension shown in the browser UI.",
    "placeholders": {}
  }
}
messagerequired

The actual string that chrome.i18n.getMessage() returns. This is the only required field. It is the text shown to the user.

descriptionoptional

A hint for translators explaining what the string is used for. It is never shown to end users. LocalePack reads this field to give the AI more context, which improves translation accuracy.

placeholdersoptional

An object defining named dynamic values that appear in the message string. Defined placeholders survive translation untouched.

Placeholders: $PLACEHOLDER$ syntax

When a string contains dynamic values — a user name, a count, a URL — you define them as placeholders rather than embedding them literally. This prevents the AI (or human translator) from accidentally altering the variable name.

{
  "welcomeMessage": {
    "message": "Welcome, $USER$! You have $COUNT$ new messages.",
    "description": "Greeting shown on the dashboard after login.",
    "placeholders": {
      "user": {
        "content": "$1",
        "example": "Alice"
      },
      "count": {
        "content": "$2",
        "example": "3"
      }
    }
  }
}

The key rules for placeholder names:

  • •In the message string, placeholders are referenced as $NAME$ — always uppercase with dollar signs on both sides.
  • •In the placeholders object, the key is the same name but lowercase (e.g. user for $USER$).
  • •The content field specifies how the placeholder value is supplied at runtime: $1 means the first argument to getMessage(), $2 the second, and so on (up to $9).
  • •The example field is optional but helps translators understand what the placeholder represents.

Calling getMessage() with substitutions

Pass substitution values as the second argument to chrome.i18n.getMessage() (or browser.i18n.getMessage() in Firefox/Safari). The second argument is an array of strings mapped to $1, $2, etc.:

// Chrome / Edge / Opera
const msg = chrome.i18n.getMessage("welcomeMessage", ["Alice", "3"]);
// → "Welcome, Alice! You have 3 new messages."

// Firefox / Safari
const msg = browser.i18n.getMessage("welcomeMessage", ["Alice", "3"]);
// → "Welcome, Alice! You have 3 new messages."
The API accepts up to 9 substitution strings. If you need more than 9 dynamic values, split the message into multiple keys.

Placeholders without positional substitution

Sometimes the placeholder value is a literal string that never changes — for example, a brand name or URL you want to keep in every locale. Set content to the literal string instead of $1:

{
  "openDashboard": {
    "message": "Open $PRODUCT_NAME$ dashboard",
    "description": "Button label for the main dashboard link.",
    "placeholders": {
      "product_name": {
        "content": "LocalePack"
      }
    }
  }
}

In this case getMessage("openDashboard") returns "Open LocalePack dashboard" — no substitution array needed. The brand name is baked in and safe from translation.

Common mistakes

Translating the placeholder name

A translator changes $USER$ to $BENUTZER$ in the German file. The browser looks for a benutzer key in placeholders, finds nothing, and outputs $BENUTZER$ literally. Always preserve placeholder tokens verbatim. LocalePack does this automatically.

Missing default_locale

Creating a _locales directory without declaring default_locale in manifest.json. Chrome will refuse to load the extension and the Web Store will reject the upload.

Using locale codes not in the allowed list

Chrome only accepts the locale codes listed in its documentation (e.g. pt_BR, zh_CN). A folder named zh-Hant or pt-BR (hyphen instead of underscore) will be silently ignored.

Forgetting trailing comma in older JSON

messages.json must be valid JSON. Trailing commas after the last key are invalid JSON and will prevent the extension from loading. Use a JSON linter or validator before shipping.

Duplicate keys

JSON parsers typically use the last value for duplicate keys, silently discarding earlier ones. This makes duplicate keys hard to detect. LocalePack validates your source file and flags duplicates before translation.

Full example: a complete messages.json

{
  "appName": {
    "message": "My Extension",
    "description": "The name of the extension shown in browser UI and the Web Store."
  },
  "appDescription": {
    "message": "Boost your productivity with one click.",
    "description": "Short description shown in browser UI and Web Store listings."
  },
  "welcomeMessage": {
    "message": "Welcome, $USER$!",
    "description": "Greeting shown after the user signs in.",
    "placeholders": {
      "user": {
        "content": "$1",
        "example": "Alice"
      }
    }
  },
  "itemCount": {
    "message": "You have $COUNT$ items in your list.",
    "description": "Status message showing how many items the user has saved.",
    "placeholders": {
      "count": {
        "content": "$1",
        "example": "5"
      }
    }
  },
  "openDocs": {
    "message": "Open $PRODUCT$ documentation",
    "description": "Link label for the documentation page.",
    "placeholders": {
      "product": {
        "content": "My Extension"
      }
    }
  }
}

Need to translate your messages.json into 52 languages?

LocalePack reads your messages.json, uses the description field as translation context, and preserves every $PLACEHOLDER$ exactly as-is. Download a ready-to-ship _locales ZIP in minutes. Pay once, no subscription.

Translate my messages.json →
← Back to Guides
LocalePack
GuidesPrivacyTermsSupport

© 2025 LocalePack. All rights reserved.

This project was translated with LocalePack logoLocalePack