
Bootstraps translations from remote or bundled snapshot, persists local snapshots, serves cache-first reads with single-item fetch on miss, supports typed resources, Compose integration, background refresh.
translationtools-client-kmp is a Kotlin Multiplatform client for TranslationTools.
It keeps Android strings.xml as the source of truth, generates typed Translations.* accessors for shared code, bundles fallback translations into your app, and refreshes translations from TranslationTools at runtime.
Use it when you want all of this at once:
Current scope:
<string> resourcesTranslations.* and TranslationsBundledSnapshot
<plurals> and <string-array>
Maven Central:
https://repo1.maven.org/maven2/io/mvdm/translationtools/translationtools-client-kmp/2.0.0/
Repository:
repositories {
mavenCentral()
}Runtime client:
dependencies {
implementation("io.mvdm.translationtools:translationtools-client-kmp:2.0.0")
}Optional Compose helpers:
dependencies {
implementation("io.mvdm.translationtools:translationtools-client-compose:2.0.0")
}Version catalog:
[libraries]
translationtools-client-kmp = { module = "io.mvdm.translationtools:translationtools-client-kmp", version = "2.0.0" }
translationtools-client-compose = { module = "io.mvdm.translationtools:translationtools-client-compose", version = "2.0.0" }dependencies {
implementation(libs.translationtools.client.kmp)
}To generate Translations.*, your module also needs the io.mvdm.translationtools.plugin Gradle plugin. That plugin reads Android XML and generates the typed resource API used by this client.
The plugin is not yet published to a repository. Include it as a composite build from your
consumer project. Copy or clone the gradle/translationtools-plugin directory, then add it
to your settings.gradle.kts:
pluginManagement {
includeBuild("path/to/translationtools-plugin")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}Then apply it in your module:
plugins {
id("io.mvdm.translationtools.plugin")
}Compatibility: the plugin is compiled with Kotlin 2.1.20 and works with Gradle 8.x and 9.x. Consumer projects can use any Kotlin version from 1.9.25 through current 2.x releases.
src/androidMain/res/values*/**/*.xml.Translations.home_title.TranslationsBundledSnapshot, a bundled fallback snapshot from your local XML.TranslationToolsClient and calls initialize().Translations.*.plugins {
id("org.jetbrains.kotlin.multiplatform")
id("com.android.library")
id("io.mvdm.translationtools.plugin")
}The runtime dependency alone does not generate resources.
Example src/androidMain/res/values/strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="home_title">Home</string>
<string name="checkout_title">Checkout</string>
</resources>Example src/androidMain/res/values-nl/strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="home_title">Start</string>
<string name="checkout_title">Afrekenen</string>
</resources>apiKey: your-project-api-key
defaultLocale: en
locales:
- en
- nl
generated:
packageName: com.example.translations
androidResources:
resourceDirectories:
- src/androidMain/res
keyOverrides: {}API key lookup order (Gradle plugin):
-Ptranslationtools.apiKey=...
TRANSLATIONTOOLS_API_KEY
apiKey in translationtools.yaml
The runtime client also needs the API key to refresh translations at app startup. On
Android, a common approach is to expose it via BuildConfig:
// androidApp/build.gradle.kts
buildTypes {
debug {
buildConfigField("String", "TRANSLATION_TOOLS_API_KEY",
"\"${localProperties.getProperty("TRANSLATIONTOOLS_API_KEY") ?: System.getenv("TRANSLATIONTOOLS_API_KEY") ?: ""}\"")
}
}Then set the key in local.properties (not committed to version control):
TRANSLATIONTOOLS_API_KEY=your-project-api-keyPass it when creating the client:
TranslationToolsClientOptions(
apiKey = BuildConfig.TRANSLATION_TOOLS_API_KEY,
backgroundRefreshEnabled = BuildConfig.TRANSLATION_TOOLS_API_KEY.isNotBlank(),
// ...
)Without a valid API key, backgroundRefreshEnabled should be false and the app will
only use bundled fallback translations.
./gradlew.bat generateTranslationResourcesThis generates:
Translations.* for typed string accessTranslationsBundledSnapshot for bundled fallback dataThe plugin also wires generation into Kotlin compilation, so normal builds regenerate resources automatically.
Common shape:
import io.ktor.client.HttpClient
import io.mvdm.translationtools.client.TranslationTools
import io.mvdm.translationtools.client.TranslationToolsClientOptionsJVM example:
import com.example.translations.TranslationsBundledSnapshot
import io.ktor.client.HttpClient
import io.mvdm.translationtools.client.JvmTranslationSnapshotStores
import io.mvdm.translationtools.client.TranslationTools
import io.mvdm.translationtools.client.TranslationToolsClientOptions
val client = TranslationTools.createClient(
httpClient = HttpClient(),
options = TranslationToolsClientOptions(
apiKey = "your-project-api-key",
currentLocaleProvider = { "en" },
snapshotStore = JvmTranslationSnapshotStores.default(),
bundledSnapshot = TranslationsBundledSnapshot.value,
),
)Android example:
import android.content.Context
import com.example.translations.TranslationsBundledSnapshot
import io.ktor.client.HttpClient
import io.mvdm.translationtools.client.AndroidTranslationSnapshotStores
import io.mvdm.translationtools.client.TranslationTools
import io.mvdm.translationtools.client.TranslationToolsClientOptions
fun createTranslationsClient(context: Context) = TranslationTools.createClient(
httpClient = HttpClient(),
options = TranslationToolsClientOptions(
apiKey = "your-project-api-key",
currentLocaleProvider = {
context.resources.configuration.locales[0].toLanguageTag()
},
snapshotStore = AndroidTranslationSnapshotStores.fromContext(context),
bundledSnapshot = TranslationsBundledSnapshot.value,
),
)suspend fun startTranslations() {
client.initialize()
}initialize() does this:
import com.example.translations.Translations
val cachedTitle = client.getCached(Translations.home_title)
val title = client.get(Translations.home_title)
val titleUpdates = client.observe(Translations.home_title)Behavior:
getCached(...) returns cached value, otherwise XML fallback, otherwise the keyget(...) returns cached value first and fetches from TranslationTools on cache missobserve(...) exposes a Flow that updates when translations changeLocale resolution order:
locale argumentcurrentLocaleProviderenIf you use Compose, add translationtools-client-compose and provide the client through composition locals.
Compose artifact targets:
androidjvmiosX64iosArm64iosSimulatorArm64import androidx.compose.runtime.CompositionLocalProvider
import com.example.translations.Translations
import io.mvdm.translationtools.client.compose.LocalTranslationToolsClient
import io.mvdm.translationtools.client.compose.LocalTranslationToolsLocale
import io.mvdm.translationtools.client.compose.stringResource
CompositionLocalProvider(
LocalTranslationToolsClient provides client,
LocalTranslationToolsLocale provides "en",
) {
val title = stringResource(Translations.home_title)
}Available Gradle tasks:
./gradlew.bat initTranslationTools
Creates a starter translationtools.yaml../gradlew.bat generateTranslationResources
Generates Translations.* and TranslationsBundledSnapshot from local XML../gradlew.bat pushTranslations
Uploads local XML to TranslationTools../gradlew.bat pullTranslations
Downloads translations from TranslationTools, updates local XML, then regenerates Kotlin resources.Normal workflow:
src/androidMain/res/values*/**/*.xml.generateTranslationResources.Translations.* in shared or platform code.pushTranslations when local XML should become the remote state.pullTranslations when remote changes should be merged back into XML.If your app already uses Android strings.xml, migration is mostly mechanical.
src/androidMain/res/values*/strings.xml fileshome_title
Translations.*
TranslationToolsClient
strings.xml files under src/androidMain/res/values*/.io.mvdm.translationtools.plugin to the module that owns those files.translationtools.yaml../gradlew.bat generateTranslationResources.TranslationToolsClient.Translations.*../gradlew.bat pushTranslations once to upload your current XML to TranslationTools.Typical code migration:
// before
context.getString(R.string.home_title)
// after
client.get(Translations.home_title)Compose migration:
// before
androidx.compose.ui.res.stringResource(R.string.home_title)
// after
io.mvdm.translationtools.client.compose.stringResource(Translations.home_title)Shared code migration:
// before
"Home"
// after
client.get(Translations.home_title)<string> entries are generated<plurals> and <string-array> are skipped and need separate handlingbuild/generated/...; do not edit them by handRes.string.* to Translations.* (avoids clash with Compose's Res).ResBundledSnapshot to TranslationsBundledSnapshot.generated.objectName key in translationtools.yaml has been removed. The generated object is always named Translations. Remove objectName from your config.Res.string.foo with Translations.foo and update imports to <your.package>.Translations / <your.package>.TranslationsBundledSnapshot.For a complete production setup, make sure all of these are true:
translationtools-client-kmp is in your dependencies.io.mvdm.translationtools.plugin is applied to the module with Android XML resources.translationtools.yaml exists in the project root.src/androidMain/res/values/.TranslationToolsClient and calls initialize() at startup.Translations.*.pushTranslations and pullTranslations to sync local XML with TranslationTools.translationtools-client-kmp is a Kotlin Multiplatform client for TranslationTools.
It keeps Android strings.xml as the source of truth, generates typed Translations.* accessors for shared code, bundles fallback translations into your app, and refreshes translations from TranslationTools at runtime.
Use it when you want all of this at once:
Current scope:
<string> resourcesTranslations.* and TranslationsBundledSnapshot
<plurals> and <string-array>
Maven Central:
https://repo1.maven.org/maven2/io/mvdm/translationtools/translationtools-client-kmp/2.0.0/
Repository:
repositories {
mavenCentral()
}Runtime client:
dependencies {
implementation("io.mvdm.translationtools:translationtools-client-kmp:2.0.0")
}Optional Compose helpers:
dependencies {
implementation("io.mvdm.translationtools:translationtools-client-compose:2.0.0")
}Version catalog:
[libraries]
translationtools-client-kmp = { module = "io.mvdm.translationtools:translationtools-client-kmp", version = "2.0.0" }
translationtools-client-compose = { module = "io.mvdm.translationtools:translationtools-client-compose", version = "2.0.0" }dependencies {
implementation(libs.translationtools.client.kmp)
}To generate Translations.*, your module also needs the io.mvdm.translationtools.plugin Gradle plugin. That plugin reads Android XML and generates the typed resource API used by this client.
The plugin is not yet published to a repository. Include it as a composite build from your
consumer project. Copy or clone the gradle/translationtools-plugin directory, then add it
to your settings.gradle.kts:
pluginManagement {
includeBuild("path/to/translationtools-plugin")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}Then apply it in your module:
plugins {
id("io.mvdm.translationtools.plugin")
}Compatibility: the plugin is compiled with Kotlin 2.1.20 and works with Gradle 8.x and 9.x. Consumer projects can use any Kotlin version from 1.9.25 through current 2.x releases.
src/androidMain/res/values*/**/*.xml.Translations.home_title.TranslationsBundledSnapshot, a bundled fallback snapshot from your local XML.TranslationToolsClient and calls initialize().Translations.*.plugins {
id("org.jetbrains.kotlin.multiplatform")
id("com.android.library")
id("io.mvdm.translationtools.plugin")
}The runtime dependency alone does not generate resources.
Example src/androidMain/res/values/strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="home_title">Home</string>
<string name="checkout_title">Checkout</string>
</resources>Example src/androidMain/res/values-nl/strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="home_title">Start</string>
<string name="checkout_title">Afrekenen</string>
</resources>apiKey: your-project-api-key
defaultLocale: en
locales:
- en
- nl
generated:
packageName: com.example.translations
androidResources:
resourceDirectories:
- src/androidMain/res
keyOverrides: {}API key lookup order (Gradle plugin):
-Ptranslationtools.apiKey=...
TRANSLATIONTOOLS_API_KEY
apiKey in translationtools.yaml
The runtime client also needs the API key to refresh translations at app startup. On
Android, a common approach is to expose it via BuildConfig:
// androidApp/build.gradle.kts
buildTypes {
debug {
buildConfigField("String", "TRANSLATION_TOOLS_API_KEY",
"\"${localProperties.getProperty("TRANSLATIONTOOLS_API_KEY") ?: System.getenv("TRANSLATIONTOOLS_API_KEY") ?: ""}\"")
}
}Then set the key in local.properties (not committed to version control):
TRANSLATIONTOOLS_API_KEY=your-project-api-keyPass it when creating the client:
TranslationToolsClientOptions(
apiKey = BuildConfig.TRANSLATION_TOOLS_API_KEY,
backgroundRefreshEnabled = BuildConfig.TRANSLATION_TOOLS_API_KEY.isNotBlank(),
// ...
)Without a valid API key, backgroundRefreshEnabled should be false and the app will
only use bundled fallback translations.
./gradlew.bat generateTranslationResourcesThis generates:
Translations.* for typed string accessTranslationsBundledSnapshot for bundled fallback dataThe plugin also wires generation into Kotlin compilation, so normal builds regenerate resources automatically.
Common shape:
import io.ktor.client.HttpClient
import io.mvdm.translationtools.client.TranslationTools
import io.mvdm.translationtools.client.TranslationToolsClientOptionsJVM example:
import com.example.translations.TranslationsBundledSnapshot
import io.ktor.client.HttpClient
import io.mvdm.translationtools.client.JvmTranslationSnapshotStores
import io.mvdm.translationtools.client.TranslationTools
import io.mvdm.translationtools.client.TranslationToolsClientOptions
val client = TranslationTools.createClient(
httpClient = HttpClient(),
options = TranslationToolsClientOptions(
apiKey = "your-project-api-key",
currentLocaleProvider = { "en" },
snapshotStore = JvmTranslationSnapshotStores.default(),
bundledSnapshot = TranslationsBundledSnapshot.value,
),
)Android example:
import android.content.Context
import com.example.translations.TranslationsBundledSnapshot
import io.ktor.client.HttpClient
import io.mvdm.translationtools.client.AndroidTranslationSnapshotStores
import io.mvdm.translationtools.client.TranslationTools
import io.mvdm.translationtools.client.TranslationToolsClientOptions
fun createTranslationsClient(context: Context) = TranslationTools.createClient(
httpClient = HttpClient(),
options = TranslationToolsClientOptions(
apiKey = "your-project-api-key",
currentLocaleProvider = {
context.resources.configuration.locales[0].toLanguageTag()
},
snapshotStore = AndroidTranslationSnapshotStores.fromContext(context),
bundledSnapshot = TranslationsBundledSnapshot.value,
),
)suspend fun startTranslations() {
client.initialize()
}initialize() does this:
import com.example.translations.Translations
val cachedTitle = client.getCached(Translations.home_title)
val title = client.get(Translations.home_title)
val titleUpdates = client.observe(Translations.home_title)Behavior:
getCached(...) returns cached value, otherwise XML fallback, otherwise the keyget(...) returns cached value first and fetches from TranslationTools on cache missobserve(...) exposes a Flow that updates when translations changeLocale resolution order:
locale argumentcurrentLocaleProviderenIf you use Compose, add translationtools-client-compose and provide the client through composition locals.
Compose artifact targets:
androidjvmiosX64iosArm64iosSimulatorArm64import androidx.compose.runtime.CompositionLocalProvider
import com.example.translations.Translations
import io.mvdm.translationtools.client.compose.LocalTranslationToolsClient
import io.mvdm.translationtools.client.compose.LocalTranslationToolsLocale
import io.mvdm.translationtools.client.compose.stringResource
CompositionLocalProvider(
LocalTranslationToolsClient provides client,
LocalTranslationToolsLocale provides "en",
) {
val title = stringResource(Translations.home_title)
}Available Gradle tasks:
./gradlew.bat initTranslationTools
Creates a starter translationtools.yaml../gradlew.bat generateTranslationResources
Generates Translations.* and TranslationsBundledSnapshot from local XML../gradlew.bat pushTranslations
Uploads local XML to TranslationTools../gradlew.bat pullTranslations
Downloads translations from TranslationTools, updates local XML, then regenerates Kotlin resources.Normal workflow:
src/androidMain/res/values*/**/*.xml.generateTranslationResources.Translations.* in shared or platform code.pushTranslations when local XML should become the remote state.pullTranslations when remote changes should be merged back into XML.If your app already uses Android strings.xml, migration is mostly mechanical.
src/androidMain/res/values*/strings.xml fileshome_title
Translations.*
TranslationToolsClient
strings.xml files under src/androidMain/res/values*/.io.mvdm.translationtools.plugin to the module that owns those files.translationtools.yaml../gradlew.bat generateTranslationResources.TranslationToolsClient.Translations.*../gradlew.bat pushTranslations once to upload your current XML to TranslationTools.Typical code migration:
// before
context.getString(R.string.home_title)
// after
client.get(Translations.home_title)Compose migration:
// before
androidx.compose.ui.res.stringResource(R.string.home_title)
// after
io.mvdm.translationtools.client.compose.stringResource(Translations.home_title)Shared code migration:
// before
"Home"
// after
client.get(Translations.home_title)<string> entries are generated<plurals> and <string-array> are skipped and need separate handlingbuild/generated/...; do not edit them by handRes.string.* to Translations.* (avoids clash with Compose's Res).ResBundledSnapshot to TranslationsBundledSnapshot.generated.objectName key in translationtools.yaml has been removed. The generated object is always named Translations. Remove objectName from your config.Res.string.foo with Translations.foo and update imports to <your.package>.Translations / <your.package>.TranslationsBundledSnapshot.For a complete production setup, make sure all of these are true:
translationtools-client-kmp is in your dependencies.io.mvdm.translationtools.plugin is applied to the module with Android XML resources.translationtools.yaml exists in the project root.src/androidMain/res/values/.TranslationToolsClient and calls initialize() at startup.Translations.*.pushTranslations and pullTranslations to sync local XML with TranslationTools.