Skip to main content

Kotlin Multiplatform i18n:プラットフォーム間で共有するローカライズ

共有 Kotlin コード内で翻訳を一度だけ記述し、適切なロケールフォールバックチェーンとともに Android、iOS、Web へ配布します。

1

KMP i18n 向け Gradle の設定

共有モジュールの commonMain ソースセットに i18n の依存関係を追加します。XML ベースの文字列には moko-resources、型安全な Compose 文字列には Lyricist、または両方を選択できます。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")
            }
        }
    }
}
3 つのライブラリはすべて Maven Central で公開されています。commonMain の依存関係に追加し、すべてのターゲット(Android、iOS、JS)で利用できるようにしてください。
2

共有文字列型の定義

翻訳対象のすべての文字列を保持する Kotlin データクラスを commonMain に作成します。これが唯一の信頼できる情報源となり、すべてのプラットフォームが同じ型安全な定義から読み取ります。文字列ファイルの重複やプラットフォーム間の差異がなくなります。

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 文字列を使用しながら、通知やウィジェットなどのシステム UI 要素には標準の 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() は「pt-BR」のような IETF タグを返しますが、一部の Android バージョンでは古い API から「pt-rBR」が返されます。一貫した結果を得るため、必ず toLanguageTag()(API 21 以降)を使用してください。
4

iOS 側の連携設定

iosMain で Foundation の NSLocale を使用し、currentLocale() を実装します。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 コードから import できるようにします。
5

JS/ブラウザー側の連携設定

jsMain で window.navigator.language からブラウザーのロケールを読み取ります。Kotlin/JS Web アプリと 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)では、window.navigator.language ではなく Accept-Language ヘッダーまたは設定変数からロケールを読み取ってください。
6

Compose Multiplatform 向け Lyricist

Lyricist は、Compose に適した i18n 手法を提供します。文字列オブジェクトに @LyricistStrings アノテーションを付けると、Lyricist が CompositionLocal プロバイダーを生成します。languageTag を変更して実行時に言語を切り替えると、UI が自動的に再コンポーズされます。

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、Compose for iOS 経由の iOS、Desktop、Web の各ターゲットで動作します。
7

XML 文字列向け moko-resources

moko-resources は、Android 形式の XML 文字列ファイルを正式なデータソースとして使用し、型安全なアクセサーを生成します。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 sync を実行してください。IDE が MR アクセサーを認識するには、コード生成手順が完了している必要があります。
8

kmp-localechain による高度なロケールフォールバック

KMP i18n ライブラリには、設定可能なフォールバックチェーンがありません。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

翻訳の自動化

KMP i18n の設定が完了したら、AI を使用して翻訳を自動化します。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 の疑似翻訳で UI をテストしてください。

よくある問題

expect/actual の不一致

commonMain 内のすべての expect 宣言には、各ターゲット(androidMain、iosMain、jsMain)の actual 実装が必要です。後から新しいプラットフォームターゲットを追加した場合、actual を用意するまでコンパイラーエラーになります。IDE のクイックフィックスを使用してスタブを生成してください。

複数形ロジックのハードコード

単数形の判定に count == 1 を使用しないでください。フランス語では 0 を単数として扱い、アラビア語には 6 つの複数形があります。ロシア語では、末尾が 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 の文字列を追加した後、コードで MR.strings.* を使用する前に Gradle sync を実行してください。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

またはクリックしてファイルを選択

翻訳先言語

登録不要すぐに見積もり

よくある質問