References: Compose Multiplatform Resources | Android Localization
Define all user-facing strings in commonMain/composeResources/values/strings.xml. Never hardcode strings in Composables:
<!-- commonMain/composeResources/values/strings.xml -->
<resources>
<string name="app_name">My App</string>
<string name="home_title">Welcome, %1$s</string>
<string name="items_count">%1$d items found</string>
<string name="error_no_connection">No internet connection. Please check your network.</string>
<string name="action_retry">Retry</string>
<string name="action_cancel">Cancel</string>
<string name="content_desc_loading">Loading</string>
<string name="content_desc_close">Close</string>
</resources>
Add locale-specific overrides in separate directories:
commonMain/composeResources/
├── values/
│ └── strings.xml ← default (English)
├── values-es/
│ └── strings.xml ← Spanish
├── values-fr/
│ └── strings.xml ← French
├── values-ar/
│ └── strings.xml ← Arabic (RTL)
└── values-ja/
└── strings.xml ← Japanese
Use stringResource() in Composables — never access raw string IDs:
import org.jetbrains.compose.resources.stringResource
import com.example.shared.generated.resources.Res
import com.example.shared.generated.resources.*
@Composable
fun WelcomeHeader(userName: String) {
Text(text = stringResource(Res.string.home_title, userName))
}
@Composable
fun ErrorMessage(onRetry: () -> Unit) {
Column {
Text(text = stringResource(Res.string.error_no_connection))
Button(onClick = onRetry) {
Text(text = stringResource(Res.string.action_retry))
}
}
}
Use pluralStringResource for quantities that change grammatically:
<!-- commonMain/composeResources/values/strings.xml -->
<resources>
<plurals name="items_count">
<item quantity="one">%1$d item</item>
<item quantity="other">%1$d items</item>
</plurals>
<plurals name="messages_unread">
<item quantity="zero">No unread messages</item>
<item quantity="one">%1$d unread message</item>
<item quantity="other">%1$d unread messages</item>
</plurals>
</resources>
import org.jetbrains.compose.resources.pluralStringResource
@Composable
fun ItemCount(count: Int) {
Text(text = pluralStringResource(Res.plurals.items_count, count, count))
}
Use positional arguments (%1$s, %2$d) — never concatenate strings because word order differs by language:
// GOOD — position-based, translatable
Text(text = stringResource(Res.string.home_title, userName)) // "Welcome, Alice"
// BAD — order is wrong in many languages
Text(text = "Welcome, $userName")
Text(text = stringResource(Res.string.welcome) + userName)
For complex formatting (dates, numbers, currency), use kotlinx-datetime + platform formatters:
// commonMain — locale-aware date formatting
fun formatDate(date: LocalDate, locale: String = "en"): String {
// Format using ISO-8601 as a safe default
return "${date.month.name.lowercase().replaceFirstChar { it.uppercase() }} ${date.dayOfMonth}, ${date.year}"
}
// androidMain — use Android's DateFormat for locale-aware formatting
actual fun formatDateLocalized(date: LocalDate): String {
val javaDate = java.util.Date.from(
date.atStartOfDayIn(TimeZone.currentSystemDefault()).toJavaInstant()
)
return android.text.format.DateFormat.getDateFormat(context).format(javaDate)
}
Compose handles RTL automatically when the system locale is RTL (Arabic, Hebrew, Farsi). Key rules:
start/end instead of left/right// GOOD — mirrors correctly in RTL
Text(modifier = Modifier.padding(start = 16.dp, end = 8.dp))
Row(horizontalArrangement = Arrangement.Start)
Alignment.TopStart
// BAD — does not mirror in RTL
Text(modifier = Modifier.padding(left = 16.dp, right = 8.dp))
Row(horizontalArrangement = Arrangement.AbsoluteLeft)
Alignment.TopLeft
Text(
text = phoneNumber,
modifier = Modifier.semantics { this.layoutDirection = LayoutDirection.Ltr }
)
@Preview(locale = "ar")
@Composable
private fun HomeScreenPreview_RTL() {
AppTheme {
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
HomeContent(uiState = HomeUiState(), onAction = {})
}
}
}
Use AutoMirrored icons for directional icons (arrows, back, forward) that should flip in RTL:
// GOOD — mirrors automatically in RTL
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
Icon(Icons.AutoMirrored.Filled.Send, contentDescription = null)
// BAD — same direction in both LTR and RTL
Icon(Icons.Default.ArrowBack, contentDescription = null)
Support changing the app language at runtime without restarting:
// androidMain — update locale via AppCompatDelegate
actual fun setAppLocale(languageCode: String) {
AppCompatDelegate.setApplicationLocales(
LocaleListCompat.forLanguageTags(languageCode)
)
// Activity will be recreated automatically — Compose re-renders with new locale
}
<!-- AndroidManifest.xml — declare per-app language support -->
<application>
<service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
android:enabled="false"
android:exported="false">
<meta-data
android:name="autoStoreLocales"
android:value="true" />
</service>
</application>
Store the user's language preference in DataStore and apply on app startup:
class LocaleRepository(private val dataStore: DataStore<Preferences>) {
private val KEY_LANGUAGE = stringPreferencesKey("language_code")
val languageCode: Flow<String> = dataStore.data
.map { prefs -> prefs[KEY_LANGUAGE] ?: "system" }
suspend fun setLanguage(code: String) {
dataStore.edit { prefs -> prefs[KEY_LANGUAGE] = code }
}
}
Mark strings that must not be translated (product names, trademarks) with translatable="false":
<resources>
<string name="app_name" translatable="false">MyApp</string>
<string name="company_name" translatable="false">Acme Corp</string>
</resources>
Never format numbers or currencies by hand — use locale-aware formatters:
// androidMain
actual fun formatCurrency(amount: Double, currencyCode: String): String {
val format = java.text.NumberFormat.getCurrencyInstance()
format.currency = java.util.Currency.getInstance(currencyCode)
return format.format(amount)
}
actual fun formatNumber(number: Double): String {
return java.text.NumberFormat.getNumberInstance().format(number)
}
// iosMain
import platform.Foundation.*
actual fun formatCurrency(amount: Double, currencyCode: String): String {
val formatter = NSNumberFormatter()
formatter.numberStyle = NSNumberFormatterCurrencyStyle
formatter.currencyCode = currencyCode
return formatter.stringFromNumber(NSNumber(double = amount)) ?: amount.toString()
}
stringResource() — never hardcode user-facing strings in Composables%1$s) — never string concatenation for translatable contentstart/end padding and alignment — never left/right for locale-mirroringAutoMirrored icons for directional iconsar, he) and font scale 1.5x in every screen previewtranslatable="false" to prevent translator confusionif (count == 1) "item" else "items"