Upload your extension's messages.json file, translate each message, and export straight into a _locales/{lang}/ folder.
{
"appName": {
"message": "Moje rozszerzenie",
"description": "The name of the extension"
},
"appDescription": {
"message": "Przydatne rozszerzenie do przeglądarki.",
"description": "The description shown in the extension store"
},
"greeting": {
"message": "Witaj, $USER$! Masz $COUNT$ nowych wiadomości.",
"description": "Greets the user and shows the number of new messages",
"placeholders": {
"user": { "content": "$1", "example": "Anna" },
"count": { "content": "$2", "example": "5" }
}
},
"buttonSave": {
"message": "Zapisz"
}
}Chrome, Firefox, Edge and Safari all read extension translations from the same _locales/{lang}/messages.json layout, but the value under each key is an object, not a string: a required message field and an optional description field the browser ignores at runtime but translators actually rely on for context.
Placeholders follow their own convention too, written as $NAME$ in the message text and defined separately in a placeholders object that maps each name to a positional value like $1. SimpleLocalize keeps translators editing just the message text while the surrounding structure stays intact.
{
"name": "__MSG_appName__",
"description": "__MSG_appDescription__",
"default_locale": "en"
}Two kinds of description
Uploaded from your source, stored as reference context, and never silently overwritten.
Written by a translator in the editor, kept separate from whatever your codebase originally sent.
Each $NAME$ in a message maps to a positional value, and both round-trip on export.
End-to-end workflow
Follow these steps to translate a messages.json file from start to finish using SimpleLocalize.
simplelocalize upload \
--apiKey PROJECT_API_KEY \
--uploadFormat browser-extension-json \
--uploadLanguageKey en \
--uploadPath ./_locales/en/messages.json_locales/en/messages.json. Message and description fields import correctly without extra configuration.Key: greeting
Source: Hello, $USER$! You have $COUNT$
new messages.
Code description: Greets the user and
shows the number of new messages"greeting": {
"message": "Witaj, $USER$! Masz $COUNT$
nowych wiadomości."
}$USER$ and $COUNT$ stay exactly where they belong in the translated text.simplelocalize download \
--apiKey PROJECT_API_KEY \
--downloadFormat browser-extension-json \
--downloadLanguageKey pl,de,es \
--downloadPath ./_locales/{lang}/messages.jsonFor developers
A description written by a translator in the editor is worth protecting, even when your codebase uploads a new one.
A description uploaded from your codebase is stored separately from one a translator writes in the editor, so a new code upload never silently overwrites their notes.
Named placeholders and their positional mappings round-trip exactly, so substitutions still work after translation.
Translate new and changed messages with DeepL, Google Translate, OpenAI, Claude or Gemini.
Upload and download messages.json files from the CLI, or automate the same logic through the REST API.

More conversions
Free online converter, no account required. Upload a messages.json file and download it in the format you need.
More on localization workflows and translation file formats.

Stop treating translation as a manual step before release. Learn how to trigger AI translations automatically on every GitHub push, using SimpleLocalize CLI or the GitHub App.

Machine translation often breaks UI strings through missing context, character limits, and grammar rules other languages don't follow. Here's why, and how to fix it.

Why context is the single biggest factor in AI translation quality, and how to structure project context, key descriptions, and glossaries for accurate results.
Browser extensions require each key to hold an object with a message field, and often a description field for translator context. A plain string value would not load correctly in the extension.
Yes. All four follow the same WebExtensions _locales/{lang}/messages.json specification, so one exported file works across them.
Not by default. A description uploaded from your code is kept separate as a code description and won't overwrite what a translator wrote manually, unless you explicitly add the UPDATE_DESCRIPTIONS option.
Yes. Named placeholders and their positional content mappings are protected during auto-translation and stay valid in the exported file.