
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.
Kotlin Multiplatform TranslationTools runtime client.
Maven Central:
https://repo1.maven.org/maven2/io/mvdm/translationtools/translationtools-client-kmp/0.6.0/
Repository:
repositories {
mavenCentral()
}Dependency:
dependencies {
implementation("io.mvdm.translationtools:translationtools-client-kmp:0.6.0")
}Version catalog:
[libraries]
translationtools-client-kmp = { module = "io.mvdm.translationtools:translationtools-client-kmp", version = "0.6.0" }dependencies {
implementation(libs.translationtools.client.kmp)
}https://translations.mvdm.io
snapshot.json
Runtime model:
TranslationToolsClient
initialize() during startuprefreshIfStale() when returning to foregroundDefault refresh behavior:
initialize() only when no persisted or bundled snapshot existsandroidjvmiosX64iosArm64iosSimulatorArm64Use the factory:
import io.ktor.client.HttpClient
import io.mvdm.translationtools.client.JvmTranslationSnapshotStores
import io.mvdm.translationtools.client.TranslationTools
import io.mvdm.translationtools.client.TranslationToolsClientOptions
val httpClient = HttpClient()
val client = TranslationTools.createClient(
httpClient = httpClient,
options = TranslationToolsClientOptions(
apiKey = "your-project-api-key",
currentLocaleProvider = { "en" },
snapshotStore = JvmTranslationSnapshotStores.default(),
bundledSnapshot = ResBundledSnapshot.value,
),
)You provide:
HttpClient
Call initialize() once during startup:
client.initialize()Call refreshIfStale() when the app returns to foreground:
client.refreshIfStale()Guidance:
refresh() on every screenbackgroundRefreshEnabled = false to opt out of startup background refreshJVM:
val snapshotStore = JvmTranslationSnapshotStores.default()Android:
val snapshotStore = AndroidTranslationSnapshotStores.fromContext(context)Custom path:
import okio.FileSystem
import io.mvdm.translationtools.client.TranslationSnapshotStores
val snapshotStore = TranslationSnapshotStores.file(
"/some/path/translations.json",
FileSystem.SYSTEM,
)Persistence behavior:
NoOpTranslationSnapshotStore keeps runtime fully in-memoryCache-only read:
val title = client.getCached("home.title") ?: "Home"Fetch on miss:
val title = client.get("home.title", defaultValue = "Home")Observe updates:
client.observe("home.title")Read behavior:
getCached(...) = cache onlyget(...) = cache first, then single-item fetch on missTyped resource read:
import io.mvdm.translationtools.client.TranslationStringResource
val homeTitle = TranslationStringResource(
key = "home.title",
fallback = "Home",
)
val title = client.getCached(homeTitle)Typed resource behavior:
getCached(resource) = cached value, else resource fallback, else resource keyget(resource) = cache first, then single-item fetch on miss, using resource fallback as default valueobserve(resource) = reactive read with the same fallback chainGenerated resources:
import io.mvdm.translationtools.client.resources.Res
val title = client.getCached(Res.string.home_title)Additional artifact:
dependencies {
implementation("io.mvdm.translationtools:translationtools-client-compose:0.6.0")
}Compose usage:
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.material3.Text
import io.mvdm.translationtools.client.compose.LocalTranslationToolsClient
import io.mvdm.translationtools.client.compose.LocalTranslationToolsLocale
import io.mvdm.translationtools.client.compose.stringResource
import io.mvdm.translationtools.client.resources.Res
CompositionLocalProvider(
LocalTranslationToolsClient provides client,
LocalTranslationToolsLocale provides "en",
) {
val title = stringResource(Res.string.home_title)
}Full screen example:
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import io.mvdm.translationtools.client.TranslationToolsClient
import io.mvdm.translationtools.client.compose.LocalTranslationToolsClient
import io.mvdm.translationtools.client.compose.LocalTranslationToolsLocale
import io.mvdm.translationtools.client.compose.stringResource
import io.mvdm.translationtools.client.resources.Res
@Composable
fun HomeScreen(
client: TranslationToolsClient,
locale: String = "en",
) {
CompositionLocalProvider(
LocalTranslationToolsClient provides client,
LocalTranslationToolsLocale provides locale,
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
) {
Text(
text = stringResource(Res.string.home_title),
style = MaterialTheme.typography.headlineMedium,
)
Text(
text = stringResource(Res.string.checkout_title),
style = MaterialTheme.typography.bodyLarge,
)
}
}
}Compose behavior:
stringResource(...) reads from the typed runtime APILocalTranslationToolsLocale
Add translationtools.yaml at the repo root:
apiKey: your-project-api-key
locales:
- en
generated:
packageName: io.mvdm.translationtools.client.resources
objectName: Res
androidResources:
resourceDirectories:
- src/androidMain/res
keyOverrides:
action_save: action.saveOptional API key sources, highest priority first:
-Ptranslationtools.apiKey=...TRANSLATIONTOOLS_API_KEYapiKey in translationtools.yaml
Notes:
https://translations.mvdm.io
translationtools.yaml should include an apiKey entry, even if you override it locallyInit command:
./gradlew.bat initTranslationToolsMigrate command:
./gradlew.bat migrateTranslationsSync command:
./gradlew.bat pullTranslationsGeneration command:
./gradlew.bat generateTranslationResourcesWorkflow:
initTranslationTools creates a starter translationtools.yaml
pullTranslations refreshes root snapshot.json and regenerates Kotlin resourcesmigrateTranslations requires translationtools.yaml, imports full Android locale/value state, refreshes root snapshot.json, and regenerates Kotlin resourcessnapshot.json
build/generated/...
build and test regenerate Kotlin from the local snapshot onlygenerateTranslationResources
pullTranslations or migrateTranslations
Generated output includes:
Res.kt for typed translation keys/fallbacksResBundledSnapshot.kt for first-launch bundled bootstrap dataSelection order:
currentLocaleProvideren
If preferredLocales is set, snapshot bootstrap uses that list.
class App : Application(), DefaultLifecycleObserver {
lateinit var translations: TranslationToolsClient
override fun onCreate() {
super<Application>.onCreate()
val httpClient = HttpClient()
translations = TranslationTools.createClient(
httpClient = httpClient,
options = TranslationToolsClientOptions(
apiKey = BuildConfig.TRANSLATIONTOOLS_API_KEY,
currentLocaleProvider = {
resources.configuration.locales[0].toLanguageTag()
},
snapshotStore = AndroidTranslationSnapshotStores.fromContext(this),
),
)
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
}
suspend fun initializeTranslations() {
translations.initialize()
}
override fun onStart(owner: LifecycleOwner) {
owner.lifecycleScope.launch {
translations.refreshIfStale()
}
}
}Typed exceptions:
TranslationToolsExceptionTranslationToolsValidationExceptionTranslationToolsHttpExceptionTranslationToolsSerializationExceptionTranslationToolsNetworkExceptionExample:
try {
client.initialize()
}
catch (exception: TranslationToolsHttpException) {
// auth / permission / remote error
}
catch (exception: TranslationToolsNetworkException) {
// offline / dns / timeout / transport issue
}https://translations.mvdm.io
Authorization headerAccept-Encoding: gzip
This repo includes opencode.jsonc for project-local OpenCode defaults.
AGENTS.md
Keep opencode.jsonc and AGENTS.md in sync when updating contributor guidance.
Kotlin Multiplatform TranslationTools runtime client.
Maven Central:
https://repo1.maven.org/maven2/io/mvdm/translationtools/translationtools-client-kmp/0.6.0/
Repository:
repositories {
mavenCentral()
}Dependency:
dependencies {
implementation("io.mvdm.translationtools:translationtools-client-kmp:0.6.0")
}Version catalog:
[libraries]
translationtools-client-kmp = { module = "io.mvdm.translationtools:translationtools-client-kmp", version = "0.6.0" }dependencies {
implementation(libs.translationtools.client.kmp)
}https://translations.mvdm.io
snapshot.json
Runtime model:
TranslationToolsClient
initialize() during startuprefreshIfStale() when returning to foregroundDefault refresh behavior:
initialize() only when no persisted or bundled snapshot existsandroidjvmiosX64iosArm64iosSimulatorArm64Use the factory:
import io.ktor.client.HttpClient
import io.mvdm.translationtools.client.JvmTranslationSnapshotStores
import io.mvdm.translationtools.client.TranslationTools
import io.mvdm.translationtools.client.TranslationToolsClientOptions
val httpClient = HttpClient()
val client = TranslationTools.createClient(
httpClient = httpClient,
options = TranslationToolsClientOptions(
apiKey = "your-project-api-key",
currentLocaleProvider = { "en" },
snapshotStore = JvmTranslationSnapshotStores.default(),
bundledSnapshot = ResBundledSnapshot.value,
),
)You provide:
HttpClient
Call initialize() once during startup:
client.initialize()Call refreshIfStale() when the app returns to foreground:
client.refreshIfStale()Guidance:
refresh() on every screenbackgroundRefreshEnabled = false to opt out of startup background refreshJVM:
val snapshotStore = JvmTranslationSnapshotStores.default()Android:
val snapshotStore = AndroidTranslationSnapshotStores.fromContext(context)Custom path:
import okio.FileSystem
import io.mvdm.translationtools.client.TranslationSnapshotStores
val snapshotStore = TranslationSnapshotStores.file(
"/some/path/translations.json",
FileSystem.SYSTEM,
)Persistence behavior:
NoOpTranslationSnapshotStore keeps runtime fully in-memoryCache-only read:
val title = client.getCached("home.title") ?: "Home"Fetch on miss:
val title = client.get("home.title", defaultValue = "Home")Observe updates:
client.observe("home.title")Read behavior:
getCached(...) = cache onlyget(...) = cache first, then single-item fetch on missTyped resource read:
import io.mvdm.translationtools.client.TranslationStringResource
val homeTitle = TranslationStringResource(
key = "home.title",
fallback = "Home",
)
val title = client.getCached(homeTitle)Typed resource behavior:
getCached(resource) = cached value, else resource fallback, else resource keyget(resource) = cache first, then single-item fetch on miss, using resource fallback as default valueobserve(resource) = reactive read with the same fallback chainGenerated resources:
import io.mvdm.translationtools.client.resources.Res
val title = client.getCached(Res.string.home_title)Additional artifact:
dependencies {
implementation("io.mvdm.translationtools:translationtools-client-compose:0.6.0")
}Compose usage:
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.material3.Text
import io.mvdm.translationtools.client.compose.LocalTranslationToolsClient
import io.mvdm.translationtools.client.compose.LocalTranslationToolsLocale
import io.mvdm.translationtools.client.compose.stringResource
import io.mvdm.translationtools.client.resources.Res
CompositionLocalProvider(
LocalTranslationToolsClient provides client,
LocalTranslationToolsLocale provides "en",
) {
val title = stringResource(Res.string.home_title)
}Full screen example:
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import io.mvdm.translationtools.client.TranslationToolsClient
import io.mvdm.translationtools.client.compose.LocalTranslationToolsClient
import io.mvdm.translationtools.client.compose.LocalTranslationToolsLocale
import io.mvdm.translationtools.client.compose.stringResource
import io.mvdm.translationtools.client.resources.Res
@Composable
fun HomeScreen(
client: TranslationToolsClient,
locale: String = "en",
) {
CompositionLocalProvider(
LocalTranslationToolsClient provides client,
LocalTranslationToolsLocale provides locale,
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
) {
Text(
text = stringResource(Res.string.home_title),
style = MaterialTheme.typography.headlineMedium,
)
Text(
text = stringResource(Res.string.checkout_title),
style = MaterialTheme.typography.bodyLarge,
)
}
}
}Compose behavior:
stringResource(...) reads from the typed runtime APILocalTranslationToolsLocale
Add translationtools.yaml at the repo root:
apiKey: your-project-api-key
locales:
- en
generated:
packageName: io.mvdm.translationtools.client.resources
objectName: Res
androidResources:
resourceDirectories:
- src/androidMain/res
keyOverrides:
action_save: action.saveOptional API key sources, highest priority first:
-Ptranslationtools.apiKey=...TRANSLATIONTOOLS_API_KEYapiKey in translationtools.yaml
Notes:
https://translations.mvdm.io
translationtools.yaml should include an apiKey entry, even if you override it locallyInit command:
./gradlew.bat initTranslationToolsMigrate command:
./gradlew.bat migrateTranslationsSync command:
./gradlew.bat pullTranslationsGeneration command:
./gradlew.bat generateTranslationResourcesWorkflow:
initTranslationTools creates a starter translationtools.yaml
pullTranslations refreshes root snapshot.json and regenerates Kotlin resourcesmigrateTranslations requires translationtools.yaml, imports full Android locale/value state, refreshes root snapshot.json, and regenerates Kotlin resourcessnapshot.json
build/generated/...
build and test regenerate Kotlin from the local snapshot onlygenerateTranslationResources
pullTranslations or migrateTranslations
Generated output includes:
Res.kt for typed translation keys/fallbacksResBundledSnapshot.kt for first-launch bundled bootstrap dataSelection order:
currentLocaleProvideren
If preferredLocales is set, snapshot bootstrap uses that list.
class App : Application(), DefaultLifecycleObserver {
lateinit var translations: TranslationToolsClient
override fun onCreate() {
super<Application>.onCreate()
val httpClient = HttpClient()
translations = TranslationTools.createClient(
httpClient = httpClient,
options = TranslationToolsClientOptions(
apiKey = BuildConfig.TRANSLATIONTOOLS_API_KEY,
currentLocaleProvider = {
resources.configuration.locales[0].toLanguageTag()
},
snapshotStore = AndroidTranslationSnapshotStores.fromContext(this),
),
)
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
}
suspend fun initializeTranslations() {
translations.initialize()
}
override fun onStart(owner: LifecycleOwner) {
owner.lifecycleScope.launch {
translations.refreshIfStale()
}
}
}Typed exceptions:
TranslationToolsExceptionTranslationToolsValidationExceptionTranslationToolsHttpExceptionTranslationToolsSerializationExceptionTranslationToolsNetworkExceptionExample:
try {
client.initialize()
}
catch (exception: TranslationToolsHttpException) {
// auth / permission / remote error
}
catch (exception: TranslationToolsNetworkException) {
// offline / dns / timeout / transport issue
}https://translations.mvdm.io
Authorization headerAccept-Encoding: gzip
This repo includes opencode.jsonc for project-local OpenCode defaults.
AGENTS.md
Keep opencode.jsonc and AGENTS.md in sync when updating contributor guidance.