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 / ComposestringResource(opt-in), or through an explicit API. - Offline first: the last downloaded content is cached in
filesDir,res/values*/strings.xmlstays the final fallback. - Cheap refresh: conditional
If-None-Matchrequests, so an unchanged refresh transfers no body.
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.
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.
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)) }
}| 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 |
- over-the-air translation in the current language,
- over-the-air translation in
fallbackLanguage, res/values*/strings.xmlcompiled into the APK (when usingwrapContext),- 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.
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.
- Plurals (
getQuantityString) are not supported yet.
A runnable app lives in sample/ - ./gradlew :sample:installDebug.
./gradlew :simplelocalize-ota:testDebugUnitTest # JVM tests, incl. a real HTTP server with ETag revalidation
./gradlew :simplelocalize-ota:assembleRelease