Android (OTA SDK)

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

Overview

The app downloads over-the-air (OTA) translations at runtime instead of being frozen in the APK. Fixing a typo, retranslating a sentence or adding a language becomes a publication in SimpleLocalize, not a Play Store release.

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. res/values*/strings.xml compiled into the APK,
  4. the key / resource default.

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 in filesDir, so the app starts with the last known translations even offline, and a failed refresh never drops what was already downloaded.

simplelocalize-ota-sdk-android

Requirements

  • minSdk 21, JDK 17 for the build. The library has no runtime dependencies
  • Translations published to Translation Hosting.
  • Project token from Settings > Credentials. It is public by design and meant to be shipped in client code

Installation

The library is not published to a Maven repository yet, so consume it from the repository as a Gradle module:

git clone https://github.com/simplelocalize/simplelocalize-ota-sdk-android.git
// settings.gradle.kts
include(":simplelocalize-ota")
project(":simplelocalize-ota").projectDir =
  file("../simplelocalize-ota-sdk-android/simplelocalize-ota")
// app/build.gradle.kts
dependencies {
  implementation(project(":simplelocalize-ota"))
}

If your root build file does not declare the Android library plugin yet, add id("com.android.library") version "<your AGP version>" apply false next to the application plugin.

Usage

Start the SDK once in Application.onCreate(). It reads the disk cache synchronously, so the first frame already shows the last known translations, and refreshes on a background thread.

class MyApplication : Application() {
  override fun onCreate() {
    super.onCreate()
    SimpleLocalize.start(
      this,
      SimpleLocalizeConfiguration(
        projectToken = "5a5b1f...",   // Settings -> Credentials
        environment = "_production",  // or "_latest"
        fallbackLanguage = "en"
      )
    )
  }
}

Existing getString() and Compose

Wrap the activity context and every getString(R.string.…), XML layout and Compose stringResource() resolves over the air first, with no call site changes:

class BaseActivity : AppCompatActivity() {
  override fun attachBaseContext(newBase: Context) {
    super.attachBaseContext(SimpleLocalize.wrapContext(newBase))
  }
}

The wrapped Resources translate a resource id to its entry name - R.string.home_title becomes the key home_title and look that key up over the air, falling back to strings.xml. Use resource entry names as translation keys in SimpleLocalize, a key published as home.title will never match R.string.home_title.

Keys that have no resource in the APK are read explicitly:

val text = SimpleLocalize.getString("checkout.summary") ?: "Summary"

Views already on screen are not redrawn when new translations arrive and listen for changes:

SimpleLocalize.addOnTranslationsChangedListener { recreate() }

In Compose, hold the revision in state and key the subtree on it:

var revision by remember { mutableIntStateOf(SimpleLocalize.revision) }
DisposableEffect(Unit) {
  val listener = SimpleLocalize.OnTranslationsChangedListener { revision = SimpleLocalize.revision }
  SimpleLocalize.addOnTranslationsChangedListener(listener)
  onDispose { SimpleLocalize.removeOnTranslationsChangedListener(listener) }
}
key(revision) { Text(stringResource(R.string.home_title)) }

Languages and refreshing

The language is resolved from the device locales 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.setLanguage("pl") overrides it at runtime.

Translations refresh on start(), when an activity is resumed (throttled by minimumRefreshIntervalMillis, 10 minutes by default) and on demand:

SimpleLocalize.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, passed as the namespace argument of getString
languagenullforced language key instead of the device locale
fallbackLanguagenulllanguage used for keys missing in the current one
customerIdnullcustomer-specific translations
minimumRefreshIntervalMillis600_000throttle for automatic refreshes
refreshOnForegroundtruerefresh when an activity is resumed

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. Match the keys to resource names. Keys are case-sensitive and, for the wrapContext() integration, must equal the strings.xml entry names (home_title, not home.title). Upload your strings.xml with the Android strings file format and the keys match automatically.

Limitations

  • Plurals are not supported. getQuantityString() and string arrays are never overridden, they always come from the resources compiled into the APK. Publish plural forms as separate keys if you need them over the air.
  • The SDK is read-only: it never sends anything to SimpleLocalize, and the CDN it reads is public.