
Kotlin Multiplatform i18n: Bản địa hóa dùng chung trên nhiều nền tảng
Chỉ cần viết bản dịch một lần trong mã Kotlin dùng chung. Phát hành lên Android, iOS và web với chuỗi dự phòng locale phù hợp.
Cấu hình Gradle cho KMP i18n
Thêm các phần phụ thuộc i18n vào tập hợp mã nguồn commonMain của mô-đun dùng chung. Bạn có thể chọn moko-resources cho chuỗi dựa trên XML, Lyricist cho chuỗi Compose an toàn kiểu hoặc cả hai. kmp-localechain bổ sung cơ chế dự phòng locale thông minh cho một trong hai thư viện.
// 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")
}
}
}
}Kết nối Android
Trong androidMain, triển khai mẫu expect/actual để đọc locale của thiết bị qua java.util.Locale. Android có thể dùng chuỗi Kotlin dùng chung cho logic nghiệp vụ bên cạnh values/strings.xml tiêu chuẩn dành cho các thành phần giao diện hệ thống như thông báo và tiện ích.
// 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))
}Kết nối iOS
Trong iosMain, triển khai currentLocale() bằng NSLocale từ Foundation. Framework KMP dùng chung xuất các định nghĩa chuỗi sang Swift để khung nhìn SwiftUI có thể gọi trực tiếp qua framework Kotlin đã tạo.
// 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)Kết nối JS/trình duyệt
Trong jsMain, đọc locale của trình duyệt từ window.navigator.language. Cách này hỗ trợ cả ứng dụng web Kotlin/JS và các nền tảng đích Compose for Web. Các chuỗi dùng chung tương tự hiển thị trong trình duyệt mà không cần trùng lặp.
// 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
}Lyricist cho Compose Multiplatform
Lyricist cung cấp phương pháp i18n dành riêng cho Compose. Thêm chú thích @LyricistStrings vào các đối tượng chuỗi và Lyricist sẽ tạo trình cung cấp CompositionLocal. Chuyển đổi ngôn ngữ khi chạy bằng cách thay đổi languageTag — giao diện sẽ tự động kết hợp lại.
// 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 automaticallymoko-resources cho chuỗi XML
moko-resources dùng các tệp chuỗi XML theo kiểu Android làm nguồn dữ liệu chính và tạo các trình truy cập an toàn kiểu. Định nghĩa chuỗi trong commonMain/resources/MR/base/ (tiếng Anh) rồi thêm thư mục locale cho từng ngôn ngữ. Đối tượng MR đã tạo cung cấp quyền truy cập có kiểm tra khi biên dịch.
// 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)Cơ chế dự phòng locale thông minh với kmp-localechain
Các thư viện KMP i18n thiếu chuỗi dự phòng có thể cấu hình. Khi không có bản dịch pt-BR, chúng bỏ qua hoàn toàn pt-PT và hiển thị tiếng Anh. kmp-localechain khắc phục vấn đề này bằng tiện ích hợp nhất thông báo độc lập. Tiện ích nhận thông báo Map<String, String> phẳng cho từng locale rồi trả về bản đồ đã hợp nhất theo mức ưu tiên của chuỗi dự phòng.
// 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 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>?
}Tự động hóa bản dịch
Sau khi hoàn tất thiết lập KMP i18n, hãy tự động hóa bản dịch bằng AI. Dịch trực tiếp các tệp chuỗi dùng chung — dù là lớp dữ liệu Kotlin, tài nguyên XML hay JSON — từ IDE hoặc quy trình CI/CD của bạn.
# 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,esTự động hóa chất lượng bản dịch
Các lỗi thường gặp
expect/actual không khớp
Logic số nhiều được mã hóa cứng
Bản đồ lồng nhau trong kmp-localechain
Thiếu mã đã tạo sau khi thêm moko-resources
Cấu trúc dự án đề xuất
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.ktsDùng thử i18n Agent ngay
Thả tệp bản dịch của bạn vào đây
JSON, YAML, PO, XML, CSV, Markdown, Properties
hoặc nhấp để duyệt
Ngôn ngữ đích