Software localization

Internationalizing Server Responses in Android

Get to know all the tips, tricks, and best practices for internationalizing server responses in Android apps.
Software localization blog category featured image | Phrase

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:

  • A backend designed for users in the US returns the same piece of text in English for users in the Netherlands who should, otherwise, only see text in Dutch.
  • A user can change the phone locale in their settings manually; for example, a user in the US can set their locale to de-DE (German in Germany); in that case, the user will see most of the text in German, except for the text returned from the server, which will still be in English.

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.

Getting started 🏁

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.

Demo app | Phrase

Examining server responses

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:

  • The treed name is valid, and a success response is returned
  • The breed name is invalid, and an error response is returned
  • No internet connection

Now, let us introspect how the Paws app behaves currently in all the above scenarios.

Use case no. 1: The breed name is valid, and a success response is returned

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:

Successful fetch request in demo app | Phrase

Use case no. 2: The breed name is invalid, and an error response is returned

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.

Breed not found (master breed does not exist) in demo app | Phrase

Use case no. 3: no internet connection

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".

No internet connection. Please try again in demo app | Phrase

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.

English and German demo app version next to each other without translation | Phrase

Note the status text "Success" in the screenshot above.

English and German demo app version next to each other without translation | Phrase

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.

Creating localized values

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.

Create a status message class

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.

Returning resource Ids for errors instead of a server response text

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.

Setting resource Ids in the ViewModel

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

           ...

             }

        }

Observing from the view

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.

We're done 

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:

English and German demo app version next to each other with translation | Phrase

In case of a success response, you will see the following:

English and German demo app version next to each other with translation | Phrase

If you want to explore more of Android internationalization, we suggest the following guides: