| name | kmp-localization |
| description | Internationalization with `compose-resources` — string qualifiers, ICU plurals, RTL layout, per-locale fallbacks, and runtime locale switching. Use when shipping to multiple markets. |
KMP Localization
Instructions
Localization in Compose Multiplatform piggybacks on the compose-resources plugin. One strings.xml per language; the plugin loads the right one based on the platform locale.
1. Directory structure
shared/src/commonMain/composeResources/
├── values/strings.xml # default (English)
├── values-es/strings.xml # Spanish
├── values-es-rMX/strings.xml # Spanish (Mexico) overrides
├── values-ar/strings.xml # Arabic — triggers RTL
├── values-ja/strings.xml # Japanese
└── values-pt-rBR/strings.xml # Portuguese (Brazil)
Region qualifiers use -r<REGION> (Android convention). The plugin maps locales per platform at build time.
2. ICU plurals and positional args
<resources>
<string name="welcome">Welcome, %1$s. You have %2$d new messages.</string>
<plurals name="cart_items">
<item quantity="zero">No items in cart</item>
<item quantity="one">%d item in cart</item>
<item quantity="other">%d items in cart</item>
</plurals>
</resources>
Text(stringResource(Res.string.welcome, user.name, messageCount))
Text(pluralStringResource(Res.plurals.cart_items, count, count))
Never concatenate translated fragments. Always use positional arguments; word order differs between languages.
3. Right-to-left
Arabic/Hebrew/Persian flip layout automatically when the locale is RTL. Make sure your layout uses start/end instead of left/right:
Row {
Icon(Icons.AutoMirrored.Filled.ArrowForward, null)
Spacer(Modifier.width(8.dp))
Text(stringResource(Res.string.next))
}
Modifier.padding(start = 16.dp, end = 8.dp)
Modifier.padding(PaddingValues(left = 16.dp, right = 8.dp))
Use Icons.AutoMirrored.* for chevrons, back/forward arrows. Leave non-directional icons (add, delete) as-is.
4. Force a layout direction (preview / testing)
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
App()
}
5. Runtime locale switching
The platform locale drives resource selection. To let users switch in-app:
interface LocaleController {
fun setLocale(tag: String)
val current: StateFlow<String>
}
class AndroidLocaleController(private val context: Context) : LocaleController {
override fun setLocale(tag: String) {
AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(tag))
}
}
class IosLocaleController : LocaleController {
override fun setLocale(tag: String) {
NSUserDefaults.standardUserDefaults.setObject(listOf(tag), "AppleLanguages")
}
}
Compose MP 1.7+ re-reads resources when AppCompatDelegate.setApplicationLocales changes on Android. On iOS, either relaunch the Compose view controller or swap the root composable with a key bound to the selected tag.
6. Numbers, dates, currency
Use kotlinx-datetime for dates in commonMain, then format per-locale on the platform:
expect fun formatCurrency(amount: Double, currency: String): String
actual fun formatCurrency(amount: Double, currency: String): String =
java.text.NumberFormat.getCurrencyInstance().apply {
this.currency = java.util.Currency.getInstance(currency)
}.format(amount)
actual fun formatCurrency(amount: Double, currency: String): String {
val f = NSNumberFormatter().apply {
numberStyle = NSNumberFormatterCurrencyStyle
currencyCode = currency
}
return f.stringFromNumber(NSNumber(amount)) ?: "$amount"
}
7. Pseudo-localization
Keep a values-en-rXA/strings.xml with accented + expanded strings ([!!! Ŵélçömé !!!]) to catch clipped layouts during development. Enable with a hidden dev toggle.
Checklist