Skip to main content

Kotlin Multiplatform i18n: gedeelde lokalisatie voor alle platforms

Schrijf je vertalingen één keer in gedeelde Kotlin-code. Lever ze aan Android, iOS en het web met de juiste locale-fallbackketens.

1

Gradle configureren voor KMP i18n

Voeg je i18n-afhankelijkheden toe aan de commonMain-bronset van de gedeelde module. Kies moko-resources voor XML-teksten, Lyricist voor typeveilige Compose-teksten of beide. kmp-localechain voegt aan beide opties slimme locale-fallback toe.

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")
            }
        }
    }
}
Alle drie de bibliotheken worden via Maven Central gepubliceerd. Voeg ze aan de commonMain-afhankelijkheden toe zodat ze op elk doel beschikbaar zijn: Android, iOS en JS.
2

Gedeelde teksttypen definiëren

Maak in commonMain Kotlin-dataklassen die alle vertaalbare teksten bevatten. Dit is de enige gezaghebbende bron: elk platform leest dezelfde typeveilige definities. Geen dubbele tekstbestanden en geen afwijkingen tussen platforms.

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.
Gebruik lambda-eigenschappen voor meervouden in plaats van afzonderlijke sleutels voor enkelvoud en meervoud. De lambda ontvangt het aantal en geeft de juiste vorm terug. Zo blijft de meervoudslogica in Kotlin, waar de compiler deze kan controleren.
3

Android aansluiten

Implementeer in androidMain het expect/actual-patroon om via java.util.Locale de locale van het apparaat te lezen. Android kan voor bedrijfslogica gedeelde Kotlin-teksten gebruiken naast de standaard values/strings.xml voor systeemelementen zoals meldingen en widgets.

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() levert IETF-tags op zoals "pt-BR", maar via oudere API's geven sommige Android-versies "pt-rBR" terug. Gebruik voor consistente resultaten altijd toLanguageTag() (API 21+).
4

iOS aansluiten

Implementeer currentLocale() in iosMain met NSLocale uit Foundation. Het gedeelde KMP-framework exporteert je tekstdefinities naar Swift zodat SwiftUI-views ze rechtstreeks via het gegenereerde Kotlin-framework kunnen aanroepen.

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)
Zorg bij het exporteren van het KMP-framework naar Xcode dat je de module met de tekstprovider exporteert. Neem het i18n-pakket in je Podspec- of XCFramework-configuratie op zodat Swift-code het kan importeren.
5

JS/browser aansluiten

Lees in jsMain de browserlocale uit window.navigator.language. Dit werkt voor zowel Kotlin/JS-webapps als Compose for Web-doelen. Dezelfde gedeelde teksten worden zonder duplicatie in de browser gerenderd.

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
}
Lees voor Kotlin/JS aan de serverzijde (Node.js) de locale uit de Accept-Language-header of een configuratievariabele in plaats van window.navigator.language.
6

Lyricist voor Compose Multiplatform

Lyricist biedt een aanpak voor i18n die rechtstreeks bij Compose past. Annoteer je tekstobjecten met @LyricistStrings. Lyricist genereert vervolgens een CompositionLocal-provider. Wissel tijdens runtime van taal door languageTag te wijzigen; de gebruikersinterface wordt automatisch opnieuw samengesteld.

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 ondersteunt tekstinterpolatie, meervouden via lambda's en geneste tekstgroepen. Het werkt voor Android, iOS via Compose for iOS, Desktop en Web.
7

moko-resources voor XML-teksten

moko-resources gebruikt XML-tekstbestanden in Android-stijl als gezaghebbende bron en genereert typeveilige accessors. Definieer Engelse teksten in commonMain/resources/MR/base/ en voeg voor elke taal localemappen toe. Het gegenereerde MR-object biedt tijdens het compileren gecontroleerde toegang.

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 heeft de Gradle-plug-in nodig om code te genereren. Voer eerst een Gradle-synchronisatie uit wanneer je 'Unresolved reference: MR' ziet. De codegeneratie moet zijn voltooid voordat je ontwikkelomgeving de MR-accessors herkent.
8

Slimme locale-fallback met kmp-localechain

i18n-bibliotheken voor KMP hebben geen configureerbare fallbackketens. Wanneer pt-BR-vertalingen ontbreken, slaan ze pt-PT volledig over en tonen ze Engels. kmp-localechain lost dit op met een zelfstandig hulpmiddel dat berichten samenvoegt. Het ontvangt per locale een platte Map&lt;String, String&gt; met berichten en levert een samengevoegde map op waarop de prioriteit van de fallbackketen is toegepast.

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 werkt met platte Map&lt;String, String&gt;-maps. Maak geneste berichten eerst plat voordat je ze aan resolve() doorgeeft. De bibliotheek ondersteunt geen diepe samenvoeging van geneste structuren.
9

Vertalingen automatiseren

Wanneer je KMP i18n-configuratie gereed is, automatiseer je vertalingen met AI. Vertaal je gedeelde tekstbestanden — Kotlin-dataklassen, XML-resources of JSON — rechtstreeks vanuit je ontwikkelomgeving of CI/CD-pipeline.

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
Vertaal stapsgewijs. Wanneer je nieuwe sleutels aan de Engelse bron toevoegt, vertaal je alleen het verschil. Zo blijven door mensen beoordeelde vertalingen behouden en voorkom je dat volledige bestanden opnieuw worden gegenereerd.

Vertaalkwaliteit automatisch bewaken

Vind ontbrekende sleutels en beschadigde placeholders vóór de release met i18n-validate. Test je gebruikersinterface met pseudovertalingen via i18n-pseudo voordat de echte vertalingen beschikbaar zijn.

Veelvoorkomende valkuilen

expect/actual komt niet overeen

Elke expect-declaratie in commonMain heeft in elk doel een actual-implementatie nodig: androidMain, iosMain en jsMain. Als je later een nieuw platformdoel toevoegt, geeft de compiler fouten totdat je de actual implementeert. Gebruik snelle oplossingen in je ontwikkelomgeving om stubs te genereren.

Hardgecodeerde meervoudslogica

Gebruik nooit count == 1 om enkelvoud te herkennen. Frans behandelt 0 als enkelvoud. Arabisch heeft zes meervoudsvormen. Russisch gebruikt verschillende vormen voor getallen die eindigen op 1, 2-4 en 5-20. Gebruik bibliotheken met CLDR-ondersteuning zoals moko-resources of expliciete lambda's per locale.

Geneste maps in kmp-localechain

kmp-localechain werkt met platte Map&lt;String, String&gt;-maps. Als je geneste maps doorgeeft, voegt de fallbackoplossing de onderliggende sleutels niet correct samen. Maak je berichten plat met sleutels in puntnotatie, bijvoorbeeld "nav.home", voordat je resolve() aanroept.

Gegenereerde code ontbreekt na toevoeging van moko-resources

Het MR-object wordt door een Gradle-plug-in gegenereerd. Voer na het toevoegen van teksten aan moko-resources een Gradle-synchronisatie uit voordat je MR.strings.* in je code gebruikt. Probeer Build > Rebuild Project als je ontwikkelomgeving nog steeds fouten toont.

Aanbevolen projectstructuur

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

Probeer i18n Agent nu

Zet je vertaalbestand hier neer

JSON, YAML, PO, XML, CSV, Markdown, Properties

of klik om een bestand te selecteren

Doeltalen

Geen registratie nodigDirecte prijsindicatie

Veelgestelde vragen