Skip to main content

Kotlin Multiplatform i18n: спільна локалізація для різних платформ

Напишіть переклади один раз у спільному коді Kotlin. Використовуйте їх в Android, iOS і вебі з належними ланцюжками резервних локалей.

1

Налаштуйте Gradle для i18n у KMP

Додайте залежності 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.
Для форм множини використовуйте властивості-лямбди замість окремих ключів для однини та множини. Лямбда отримує кількість і повертає правильну форму. Так логіка множини залишається в Kotlin, де її може перевірити компілятор.
3

Підключіть Android

У androidMain реалізуйте шаблон expect/actual, щоб отримувати локаль пристрою через java.util.Locale. Android може використовувати спільні рядки Kotlin для бізнес-логіки разом зі стандартним 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() повертає теги IETF на кшталт "pt-BR", але деякі версії Android у старіших API повертають "pt-rBR". Для узгоджених результатів завжди використовуйте toLanguageTag() (API 21+).
4

Підключіть iOS

У iosMain реалізуйте currentLocale() за допомогою NSLocale з Foundation. Спільний фреймворк 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 пропонує природний для Compose підхід до i18n. Додайте до об’єктів рядків анотацію @LyricistStrings, і Lyricist згенерує постачальник CompositionLocal. Щоб змінити мову під час виконання, змініть languageTag — інтерфейс автоматично перекомпонується.

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 підтримує інтерполяцію рядків, форми множини через лямбди та вкладені групи рядків. Бібліотека працює на цільових платформах Android, iOS (через Compose for iOS), настільних системах і вебі.
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

У бібліотеках i18n для KMP немає налаштовуваних ланцюжків резервних локалей. Якщо перекладу для 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

Автоматизуйте переклад

Завершивши налаштування i18n у KMP, автоматизуйте переклад за допомогою ШІ. Перекладайте спільні файли рядків — класи даних Kotlin, ресурси XML або JSON — безпосередньо з IDE чи pipeline 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
Перекладайте поступово. Коли Ви додаєте нові ключі до англійського джерела, перекладайте лише diff. Так Ви збережете перевірені людьми переклади й уникнете повторного генерування цілих файлів.

Автоматизуйте контроль якості перекладу

Виявляйте відсутні ключі та пошкоджені заповнювачі до випуску за допомогою i18n-validate. Тестуйте інтерфейс із псевдоперекладами через i18n-pseudo ще до появи справжніх перекладів.

Поширені помилки

Невідповідність expect/actual

Кожне оголошення expect у commonMain потребує реалізації actual для кожної цільової платформи (androidMain, iosMain, jsMain). Якщо згодом Ви додасте нову цільову платформу, компілятор повідомлятиме про помилку, доки Ви не надасте actual. Використовуйте швидкі виправлення IDE для генерування заготовок.

Жорстко закодована логіка множини

Ніколи не використовуйте count == 1 для визначення форми однини. У французькій 0 вважається одниною. В арабській є шість форм множини. У російській використовуються різні форми для чисел, що закінчуються на 1, 2-4 і 5-20. Використовуйте бібліотеки з підтримкою CLDR (moko-resources) або явні лямбди для кожної локалі.

Вкладені мапи в 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

або натисніть, щоб вибрати

Цільові мови

Реєстрація не потрібнаМиттєвий розрахунок

Поширені запитання