
Type-safe internationalization: generates typed translation keys and a locale-aware engine from JSON, YAML, Properties or code files; offers runtime lookups, composition-local helpers and hot debug loading.
English README. Chinese documentation is available at docs/README.zh-CN.md.
i18nZeal brings type-safe internationalization to Kotlin Multiplatform apps with first-class Compose Multiplatform support.
Generate typed translation keys and a locale-aware translation engine from JSON, YAML, Properties, or Kotlin locale files. Then use the lightweight runtime to resolve translations and switch languages consistently across Android, iOS, JVM, JS, and Wasm.
I18nEngine implementations at build timeCompositionLocal and runtime locale statejson, yaml, properties, or kt filesIt has two main parts:
i18n-runtime: runtime APIs for Locale, language state, CompositionLocal, and translation lookupi18n-gradle-plugin: a Gradle plugin that generates Kotlin constants and an I18nEngine implementation from locale filesi18n-runtime: publishable KMP runtime libraryi18n-gradle-plugin: publishable Gradle pluginshared: sample shared module showing plugin and runtime usageandroidApp, desktopApp, webApp, iosApp: sample app targets, including Web supportplugins {
// ...
id("io.github.xczcdjx.i18nzeal")
}
i18nZeal {
sourceLocales = listOf("en", "zh")
packageName = "com.example.app.i18n" // generated package name
// inputDir = "src/commonMain/i18n" // default input directory
// fileType = I18nFileType.JSON // default file type
}Default extension values:
fileType = I18nFileType.JSONsourceLocales = emptyList() (must be configured)inputDir = "src/commonMain/i18n"outputDir = "generated/i18nzeal/commonMain/kotlin"packageName = null (must be configured)objectName = "I18nZeal"kotlin {
sourceSets {
commonMain {
kotlin.srcDir(layout.buildDirectory.dir("generated/i18nzeal/commonMain/kotlin"))
}
}
}tasks.withType<KotlinCompilationTask<*>>().configureEach {
dependsOn(tasks.named("generateI18nKt"))
}If you want to switch to .properties input:
i18nZeal {
sourceLocales = listOf("en", "zh")
inputDir = "src/commonMain/i18n-properties"
fileType = I18nFileType.PROPERTIES
packageName = "com.example.app.i18n"
}Supported fileType values:
I18nFileType.JSONI18nFileType.YAMLI18nFileType.PROPERTIESI18nFileType.KTIf you use the published plugin:
plugins {
id("io.github.xczcdjx.i18nzeal") version "$i18nZealVersion"
}Add the runtime dependency:
commonMain.dependencies {
implementation("io.github.xczcdjx:i18n-runtime:$i18nZealVersion")
}If your project also uses Compose resources fonts or other shared resources, keep:
commonMain.dependencies {
implementation(compose.components.resources)
}The plugin generates Kotlin code like this:
package com.example.app.i18n
import com.djx.i18n.runtime.export.Locale
import com.djx.i18n.runtime.interfaces.I18nEngine
val Lang_En = Locale("en")
val Lang_Zh = Locale("zh")
object I18nKeys {
const val app_name = "app.name"
const val count = "count"
}
object I18nZeal : I18nEngine {
override fun get(
key: String?,
locale: Locale,
fallback: String,
vararg args: Any?,
): String {
TODO()
}
}Placeholders use zero-based indexes:
{
"count": "Count,{0}"
}Usage:
tr(I18nKeys.count, count)Named placeholders are also supported:
{
"welcome": "Hello, {name}! You have {count} messages."
}Pass named values as pairs or as a map:
tr(I18nKeys.welcome, "name" to userName, "count" to count)
tr(I18nKeys.welcome, mapOf("name" to userName, "count" to count))
tr(I18nKeys.welcome, userName, count) // Fills named placeholders in source orderInitialize the runtime:
I18nRuntime.init(I18nZeal)Provide the current language in Compose:
CompositionLocalProvider(
AppLocalLangProvider provides AppLangState.current.value
) {
MainContent()
}Text(tr(I18nKeys.app_name))
Text(tr(I18nKeys.count, count))
Text(I18nKeys.app_name.tri18n())
Text(I18nKeys.count.tri18n(count))val title = trn(I18nKeys.app_name)
val countText = I18nKeys.count.trnI18n(count)Notes:
tr(...) and tri18n(...) are @Composable helpers and read AppLocalLangProvider.current
trn(...) and trnI18n(...) are non-Compose helpers and read AppLangState.current.value
AppLangState.change(Lang_En)
AppLangState.change(Lang_Zh)By default, i18nZeal generates Kotlin code from locale files and compiles it into the app. This keeps lookups type-safe and stable, but editing locale files usually requires recompilation.
On Android debug builds, you can load translations from runtime files and fallback to the generated I18nZeal engine:
import com.djx.i18n.runtime.I18nRuntime
import com.djx.i18n.runtime.android.AndroidDebugI18nLoader
import com.example.app.i18n.I18nZeal
val engine = if (BuildConfig.DEBUG) {
AndroidDebugI18nLoader.fromFiles(
directory = File(filesDir, "i18n"),
locales = listOf("en", "zh"),
fallbackEngine = I18nZeal,
)
} else {
I18nZeal
}
I18nRuntime.init(engine)Runtime files can be placed at:
/data/data/your.package.name/files/i18n/en.json
/data/data/your.package.name/files/i18n/zh.json
You can also load from assets:
AndroidDebugI18nLoader.fromAssets(
context = this,
locales = listOf("en", "zh"),
assetDir = "i18n",
fallbackEngine = I18nZeal,
)Note that assets are still packaged into the APK, so editing them usually still requires reinstalling or applying changes. Use fromFiles(...) when you want true development-time runtime reloads.
The plugin supports:
jsonyaml / yml
propertiesktUse one file type per input directory and set fileType explicitly.
Directory:
shared/src/commonMain/i18n/en.json
shared/src/commonMain/i18n/zh.json
Content:
{
"app": {
"name": "i18nZeal"
},
"lang": {
"current": "Current Language",
"system": "System",
"en": "English",
"zh": "Chinese"
},
"count": "Count,{0}"
}Directory:
shared/src/commonMain/i18n-yaml/en.yaml
shared/src/commonMain/i18n-yaml/zh.yaml
Content:
app:
name: i18nZeal
lang:
current: Current Language
system: System
en: English
zh: Chinese
count: "Count,{0}"Directory:
shared/src/commonMain/i18n-properties/en.properties
shared/src/commonMain/i18n-properties/zh.properties
Content:
app.name=i18nZeal
lang.current=Current Language
lang.system=System
lang.en=English
lang.zh=Chinese
count=Count,{0}Notes:
.properties files are read as UTF-8, so you can write Chinese directly without converting to \uXXXX
Directory:
shared/src/commonMain/i18n-kt/en.kt
shared/src/commonMain/i18n-kt/zh.kt
Content:
object I18nZeal_en {
val map = mapOf(
"app.name" to "i18nZeal",
"lang.current" to "Current Language",
"lang.system" to "System",
"lang.en" to "English",
"lang.zh" to "Chinese",
"count" to "Count,{0}",
)
}Nested JSON/YAML keys are flattened to dot notation:
app.name becomes I18nKeys.app_name
English README. Chinese documentation is available at docs/README.zh-CN.md.
i18nZeal brings type-safe internationalization to Kotlin Multiplatform apps with first-class Compose Multiplatform support.
Generate typed translation keys and a locale-aware translation engine from JSON, YAML, Properties, or Kotlin locale files. Then use the lightweight runtime to resolve translations and switch languages consistently across Android, iOS, JVM, JS, and Wasm.
I18nEngine implementations at build timeCompositionLocal and runtime locale statejson, yaml, properties, or kt filesIt has two main parts:
i18n-runtime: runtime APIs for Locale, language state, CompositionLocal, and translation lookupi18n-gradle-plugin: a Gradle plugin that generates Kotlin constants and an I18nEngine implementation from locale filesi18n-runtime: publishable KMP runtime libraryi18n-gradle-plugin: publishable Gradle pluginshared: sample shared module showing plugin and runtime usageandroidApp, desktopApp, webApp, iosApp: sample app targets, including Web supportplugins {
// ...
id("io.github.xczcdjx.i18nzeal")
}
i18nZeal {
sourceLocales = listOf("en", "zh")
packageName = "com.example.app.i18n" // generated package name
// inputDir = "src/commonMain/i18n" // default input directory
// fileType = I18nFileType.JSON // default file type
}Default extension values:
fileType = I18nFileType.JSONsourceLocales = emptyList() (must be configured)inputDir = "src/commonMain/i18n"outputDir = "generated/i18nzeal/commonMain/kotlin"packageName = null (must be configured)objectName = "I18nZeal"kotlin {
sourceSets {
commonMain {
kotlin.srcDir(layout.buildDirectory.dir("generated/i18nzeal/commonMain/kotlin"))
}
}
}tasks.withType<KotlinCompilationTask<*>>().configureEach {
dependsOn(tasks.named("generateI18nKt"))
}If you want to switch to .properties input:
i18nZeal {
sourceLocales = listOf("en", "zh")
inputDir = "src/commonMain/i18n-properties"
fileType = I18nFileType.PROPERTIES
packageName = "com.example.app.i18n"
}Supported fileType values:
I18nFileType.JSONI18nFileType.YAMLI18nFileType.PROPERTIESI18nFileType.KTIf you use the published plugin:
plugins {
id("io.github.xczcdjx.i18nzeal") version "$i18nZealVersion"
}Add the runtime dependency:
commonMain.dependencies {
implementation("io.github.xczcdjx:i18n-runtime:$i18nZealVersion")
}If your project also uses Compose resources fonts or other shared resources, keep:
commonMain.dependencies {
implementation(compose.components.resources)
}The plugin generates Kotlin code like this:
package com.example.app.i18n
import com.djx.i18n.runtime.export.Locale
import com.djx.i18n.runtime.interfaces.I18nEngine
val Lang_En = Locale("en")
val Lang_Zh = Locale("zh")
object I18nKeys {
const val app_name = "app.name"
const val count = "count"
}
object I18nZeal : I18nEngine {
override fun get(
key: String?,
locale: Locale,
fallback: String,
vararg args: Any?,
): String {
TODO()
}
}Placeholders use zero-based indexes:
{
"count": "Count,{0}"
}Usage:
tr(I18nKeys.count, count)Named placeholders are also supported:
{
"welcome": "Hello, {name}! You have {count} messages."
}Pass named values as pairs or as a map:
tr(I18nKeys.welcome, "name" to userName, "count" to count)
tr(I18nKeys.welcome, mapOf("name" to userName, "count" to count))
tr(I18nKeys.welcome, userName, count) // Fills named placeholders in source orderInitialize the runtime:
I18nRuntime.init(I18nZeal)Provide the current language in Compose:
CompositionLocalProvider(
AppLocalLangProvider provides AppLangState.current.value
) {
MainContent()
}Text(tr(I18nKeys.app_name))
Text(tr(I18nKeys.count, count))
Text(I18nKeys.app_name.tri18n())
Text(I18nKeys.count.tri18n(count))val title = trn(I18nKeys.app_name)
val countText = I18nKeys.count.trnI18n(count)Notes:
tr(...) and tri18n(...) are @Composable helpers and read AppLocalLangProvider.current
trn(...) and trnI18n(...) are non-Compose helpers and read AppLangState.current.value
AppLangState.change(Lang_En)
AppLangState.change(Lang_Zh)By default, i18nZeal generates Kotlin code from locale files and compiles it into the app. This keeps lookups type-safe and stable, but editing locale files usually requires recompilation.
On Android debug builds, you can load translations from runtime files and fallback to the generated I18nZeal engine:
import com.djx.i18n.runtime.I18nRuntime
import com.djx.i18n.runtime.android.AndroidDebugI18nLoader
import com.example.app.i18n.I18nZeal
val engine = if (BuildConfig.DEBUG) {
AndroidDebugI18nLoader.fromFiles(
directory = File(filesDir, "i18n"),
locales = listOf("en", "zh"),
fallbackEngine = I18nZeal,
)
} else {
I18nZeal
}
I18nRuntime.init(engine)Runtime files can be placed at:
/data/data/your.package.name/files/i18n/en.json
/data/data/your.package.name/files/i18n/zh.json
You can also load from assets:
AndroidDebugI18nLoader.fromAssets(
context = this,
locales = listOf("en", "zh"),
assetDir = "i18n",
fallbackEngine = I18nZeal,
)Note that assets are still packaged into the APK, so editing them usually still requires reinstalling or applying changes. Use fromFiles(...) when you want true development-time runtime reloads.
The plugin supports:
jsonyaml / yml
propertiesktUse one file type per input directory and set fileType explicitly.
Directory:
shared/src/commonMain/i18n/en.json
shared/src/commonMain/i18n/zh.json
Content:
{
"app": {
"name": "i18nZeal"
},
"lang": {
"current": "Current Language",
"system": "System",
"en": "English",
"zh": "Chinese"
},
"count": "Count,{0}"
}Directory:
shared/src/commonMain/i18n-yaml/en.yaml
shared/src/commonMain/i18n-yaml/zh.yaml
Content:
app:
name: i18nZeal
lang:
current: Current Language
system: System
en: English
zh: Chinese
count: "Count,{0}"Directory:
shared/src/commonMain/i18n-properties/en.properties
shared/src/commonMain/i18n-properties/zh.properties
Content:
app.name=i18nZeal
lang.current=Current Language
lang.system=System
lang.en=English
lang.zh=Chinese
count=Count,{0}Notes:
.properties files are read as UTF-8, so you can write Chinese directly without converting to \uXXXX
Directory:
shared/src/commonMain/i18n-kt/en.kt
shared/src/commonMain/i18n-kt/zh.kt
Content:
object I18nZeal_en {
val map = mapOf(
"app.name" to "i18nZeal",
"lang.current" to "Current Language",
"lang.system" to "System",
"lang.en" to "English",
"lang.zh" to "Chinese",
"count" to "Count,{0}",
)
}Nested JSON/YAML keys are flattened to dot notation:
app.name becomes I18nKeys.app_name