Localization, Plurals, and Right-to-Left Layouts in Compose

Quick answer: Keep every user-facing message in Android resources, read it in Compose with stringResource(), use pluralStringResource() only for real grammatical plurals, and write layouts with start and end rather than left and right. With android:supportsRtl="true", Compose can mirror directional layouts for right-to-left locales. Then test both translated text expansion and RTL—not just English screenshots.

Localization is not a final translation pass. It affects sentence order, grammar, text length, icons, content descriptions, and the direction in which people scan a screen. Compose handles much of that well when UI code describes meaning rather than an English sentence or a physical screen edge.

This guide establishes a workflow that works for ordinary strings, quantities, and right-to-left (RTL) layouts without turning every composable into a locale-specific branch.

Make resources the source of truth for user-visible copy

Put default strings in res/values/strings.xml, then add locale-specific alternatives such as res/values-es/strings.xml or res/values-ar/strings.xml. Android selects the best available resource for the active locale and falls back to the default resource set.

The default set is not optional. Android’s localization guidance requires the unqualified res/values/ resources to contain every resource the app uses; an incomplete default set can fail on an unsupported locale.

<!-- res/values/strings.xml -->
<resources>
    <string name="welcome_user">Welcome, %1$s</string>
    <string name="save_article">Save article</string>
</resources>
<!-- res/values-es/strings.xml -->
<resources>
    <string name="welcome_user">Te damos la bienvenida, %1$s</string>
    <string name="save_article">Guardar artículo</string>
</resources>

Read the resource inside a composable with stringResource():

import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource

@Composable
fun WelcomeMessage(displayName: String) {
    Text(
        text = stringResource(
            R.string.welcome_user,
            displayName,
        ),
    )
}

Positional placeholders such as %1$s matter. A translator can move the name to the grammatically correct place without needing an English-shaped string concatenation in Kotlin. Avoid patterns such as "Welcome, " + displayName; they split one sentence into separately translated fragments and make the final word order impossible to control.

The same rule includes labels, error messages, button text, and contentDescription values. For a focused guide to meaningful, localized descriptions, see Accessibility in Jetpack Compose: Content Descriptions Done Right.

Give translators enough context

Resource IDs tell a developer where to find text, but not necessarily how it appears. A string such as continue can mean a button action, a reading prompt, or an error-recovery step. Add a short XML comment when the UI role, space constraint, or variables could be ambiguous.

<!-- Button action; continues the account-setup flow. Keep concise. -->
<string name="account_setup_continue">Continue</string>

Also identify text that must remain unchanged, such as a product name, a command, a URL, or a code token. Do not hide that information in variable names or assume a translator can infer it from a one-word English source string. Android’s string-resource guidance recommends providing context because it improves translation quality and makes resource maintenance clearer.

Use plurals for grammar, not for product state

Languages do not share English’s simple one-versus-many rule. Android quantity resources support zero, one, two, few, many, and other; the active locale determines which form applies. Supply one and other in the default resource, then let translators add the forms their language requires.

<!-- res/values/strings.xml -->
<resources>
    <plurals name="downloads_available">
        <item quantity="one">%1$d download available</item>
        <item quantity="other">%1$d downloads available</item>
    </plurals>
</resources>

Inside Compose, the first count chooses the grammatical form. If the string also displays that number, pass it again as the formatting argument:

import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.pluralStringResource

@Composable
fun DownloadCount(count: Int) {
    Text(
        text = pluralStringResource(
            R.plurals.downloads_available,
            count,
            count,
        ),
    )
}

The Compose resources documentation currently labels pluralStringResource() experimental, so confirm its status against the Compose version in your project before standardizing on it. The Android quantity-string documentation also explains the important constraint: a plural resource is for grammatical agreement, not arbitrary UI states.

For example, Inbox and Inbox (12) are different product states, not singular and plural forms. A quantity-neutral string such as Downloads: 12 can be easier to translate if it fits the product’s voice. If the message does not show a quantity at all, it is usually a poor candidate for a plural resource.

Let RTL layouts mirror through directional APIs

Arabic, Hebrew, Persian, and other RTL scripts require more than translated copy. The order and direction of a screen must feel natural too. Compose is designed around directional concepts: start, end, Alignment.Start, Alignment.End, and ordinary Row arrangement respond to the current LayoutDirection.

Enable RTL at the application level:

<application
    android:name=".App"
    android:supportsRtl="true"
    ... />

