Localization, RTL, Formatting & Resource Management
Internationalize your Android app the right way — strings, plurals, RTL, and locale-aware formatting
Open interactive version (quiz + challenge)Real-world analogy
What is it?
Localization (l10n) is the process of adapting an app for a specific locale — language, region, and cultural conventions. Internationalization (i18n) is designing the app so localization is possible. In Android, this means string resources, locale-aware formatting APIs, RTL layout support, and a structured translation workflow.
Real-world relevance
WhatsApp supports 60+ languages and 180+ countries. Its strings.xml has thousands of entries managed through an internal TMS. Duolingo, serving 40 languages, uses Android pseudolocales in their CI pipeline to catch layout bugs before translation is even complete. Google Maps switches street name direction and text alignment dynamically based on the map region's primary script.
Key points
- strings.xml — the foundation — All user-visible text lives in res/values/strings.xml. Locale-specific overrides go in res/values-fr/strings.xml, res/values-ar/strings.xml, etc. Android resolves the best-match file at runtime. Never hardcode strings in code — they cannot be translated.
- Plurals — quantity strings — Use in strings.xml for quantity-dependent text (e.g. '1 item' vs '2 items'). Quantities: zero, one, two, few, many, other. Load with resources.getQuantityString(R.plurals.item_count, count, count). Arabic has six plural forms — plurals.xml handles them all without code changes.
- String arrays — for lists of items — Use in strings.xml for ordered lists (e.g. day names, error messages). Load with resources.getStringArray(R.array.days). These are also locale-overridable.
- Locale-specific resources beyond strings — Any resource folder can be locale-qualified: res/drawable-ar/ for RTL images (mirrored icons), res/layout-land-fr/ for French landscape layouts. Android's resource qualifier system supports language, region, layout direction, screen size, density, and more — combinable.
- RTL layout support — Set android:supportsRtl='true' in AndroidManifest.xml. Use start/end instead of left/right in XML (paddingStart, layout_marginEnd). Compose uses Modifier.padding(start=) or LayoutDirection from LocalLayoutDirection. RTL is automatic when the user's locale is Arabic, Hebrew, or Persian.
- Compose and string resources — Use stringResource(R.string.label) in Compose — never context.getString() in composables. For plural strings: pluralStringResource(R.plurals.item_count, count, count). These are composable-safe and work correctly across recompositions and locale changes.
- Date and time formatting — DateTimeFormatter and android.text.format — Never hardcode date formats. Use DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) with the user's Locale, or DateFormat.getDateFormat(context). These adapt to locale conventions: MM/dd/yyyy (US) vs dd/MM/yyyy (UK) vs yyyy-MM-dd (ISO). Joda-Time and ThreeTenABP are legacy — use java.time (API 26+) or kotlinx-datetime.
- Number and currency formatting — Use NumberFormat.getCurrencyInstance(locale) for money, NumberFormat.getNumberInstance(locale) for decimals. In India, 1,00,000 is one lakh; in the US it is 100,000. In Germany, the decimal separator is a comma: 1.234,56 EUR. Always format with the user's locale, not Locale.US.
- Compose ProvideTextStyle and LocalConfiguration — LocalConfiguration.current.locales.get(0) gives the current locale in Compose without a context. Wrap locale-sensitive composables in CompositionLocalProvider if you need to force a specific locale for previews or testing.
- Translation workflow — Export strings.xml to translators via Android Studio's Translations Editor or integrate with a TMS (Crowdin, Phrase, Lokalise). The Translations Editor shows all languages in a grid, flags missing translations, and exports XLIFF files. Crowdin integrates with GitHub Actions for automatic pull request updates with new translations.
- Pseudolocales — en-XA and ar-XB — Android ships two pseudolocales for testing. en-XA adds accents and expands strings by 30% — reveals truncation bugs. ar-XB forces RTL layout — reveals start/end vs left/right bugs. Enable them in Developer Options > Language. Test every screen with both before shipping.
- App language picker (Android 13+) — Android 13 introduced per-app language preferences (AppCompatDelegate.setApplicationLocales()). Users can set an app to French while keeping the phone in English. Support this with the LocaleListCompat API and declare supported locales in res/xml/locales_config.xml.
Code example
<!-- res/values/strings.xml -->
<resources>
<string name="welcome_message">Welcome, %1$s!</string>
<string name="item_count_label">Items in cart</string>
<plurals name="item_count">
<item quantity="one">%d item</item>
<item quantity="other">%d items</item>
</plurals>
<string-array name="sort_options">
<item>Newest first</item>
<item>Price: low to high</item>
<item>Price: high to low</item>
</string-array>
</resources>
<!-- res/values-ar/strings.xml (Arabic overrides) -->
<!-- <string name="welcome_message">مرحباً، %1$s!</string> -->
// ── Compose: locale-aware string and number formatting ─────────
@Composable
fun OrderSummary(
userName: String,
itemCount: Int,
totalPrice: Double
) {
val locale = LocalConfiguration.current.locales.get(0)
val formattedPrice = remember(totalPrice, locale) {
NumberFormat.getCurrencyInstance(locale).format(totalPrice)
}
Column(modifier = Modifier.padding(16.dp)) {
Text(stringResource(R.string.welcome_message, userName))
Text(
pluralStringResource(
id = R.plurals.item_count,
count = itemCount,
itemCount
)
)
Text(text = formattedPrice)
}
}
// ── Date formatting ───────────────────────────────────────────
fun formatOrderDate(epochMillis: Long, locale: java.util.Locale): String {
val formatter = DateTimeFormatter
.ofLocalizedDate(FormatStyle.MEDIUM)
.withLocale(locale)
val date = Instant.ofEpochMilli(epochMillis)
.atZone(ZoneId.systemDefault())
.toLocalDate()
return formatter.format(date)
}Line-by-line walkthrough
- 1. strings.xml welcome_message uses %1$s positional format — positional arguments are mandatory in Android strings because translation word order may differ from English
- 2. The plurals element item_count has quantity='one' and quantity='other' — Android picks the right form based on the integer passed to getQuantityString
- 3. The Arabic values-ar/strings.xml override automatically applies when the device locale is Arabic — zero code changes needed
- 4. LocalConfiguration.current.locales.get(0) retrieves the current locale inside a Composable without needing a Context parameter
- 5. remember(totalPrice, locale) caches the formatted price string — NumberFormat.format() is not free; avoid calling it on every recomposition
- 6. pluralStringResource takes the plural resource ID, the count for form selection, and vararg format args — both the count for form and the display number must be passed
- 7. DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(locale) produces locale-correct date strings automatically — no manual format patterns needed
- 8. Instant.ofEpochMilli().atZone(ZoneId.systemDefault()).toLocalDate() correctly converts epoch millis to a local calendar date accounting for the user's timezone
Spot the bug
@Composable
fun CartBadge(count: Int) {
val message = if (count == 1) {
"$count item in cart"
} else {
"$count items in cart"
}
Text(text = message)
}Need a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Localize your app — Android Guide (Android Developers)
- Support different languages and cultures (Android Developers)
- Per-app language preferences (Android 13+) (Android Developers)
- String resources — plurals (Android Developers)
- Pseudolocales for testing (Android Developers)