Switching locales with Jetpack Compose
vaclavhodek
Posted on November 12, 2020
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.
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:
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:
Which exactly matches my app on Localazy at the given time:
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.
This post was originally published on Localazy.
Posted on November 12, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.