Safari Web Extensions localization notes
Safari Web Extensions use the same _locales/ / messages.json format as Chrome and Firefox. The file format, placeholder syntax, and default_locale rule all carry over. What’s different is distribution — Safari extensions ship through the Mac App Store (and iOS App Store), which introduces Xcode project structure and App Store Connect review.
The common WebExtension format
Apple adopted the WebExtension standard starting with Safari 14 (macOS Big Sur, 2020). This means the i18n file structure is identical to Chrome and Firefox:
my-safari-extension/
├── manifest.json ← "default_locale": "en" required
└── _locales/
├── en/
│ └── messages.json
├── de/
│ └── messages.json
├── ja/
│ └── messages.json
└── fr/
└── messages.jsonA messages.json file that works in Chrome works in Safari without modification. The same $PLACEHOLDER$ syntax, the same $1–$9 positional substitutions, and the same __MSG_key__ manifest substitution all work. If you already have a working Chrome or Firefox extension with _locales, the Safari version will use the same files.
browser.i18n in Safari
Safari exposes the i18n API as browser.i18n — the same namespace as Firefox. It also supports chrome.i18n as an alias for Chrome compatibility. The method signatures are identical across all three browsers:
// All three work in Safari:
browser.i18n.getMessage("appName");
browser.i18n.getMessage("welcomeMessage", ["Alice"]);
browser.i18n.getUILanguage();
// Chrome-compat alias also works:
chrome.i18n.getMessage("appName");browser.i18n.getMessage() is synchronous in Safari — it returns a plain string, not a Promise. This is consistent across all browsers. The browser.* namespace is preferred in cross-browser code.If you maintain a single codebase for Chrome, Firefox, and Safari, the cross-browser shim from the Firefox i18n article works for all three:
// i18n.js — cross-browser shim (Chrome + Firefox + Safari)
const i18n = (typeof browser !== "undefined" ? browser : chrome).i18n;
export function t(key, substitutions) {
return i18n.getMessage(key, substitutions);
}messages.json format in Safari
Safari supports the full WebExtension message schema — all three fields work exactly as in Chrome and Firefox:
{
"appName": {
"message": "My Safari Extension",
"description": "Name shown in Safari toolbar and App Store listing."
},
"tabCount": {
"message": "$COUNT$ tabs open",
"description": "Status text showing the number of open tabs.",
"placeholders": {
"count": {
"content": "$1",
"example": "5"
}
}
},
"welcomeMessage": {
"message": "Welcome, $USER_NAME$!",
"description": "Greeting shown when user opens the extension popup.",
"placeholders": {
"user_name": {
"content": "$1",
"example": "Alice"
}
}
}
}messagerequiredThe translated string. getMessage() returns this value with placeholder tokens replaced. Safari handles $PLACEHOLDER$ tokens identically to Chrome.
descriptionoptionalContext for translators. Safari ignores this at runtime, but AI translation tools (including LocalePack) use it to produce more accurate translations.
placeholdersoptionalNamed dynamic values mapping $NAME$ tokens to positional substitutions ($1–$9). Safari preserves these identically to Chrome and Firefox.
Safari extension project structure in Xcode
Unlike Chrome and Firefox where you load an unpacked folder or ZIP, Safari Web Extensions are wrapped in a native macOS/iOS app. Apple provides a conversion tool and Xcode template:
# Convert an existing Chrome/Firefox extension to Safari
xcrun safari-web-extension-converter /path/to/your/extension
# This creates an Xcode project with:
# MyExtension/
# ├── MyExtension.xcodeproj
# ├── Shared (Extension)/
# │ └── Resources/
# │ ├── manifest.json
# │ ├── _locales/
# │ │ ├── en/messages.json
# │ │ ├── de/messages.json
# │ │ └── ...
# │ ├── popup.html
# │ └── background.js
# └── macOS (App)/
# └── iOS (App)/The important path is Shared (Extension)/Resources/. This is where your _locales/ folder lives inside the Xcode project. When you extract LocalePack’s ZIP output, copy the locale folders into this Resources/ directory and make sure they are included in the Xcode build target.
Develop → Allow Unsigned Extensions in Safari to enable unsigned development builds.Manifest V2 vs V3 in Safari
Safari supports both Manifest V2 and Manifest V3. The i18n system is identical in both versions — the _locales folder, messages.json format, default_locale, and __MSG_key__ substitution all work the same way regardless of manifest version.
{
"manifest_version": 3,
"name": "__MSG_appName__",
"description": "__MSG_appDescription__",
"default_locale": "en",
"version": "1.0.0"
}Apple’s safari-web-extension-converter tool handles both MV2 and MV3 input. If you’re porting an existing Chrome MV3 extension, the converter will preserve your manifest and _locales folder as-is.
Mac App Store & iOS App Store submission
Safari extensions are distributed through the App Store, not a web-based add-on store. This creates an important distinction in how localization works:
Two separate localization layers
- 1.In-extension strings — from
_locales/*/messages.json. These are whatbrowser.i18n.getMessage()returns at runtime (popup text, toolbar labels, options page strings). - 2.App Store listing — managed in App Store Connect. App name, subtitle, description, keywords, and screenshots must each be localized separately in App Store Connect for each target market. This is not read from
messages.json.
App Store Connect localization
Xcode Localizable.strings vs messages.json
Localizable.strings or .xcstrings format — not messages.json. Keep these separate: LocalePack handles messages.json for the web extension part; use Xcode’s built-in export/import for native strings.Locale codes in Safari
Safari follows the WebExtension locale code convention — lowercase language code with optional underscore-separated region. The codes used in _locales/ folder names are the same as Chrome:
Widely supported (Chrome + Firefox + Safari)
endefresjakozh_CNzh_TWpt_BRruarplnlitApp Store Connect uses different codes (BCP 47 format)
en-USde-DEfr-FRjazh-Hanszh-Hantpt-BRkozh-Hans) with WebExtension _locales folder names (underscores, e.g. zh_CN). These are two different systems. Your _locales folders use the Chrome/WebExtension convention; App Store Connect uses Apple’s own BCP 47 variant.Testing Safari extension i18n locally
To test your Safari Web Extension in different locales during development:
Enable unsigned extensions
Open Safari → Develop menu → Allow Unsigned Extensions. This must be re-enabled every time Safari launches.
Change system language
Go to System Settings → General → Language & Region. Drag the target language to the top of the Preferred Languages list. Log out and back in (or restart) for the change to take effect. Safari picks up the system locale automatically.
Or use Xcode scheme settings
In Xcode, edit the run scheme for your extension app: Product → Scheme → Edit Scheme → Run → Options → App Language. Choose the locale to test. This overrides the system locale for that run session only — no logout required.
Rebuild and reload
After changing locale, rebuild in Xcode (⌘B) and run (⌘R). Open Safari, enable the extension in Safari → Settings → Extensions, and verify the translated strings appear.
# Alternatively, run from command line with a specific locale:
# (requires the extension app to be built first in Xcode)
open -a Safari --args -AppleLanguages "(de)"Common Safari-specific pitfalls
Forgetting to add _locales to the Xcode build target
After copying locale folders into Shared (Extension)/Resources/, you must verify they appear in the Xcode project navigator and are included in the target’s “Copy Bundle Resources” build phase. If the files exist on disk but are not in the build target, they will not be included in the final app bundle and Safari will silently fall back to default_locale.
Confusing messages.json with Localizable.strings
Your Safari Web Extension has two separate i18n systems. messages.json handles the web extension part (popup, content scripts, toolbar). Localizable.strings handles the native app wrapper (onboarding screens, about page). Translating one does not affect the other.
App Store listing not matching in-extension language
Your extension popup may be translated into German via messages.json, but the App Store listing still shows English because App Store Connect localizations are separate. Users see both — the store page before install, and the extension UI after. Both should be localized for a professional result.
Invalid JSON blocks extension loading
Like all browsers, Safari parses every messages.json when the extension loads. A trailing comma, missing brace, or unescaped quote in any locale file will prevent the entire extension from loading — even if that locale is never used. Always validate JSON in every locale file before building.
iOS Safari Web Extensions have the same i18n rules
Since iOS 15, Safari Web Extensions work on iPhone and iPad using the same WebExtension format. The _locales/ folder and messages.json format are shared between the macOS and iOS targets in the same Xcode project. One set of locale files serves both platforms.
Cross-browser compatibility summary
| Feature | Chrome | Firefox | Safari |
|---|---|---|---|
| File format | messages.json | messages.json | messages.json (identical) |
| Folder structure | _locales/{locale}/ | _locales/{locale}/ | _locales/{locale}/ (in Resources/) |
| Runtime API | chrome.i18n | browser.i18n | browser.i18n (chrome.i18n alias) |
| getMessage() | Synchronous | Synchronous | Synchronous |
| $PLACEHOLDER$ syntax | Supported | Supported | Supported |
| __MSG_key__ in manifest | Supported | Supported | Supported |
| Manifest versions | MV3 (MV2 deprecated) | MV2 + MV3 | MV2 + MV3 |
| Distribution | Chrome Web Store | AMO | Mac / iOS App Store |
| Store listing i18n | Chrome Developer Dashboard | AMO developer hub | App Store Connect |
| Dev workflow | Load unpacked | web-ext run | Xcode build → Safari |
Translate your Safari extension into 52 languages
LocalePack generates a _locales ZIP compatible with Safari, Chrome, and Firefox — correct locale codes, preserved $PLACEHOLDER$ tokens, validated JSON in every file. Upload your source messages.json, pay once, and extract the locale folders into your Xcode project’s Resources directory.