The (un)obvious translation mechanism in Android

Hello! Localization of applications in Android is a fairly simple and straightforward procedure. That’s what I thought until I ran into an unusual bug. As it turned out later, it was not a bug at all, but one very interesting feature, which will be discussed today.

The project, within the framework of which this whole story was born, is a mobile platform for companies selling clothes. In short, customers, having some kind of CRM and an online store site attached to it, receive Android and iOS applications on a turnkey basis. Our clients are from different parts of the world, so one of the important features is the support for a dozen languages ​​out of the box.

An equally important component is the support of the so-called “showcases”. Showcase is, in a way, a regional branch. The user has the right to choose the appropriate region, regardless of his actual location or the language selected on the device. So he gets the goods, prices and, of course, the language of the country/region he has chosen. For this reason, clients often ask us to make adjustments to various translations in order to reflect the linguistic features of a certain area as accurately as possible. So, for example, what in the UK is usually called Postcode, in the US it is customary to call ZIP Code. In our practice, there were cases when users simply did not understand how to place an order correctly, because it was not completely clear to them what was generally required of them.

One ordinary working day, we were approached with another similar request. This time we needed to update the translations for the US. This process is as trivial as possible, but for the sake of completeness and for those who are only familiar with localization in Android, I will also touch on it. Who is already prosharenny, go immediately to the point.

Multilingual application

When we create a project, we have a translation file called strings.xml. It’s in a folder values resources, and is the base for the entire application: if we do not have overrides, the application uses the translations from here. In case we want to add support for other languages, such as Russian and German, we add two additional strings.xml files, however in other folders – values-en And values ​​de, respectively. To override the translation, just add a string to the appropriate file with the same key as in the base strings.xml and the desired translation. Overriding translations is optional, meaning you don’t have to generate all the base keys in the localized version, although Android Studio recommends that you do so. It doesn’t work the other way: if you declare a translation in one of the localized strings.xml, but don’t declare it in the base one, the project simply won’t compile. Thus, when switching the device language, the application will try to find suitable translations and, if there are none, will use the base version.

In addition to global languages, like the mentioned Russian and German, it is possible to add dialect versions. One common example is British and American English. For example, for the first we use Favorites, while for the second we use Favorites. In an application, this is implemented in a way similar to conventional languages. Using the example of English, we need to create folders values-en-rGB And value-en-US respectively, and inside already familiar strings.xml.

What are you doing here?

We return to the task – to update the translations for the United States. We open values-en-us/strings.xml and add translations for the necessary keys. We launch the application, select the United States region, make sure that the translations are updated, compile the build and send it for testing. Usually, this is the end of the job, but not this time. Literally in 15 minutes, the ticket-bug “Incorrect translations for most storefronts” arrives. Looking at the screenshots, we observe the following. For en-US everything works fine. For others, like en-GB, you can see translations from en-US, as well as en-JP and even en-TR. The client has many English regions, so we could see translations from each where there were overrides. In summary, the problem was that the application takes translations from other dialects of English, and not from the base strings.xml. Well then, let’s figure it out.

First of all, I decided to study how language switching is generally implemented. It turned out to be very simple and without pitfalls. In fact, the Application Context is taken and modified to the desired Locale:

private static Context updateResources(Context context, Locale locale) {
   Configuration configuration = context.getResources().getConfiguration();
   configuration.setLocale(locale);
   configuration.setLayoutDirection(locale);
   return context.createConfigurationContext(configuration);
}

Above this context there is a wrapper through which all accesses to resources are made, including strings, thereby allowing you to get a translation suitable for the selected Locale. Simply put, there is nothing in our code that would affect the selection of the translation, so the dog is buried somewhere deeper.

Naturally, the next place to look for answers was the Internet, in particular, StackOverflow, with the help of which I managed to reach documentshedding light on the current situation, and explaining that formally we do not even have a bug.

For clarity, I created a mini-project. 5 messages are displayed on the screen, each of which has its own key in strings.xml:

<!-- values/strings.xml -->
<resources>
    <string name="app_name">LocalizationSample</string>
    <string name="locale_1">The locale of the text is EN (default)</string>
    <string name="locale_2">The locale of the text is EN (default)</string>
    <string name="locale_3">The locale of the text is EN (default)</string>
    <string name="locale_4">The locale of the text is EN (default)</string>
    <string name="locale_5">The locale of the text is EN (default)</string>
