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", но при по-старите API някои версии на Android връщат "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), 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

Библиотеките за i18n в KMP не разполагат с конфигурируеми вериги за резервен локал. Когато липсват преводи за pt-BR, те пропускат изцяло pt-PT и показват английски. kmp-localechain решава проблема чрез самостоятелен помощен инструмент за обединяване на съобщения. Той приема за всеки локал съобщения във вид на плосък Map&lt;String, String&gt; и връща обединен Map с приложен приоритет на веригата за резервен локал.

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), или изрични ламбди за всеки локал.

Вложени структури Map в kmp-localechain

kmp-localechain работи с плосък Map&lt;String, String&gt;. Ако подадете вложени структури Map, разрешаването на резервния локал няма да обедини правилно вътрешните ключове. Изравнете съобщенията си чрез ключове с точкова нотация (например "nav.home"), преди да извикате resolve().

Липсва генериран код след добавяне на 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

или натиснете, за да изберете файл

Целеви езици

Не се изисква регистрацияНезабавна оценка

Често задавани въпроси