Compose Multiplatform: Localization in Jetpack Compose

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.