Skip to main content

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.

1

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
// 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")
            }
        }
    }
}
Cả ba thư viện đều phát hành lên Maven Central. Hãy thêm chúng vào phần phụ thuộc commonMain để mọi nền tảng đích (Android, iOS, JS) đều có thể sử dụng.
2

Định nghĩa các kiểu chuỗi dùng chung

Tạo các lớp dữ liệu Kotlin trong commonMain để chứa mọi chuỗi có thể dịch. Đây là nguồn dữ liệu chính duy nhất — mọi nền tảng đều đọc từ cùng các định nghĩa an toàn kiểu. Không còn tệp chuỗi trùng lặp hay sai lệch giữa các nền tảng.

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.
Hãy dùng thuộc tính lambda cho dạng số nhiều thay vì các khóa số ít và số nhiều riêng biệt. Lambda nhận số lượng rồi trả về dạng phù hợp. Cách này giữ logic số nhiều trong Kotlin để trình biên dịch có thể kiểm tra.
3

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/.../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() trả về các thẻ IETF như "pt-BR", nhưng một số phiên bản Android trả về "pt-rBR" từ các API cũ. Luôn dùng toLanguageTag() (API 21+) để nhận kết quả nhất quán.
4

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/.../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)
Khi xuất framework KMP sang Xcode, hãy nhớ xuất mô-đun cung cấp chuỗi. Trong cấu hình Podspec hoặc XCFramework, hãy đưa gói i18n vào để mã Swift có thể nhập gói này.
5

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/.../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
}
Với Kotlin/JS phía máy chủ (Node.js), hãy đọc locale từ tiêu đề Accept-Language hoặc biến cấu hình thay vì window.navigator.language.
6

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.

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 hỗ trợ nội suy chuỗi, dạng số nhiều qua lambda và nhóm chuỗi lồng nhau. Thư viện hoạt động trên Android, iOS (qua Compose for iOS), Desktop và các nền tảng đích Web.
7

moko-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.

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 cần plugin Gradle để tạo mã. Nếu bạn thấy 'Unresolved reference: MR', trước tiên hãy chạy đồng bộ Gradle. Bước tạo mã phải hoàn tất thì IDE mới nhận diện các trình truy cập MR.
8

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&lt;String, String&gt; 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.

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 hoạt động với bản đồ Map&lt;String, String&gt; phẳng. Nếu thông báo của bạn lồng nhau, hãy làm phẳng trước khi chuyển vào resolve(). Thư viện không hỗ trợ hợp nhất sâu các cấu trúc lồng nhau.
9

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.

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
Hãy dịch theo từng phần tăng thêm. Khi thêm khóa mới vào nguồn tiếng Anh, chỉ dịch phần khác biệt. Cách này bảo toàn các bản dịch đã qua con người duyệt và tránh tạo lại toàn bộ tệp.

Tự động hóa chất lượng bản dịch

Phát hiện khóa còn thiếu và placeholder bị hỏng trước khi phát hành bằng i18n-validate. Kiểm thử giao diện bằng bản dịch giả với i18n-pseudo trước khi có bản dịch thật.

Các lỗi thường gặp

expect/actual không khớp

Mỗi khai báo expect trong commonMain đều cần một phần triển khai actual ở mọi nền tảng đích (androidMain, iosMain, jsMain). Nếu sau này bạn thêm nền tảng đích mới, trình biên dịch sẽ báo lỗi cho đến khi bạn cung cấp actual. Hãy dùng tính năng sửa nhanh của IDE để tạo mã khung.

Logic số nhiều được mã hóa cứng

Không bao giờ dùng count == 1 để xác định dạng số ít. Tiếng Pháp coi 0 là số ít. Tiếng Ả Rập có sáu dạng số nhiều. Tiếng Nga dùng các dạng khác nhau cho số kết thúc bằng 1, 2-4 và 5-20. Hãy dùng thư viện hỗ trợ CLDR (moko-resources) hoặc lambda riêng cho từng locale.

Bản đồ lồng nhau trong kmp-localechain

kmp-localechain hoạt động trên Map&lt;String, String&gt; phẳng. Nếu bạn chuyển bản đồ lồng nhau, quá trình phân giải dự phòng sẽ không hợp nhất chính xác các khóa bên trong. Hãy làm phẳng thông báo bằng khóa ký pháp dấu chấm (ví dụ: "nav.home") trước khi gọi resolve().

Thiếu mã đã tạo sau khi thêm moko-resources

Plugin Gradle tạo đối tượng MR. Sau khi thêm chuỗi moko-resources, hãy chạy đồng bộ Gradle trước khi dùng MR.strings.* trong mã. Nếu IDE vẫn báo lỗi, hãy thử Build > Rebuild Project.

Cấu trúc dự án đề xuất

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

Dù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

Không cần đăng kýBáo giá tức thì

Câu hỏi thường gặp