Platforms

Android

Localize a module through its own strings.xml files. One project can serve the iOS app as well; keys are shared unless tagged otherwise.

What you get

Everything the editor does, through the files you already ship.

  • AI drafts, a person verifies

    Every language is drafted against your glossary and each key's length limit. A draft never ships until someone has verified it.

  • Every plural form a language has

    Each locale gets exactly the CLDR categories it uses, Arabic six and Japanese one, written in the format's own plural shape.

  • Updates over the air

    Publish the verified strings and let the running app fetch them, so a fixed translation reaches users without a store release.

    Over-the-air delivery
  • Runs in CI

    Push the source language on merge and pull every language before the release build, on any CI that can run a shell.

    CI pipelines
  • One catalog, every platform

    The iOS app, the Android app and the web app share one project. Tag the keys only one of them uses and pull that subset alone.

  • Scriptable from the CLI

    Push, pull and sync from a terminal, a build phase or a coding agent's shell, with a token in the environment.

    The CLI

Set it up

From nothing to every language.

  1. Step 1: Install the CLI and log in

    Homebrew or the one-line installer, then a token issued per project under Settings, Integrations. The read scope pulls; push writes source files and values; manage creates and deletes keys.

    shell
    brew install anylocale/tap/anylocale
    anylocale login --project my-app
  2. Step 2: Push the source file

    The push is additive: keys missing from the file stay in place, so a truncated file cannot empty a project. Descriptions in the file become translator notes.

    shell
    curl -fsS -X POST "https://anylocale.com/api/loc/push?project=my-app&format=android-xml" \
        -H "Authorization: Bearer $ANYLOCALE_TOKEN" \
        --data-binary @app/src/main/res/values/strings.xml
  3. Step 3: Pull each language into its values folder

    One command per locale. Add --platform android to leave out keys tagged for iOS only.

    shell
    anylocale pull --project my-app --locale de --format android-xml \
        --platform android --out app/src/main/res/values-de/strings.xml
  4. Step 4: Pull as a Gradle task

    An exec task per locale, wired in front of preBuild, so a build on any machine carries the latest verified text. The token comes from the environment; the CI pipelines guide shows the secret handling.

    app/build.gradle.kts
    val locales = listOf("de", "fr", "ja")
    
    val pullStrings = tasks.register("pullStrings") {
        doLast {
            locales.forEach { locale ->
                exec {
                    commandLine(
                        "anylocale", "pull", "--project", "my-app",
                        "--locale", locale, "--format", "android-xml",
                        "--platform", "android",
                        "--out", "src/main/res/values-$locale/strings.xml"
                    )
                }
            }
        }
    }
    
    tasks.named("preBuild") { dependsOn(pullStrings) }
  5. Step 5: Add the library

    Releases are served by JitPack from the repository's tags. sdk covers Views and plain Kotlin, sdk-compose adds the composables and already pulls in sdk. Android 5.0 (API 21) upward.

    settings.gradle.kts
    dependencyResolutionManagement {
        repositories {
            google()
            mavenCentral()
            maven("https://jitpack.io")
        }
    }
    gradle/libs.versions.toml
    [versions]
    anylocale = "v1.0.2"
    
    [libraries]
    anylocale = { group = "com.github.anylocale.anylocale-android", name = "sdk", version.ref = "anylocale" }
    anylocale-compose = { group = "com.github.anylocale.anylocale-android", name = "sdk-compose", version.ref = "anylocale" }
    app/build.gradle.kts
    dependencies {
        implementation(libs.anylocale)          // Views
        implementation(libs.anylocale.compose)  // Compose, instead of or in addition to the line above
    }
  6. Step 6: Initialise once, before any string is read

    Under Delivery, publish a distribution and create a key for the Android SDK. The storage provider keeps the download for offline starts and is keyed by version code, so an app update starts from its own compiled strings.

    MyApplication.kt
    class MyApplication : Application() {
        override fun onCreate() {
            super.onCreate()
    
            Anylocale.init {
                contentDelivery {
                    url = "https://anylocale.com/ota/v1/<distribution-key>"
                    storage = AnylocaleStorageProviderAndroid(this@MyApplication, BuildConfig.VERSION_CODE)
                }
                defaultLanguage("en")
            }
        }
    }
  7. Step 7: Read strings in Views and in Compose

    Wrap every Activity's base context and getString, getQuantityString and the android:text attributes resolve through the SDK; preload fetches in the background. In Compose, import stringResource and pluralStringResource from com.anylocale instead of the AndroidX ones.

    MainActivity.kt
    class MainActivity : ComponentActivity() {
        private val anylocale = Anylocale.instance
    
        override fun attachBaseContext(newBase: Context?) {
            super.attachBaseContext(AnylocaleContextWrapper.wrap(newBase))
        }
    
        override fun onStart() {
            super.onStart()
            anylocale.preload(this)
        }
    }
    Screen.kt
    import com.anylocale.pluralStringResource
    import com.anylocale.stringResource
    
    @Composable
    fun Screen(name: String, count: Int) {
        Text(stringResource(R.string.greeting, name))
        Text(pluralStringResource(R.plurals.items, count, count))
    }

Good to know

  • Placeholders such as %d and %s round-trip; positional forms like %1$s are kept as written. Use positional ones in any string with more than one, because formatted="false" does not survive a pull.
  • --platform is exclusive: a key ships unless it is tagged for a different platform, so only the exceptions need the iOS or Android tag. An empty or unknown value fails the run instead of shipping the other platform's strings.
  • A Gradle plugin that does the pull as a task is planned; until then the CLI in a Gradle exec task does the same.
  • Menus inflated from XML, preference screens and WebView content do not go through the wrapped context.

Questions

Do I need the SDK?
No. Without it, the text ships with each build through sync, and nothing changes in how the app reads strings. The SDK adds one thing: fetching the verified strings between releases, so a fix reaches users without a store update.Over-the-air delivery
Does the SDK work with Jetpack Compose?
Yes. sdk-compose adds a stringResource under com.anylocale that reads the downloaded text and recomposes when it changes. Views read through the wrapped context.
Is there a Gradle plugin?
Planned. Until then the CLI in a Gradle exec task does the same: pull each locale into its values folder before the build.
Which Android versions does the SDK support?
Android 5.0, API 21, and upward. Releases are served by JitPack from the repository's tags, so pinning a version is one line in the version catalog.

Start with the file your app already ships.

After checkout, import one strings file and read the first drafts within minutes.

Nothing to connect and nothing to install. The CLI is optional.