Skip to main content

Kotlin Multiplatform i18n : localisation partagée entre plateformes

Écrivez vos traductions une seule fois dans du code Kotlin partagé. Livrez-les sur Android, iOS et le web avec des chaînes de repli de locale appropriées.

1

Configurer Gradle pour l'i18n KMP

Ajoutez vos dépendances i18n au source set commonMain du module partagé. Vous pouvez choisir moko-resources pour les chaînes basées sur XML, Lyricist pour des chaînes Compose type-safe, ou les deux. kmp-localechain ajoute un repli de locale intelligent au-dessus de l'un ou l'autre.

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")
            }
        }
    }
}
Les trois bibliothèques sont publiées sur Maven Central. Ajoutez-les aux dépendances commonMain afin qu'elles soient disponibles sur chaque cible (Android, iOS, JS).
2

Définir des types de chaînes partagés

Créez des data classes Kotlin dans commonMain qui contiennent toutes les chaînes traduisibles. Il s'agit de la source de référence unique : chaque plateforme lit les mêmes définitions typées. Aucun fichier de chaînes en double, aucune divergence entre plateformes.

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.
Utilisez des propriétés lambda pour les pluriels plutôt que des clés singulier/pluriel distinctes. La lambda reçoit le nombre et renvoie la forme correcte. Cela permet de garder la logique des pluriels en Kotlin, où le compilateur peut la vérifier.
3

Intégrer Android

Dans androidMain, implémentez le mécanisme expect/actual pour lire la locale de l'appareil via java.util.Locale. Android peut utiliser les chaînes Kotlin partagées pour la logique métier, en complément du fichier standard values/strings.xml pour les éléments d'UI système comme les notifications et les 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() renvoie des tags IETF comme « pt-BR », mais certaines versions d'Android renvoient « pt-rBR » avec les anciennes API. Utilisez toujours toLanguageTag() (API 21+) pour obtenir des résultats cohérents.
4

Intégrer iOS

Dans iosMain, implémentez currentLocale() à l'aide de NSLocale depuis Foundation. Le framework partagé KMP exporte vos définitions de chaînes vers Swift, ce qui permet aux vues SwiftUI de les appeler directement via le framework Kotlin généré.

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)
Lorsque vous exportez le framework KMP vers Xcode, veillez à exporter le module fournisseur de chaînes. Dans votre configuration Podspec ou XCFramework, incluez le package i18n afin que le code Swift puisse l'importer.
5

Intégrer JS/le navigateur

Dans jsMain, lisez la locale du navigateur depuis window.navigator.language. Cela couvre à la fois les applications web Kotlin/JS et les cibles Compose for Web. Les mêmes chaînes partagées s'affichent dans le navigateur sans aucune duplication.

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
}
Pour Kotlin/JS côté serveur (Node.js), lisez la locale depuis l'en-tête Accept-Language ou une variable de configuration plutôt que depuis window.navigator.language.
6

Lyricist pour Compose Multiplatform

Lyricist propose une approche native de Compose pour l'i18n. Annotez vos objets de chaînes avec @LyricistStrings, et Lyricist génère un fournisseur CompositionLocal. Changez de langue au moment de l'exécution en modifiant le languageTag : l'UI se recompose automatiquement.

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 prend en charge l'interpolation de chaînes, les pluriels via des lambdas et les groupes de chaînes imbriqués. Il fonctionne sur les cibles Android, iOS (via Compose for iOS), Desktop et Web.
7

moko-resources pour les chaînes XML

moko-resources utilise des fichiers de chaînes XML au format Android comme source de référence et génère des accesseurs typés. Définissez les chaînes dans commonMain/resources/MR/base/ (anglais) et ajoutez des dossiers de locale pour chaque langue. L'objet MR généré fournit un accès vérifié à la compilation.

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 nécessite le plugin Gradle pour générer le code. Si vous voyez « Unresolved reference: MR », lancez d'abord une synchronisation Gradle. L'étape de génération de code doit se terminer avant que votre IDE ne voie les accesseurs MR.
8

Repli intelligent de locale avec kmp-localechain

Les bibliothèques i18n pour KMP manquent de chaînes de repli configurables. Lorsque les traductions pt-BR sont manquantes, elles ignorent totalement pt-PT et affichent l'anglais. kmp-localechain corrige ce problème grâce à un utilitaire autonome de fusion de messages. Il prend en entrée des messages Map&lt;String, String&gt; à plat par locale et renvoie une map fusionnée en appliquant la priorité de la chaîne de repli.

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 fonctionne avec des maps Map&lt;String, String&gt; à plat. Si vos messages sont imbriqués, aplatissez-les avant de les transmettre à resolve(). La bibliothèque ne prend pas en charge la fusion profonde de structures imbriquées.
9

Automatiser les traductions

Une fois votre configuration i18n KMP terminée, automatisez les traductions grâce à l'IA. Traduisez vos fichiers de chaînes partagés (data classes Kotlin, ressources XML ou JSON) directement depuis votre IDE ou votre 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
Traduisez de façon incrémentale. Lorsque vous ajoutez de nouvelles clés à votre source anglaise, ne traduisez que le diff. Cela préserve les traductions relues par des humains et évite de régénérer des fichiers entiers.

Automatiser la qualité des traductions

Détectez les clés manquantes et les espaces réservés cassés avant leur mise en production grâce à i18n-validate. Testez votre interface avec des pseudo-traductions grâce à i18n-pseudo avant l'arrivée des traductions réelles.

Pièges courants

Incohérence expect/actual

Chaque déclaration expect dans commonMain nécessite une implémentation actual dans chaque cible (androidMain, iosMain, jsMain). Si vous ajoutez une nouvelle cible de plateforme par la suite, le compilateur générera une erreur tant que vous n'aurez pas fourni l'implémentation actual. Utilisez les correctifs rapides de l'IDE pour générer les stubs.

Logique de pluriel codée en dur

N'utilisez jamais count == 1 pour détecter les formes singulières. Le français traite 0 comme un singulier. L'arabe possède six formes de pluriel. Le russe utilise des formes différentes selon que le nombre se termine par 1, par 2 à 4, ou par 5 à 20. Utilisez des bibliothèques compatibles CLDR (moko-resources) ou des lambdas explicites par locale.

Maps imbriquées dans kmp-localechain

kmp-localechain fonctionne sur des Map&lt;String, String&gt; à plat. Si vous transmettez des maps imbriquées, la résolution du repli ne fusionnera pas correctement les clés internes. Aplatissez vos messages à l'aide de clés en notation pointée (par exemple « nav.home ») avant d'appeler resolve().

Code généré manquant après l'ajout de moko-resources

L'objet MR est généré par un plugin Gradle. Après avoir ajouté des chaînes moko-resources, lancez une synchronisation Gradle avant d'utiliser MR.strings.* dans votre code. Si votre IDE affiche toujours des erreurs, essayez Build > Rebuild Project.

Structure de projet recommandée

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

Essayez i18n Agent maintenant

Déposez votre fichier de traduction ici

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

ou cliquez pour parcourir

Langues cibles

Aucune inscription requiseEstimation instantanée

Questions fréquentes