Skip to main content

Kotlin Multiplatform i18n:跨平台共享本地化

在共享 Kotlin 代码中只编写一次翻译,即可将其用于 Android、iOS 和 Web,并获得正确的区域设置回退链。

1

为 KMP i18n 配置 Gradle

将 i18n 依赖项添加到共享模块的 commonMain 源集。您可以选择 moko-resources 来处理基于 XML 的字符串,选择 Lyricist 来处理类型安全的 Compose 字符串,也可同时使用两者。kmp-localechain 可在任一方案上添加智能区域设置回退。

build.gradle.kts
// build.gradle.kts (shared module)
plugins {
    kotlin("multiplatform")
    id("com.android.library")
}

kotlin {
    androidTarget()
    iosArm64()
    iosSimulatorArm64()
    js(IR) { browser(); nodejs() }

    sourceSets {
        val commonMain by getting {
            dependencies {
                // Option A: moko-resources (code-gen from XML)
                implementation("dev.icerock.moko:resources:0.24.4")

                // Option B: Lyricist (type-safe Compose strings)
                implementation("cafe.adriel.lyricist:lyricist:1.7.0")

                // Locale fallback chains (works with any library)
                implementation("com.i18nagent:locale-chain-kmp:0.1.0")
            }
        }
    }
}
这三个库均发布到 Maven Central。将它们添加到 commonMain 依赖项,使其可用于每个目标(Android、iOS、JS)。
2

定义共享字符串类型

在 commonMain 中创建保存所有可翻译字符串的 Kotlin 数据类。这是唯一事实来源,每个平台都读取相同的类型安全定义。无需重复字符串文件,也不会出现平台间偏差。

commonMain/.../Strings.kt
// commonMain/kotlin/com/myapp/Strings.kt
package com.myapp.i18n

/**
 * Shared string definitions — the single source of truth.
 * Each platform reads from the same keys.
 */
data class AppStrings(
    val greeting: String,
    val farewell: String,
    val itemCount: (count: Int) -> String,
    val nav: NavStrings,
)

data class NavStrings(
    val home: String,
    val settings: String,
    val about: String,
)

// English defaults
val EnStrings = AppStrings(
    greeting = "Hello!",
    farewell = "Goodbye!",
    itemCount = { count ->
        if (count == 1) "$count item" else "$count items"
    },
    nav = NavStrings(
        home = "Home",
        settings = "Settings",
        about = "About",
    ),
)

// Japanese
val JaStrings = AppStrings(
    greeting = "こんにちは!",
    farewell = "さようなら!",
    itemCount = { count -> "${count}個のアイテム" },
    nav = NavStrings(
        home = "ホーム",
        settings = "設定",
        about = "概要",
    ),
)

// Add more locales following the same pattern: DeStrings, EsStrings, etc.
使用 lambda 属性处理复数,而不是使用独立的单数/复数键。lambda 接收数量并返回正确形式。这样可将复数逻辑保留在 Kotlin 中,供编译器检查。
3

接入 Android

在 androidMain 中实现 expect/actual 模式,通过 java.util.Locale 读取设备区域设置。Android 可将共享 Kotlin 字符串用于业务逻辑,同时继续对通知和 widget 等系统 UI 元素使用标准 values/strings.xml。

androidMain/.../StringProvider.kt
// androidMain/kotlin/com/myapp/StringProvider.kt
package com.myapp.i18n

import java.util.Locale

actual fun currentLocale(): String =
    Locale.getDefault().toLanguageTag()  // e.g. "pt-BR"

// Android can also use standard resources/values-*/strings.xml
// alongside the shared Kotlin definitions.
// Use shared strings for business logic, XML for system UI.

// In your Activity or Compose screen:
@Composable
fun GreetingScreen() {
    val strings = rememberStrings()  // resolves via locale
    Text(text = strings.greeting)
    Text(text = strings.itemCount(cartSize))
}
Locale.getDefault().toLanguageTag() 返回类似 "pt-BR" 的 IETF 标签,但某些 Android 版本的旧 API 会返回 "pt-rBR"。请始终使用 toLanguageTag()(API 21 及更高版本),确保结果一致。
4

接入 iOS

在 iosMain 中使用 Foundation 的 NSLocale 实现 currentLocale()。KMP 共享框架会将字符串定义导出到 Swift,因此 SwiftUI 视图可通过生成的 Kotlin 框架直接调用它们。

iosMain/.../StringProvider.kt
// iosMain/kotlin/com/myapp/StringProvider.kt
package com.myapp.i18n

import platform.Foundation.NSLocale
import platform.Foundation.currentLocale
import platform.Foundation.languageCode
import platform.Foundation.countryCode

actual fun currentLocale(): String {
    val locale = NSLocale.currentLocale
    val lang = locale.languageCode
    val country = locale.countryCode
    return if (country != null) "$lang-$country" else lang
}

