Switching locales with Jetpack Compose

Learn how to easily allow users of your app to switch languages in Android apps.

I’ve already published an article on how it’s important to allow users to switch languages. There’s plenty of reasons to do so.

Now, let’s see how simple it can be with Jetpack Compose to implement custom language switching.

Prepare your app

Be sure that you have all strings in res/values/strings.xml and whenever you need them, get them using Android’s standard mechanism getString().

It’s best to use English as the base language as it works best with translation tools, and it’s usually easier to find collaborators and volunteers.

And that’s all. You don’t need to care about XML files in other languages. We use a localization platform for that.

Manage translations

Once you have your strings.xml file ready, sign up for Localazy and follow Android integration instruction. It’s simple as it only means a few lines to be added to your root’s and app’sbuild.gradle — no need to change source code or resources.

All changes you will need to do will be as simple as:

add Localazy to the root’s build.gradle file:

buildscript {

    repositories {
        // ...
        maven { url "https://maven.localazy.com/repository/release/" }
    }

    dependencies {
        // ...
        classpath "com.localazy:gradle:1.5.2"
    }

}

Add Localazy to the app’s build.gradle file:

apply plugin: 'com.localazy.gradle'

localazy {
    readKey "your-read-key"
    writeKey "your-write-key"
}

And that’s it! Now, you can upload strings using the uploadStrings task using Gradle. It’s available on the command line:

./gradlew uploadStrings

And also in the Gradle view in Android Studio.

|Gradle View in Android Studio

From that moment on, you can manage translations easily using Localazy, and there are also shared translations to translate a huge portion of your app to up to 80 languages for free.

Jetpack: Hello World

To test how our locale switching works, let’s create a simple Hello World app.

Here goes a code for a composable, a text, rendered in the center of the screen:

@Composable
fun WelcomeText(text: String) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .fillMaxHeight(),
        verticalArrangement = Arrangement.Center
    ) {
        Text(
            text = text,
            modifier = Modifier
                .gravity(Alignment.CenterHorizontally)
        )
    }
}

To allow users to switch their language, we can use the floating action button. We can add it to our activity and give it an action - to open SwitchActivity that we will discuss later.

class MainActivity : AppCompatActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Scaffold(
                floatingActionButton = {
                    FloatingActionButton(
                        onClick = {
                            startActivity(
                                Intent(this@MainActivity, SwitchActivity::class.java)
                            )
                        },
                        elevation = 10.dp
                    ) {
                        Icon(Icons.Default.Translate)
                    }
                },
                bodyContent = {
                    WelcomeText(text = getString(R.string.welcome_message))
                }
            )
        }
    }

}

The resulting screen is expected:

MainActivity screenshot

Now, let’s implement SwitchActivity.

SwitchActivity

Localazy Android library has been automatically integrated with our app by the few lines in the build script mentioned above, so it’s available. The whole documentation for the Localazy Android library is available on the website.

First, we wrap a simple ViewModel around it to make their data easily accessible to our newly created activity.

class LocaleViewModel : ViewModel() {

    private val localazyListener = LocalazyWrappedListener {
        viewModelScope.launch {
            update()
        }
    }

    var locales by mutableStateOf(listOf<LocalazyLocale>())
        private set

    init {
        Localazy.setListener(localazyListener)
        update()
    }

    private fun update() {
        locales = Localazy.getLocales() ?: emptyList()
    }

}

We wrapped LocalazyListener, so we don’t need to implement all the overrides to listen for a single event. Here goes the implementation:

/**
 * A simple class to wrap LocalazyListener, so we don't need to implement
 * all functions, and can use a lambda to monitor changes.
 */
class LocalazyWrappedListener(val body: () -> Unit) : LocalazyListener {

    override fun missingTextFound(p0: LocalazyId?, p1: Locale?, p2: String?) {}

    override fun missingKeyFound(p0: Locale?, p1: String?) {}

    override fun stringsUpdateStarted() {}

    /**
     * This function is called when updated data is downloaded.
     */
    override fun stringsUpdateFinished() {
        body()
    }

    override fun stringsUpdateFailed(p0: Int) {}

    override fun stringsUpdateNotNecessary() {}

    /**
     * This function is called when the strings are loaded.
     */
    override fun stringsLoaded(fromUpdate: Boolean, success: Boolean) {
        if (success) {
            body()
        }
    }

}

Great! That was pretty simple. Now, let’s render the language selector using Jetpack’s composables. Not only that we show the available languages, but we can also indicate that the given language is not yet fully translated (which is extremely handy as it can attract more contributors and volunteers to help with translating) and, of course, we also need to point users to our project on Localazy, so they can actually help us.

@Composable
fun LocaleSwitcher(
    items: List<LocalazyLocale>,
    onChange: (LocalazyLocale) -> Unit,
    onHelp: () -> Unit
) {
    Column {
        LazyColumnFor(items = items, modifier = Modifier.padding(0.dp, 8.dp)) {
            TextButton(
                onClick = {
                    onChange(it)
                },
                modifier = Modifier.padding(16.dp, 4.dp, 4.dp, 4.dp).fillMaxWidth()
            ) {
                val name =
                    "${it.localizedName}${if (!it.isFullyTranslated) " (incomplete)" else ""}"
                Text(name)
            }
        }
        TextButton(
            onClick = {
                onHelp()
            },
            modifier = Modifier.padding(16.dp, 12.dp, 4.dp, 4.dp).fillMaxWidth()
        ) {
            Text("Help us translate the app!")
        }
    }
}

We have everything ready for SwitchActivity. Let’s make the drum rolls.

class SwitchActivity : AppCompatActivity() {

    private val localeViewModel by viewModels<LocaleViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            LocaleSwitcher(
                items = localeViewModel.locales,
                onChange = {

                    // Change the locale and persist the new choice.
                    Localazy.forceLocale(it.locale, true)

                    // Reopen MainActivity with clearing top.
                    startActivity(
                        Intent(
                            this@SwitchActivity,
                            MainActivity::class.java
                        ).apply {
                            flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
                        }
                    )

                },
                onHelp = {
                    // Open the project on Localazy to allow contributors to help us with translating.
                    startActivity(
                        Intent(Intent.ACTION_VIEW, Localazy.getProjectUri()).apply {
                            flags = Intent.FLAG_ACTIVITY_NEW_TASK
                        }
                    )
                }
            )
        }
    }

}

And the result:

SwitchActivity screenshot

Which exactly matches my app on Localazy at the given time:

Localazy

To translate my app to more languages, I don’t need to touch XML files or source code. Everything can be managed through Localazy.

Updated translations and new languages are delivered online to existing users without the need to re-submit the app to Play Store 😉. Awesome, well?

Source Code

You can find the whole source code on Github.

cmd line friendly app localization

Make sure you do not miss this update. Whether it is iOS or TypeScript app, you can localize your app using brand new Localazy CLI.

Read more