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

或點擊選擇檔案

目標語言

無需註冊即時估價

常見問題