Overview
Over-the-air (OTA) translations are downloaded by the app at runtime instead of being frozen in the binary. Fixing a typo, retranslating a sentence or adding a language becomes a publication in SimpleLocalize, not an App Store review cycle.
The SDK is a per-key overlay on top of the localization you already have. A string is resolved in this order:
- over-the-air translation in the current language,
- over-the-air translation in the configured fallback language,
- the string compiled into the app (
.strings/ String Catalog), - the key itself.
Anything you have not published keeps rendering the bundled string, key by key on the same screen publishing three keys overrides exactly those three. Downloaded content is cached on disk, so the app starts with the last known translations even offline, and a failed refresh never drops what was already downloaded.
Requirements
- iOS 13+, macOS 11+, tvOS 13+ or watchOS 6+, Swift 5.9+.
- Translations published to Translation Hosting.
- Project token from Settings → Credentials. It is public by design and meant to be shipped in client code.
Installation
Add the package in Xcode (File > Add Package Dependencies...) or in Package.swift:
.package(url: "https://github.com/simplelocalize/simplelocalize-ota-sdk-apple.git", branch: "main")
The package has no dependencies of its own.
Usage
Start the SDK once, as early as possible. It loads the disk cache synchronously, so the first frame already shows the last known translations, and refreshes in the background.
import SimpleLocalizeOTA
@main
struct MyApp: App {
init() {
SimpleLocalize.shared.start(
SimpleLocalizeConfiguration(
projectToken: "5a5b1f...", // Settings -> Credentials
environment: "_production", // or "_latest"
fallbackLanguage: "en"
)
)
}
}
Read strings through the SDK:
label.text = SLLocalizedString("home.title")
label.text = "home.title".simpleLocalized
label.text = "cart.items".simpleLocalized(arguments: 3)
UIKit, storyboards and NSLocalizedString
One call makes existing code resolve over the air, with no call site changes:
SimpleLocalize.enableBundleIntegration()
It swaps the class of Bundle.main, so NSLocalizedString and Bundle.main.localizedString - and
with them UIKit code, storyboards and XIBs - ask the SDK first and fall back to the bundled strings.
SwiftUI
Text("key") and String(localized:) do not go through Bundle.main.localizedString, so the
bundle integration does not reach them. In SwiftUI read strings explicitly:
struct ContentView: View {
@StateObject private var localization = SimpleLocalizeObservable()
var body: some View {
VStack {
Text(simpleLocalized: "home.title")
Text(localization.string("home.subtitle"))
Button("Polski") { localization.setLanguage("pl") }
}
.simpleLocalizeAware() // rebuilds the subtree when new translations arrive
}
}
Languages and refreshing
The language is resolved from Locale.preferredLanguages unless you set language explicitly. Keys
are probed in the order en_GB, en-GB, en; the first one published wins and is remembered
across launches. SimpleLocalize.shared.setLanguage("pl") overrides it at runtime.
Translations refresh on start(_:), when the app returns to the foreground (throttled by
minimumRefreshInterval, 10 minutes by default) and on demand:
SimpleLocalize.shared.refresh()
Configuration
| Option | Default | Meaning |
|---|---|---|
projectToken | – | Settings → Credentials |
environment | _production | _latest, _production or a custom environment |
baseURL | https://cdn.simplelocalize.io | change it for a custom hosting provider |
namespaces | [] | downloaded namespaces; a namespace maps to the table of NSLocalizedString |
language | nil | forced language key instead of the device locale |
fallbackLanguage | nil | language used for keys missing in the current one |
customerId | nil | customer-specific translations |
minimumRefreshInterval | 600 s | throttle for automatic refreshes |
refreshOnForeground | true | refresh when the app becomes active |
Translation Hosting setup
The SDK reads exactly what you publish, so hosting configuration is the other half of the setup.
-
Publish. Nothing is served until a publication happens, editing translations changes nothing on the CDN. Publish from the Hosting tab, with the CLI or through the API:
simplelocalize publish --apiKey <PROJECT_API_KEY> --environment _latestFull publications always hit
_latestfirst and cascade to_productionfrom there. See publishing translations. -
Pick an environment per build. Point debug builds at
_latestand release builds at_production._productionis served withCache-Control: max-age=3600, so a publication reaches users within about an hour;_latestis cached briefly and is the right target while iterating. See environments. -
Check Settings → Hosting. Both the flat (
{"home.title": "Hi"}) and the nested ({"home": {"title": "Hi"}}) JSON format work, nested payloads are flattened to dot separated keys. Missing translations may be published as empty strings; the SDK treats an empty translation as missing and falls back to the bundled string, so the UI never goes blank. See hosting settings. -
Use the same keys. A key published as
home.titleis the key you pass toText(simpleLocalized: "home.title")or that lives in yourLocalizable.strings. Keys are case-sensitive.
Limitations
- Plurals are not supported.
.stringsdictentries and String Catalog plural variations are never overridden. They always come from the resources compiled into the app. Publish plural forms as separate keys if you need them over the air. - SwiftUI
Text("key")andString(localized:)are not intercepted by the bundle integration, see SwiftUI above. - The SDK is read-only: it never sends anything to SimpleLocalize, and the CDN it reads is public.