Skip to main content

Kotlin Multiplatform i18n: التوطين المشترك عبر المنصات

اكتب ترجماتك مرة واحدة في شيفرة Kotlin المشتركة. وانشرها على Android و iOS والويب مع سلاسل تراجع صحيحة للإعدادات المحلية.

1

تهيئة Gradle لـ KMP i18n

أضف تبعيات 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

تعريف أنواع السلاسل المشتركة

أنشئ فئات بيانات 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.
استخدم خصائص lambda لصيغ الجمع بدلاً من مفاتيح منفصلة للمفرد/الجمع. تستقبل lambda العدد وتعيد الصيغة الصحيحة. هذا يُبقي منطق الجمع في Kotlin حيث يستطيع المترجم التحقق منه.
3

توصيل Android

في androidMain، نفّذ نمط expect/actual لقراءة الإعداد المحلي للجهاز عبر java.util.Locale. يمكن لـ Android استخدام سلاسل Kotlin المشتركة لمنطق الأعمال إلى جانب values/strings.xml القياسي لعناصر واجهة النظام مثل الإشعارات و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() وسوماً بصيغة IETF مثل "pt-BR"، لكن بعض إصدارات 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/Browser

في 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 إدراج السلاسل، وصيغ الجمع عبر lambdas، ومجموعات السلاسل المتداخلة. وهو يعمل على أهداف 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 plugin لتوليد الشيفرة. إذا ظهرت لديك الرسالة '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، يمكنك أتمتة الترجمات باستخدام الذكاء الاصطناعي. ترجم ملفات السلاسل المشتركة لديك؛ سواء كانت فئات بيانات 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) أو lambdas صريحة لكل إعداد محلي.

خرائط متداخلة في kmp-localechain

يعمل kmp-localechain على Map&lt;String, String&gt; مسطّحة. إذا مرّرت خرائط متداخلة، فلن يدمج حلّ التراجع المفاتيح الداخلية بشكل صحيح. قم بتسطيح رسائلك باستخدام مفاتيح بصياغة النقاط (مثل "nav.home") قبل استدعاء resolve().

غياب الشيفرة المولَّدة بعد إضافة moko-resources

يتم توليد كائن MR بواسطة Gradle plugin. بعد إضافة سلاسل moko-resources، نفّذ Gradle sync قبل استخدام 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

أو انقر للاستعراض

اللغات المستهدفة

لا حاجة إلى التسجيلتقدير فوري

الأسئلة الشائعة