Then use directional spacing and alignment in composables:

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun ArticleSummary(title: String, summary: String) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(start = 20.dp, top = 16.dp, end = 20.dp, bottom = 16.dp),
        horizontalAlignment = Alignment.Start,
        verticalArrangement = Arrangement.spacedBy(8.dp),
    ) {
        Text(title)
        Text(summary)
    }
}

When the locale is RTL, start becomes the physical right edge and end becomes the physical left edge. The Compose RTL guidance confirms that directional Compose APIs mirror automatically once the app opts into RTL support.

Avoid absolute left and right choices for content that has reading direction. Reserve absolute positioning only for a genuinely physical meaning, such as a map coordinate or media timeline. If you must inspect or override the current layout direction in a narrow subtree, LocalLayoutDirection provides the current LayoutDirection; do not use it to reimplement the automatic behavior that normal directional APIs already provide.

Mirror directional icons, not every icon

An arrow that means “Back,” “Forward,” “Send,” or “Exit” generally follows the reading direction. A play icon, a settings gear, and a brand mark normally do not. Use Compose’s auto-mirrored Material icon set for the directional category.

import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource

@Composable
fun NavigateUpButton(onNavigateUp: () -> Unit) {
    IconButton(onClick = onNavigateUp) {
        Icon(
            imageVector = Icons.AutoMirrored.Filled.ArrowBack,
            contentDescription = stringResource(R.string.navigate_up),
        )
    }
}

Icons.AutoMirrored mirrors the supported directional icons for RTL layouts. The API reference documents the available sets and notes that only icons whose meaning follows layout direction belong there. Do not flip visual assets automatically just because the locale is RTL; review what the image actually communicates.

Handle mixed-direction text deliberately

An RTL sentence can include an LTR URL, code identifier, number, or product name. Compose normally uses TextDirection.Content, which infers text direction from the first strong directional character and follows the Unicode bidirectional algorithm. That default is correct for most mixed content.

Only override it for an isolated element with a real fixed-direction requirement, such as a code token or a long technical identifier. For example, a code snippet should remain LTR even when it appears in an Arabic settings screen:

import androidx.compose.material3.Text
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextDirection

Text(
    text = "./gradlew assembleDebug",
    style = TextStyle(textDirection = TextDirection.Ltr),
)

For normal prose, forcing direction is usually a bug: it can make punctuation, inline numbers, and translated content harder to read. Keep dynamic values in a full localized resource whenever possible instead of manually assembling a sentence around them.

Test language, expansion, and direction as separate risks

English text fitting in a layout says little about German text expansion, Arabic direction, or a sentence whose arguments reorder. Build localization checks into ordinary UI review.

import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview

@Preview(name = "French", locale = "fr-rFR", showBackground = true)
@Composable
private fun ArticleSummaryFrenchPreview() {
    AppTheme {
        ArticleSummary(
            title = "Titre d’article volontairement plus long",
            summary = "Un résumé assez long pour vérifier que la mise en page peut grandir.",
        )
    }
}

@Preview(name = "Arabic RTL", locale = "ar", showBackground = true)
@Composable
private fun ArticleSummaryArabicPreview() {
    AppTheme {
        ArticleSummary(
            title = "عنوان طويل للمقال",
            summary = "ملخص لاختبار اتجاه التخطيط والنص.",
        )
    }
}

The Preview API supports the locale parameter, including RTL locales. Add large font scale to the review as well: translated text and accessibility text scaling compound each other. Support Large Font Sizes and Font Scaling in Compose covers layouts that reflow instead of clipping when text grows.

Pseudolocales expose problems before translations arrive. English (XA) expands and accents localizable text, while AR (XB) simulates RTL. Android’s pseudolocale documentation specifically calls out hardcoded strings, concatenation, text expansion, bidirectional content, and unmirrored elements as issues they can reveal.

Localization review checklist

  • Keep complete default resources and move every user-visible message out of Kotlin.
  • Use positional placeholders instead of concatenation, and give translators useful context.
  • Use plurals only when grammar depends on a visible quantity; test counts beyond 0, 1, and 2 in each supported language.
  • Turn on android:supportsRtl, then use start/end, directional alignment, and auto-mirrored icons where their meaning calls for it.
  • Check translated content at large font sizes, in a narrow window, and in RTL—not only in a wide English preview.
  • Run pseudolocale checks before translation handoff, then test real translated builds with native readers when possible.

Compose removes much of the mechanics of localization, but it cannot fix source strings that assume English grammar or a layout that treats the left edge as universal. Keep copy, grammar, and direction as first-class parts of the UI contract, and each new locale becomes an extension of a sound design rather than a separate implementation.