Skip to content

Repository files navigation

SimpleLocalize OTA SDK for Android

CI

Over-the-air translations for Android apps. Translations are fetched at runtime from SimpleLocalize Translation Hosting (cdn.simplelocalize.io), so fixing a typo or adding a language is a publish, not a Play Store release.

  • Zero runtime dependencies (no OkHttp, no coroutines, no AndroidX), minSdk 21.
  • Works with existing getString(R.string.key) / XML layouts / Compose stringResource (opt-in), or through an explicit API.
  • Offline first: the last downloaded content is cached in filesDir, res/values*/strings.xml stays the final fallback.
  • Cheap refresh: conditional If-None-Match requests, so an unchanged refresh transfers no body.

Installation

Not published to a Maven repository yet, so consume it as a Gradle module:

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

Quick start

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

Read strings:

textView.text = SimpleLocalize.getStringOrDefault("home.title", getString(R.string.home_title))

start() is non-blocking: the disk cache is read synchronously (so the first frame already shows the last known translations) and the network refresh happens on a background thread.

Zero-touch integration with getString(R.string.…)

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 -> home_title) and look that key up over the air first, falling back to the strings compiled into the APK. Use the same resource entry names as translation keys in SimpleLocalize and no call site changes are needed - XML layouts and Compose stringResource go through the same Resources.

Views already on screen are not redrawn automatically; listen for changes:

SimpleLocalize.addOnTranslationsChangedListener { recreate() }

Compose:

@Composable
fun rememberTranslationsRevision(): Int {
  var revision by remember { mutableIntStateOf(SimpleLocalize.revision) }
  DisposableEffect(Unit) {
    val listener = SimpleLocalize.OnTranslationsChangedListener { revision = SimpleLocalize.revision }
    SimpleLocalize.addOnTranslationsChangedListener(listener)
    onDispose { SimpleLocalize.removeOnTranslationsChangedListener(listener) }
  }
  return revision
}

@Composable
fun Title() {
  val revision = rememberTranslationsRevision()
  key(revision) { Text(stringResource(R.string.home_title)) }
}

Configuration

Option Default Meaning
projectToken Settings -> Credentials
environment _production _latest, _production or a custom environment key
baseUrl https://cdn.simplelocalize.io change it for a custom hosting provider (S3/GCS/Azure)
namespaces [] downloaded namespaces; passed as the namespace argument of getString
language null forced language key; null resolves it from the device locales
fallbackLanguage null language used for keys missing in the current one
customerId null customer specific translations ({language}_{customerId})
minimumRefreshIntervalMillis 600_000 throttle for automatic refreshes
refreshOnForeground true refresh when an activity is resumed
logger null diagnostics sink

How lookup works

  1. over-the-air translation in the current language,
  2. over-the-air translation in fallbackLanguage,
  3. res/values*/strings.xml compiled into the APK (when using wrapContext),
  4. the key / resource default.

Over-the-air content is a per-key overlay on top of the app's own resources, never a replacement: anything that is not published keeps rendering the string compiled into the app, key by key on the same screen. An empty translation counts as missing, so publishing missing translations as empty strings does not blank out the UI.

The same layering applies to the network: memory, then the disk cache, then the app. A failed refresh - offline, HTTP error, unpublished resource - never drops content already downloaded, and an app launched offline with an empty cache simply shows its bundled strings.

Two limits worth knowing:

  • The overlay only reaches call sites that go through the SDK (see the two sections above); plurals and string arrays always stay with the bundled resources, see Roadmap.
  • If everything published for the resolved language disappears from hosting (404 on every resource), the SDK forgets that language and falls back to the bundled strings, even when it still holds them in the cache.

Language keys are matched against device locales in this order: en_GB, en-GB, en - the first one published on the CDN wins and is remembered across launches.

Hosting can publish flat ({"home.title": "Hi"}) or nested ({"home": {"title": "Hi"}}) JSON; both are supported and nested payloads are flattened to dot separated keys.

Refresh model

  • start() - disk cache immediately, network refresh in the background.
  • activity resumed - throttled by minimumRefreshIntervalMillis.
  • SimpleLocalize.refresh() - manual, ignores the throttle.

_production is served with Cache-Control: max-age=3600, so a publication reaches users within about an hour; point debug builds at _latest to iterate faster.

Roadmap

  • Plurals (getQuantityString) are not supported yet.

Example app

A runnable app lives in sample/ - ./gradlew :sample:installDebug.

Development

./gradlew :simplelocalize-ota:testDebugUnitTest   # JVM tests, incl. a real HTTP server with ETag revalidation
./gradlew :simplelocalize-ota:assembleRelease

About

Over-the-air translations for Android - powered by SimpleLocalize Translation Hosting

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages