Skip to main content

i18n Kotlin Multiplatform: общая локализация на разных платформах

Напишите переводы один раз в общем коде 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 убедитесь, что экспортируется модуль поставщика строк. Включите пакет i18n в Podspec или конфигурацию XCFramework, чтобы код 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, Desktop и веб-платформах.
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 или конвейера 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, пока настоящие переводы ещё не готовы.

Распространённые ошибки

Несоответствие 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

или нажмите, чтобы выбрать

Целевые языки

Регистрация не требуетсяМгновенный расчёт

Часто задаваемые вопросы