Setting up Localization in Compose Multiplatform App

May 06, 2025

Introduction

Modern mobile apps need to handle localization in two ways:
  1. System-level Language Settings: Both iOS and newer versions of Android allow users to change app language directly from system settings. This is the preferred method for many users as it provides a centralized way to manage language preferences across all apps.
  2. In-app Language Settings: Apps should also provide the ability to change language within the app itself. This is useful for:
    • Allowing users to switch languages without leaving the app
    • Providing language selection during onboarding
    • Offering a more accessible way to change language for users who might not be familiar with system settings
The challenge is to keep these two methods in sync - when a user changes the language in system settings, the app should reflect that change, and vice versa. This guide shows how to implement a robust localization system that handles both scenarios while maintaining consistency across the app.

1. Project Structure

First, create the following directory structure in your project:
composeApp/src/commonMain/ ├── composeResources/ │ ├── values/ │ │ └── strings.xml │ └── values-bn/ │ └── strings.xml

2. Define Supported Languages

Create an enum class to define supported languages:
enum class AppLanguage( val code: String, val label: String ) { English("en-US", "English"), Bangla("bn-BD", "বাংলা") }

3. Create String Resources

English (values/strings.xml)

<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">My app</string> <string name="welcome_message">আমার অ্যাপে স্বাগতম</string> <!-- Add more strings here --> </resources>

Bangla (values-bn/strings.xml)

<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">আমার অ্যাপ</string> <string name="welcome_message">Welcome to my app</string> <!-- Add more strings here --> </resources>

4. Platform-Specific Language Implementation

The getAppLanguageShortCode() and changeAppLanguage() functions are implemented differently for each platform to get and set the current app language:

Common Interface (expect)

expect fun getAppLanguageShortCode(): String? expect fun changeAppLanguage(language: AppLanguage)

iOS Implementation (actual)

actual fun getAppLanguageShortCode(): String? { return NSLocale.currentLocale.languageCode } actual fun changeAppLanguage(language: AppLanguage) { NSUserDefaults.standardUserDefaults.setObject(listOf(language.code), forKey = "AppleLanguages") }
The iOS implementation uses NSLocale.currentLocale.languageCode to get the current system language code. For changing the language, it uses NSUserDefaults to set the preferred languages list, as the iOS system stores the app lanaguage in NSUserDefaults with a key: AppleLanguages.

Android Implementation (actual)

actual fun getAppLanguageShortCode(): String? { return AppCompatDelegate.getApplicationLocales() .toLanguageTags() .takeIf { it.isNotEmpty() }?.split('-') ?.firstOrNull() } actual fun changeAppLanguage(language: AppLanguage) { val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(language.code) AppCompatDelegate.setApplicationLocales(appLocale) }

Android-Specific Requirements

For the Android implementation (setApplicationLocales) to work properly, you need to ensure:
  1. Your MainActivity extends AppCompatActivity:
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Your activity code } }
  1. Your app theme extends an AppCompat theme in res/values/themes.xml:
<?xml version="1.0" encoding="utf-8"?> <resources> <style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar"> <!-- Your theme customizations --> </style> </resources>
  1. Apply the theme in your AndroidManifest.xml:
<application android:theme="@style/AppTheme" ...> <activity android:name=".MainActivity" ...> </activity> </application>
These requirements are necessary because AppCompatDelegate is part of the AndroidX AppCompat library, and the localization features require the proper AppCompat integration to function correctly.
While we’re at it, let’s add this service so that it works on Android 12 or older:
<application android:theme="@style/AppTheme" ...> ... <service android:name="androidx.appcompat.app.AppLocalesMetadataHolderService" android:enabled="false" android:exported="false"> <meta-data android:name="autoStoreLocales" android:value="true" /> </service> </activity> </application>
Learn more about why this is needed.

5. Provide platform with the available languages

iOS

Add the available language codes to your info.plist file like so
<key>CFBundleLocalizations</key> <array> <string>bn</string> <string>en</string> </array>

Android

On Android, the available languages are generated automatically from the string resources, when you add generateLocaleConfig = true in the gradle file.
In build.gradle.kts of commonMain add:
android { //.... androidResources { generateLocaleConfig = true localeFilters += listOf("en", "bn") } //... }
localeFilters makes sure no other locale resources (e.g. from a third-party library) get added to the app bundle.

6. Using CompositionLocal for Language Management

To make the app language accessible throughout the app and allow changing it from the settings screen, we can use CompositionLocal with mutable state. Here’s how to implement it:

1. Define the CompositionLocal

First, create a CompositionLocal for the app language:
val LocalAppLanguage = compositionLocalOf<AppLanguage> { error("LocalAppLanguage not provided") }

2. Provide the Language State at the Root

In your root composable (usually in your navigation setup), provide the language state:
@Composable fun App() { val appLanguage: MutableState<AppLanguage> = remember { mutableStateOf(codeToLanguage(getAppLanguageShortCode())) } CompositionLocalProvider(LocalAppLanguage provides appLanguage.value) { AppScaffold(changeLanguage = { appLanguage.value = it }) } } @Composable fun AppScaffold( changeLanguage: (AppLanguage) -> Unit = {} ){ Scaffold( //... ) { innerPadding -> NavHost( //.... ) { composable(route = "settings") { SettingsScreen(changeLanguage = changeLanguage) } } } } fun codeToLanguage(shortCode: String?): AppLanguage { if (shortCode == null) return AppLanguage.Bangla // Default return when (shortCode) { "bn" -> AppLanguage.Bangla else -> AppLanguage.English } }

3. Access and Change Language from Any Screen

You can now access and change the language from any screen in your app:
@Composable fun SettingsScreen( changeLanguage: (AppLanguage) -> Unit = {} ) { val currentLanguage = LocalAppLanguage.current Column( modifier = Modifier .fillMaxSize() .padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { Text( text = "Current Language: ${currentLanguage.label}", style = MaterialTheme.typography.titleMedium ) Button( onClick = { changeLanguage(AppLanguage.English) }, enabled = currentLanguage != AppLanguage.English, modifier = Modifier.fillMaxWidth() ) { Text("Switch to English") } Button( onClick = { changeLanguage(AppLanguage.Bangla) }, enabled = currentLanguage != AppLanguage.Bangla, modifier = Modifier.fillMaxWidth() ) { Text("বাংলায় পরিবর্তন করুন") } } }