Jetpack Compose has revolutionized Android UI development with its declarative approach, but its impact extends beyond the Android ecosystem. Compose Multiplatform enables you to write UIs that run on Android, iOS, desktop (JVM), and the web—all from a single codebase. Localization (l10n), or internationalization, is essential for reaching a global audience. This comprehensive guide will explore how to implement robust multiplatform localization in Jetpack Compose.
What is Compose Multiplatform?
Compose Multiplatform, powered by Kotlin Multiplatform, is a declarative UI framework for building applications that can run on various platforms (Android, iOS, desktop, and web) from a shared codebase. This approach significantly reduces development time and maintenance costs.
Why is Localization Important in Compose Multiplatform?
- Global Reach: Ensures your application is accessible to users worldwide.
- Enhanced User Experience: Delivers content in users’ native languages, improving engagement and satisfaction.
- Business Expansion: Opens your product to new markets, increasing potential revenue.
Implementing Localization in Compose Multiplatform: A Step-by-Step Guide
Localization in Compose Multiplatform involves several key steps, including setting up resource files, accessing localized strings, and handling dynamic locale changes.
Step 1: Set Up the Project
Begin by setting up a Compose Multiplatform project in IntelliJ IDEA with the Kotlin Multiplatform plugin.
plugins {
id("org.jetbrains.compose") version "1.5.1"
kotlin("multiplatform") version "1.9.20"
}
kotlin {
androidTarget {
compilations.all {
kotlinOptions {
jvmTarget = "1.8"
}
}
}
jvm("desktop")
sourceSets {
val commonMain by getting {
dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material)
implementation(compose.uiToolingPreview)
}
}
val androidMain by getting {
dependencies {
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.11.0")
implementation(compose.uiTooling)
}
}
val desktopMain by getting {
dependencies {
implementation(compose.desktop.common)
implementation(compose.desktop.currentOs)
}
}
}
}
compose {
kotlinCompilerExtensionVersion = "1.5.1"
}
Step 2: Create Resource Folders
Create separate resource directories for each target platform in the appropriate src
directories. Organize these directories by locale.
src/
├── commonMain/
│ └── resources/
│ └── strings/
│ └── strings.properties # Default language strings
├── androidMain/
│ └── res/
│ └── values-en/
│ └── strings.xml
│ └── values-fr/
│ └── strings.xml
├── desktopMain/
│ └── resources/
│ └── strings/
│ └── strings_en.properties
│ └── strings_fr.properties
Step 3: Define String Resources
Each resource file will contain key-value pairs for localized strings.
Example commonMain/resources/strings/strings.properties
:
greeting=Hello, %s!
app_name=My Application
Example androidMain/res/values-fr/strings.xml
:
<resources>
<string name="greeting">Bonjour, %s!</string>
<string name="app_name">Mon Application</string>
</resources>
Example desktopMain/resources/strings/strings_fr.properties
:
greeting=Bonjour, %s!
app_name=Mon Application
Step 4: Access Localized Strings
Implement a utility function to retrieve localized strings based on the platform.
In commonMain
:
import dev.icerock.moko.resources.StringResource
import dev.icerock.moko.resources.desc.ResourceStringDesc
import dev.icerock.moko.resources.desc.StringDesc
expect fun getStringResource(stringResource: StringResource, vararg args: Any): String
Android Implementation (androidMain
):
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import dev.icerock.moko.resources.StringResource
actual fun getStringResource(stringResource: StringResource, vararg args: Any): String {
val context = LocalContext.current
val resId = stringResource.resourceId
return if (args.isEmpty()) {
context.getString(resId)
} else {
context.getString(resId, *args)
}
}
Desktop Implementation (desktopMain
):
import dev.icerock.moko.resources.StringResource
import java.util.PropertyResourceBundle
import java.util.ResourceBundle
import java.util.Locale
actual fun getStringResource(stringResource: StringResource, vararg args: Any): String {
val bundle: ResourceBundle = ResourceBundle.getBundle("strings/strings", Locale.getDefault())
val key = stringResource.key
val value = bundle.getString(key)
return if (args.isEmpty()) {
value
} else {
String.format(value, *args)
}
}
Example StringResource
usage:
import dev.icerock.moko.resources.StringResource
val greetingResource = StringResource(
key = "greeting",
resourceId = dev.icerock.moko.resources.R.string.greeting
)
Step 5: Use Localized Strings in Compose
Integrate the utility function within your Compose UI to display localized content.
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
@Composable
fun Greeting(name: String, stringResource: StringResource) {
Text(text = getStringResource(stringResource, name))
}
@Composable
fun App(greetingResource : StringResource) {
Greeting(name = "User", greetingResource)
}
Step 6: Handling Locale Changes
To handle runtime locale changes, you can utilize a reactive approach with State
in Compose.
Create a method that observes locale changes:
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import java.util.Locale
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
@Composable
fun rememberLocale(observeLocaleChanges: Flow<Locale>): Locale {
val currentLocale = remember { mutableStateOf(Locale.getDefault()) }
LaunchedEffect(observeLocaleChanges) {
observeLocaleChanges.collect { newLocale ->
currentLocale.value = newLocale
}
}
return currentLocale.value
}
// Example usage in Android
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import java.util.Locale
@Composable
fun observeLocaleChanges(): Flow<Locale> {
val context = LocalContext.current
return callbackFlow {
val receiver = object : android.content.BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_LOCALE_CHANGED) {
trySend(context.resources.configuration.locales[0])
}
}
}
context.registerReceiver(receiver, IntentFilter(Intent.ACTION_LOCALE_CHANGED))
awaitClose { context.unregisterReceiver(receiver) }
}
}
Advanced Techniques for Localization
1. ICU MessageFormat
Consider using ICU MessageFormat for more sophisticated handling of plurals, genders, and selection rules in localized strings.
2. Resource Management Libraries
Explore libraries like `moko-resources` from IceRock, which offer convenient resource management and type-safe access to resources in Kotlin Multiplatform projects. Example has been added for common method getStringResource on resources for mobile
implementation("dev.icerock.moko:resources:0.23.0")
ksp("dev.icerock.moko:ksp:0.23.0")
3. Testing Localization
Automate UI testing for different locales to ensure correctness.
Conclusion
Implementing effective localization in Compose Multiplatform requires careful planning, consistent resource management, and robust runtime handling. By following the guidelines and best practices outlined in this guide, you can create truly global applications that deliver exceptional user experiences on every platform.