iOS & macOS (OTA SDK)

Last updated: September 03, 2026Author: Jakub Pomykała

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:

  1. over-the-air translation in the current language,
  2. over-the-air translation in the configured fallback language,
  3. the string compiled into the app (.strings / String Catalog),
  4. 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.

simplelocalize-ota-sdk-apple

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

OptionDefaultMeaning
projectTokenSettings → Credentials
environment_production_latest, _production or a custom environment
baseURLhttps://cdn.simplelocalize.iochange it for a custom hosting provider
namespaces[]downloaded namespaces; a namespace maps to the table of NSLocalizedString
languagenilforced language key instead of the device locale
fallbackLanguagenillanguage used for keys missing in the current one
customerIdnilcustomer-specific translations
minimumRefreshInterval600 sthrottle for automatic refreshes
refreshOnForegroundtruerefresh 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.

  1. 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 _latest
    

    Full publications always hit _latest first and cascade to _production from there. See publishing translations.

  2. Pick an environment per build. Point debug builds at _latest and release builds at _production. _production is served with Cache-Control: max-age=3600, so a publication reaches users within about an hour; _latest is cached briefly and is the right target while iterating. See environments.

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

  4. Use the same keys. A key published as home.title is the key you pass to Text(simpleLocalized: "home.title") or that lives in your Localizable.strings. Keys are case-sensitive.

Limitations

  • Plurals are not supported. .stringsdict entries 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") and String(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.