Skip to main content

Kotlin Multiplatform i18n : โลคัลไลเซชันร่วมกันข้ามแพลตฟอร์ม

เขียนคำแปลครั้งเดียวในโค้ด Kotlin ที่ใช้ร่วมกัน แล้วส่งไปยัง Android, iOS และเว็บพร้อมลำดับการใช้ภาษาสำรองที่ถูกต้อง

1

กำหนดค่า Gradle สำหรับ KMP i18n

เพิ่มการพึ่งพา i18n ลงใน source set 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

กำหนดชนิดข้อความร่วม

สร้างคลาสข้อมูล Kotlin ใน commonMain เพื่อเก็บข้อความที่แปลได้ทั้งหมด นี่คือแหล่งข้อมูลจริงเพียงแห่งเดียว ทุกแพลตฟอร์มอ่านจากคำจำกัดความที่ปลอดภัยด้านชนิดเดียวกัน จึงไม่มีไฟล์ข้อความซ้ำหรือการคลาดเคลื่อนระหว่างแพลตฟอร์ม

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 ร่วมสำหรับตรรกะธุรกิจควบคู่กับ values/strings.xml มาตรฐานสำหรับองค์ประกอบ UI ระบบอย่างการแจ้งเตือนและวิดเจ็ตได้

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() คืนแท็ก IETF อย่าง “pt-BR” แต่ Android บางเวอร์ชันคืน “pt-rBR” จาก API รุ่นเก่า ให้ใช้ toLanguageTag() (API 21+) เสมอเพื่อผลลัพธ์สอดคล้อง
4

เชื่อมต่อ iOS

ใน iosMain ให้ใช้ NSLocale จาก Foundation สร้าง 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 และเป้าหมาย 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

Lyricist สำหรับ Compose Multiplatform

Lyricist ใช้แนวทาง i18n โดยตรงของ Compose ใส่ @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, Desktop และ Web
7

moko-resources สำหรับข้อความ XML

moko-resources ใช้ไฟล์ข้อความ XML สไตล์ Android เป็นแหล่งข้อมูลจริงและสร้างตัวเข้าถึงที่ปลอดภัยด้านชนิด กำหนดข้อความใน 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 จับคีย์ที่หายไปและตัวยึดตำแหน่งเสียหายก่อนส่งขึ้นใช้งาน แล้วทดสอบ UI ด้วยคำแปลจำลองผ่าน i18n-pseudo ก่อนคำแปลจริงจะมาถึง

ข้อผิดพลาดที่พบบ่อย

expect/actual ไม่ตรงกัน

คำประกาศ expect ทุกตัวใน commonMain ต้องมีการใช้งาน actual ในทุกเป้าหมาย (androidMain, iosMain, jsMain) หากเพิ่มแพลตฟอร์มใหม่ภายหลัง คอมไพเลอร์จะแจ้งข้อผิดพลาดจนกว่าจะเพิ่ม actual ใช้การแก้ด่วนของ IDE สร้าง stub

ตรรกะพหูพจน์ที่ฮาร์ดโค้ด

อย่าใช้ count == 1 ตรวจรูปเอกพจน์ ภาษาฝรั่งเศสมอง 0 เป็นเอกพจน์ อาหรับมีหกรูป ส่วนรัสเซียใช้รูปต่างกันกับตัวเลขลงท้าย 1, 2-4 และ 5-20 ใช้ไลบรารีที่รับรู้ CLDR อย่าง moko-resources หรือ lambda ชัดเจนต่อภาษา

แผนผังซ้อนใน kmp-localechain

kmp-localechain ทำงานกับ Map&lt;String, String&gt; แบบแบน หากส่งแผนผังซ้อน การค้นหาค่าสำรองจะผสานคีย์ด้านในไม่ถูกต้อง ทำข้อความให้แบนด้วยคีย์รูปแบบจุด เช่น “nav.home” ก่อนเรียก resolve()

ไม่มีโค้ดที่สร้างหลังเพิ่ม moko-resources

ปลั๊กอิน Gradle สร้างออบเจ็กต์ MR หลังเพิ่มข้อความ 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

หรือคลิกเพื่อเลือกไฟล์

ภาษาเป้าหมาย

ไม่ต้องลงทะเบียนประเมินราคาได้ทันที

คำถามที่พบบ่อย