Lesson 51 of 83 intermediate

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

Localization is like building a restaurant menu. You don't rewrite the kitchen — you just print the menu in French, Arabic, or Japanese. But Arabic menus open from the right, prices show local currency symbols, and dates appear in local order. The kitchen (your code) stays the same; only the presentation adapts.

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

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. 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. 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. 3. The Arabic values-ar/strings.xml override automatically applies when the device locale is Arabic — zero code changes needed
  4. 4. LocalConfiguration.current.locales.get(0) retrieves the current locale inside a Composable without needing a Context parameter
  5. 5. remember(totalPrice, locale) caches the formatted price string — NumberFormat.format() is not free; avoid calling it on every recomposition
  6. 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. 7. DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(locale) produces locale-correct date strings automatically — no manual format patterns needed
  8. 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?
This works in English. What breaks in Arabic, Polish, or Croatian — languages with more than two plural forms?
Show answer
The code hardcodes English plural logic (1 = singular, else = plural). Arabic has six plural forms, Polish has four, and Croatian has three. For Arabic, 21 items should use the 'one' form, 11 items should use the 'many' form, and 100 items the 'other' form — none of which this code handles correctly. Fix: define a <plurals name='cart_item_count'> in strings.xml with all required quantity forms, and use pluralStringResource(R.plurals.cart_item_count, count, count) in Compose. Android's plural system uses CLDR plural rules per locale, handling all cases automatically. Additionally, embedding string fragments in Kotlin string templates makes translation impossible — the full sentence must be in strings.xml.

Explain like I'm 5

Imagine your app is a news presenter. For different countries, the presenter speaks a different language, shows the date the local way, and stands on the right side of the screen for Arabic news. The news itself (your code logic) doesn't change — only how it is presented to each audience does.

Fun fact

Arabic has six grammatical plural forms (zero, one, two, few, many, other) while English has only two (one, other). Android's supports all six. Apps that use String.format('%d items', count) for Arabic UI always show the wrong plural form — a small but embarrassing bug.

Hands-on challenge

Add a plurals resource for 'unread_notifications' (0, 1, N forms). Load it in a Compose composable using pluralStringResource. Then format a given epoch timestamp using DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT, FormatStyle.SHORT) and display it alongside the notification count, respecting the current device locale.

More resources

Open interactive version (quiz + challenge) ← Back to course: Android Interview Mastery