</resources>

On the screen we see that everything is displayed in accordance with what we indicated:

All OK
All OK

So our base strings.xml contains the base English translations. Let’s add 4 English-speaking regions, in each of which we will override one translation:

<!-- values-en-rAU/strings.xml -->
<resources>
    <string name="locale_2">The locale of the text is AU</string>
</resources>
<!-- values-en-rGB/strings.xml -->
<resources>
    <string name="locale_3">The locale of the text is GB</string>
</resources>
<!-- values-en-rNZ/strings.xml -->
<resources>
    <string name="locale_4">The locale of the text is NZ</string>
</resources>
<!-- values-en-rUS/strings.xml -->
<resources>
    <string name="locale_5">The locale of the text is US</string>
</resources>

Let’s simulate the situation when the en-GB language is selected on the device or through the application:

val localizedContext = createConfigurationContext(
    resources.configuration.apply {
        locale = Locale("en", "GB")
    }
)

findViewById<TextView>(R.id.localeTextView1).text = localizedContext.getString(R.string.locale_1)
findViewById<TextView>(R.id.localeTextView2).text = localizedContext.getString(R.string.locale_2)
findViewById<TextView>(R.id.localeTextView3).text = localizedContext.getString(R.string.locale_3)
findViewById<TextView>(R.id.localeTextView4).text = localizedContext.getString(R.string.locale_4)
findViewById<TextView>(R.id.localeTextView5).text = localizedContext.getString(R.string.locale_5)

We launch the application and see the following interesting picture:

Translations from different countries, unite!
Translations from different countries, unite!

We change to en-AU, en-NZ, for example, – the result is the same. But with en-US it looks different:

Oh those exceptions.
Oh those exceptions.

Let’s figure out what’s going on. Let en-GB be selected. In search of a suitable string, the system first of all turns to values-en-rgb/strings.xml. In case the required string is not found, we expect the string from values/strings.xml to be taken. But something else is happening. The system is not aware that our base strings.xml contains English translations and should be used when there is no local translation. Therefore, it will search for values-en/strings.xml, which is the main global source of English translations. And if it is not found, a search will be carried out in all dialect forms, the result of which we see on the screen. This is done so that the conditional British can get an English translation of the application. After all, the base file can contain Russian, Chinese or any other language besides English, so it is obvious that there are more chances to understand Australian English than a foreign language in general. And only if there is no translation in the main (en) and dialect (en-GB, en-US, en-AU, etc.) strings.xml, the translation will be taken from the base one. There is an exception to this rule – en-US. For the USA, the scheme described above does not work – dialect translations are not applied, the transition to the basic ones immediately occurs. All or nothing, as they say.

Summing up, the search chain for the desired translation is as follows:

en‑GB (target) → en (global) → en‑regional (children en‑AU, en‑US, en‑NZ, etc.) → base (values/strings.xml)

Now, knowing how the translation is selected, the solution to the problem is quite obvious. Since we can’t mark which language the translations are in the base strings.xml, we need to add a global English values-en/strings.xml. Hence the problem of duplication: both values/strings.xml and values-en/strings.xml will contain exactly the same translations. Moreover, we even need them to be the same, because if some key is missing, it can be taken from the dialect version. Luckily, there is a solution – a Gradle task that copies the contents of values/strings.xml before each build (thanks duongdt3):

task copyEnResource {
    copy {
        from ('src/main/res/values/strings.xml')
        into ('src/main/res/values-en/')
    }
}

preBuild.dependsOn copyEnResource

Thus, we can work only with basic translations, without doing the routine transfer to the English version.

After adding values-en/strings.xml, which duplicates values/strings.xml, our messages are displayed exactly as we expect them to be:

Bingo!
Bingo!

Another interesting feature is that if the user has several preferred languages ​​​​selected on the device, then in applications, before the base translation is selected, a search will be carried out in the global and dialect versions of each of the languages. Here here shows how it works.

That’s all for me. The bug has been fixed, the knowledge has become a little more. Share your interesting cases that you had to deal with in Android.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *