LocalePack
ChromeFirefoxEdgeOperaSafariCWS Listing
Vue.jsReact
Next.jsi18nextReact Native
Guides
Home/Guides/Safari Web Extensions localization
February 21, 2026

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.json

A 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");
Like Firefox, 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"
      }
    }
  }
}
messagerequired

The translated string. getMessage() returns this value with placeholder tokens replaced. Safari handles $PLACEHOLDER$ tokens identically to Chrome.

descriptionoptional

Context for translators. Safari ignores this at runtime, but AI translation tools (including LocalePack) use it to produce more accurate translations.

placeholdersoptional

Named 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.

Unlike Chrome and Firefox, you cannot “reload” an unpacked extension with a file watcher. You must rebuild in Xcode and reload Safari for changes to take effect. Use 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. 1.In-extension strings — from _locales/*/messages.json. These are what browser.i18n.getMessage() returns at runtime (popup text, toolbar labels, options page strings).
  2. 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

In App Store Connect, you choose which localizations to support for your listing. Each locale gets its own app name, subtitle, description, keywords field, and screenshots. This is entirely separate from the _locales folder in your extension code. Localizing messages.json does not update your App Store listing — you must enter those texts in App Store Connect.

Xcode Localizable.strings vs messages.json

The native macOS/iOS wrapper app around your Safari extension may have its own UI (an “About” screen, onboarding, etc.). Those native strings use Apple’s 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_BRruarplnlit

App Store Connect uses different codes (BCP 47 format)

en-USde-DEfr-FRjazh-Hanszh-Hantpt-BRko
Don’t confuse App Store Connect locale codes (BCP 47 with hyphens, e.g. zh-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:

1

Enable unsigned extensions

Open Safari → Develop menu → Allow Unsigned Extensions. This must be re-enabled every time Safari launches.

2

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.

3

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.

4

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

FeatureChromeFirefoxSafari
File formatmessages.jsonmessages.jsonmessages.json (identical)
Folder structure_locales/{locale}/_locales/{locale}/_locales/{locale}/ (in Resources/)
Runtime APIchrome.i18nbrowser.i18nbrowser.i18n (chrome.i18n alias)
getMessage()SynchronousSynchronousSynchronous
$PLACEHOLDER$ syntaxSupportedSupportedSupported
__MSG_key__ in manifestSupportedSupportedSupported
Manifest versionsMV3 (MV2 deprecated)MV2 + MV3MV2 + MV3
DistributionChrome Web StoreAMOMac / iOS App Store
Store listing i18nChrome Developer DashboardAMO developer hubApp Store Connect
Dev workflowLoad unpackedweb-ext runXcode 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.

Localize your Safari extension →
← Back to Guides
LocalePack
GuidesPrivacyTermsSupport

© 2025 LocalePack. All rights reserved.

This project was translated with LocalePack logoLocalePack