Skip to main content

i18n di Kotlin Multiplatform: localizzazione condivisa tra piattaforme

Scriva le traduzioni una sola volta nel codice Kotlin condiviso. Le distribuisca ad Android, iOS e al web con catene di fallback corrette.

1

Configurare Gradle per l'i18n KMP

Aggiunga le dipendenze i18n al set di origini commonMain del modulo condiviso. Può scegliere moko-resources per le stringhe basate su XML, Lyricist per le stringhe Compose tipizzate in modo sicuro oppure entrambi. kmp-localechain aggiunge un fallback intelligente a ciascuna soluzione.

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")
            }
        }
    }
}
Tutte e tre le biblioteche sono pubblicate su Maven Central. Le aggiunga alle dipendenze commonMain, affinché siano disponibili in ogni destinazione (Android, iOS, JS).
2

Definire tipi di stringhe condivisi

Crei in commonMain classi di dati Kotlin che contengano tutte le stringhe traducibili. Sono l'unica fonte di riferimento: ogni piattaforma legge le stesse definizioni tipizzate in modo sicuro. Nessun file di stringhe duplicato, nessun disallineamento tra piattaforme.

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.
Usi proprietà lambda per i plurali anziché chiavi separate per singolare e plurale. La lambda riceve il conteggio e restituisce la forma corretta. In questo modo la logica plurale rimane in Kotlin, dove il compilatore può controllarla.
3

Integrare Android

In androidMain, implementi il pattern expect/actual per leggere la lingua del dispositivo tramite java.util.Locale. Android può usare le stringhe Kotlin condivise per la logica aziendale insieme ai normali values/strings.xml per gli elementi dell'interfaccia di sistema, come notifiche e widget.

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() restituisce tag IETF come "pt-BR", ma alcune versioni di Android restituiscono "pt-rBR" dalle API precedenti. Usi sempre toLanguageTag() (API 21 e versioni successive) per risultati coerenti.
4

Integrare iOS

In iosMain, implementi currentLocale() usando NSLocale di Foundation. Il framework KMP condiviso esporta le definizioni delle stringhe in Swift, così le viste SwiftUI possono chiamarle direttamente tramite il framework Kotlin generato.

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)
Quando esporta il framework KMP in Xcode, verifichi di esportare il modulo che fornisce le stringhe. Nella configurazione Podspec o XCFramework, includa il pacchetto i18n affinché il codice Swift possa importarlo.
5

Integrare JS/browser

In jsMain, legga la lingua del browser da window.navigator.language. Copre sia le app web Kotlin/JS sia le destinazioni Compose for Web. Le stesse stringhe condivise vengono visualizzate nel browser senza duplicazioni.

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
}
Per Kotlin/JS lato server (Node.js), legga la lingua dall'intestazione Accept-Language o da una variabile di configurazione anziché da window.navigator.language.
6

Lyricist per Compose Multiplatform

Lyricist offre un approccio i18n nativo di Compose. Annoti gli oggetti delle stringhe con @LyricistStrings e Lyricist genererà un provider CompositionLocal. Cambi lingua durante l'esecuzione modificando languageTag: l'interfaccia si ricompone automaticamente.

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 supporta interpolazione delle stringhe, plurali tramite lambda e gruppi di stringhe annidati. Funziona sulle destinazioni Android, iOS (tramite Compose for iOS), desktop e web.
7

moko-resources per le stringhe XML

moko-resources usa file XML di stringhe in stile Android come fonte di riferimento e genera funzioni di accesso tipizzate in modo sicuro. Definisca le stringhe in commonMain/resources/MR/base/ (inglese) e aggiunga cartelle per ogni lingua. L'oggetto MR generato fornisce un accesso controllato in fase di compilazione.

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 richiede il plugin Gradle per generare il codice. Se vede 'Unresolved reference: MR', esegua prima una sincronizzazione Gradle. La generazione del codice deve terminare prima che l'IDE possa rilevare le funzioni di accesso MR.
8

Fallback intelligente con kmp-localechain

Le biblioteche i18n KMP non offrono catene di fallback configurabili. Quando mancano le traduzioni pt-BR, ignorano completamente pt-PT e mostrano l'inglese. kmp-localechain risolve il problema con un'utilità autonoma per unire i messaggi. Accetta messaggi Map&lt;String, String&gt; semplici per lingua e restituisce una mappa unita applicando la priorità della catena di fallback.

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 funziona con mappe Map&lt;String, String&gt; semplici. Se i messaggi sono annidati, li appiattisca prima di passarli a resolve(). La biblioteca non supporta l'unione ricorsiva delle strutture annidate.
9

Automatizzare le traduzioni

Dopo aver completato la configurazione i18n KMP, automatizzi le traduzioni con l'IA. Traduca i file di stringhe condivisi, siano essi classi di dati Kotlin, risorse XML o JSON, direttamente dall'IDE o dalla 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
Traduca in modo incrementale. Quando aggiunge nuove chiavi all'origine inglese, traduca soltanto il diff. In questo modo preserva le traduzioni revisionate da persone ed evita di rigenerare interi file.

Automatizzare la qualità

Con i18n-validate, rilevi chiavi mancanti e segnaposto non validi prima del rilascio. Testi l'interfaccia con le pseudotraduzioni di i18n-pseudo prima che arrivino le traduzioni reali.

Problemi comuni

Mancata corrispondenza expect/actual

Ogni dichiarazione expect in commonMain richiede un'implementazione actual in ogni destinazione (androidMain, iosMain, jsMain). Se aggiunge successivamente una nuova destinazione, il compilatore segnalerà un errore finché non fornirà actual. Usi le correzioni rapide dell'IDE per generare gli stub.

Logica plurale codificata direttamente

Non usi mai count == 1 per rilevare le forme singolari. Il francese considera 0 singolare. L'arabo ha sei forme plurali. Il russo usa forme diverse per i numeri che terminano con 1, 2-4 e 5-20. Usi biblioteche compatibili con CLDR (moko-resources) o lambda esplicite per ogni lingua.

Mappe annidate in kmp-localechain

kmp-localechain opera su Map&lt;String, String&gt; semplici. Se passa mappe annidate, la risoluzione del fallback non unirà correttamente le chiavi interne. Appiattisca i messaggi usando chiavi in notazione puntata, ad esempio "nav.home", prima di chiamare resolve().

Codice generato mancante dopo l'aggiunta di moko-resources

L'oggetto MR viene generato da un plugin Gradle. Dopo aver aggiunto stringhe moko-resources, esegua una sincronizzazione Gradle prima di usare MR.strings.* nel codice. Se l'IDE continua a mostrare errori, provi Build > Rebuild Project.

Struttura del progetto consigliata

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

Provi subito i18n Agent

Trascinare qui il file di traduzione

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

oppure fare clic per selezionarlo

Lingue di destinazione

Nessuna registrazione richiestaPreventivo immediato

Domande frequenti