// In SwiftUI (via KMP exported framework):
// let strings = StringProviderKt.stringsFor(locale: "ja")
// Text(strings.greeting)
将 KMP 框架导出到 Xcode 时,请确保导出字符串提供程序模块。在 Podspec 或 XCFramework 配置中包含 i18n 软件包,使 Swift 代码可以导入它。
5

接入 JS/浏览器

在 jsMain 中从 window.navigator.language 读取浏览器区域设置。这同时适用于 Kotlin/JS Web 应用和 Compose for Web 目标。相同的共享字符串可在浏览器中呈现,无需重复。

jsMain/.../StringProvider.kt
// jsMain/kotlin/com/myapp/StringProvider.kt
package com.myapp.i18n

import kotlinx.browser.window

actual fun currentLocale(): String =
    window.navigator.language  // e.g. "en-US", "pt-BR"

// In a Kotlin/JS or Compose for Web app:
fun main() {
    val locale = currentLocale()
    val strings = stringsFor(locale)
    document.getElementById("greeting")?.textContent = strings.greeting
}
对于服务器端 Kotlin/JS(Node.js),请从 Accept-Language 标头或配置变量读取区域设置,而不是使用 window.navigator.language。
6

在 Compose Multiplatform 中使用 Lyricist

Lyricist 提供 Compose 原生的 i18n 方案。使用 @LyricistStrings 注解字符串对象,Lyricist 就会生成 CompositionLocal 提供程序。更改 languageTag 即可在运行时切换语言,UI 会自动重新组合。

Lyricist integration
// Using Lyricist for Compose Multiplatform
// build.gradle.kts
plugins {
    id("cafe.adriel.lyricist") version "1.7.0"
}

// Define strings with @LyricistStrings annotation
@LyricistStrings(languageTag = Locales.EN, default = true)
val EnStrings = Strings(
    greeting = "Hello!",
    farewell = "Goodbye!",
    itemCount = { count ->
        if (count == 1) "$count item" else "$count items"
    },
)

@LyricistStrings(languageTag = Locales.JA)
val JaStrings = Strings(
    greeting = "こんにちは!",
    farewell = "さようなら!",
    itemCount = { count -> "${count}個のアイテム" },
)

// In your Compose UI
@Composable
fun App() {
    // Lyricist provides the strings via CompositionLocal
    ProvideStrings {
        val lyricist = LocalStrings.current
        Text(text = lyricist.greeting)
    }
}

// Switch language at runtime
val lyricist = rememberLyricist(
    defaultLanguageTag = Locales.EN,
)
lyricist.languageTag = Locales.JA  // UI recomposes automatically
Lyricist 支持字符串插值、通过 lambda 处理复数以及嵌套字符串组。它适用于 Android、iOS(通过 Compose for iOS)、桌面和 Web 目标。
7

使用 moko-resources 管理 XML 字符串

moko-resources 使用 Android 风格的 XML 字符串文件作为事实来源,并生成类型安全的访问器。在 commonMain/resources/MR/base/ 中定义字符串(英语),再为每种语言添加区域设置文件夹。生成的 MR 对象提供经编译时检查的访问方式。

moko-resources setup
// Using moko-resources for XML-based string management
// build.gradle.kts
plugins {
    id("dev.icerock.mobile.multiplatform-resources") version "0.24.4"
}

multiplatformResources {
    resourcesPackage.set("com.myapp")
}

// commonMain/resources/MR/base/strings.xml (English - default)
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="greeting">Hello!</string>
    <string name="farewell">Goodbye!</string>
    <plurals name="item_count">
        <item quantity="one">%d item</item>
        <item quantity="other">%d items</item>
    </plurals>
</resources>

// commonMain/resources/MR/ja/strings.xml (Japanese)
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="greeting">こんにちは!</string>
    <string name="farewell">さようなら!</string>
    <plurals name="item_count">
        <item quantity="other">%d個のアイテム</item>
    </plurals>
</resources>

// Usage in shared Kotlin code
val greeting = MR.strings.greeting.desc()
val items = MR.plurals.item_count.format(count)
moko-resources 需要 Gradle 插件来生成代码。如果看到 'Unresolved reference: MR',请先运行 Gradle 同步。代码生成步骤必须完成,IDE 才能识别 MR 访问器。
8

使用 kmp-localechain 实现智能区域设置回退

KMP i18n 库缺少可配置的回退链。pt-BR 翻译缺失时,它们会完全跳过 pt-PT 并显示英语。kmp-localechain 通过独立的消息合并实用工具解决此问题。它接收每个区域设置的扁平 Map&lt;String, String&gt; 消息,并返回已应用回退链优先级的合并映射。

LocaleChain usage
// Using kmp-localechain for smart locale fallback
import com.i18nagent.localechain.LocaleChain

// 1. Configure once at app startup
LocaleChain.configure()  // uses built-in fallback chains

// 2. Load your messages as flat maps
val messages = mapOf(
    "en" to mapOf("greeting" to "Hello", "farewell" to "Goodbye"),
    "pt" to mapOf("greeting" to "Olá", "farewell" to "Adeus"),
    "pt-PT" to mapOf("greeting" to "Olá (PT)"),
    "pt-BR" to mapOf("greeting" to "Oi"),
)

