
Machine translation
Software localization
Many Android developers tend to ignore internationalization when displaying a response fetched from a server in their multilingual app. Any text returned from the backend will get displayed directly in the user interface (UI) without taking the user's locale into consideration. This might significantly impair the user experience (UX). Let us take a look at some scenarios:
To avoid such problems and create the best user experience possible, we need to internationalize all pieces of text that an app receives as a response from the backend. That's exactly what this guide is about. Shall we start?
🗒 Note » For this tutorial, you would need a good understanding of the nuts and bolts of Android internationalization. Feel free to check out The Fundamental Guide to Localization in Android to learn more.
For the purpose of this tutorial, we will update a demo app called "Paws". It consists of a single screen where you can simply enter a dog breed in the InputText
field. Upon clicking "Fetch", the app will fetch an image associated with that breed, along with a status message (see below).
🗒 Note » You can find the starting code for the project on GitHub.
Before going further, we need to inspect all the use case scenarios and their server responses. We will use Dog API as the backend for this app to fetch the image and consider the following 3 use cases for our app:
Now, let us introspect how the Paws app behaves currently in all the above scenarios.
When a valid breed is entered, the backend returns an image link and a status key in the JSON response with a 200 code.
//<-- 200 https://dog.ceo/api/breed/hound/images/random { "message":"https:images.dog.ceo/breeds/hound-blood/n02088466_8775.jpg", "status":"success" }
Currently, we directly show both the image URL and the status
value below it:
In case the breed name is invalid or not found, the backend returns an error response. It contains a message
key and a 404
error code.
<-- 404 https://dog.ceo/api/breed/invalidBreed/images/random { "message":"Breed not found (master breed does not exist)", "status":"error", //Notice this error code "code":404 }
We show a default placeholder image and the message
value "Breed not found (master breed does not exist)" in this case.
In case of no network, Retrofit will throw an IOException; in that case, we return an error manually, as can be seen in the GetDogs.kt file.
fun execute(breed: String): Flow<ResultWrapper<String>> = flow { try { val dogs = getDogsFromNetwork(breed) } catch (throwable: Throwable) { when (throwable) { is IOException -> emit( ResultWrapper.GenericError( "No Internet Connection. Please try again" ) ) } }
We show a default placeholder image and the following status message: "No internet connection. Please try again".
The screens above are displayed for users whose locale is set to "en" (English). However, what would happen if a user whose locale is set to de-DE (German) interacted with the app? Let us examine the apps in both languages—English and German.
Note the status text "Success" in the screenshot above.
Note the status text "Breed not found (master breed does not exist)" in the screenshot above.
The backend responses in both screenshots are still the same in both locales—while all the other content in the app changes according to user's locale. We will now fix that problem.
We will create string resources for all server responses above so we can only display the localized strings in the app.
In the default strings.xml file, add the following resources:
<resources> ... <string name="error_no_internet">No internet connection. Please try again.</string> <string name="error_breed_not_found">Dog breed not found.</string> <string name="success_status">Successfully retrieved</string> </resources>
Similarly, we will now add the same resources to the Strings.xml(de) file:
<resources> ... <string name="error_no_internet">Keine Internetverbindung. Bitte versuche es erneut.</string> <string name="error_breed_not_found">Hunderasse nicht gefunden.</string> <string name="success_status">Erfolgreich abgerufen</string> </resources>
🗒 Note » You may notice that these resources are not exactly the same as the backend status messages. For example, the status response text "Success" is translated to "Successfully retrieved" or "Erfolgreich abgerufen". This is done to make status messages more meaningful rather than technical.
To internationalize the server response texts, we need to create a mapper class responsible for mapping the server error codes to resource string Ids. Create a new Kotlin file StatusMessages.kts and add the following code.
internal class StatusMessages { companion object { const val INTERNET_CONNECTION_ERROR = -1 fun resourceIdFor(statusCode: Int = 0): Int { return when (statusCode) { 200 -> R.string.success_status 404 -> R.string.error_breed_not_found //Custom error code for no internet connection INTERNET_CONNECTION_ERROR -> R.string.error_no_internet else -> R.string.error_generic } } } }
This resourceIdFor() function takes in a status code and returns a resource Id of the appropriate string. If the error code does not match any of the error codes defined in the function, the method returns a generic resource Id (R.string.error_generic).
🗒 Note » This function only returns the resource Id of the string and not the actual string. We will use this resource Id in the view (activity or fragment) to get the actual value(text) related to this Id. This way, the text in our app will dynamically update if the phone's locale is changed after the response is fetched.
In the Interactors > GetDogs.kt file, make the following changes.
fun execute(breed: String): Flow<ResultWrapper<String>> = ... //Get resource id for a 200 success response emit( ResultWrapper.Success( dogs.message, statusResourceId = StatusMessages.resourceIdFor( 200 ) ) ) } catch (throwable: Throwable) { when (throwable) { is IOException -> emit( //Get resource id for no internet connection ResultWrapper.GenericError( statusResourceId = StatusMessages.resourceIdFor( INTERNET_CONNECTION_ERROR ) ) ) is HttpException -> { val errorResponse = convertErrorBody(throwable) emit(errorResponse) } else -> { returnGenericStatusError() } } } } private fun convertErrorBody(throwable: HttpException): ResultWrapper.GenericError { return try { throwable.response()?.errorBody()?.let { val errorResponse = Gson().fromJson( it.charStream(), DogResponse::class.java ) //Get resource id for the "code" value in error response //from the server return ResultWrapper.GenericError( StatusMessages.resourceIdFor( errorResponse.code ) ) } ?: returnGenericStatusError() } catch (exception: Exception) { return returnGenericStatusError() } } //This function returns a generic error message //in case we are not able to //parse error json properly private fun returnGenericStatusError() = ResultWrapper.GenericError(StatusMessages.resourceIdFor(0)) }
As you can see above, instead of directly returning the response text fetched from the backend, we map the fetched status code to a string resource Id and then pass the string resource Id instead. This way, the text displayed in the activity/fragment will always take the user's locale into consideration.
We will now change the type of the status
variable from String to Integer in the ViewModel. Instead of passing String around from the ViewModel, we will now pass the string resource Ids. This "status" value will be observed from the View layer.
Go to MainActivityViewModel.kt and implement the following changes:
//Remove This //private val _status = MutableLiveData("") //val status: LiveData<String> = _status private val _status = MutableLiveData<Int>(null) val status: MutableLiveData<Int> = _status internal fun loadImage(breed: String) { ... when(dataState){ is ResultWrapper.Success<String> ->{ ... //Remove this // _status.value = dataState.status //Set its value to the resource Id instead, returned //from the GetDogs.kt file above _status.value = dataState.statusResourceId ... } }
Now, in the MainActivity.kt file, observe the above status
value.
🗒 Note » This project uses a MVVM architecture. The view observes any changes in the value of the "status" variable in the ViewModel and then updates accordingly.
viewModel.status.observe(this, { status -> textview_status.text = status })
Whenever the status value changes, we will fetch the associated string with that resource Id and set it to our TextView
. Since our app is now using the resource Ids to fetch the strings, it will always display text according to the user's locale.
Now, all the text in the app, inclusive of the response returned from the backend, is fully localized. Check out the screenshots below (from left to right): en-US for English in the US, and de-DE for German in Germany.
In case of dog breed not found, you get to see the following screen:
In case of a success response, you will see the following:
If you want to explore more of Android internationalization, we suggest the following guides:
Last updated on August 21, 2023.