Introduction
Localization in Android is often treated as an afterthought, but teams shipping to multiple markets quickly learn that language and regional support can make or break adoption. We've seen apps rise or fall based on how well they handle localized content, currency formatting, and right-to-left layouts across different device languages.
In this guide, we'll walk through the practical patterns we've used to ship apps in dozens of languages, from managing strings.xml per locale to handling plurals, formatted strings, per-app language preferences, and right-to-left layout support. These aren't theoretical best practices—they're lessons from real production apps across e-commerce, ride-hailing, and SaaS platforms.
Strings.xml per locale: the foundation
Android's resource qualification system makes it straightforward to support multiple languages. The standard approach is to create a values-LANGUAGE directory for each language code you support.
For English, you keep your default strings in res/values/strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">TravelApp</string>
<string name="welcome_message">Welcome to your journey</string>
<string name="book_now">Book Now</string>
<string name="no_results">No trips available</string>
</resources>For Spanish, you create res/values-es/strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">AplicaciónDeViajes</string>
<string name="welcome_message">Bienvenido a tu viaje</string>
<string name="book_now">Reservar Ahora</string>
<string name="no_results">Sin viajes disponibles</string>
</resources>Android will automatically select the correct strings based on the device's locale. In your code, you reference them the same way regardless of language:
val welcomeText = resources.getString(R.string.welcome_message)In real production apps, we've found several patterns that work well. First, always define all string keys in your default values/strings.xml. Missing keys in a specific locale will fall back to the default language, which saves you from cryptic resource not found errors in production. Second, organize strings by feature or screen to keep large files manageable:
<!-- strings.xml: organized by screen -->
<!-- Authentication strings -->
<string name="auth_login_button">Sign In</string>
<string name="auth_signup_button">Create Account</string>
<!-- Booking strings -->
<string name="booking_confirm">Confirm Booking</string>
<string name="booking_cancel">Cancel Booking</string>For teams working across multiple languages, we recommend using translation management tools like Google Play Console's built-in translation service or third-party platforms like Lokalise or Crowdin. These tools help you track which strings are translated, automate the workflow with translators, and flag missing or outdated translations before release.
Plurals and formatted strings: beyond simple text
Many apps need to handle quantity-dependent text and dynamic values. Android's plurals resource handles the first case elegantly. Instead of checking the quantity yourself, you declare rules for different plural forms in your strings file.
English typically needs only singular and plural forms, but other languages (like Polish, Russian, or Arabic) have more complex plural rules:
<!-- res/values/strings.xml (English) -->
<plurals name="trip_count">
<item quantity="one">%d trip booked</item>
<item quantity="other">%d trips booked</item>
</plurals>
<!-- res/values-pl/strings.xml (Polish - more complex rules) -->
<plurals name="trip_count">
<item quantity="one">%d zarezerwowana podróż</item>
<item quantity="few">%d zarezerwowane podróże</item>
<item quantity="many">%d zarezerwowanych podróży</item>
<item quantity="other">%d zarezerwowanych podróży</item>
</plurals>In code, use getQuantityString() to get the correct plural form:
val message = resources.getQuantityString(R.plurals.trip_count, tripCount, tripCount)For formatted strings with multiple placeholders, Android's string formatting works, but be careful with placeholder order. In English, you might write:
<string name="booking_summary">%1$s from %2$s to %3$s</string>Using numbered placeholders (%1$s, %2$s, %3$s) allows translators to reorder them without breaking your code. This is crucial because languages like Japanese or Arabic might need a different word order:
<!-- res/values-ar/strings.xml -->
<string name="booking_summary">من %2$s إلى %3$s في %1$s</string>In production apps, we've also seen teams use ICU Message Format through the android-icu4j library for truly complex formatting needs—like handling gender, case, and plurals together. For most projects, though, numbered placeholders and the plurals system cover 95% of requirements.
Per-app language preferences: giving users control
Device locale is a good default, but users often want to choose their app language independently of system settings. A user might have their phone set to Chinese but prefer your app in English for work. Implementing a per-app language selector is worth the effort.
The approach involves wrapping your app's base Context with a localized configuration. Create a helper class to manage the language preference:
object LocalizationManager {
private const val PREFS_KEY = "app_language"
private const val DEFAULT_LANGUAGE = "en"
fun getAppLanguage(context: Context): String {
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
return prefs.getString(PREFS_KEY, DEFAULT_LANGUAGE) ?: DEFAULT_LANGUAGE
}
fun setAppLanguage(context: Context, language: String) {
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
prefs.edit().putString(PREFS_KEY, language).apply()
}
fun updateLocale(context: Context, language: String): Context {
val locale = Locale(language)
Locale.setDefault(locale)
val config = Configuration(context.resources.configuration)
config.setLocale(locale)
return context.createConfigurationContext(config)
}
}Override attachBaseContext() in your Application or base Activity to apply the saved language:
class MyApplication : Application() {
override fun attachBaseContext(base: Context) {
val language = LocalizationManager.getAppLanguage(base)
val localizedContext = LocalizationManager.updateLocale(base, language)
super.attachBaseContext(localizedContext)
}
}
class MainActivity : AppCompatActivity() {
override fun attachBaseContext(newBase: Context) {
val language = LocalizationManager.getAppLanguage(newBase)
val localizedContext = LocalizationManager.updateLocale(newBase, language)
super.attachBaseContext(localizedContext)
}
}When the user changes the language in your settings screen, call LocalizationManager.setAppLanguage() and then restart the app or activity to apply the change. Many teams trigger a restart of the main activity stack to ensure all layouts and strings are reloaded cleanly:
fun changeLanguage(newLanguage: String) {
LocalizationManager.setAppLanguage(this, newLanguage)
val intent = Intent(this, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
}In production, we've learned to store the selected language before any UI refresh, so if the app crashes during the restart, the user's choice is still saved. We also provide a "System default" option that respects the device locale, giving users flexibility without forcing a choice.
Right-to-left layout support: more than text direction
Right-to-left languages like Arabic, Hebrew, and Urdu require more than just mirroring text direction. Entire layouts must be flipped: navigation drawers move to the right, buttons align differently, and padding/margins need careful handling.
Android's RTL support is built in, but it requires explicit opt-in. In your AndroidManifest.xml, add the android:supportsRtl attribute:
<application
android:supportsRtl="true"
... >
</application>With RTL enabled, Android will automatically mirror many layout properties—but you must use RTL-aware dimensions. Use start and end instead of left and right:
<!-- Good: RTL-aware -->
<Button
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:paddingStart="8dp"
android:paddingEnd="8dp" />
<!-- Avoid: will not flip in RTL -->
<Button
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:paddingLeft="8dp"
android:paddingRight="8dp" />In your XML layouts, use android:gravity, android:layout_gravity, and android:textAlignment with values like start and end rather than left and right. For drawables, Android provides a android:autoMirrored attribute that flips vector drawables in RTL contexts:
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:autoMirrored="true"
android:width="24dp"
android:height="24dp"
... >
</vector>In code, use ViewCompat.getLayoutDirection(view) to detect RTL at runtime if you need to adjust behavior programmatically:
if (ViewCompat.getLayoutDirection(myView) == ViewCompat.LAYOUT_DIRECTION_RTL) {
// Handle RTL-specific logic
}
We've encountered tricky cases where custom views or complex animations need explicit RTL handling. Test your layouts by enabling "Force RTL layout direction" in Developer Options—this forces Arabic locale behavior even on an English device, making it easy to catch RTL issues during development.
Common mistakes
- Hardcoding strings: Never concatenate strings directly in code. Always use resource strings so translators can adjust them. Even debug messages should come from resources.
- Forgetting fallback keys: If a translated string is missing, Android will crash if the key doesn't exist in the default language. Always ensure your default
strings.xmlhas every key you reference. - Ignoring text length variance: English text is often shorter than German or Spanish equivalents. Design layouts with generous space and test with long strings to avoid truncation or overflow.
- Mixing left/right with start/end: Using both
android:marginLeftandandroid:marginStartcauses confusion. Commit to start/end across your entire codebase. - Assuming locale order: Don't assume a user's language is "en-US" or "es-ES". Let Android handle locale selection and always test with multiple locales.
- Not testing RTL layouts: Many apps claim RTL support but never actually test with an RTL language. Force RTL mode during development and fix layout issues before release.
Key takeaways
- Organize strings by feature in
values/strings.xmland createvalues-LANGUAGEdirectories for each locale; use translation management tools to keep translations in sync with your source. - Use Android's plurals system for quantity-dependent text and numbered placeholders (%1$s, %2$s) for formatted strings so translators can reorder them without breaking code.
- Implement per-app language selection via
attachBaseContext()and SharedPreferences, allowing users to override their device's system locale. - Enable RTL support with
android:supportsRtl="true"and always use start/end padding and margins instead of left/right to ensure layouts flip correctly for Arabic, Hebrew, and other RTL languages. - Test every locale and RTL layout by forcing locale changes in Developer Options; don't assume your translations are complete until you've tested on real devices in each target market.






Comments
Loading comments…