// 3. Resolve with chain priority
val resolved = LocaleChain.resolve("pt-BR", messages)
// "greeting" -> "Oi"       (from pt-BR, most specific)
// "farewell" -> "Adeus"    (from pt, next in chain)

// Without LocaleChain, pt-BR users would see English "Goodbye"
// because pt-BR has no "farewell" key.
Custom configuration
// Custom fallback configuration
LocaleChain.configure(
    defaultLocale = "en",
    overrides = mapOf(
        "es-MX" to listOf("es-419", "es"),
        "fr-CA" to listOf("fr"),
    )
)

// Inspect any chain
LocaleChain.chainFor("pt-BR")
// Returns: ["pt-BR", "pt-PT", "pt", "en"]

// Async resolve (lazy loading from network/disk)
val resolved = LocaleChain.resolve("pt-BR") { localeTag ->
    api.fetchMessages(localeTag)  // returns Map<String, String>?
}
kmp-localechain 适用于扁平 Map&lt;String, String&gt; 映射。如果消息是嵌套结构,请先将其扁平化,再传给 resolve()。该库不支持嵌套结构的深度合并。
9

自动翻译

完成 KMP i18n 设置后,使用 AI 自动翻译共享字符串文件——无论是 Kotlin 数据类、XML 资源还是 JSON,均可直接从 IDE 或 CI/CD 管线翻译。

Terminal
# Translate your shared string files with i18n Agent
# Works with JSON, XML (moko-resources), or any i18n format

# From your IDE (Claude Code, Cursor, VS Code):
> Translate commonMain/resources/MR/base/strings.xml to Japanese, German, and Spanish

✓ MR/ja/strings.xml created (1.2s)
✓ MR/de/strings.xml created (1.1s)
✓ MR/es/strings.xml created (1.3s)

# Or use the CLI in CI/CD:
npx i18n-agent translate resources/base/strings.xml --lang ja,de,es
采用渐进式翻译。向英语源文件添加新键时,只翻译差异内容。这样可保留经人工审核的翻译,并避免重新生成整个文件。

自动保证翻译质量

使用 i18n-validate 在发布前发现缺失键和损坏的占位符。真实译文完成前,可使用 i18n-pseudo 生成伪译文来测试 UI。

常见问题

expect/actual 不匹配

commonMain 中的每个 expect 声明都需要在每个目标(androidMain、iosMain、jsMain)中具有 actual 实现。如果之后添加新的平台目标,编译器会报错,直至您提供 actual。使用 IDE 快速修复生成存根。

硬编码复数逻辑

切勿使用 count == 1 检测单数形式。法语将 0 视为单数,阿拉伯语有六种复数形式,俄语对以 1、2-4 和 5-20 结尾的数字使用不同形式。请使用可识别 CLDR 的库(moko-resources),或为每个区域设置显式定义 lambda。

kmp-localechain 中的嵌套映射

kmp-localechain 对扁平 Map&lt;String, String&gt; 进行操作。如果传入嵌套映射,回退解析无法正确合并内部键。调用 resolve() 前,请使用点号表示法的键(例如 "nav.home")将消息扁平化。

添加 moko-resources 后缺少生成的代码

MR 对象由 Gradle 插件生成。添加 moko-resources 字符串后,请先运行 Gradle 同步,再在代码中使用 MR.strings.*。如果 IDE 仍显示错误,请尝试 Build > Rebuild Project。

推荐的项目结构

Project Structure
my-kmp-app/
├── shared/
│   ├── build.gradle.kts
│   └── src/
│       ├── commonMain/
│       │   ├── kotlin/com/myapp/i18n/
│       │   │   ├── Strings.kt           # Shared string definitions
│       │   │   ├── StringProvider.kt     # expect fun currentLocale()
│       │   │   └── LocaleSetup.kt       # LocaleChain configuration
│       │   └── resources/MR/            # moko-resources XML (optional)
│       │       ├── base/strings.xml     # English (default)
│       │       ├── ja/strings.xml
│       │       ├── de/strings.xml
│       │       └── es/strings.xml
│       ├── androidMain/
│       │   └── kotlin/com/myapp/i18n/
│       │       └── StringProvider.kt     # actual fun currentLocale()
│       ├── iosMain/
│       │   └── kotlin/com/myapp/i18n/
│       │       └── StringProvider.kt     # actual fun currentLocale()
│       └── jsMain/
│           └── kotlin/com/myapp/i18n/
│               └── StringProvider.kt     # actual fun currentLocale()
├── androidApp/
│   └── src/main/res/values/strings.xml   # Android-specific overrides
├── iosApp/
│   └── iosApp/Localizable.strings        # iOS-specific overrides
└── settings.gradle.kts

立即试用 i18n Agent

将翻译文件拖放到此处

JSON, YAML, PO, XML, CSV, Markdown, Properties

或点击选择文件

目标语言

无需注册即时估价

常见问题