Android Localization: What the docs don’t tell you.

Hector Ricardo
9 min readSep 16, 2021

--

TLDR: Ctrl-F and look for “Key-Takeaway” in this document.

Android localization is way more complicated than it should be. The documentation isn’t clear enough, and you need to check external sources (like StackOverflow or blog posts) and mess around with sample Hello World apps to understand what’s going on.

In this post, I explain some localization-related issues I have faced while working on my apps, along with their solutions. These solutions are not explained in the documentation, so I will try to be as explicit as possible. (Quick note: I will be using the terms “locale” and “language” interchangeably).

Before starting

This article intends to present some localization issues that the official documentation doesn’t address. It doesn’t intend to be a 101 Localization guide. To get the most out of this article, you should already know some Android localization basics.

If you’re not familiar with the topic, please read the following two documents before proceeding:

https://developer.android.com/guide/topics/resources/localization

https://developer.android.com/guide/topics/resources/multilingual-support

Problem #1: My app doesn’t display in the correct language.

Let me lay some background for this problem. Assume that your phone has these language settings:

If we convert the meaning of this list to pseudocode, we get the following:

function language_to_use_for_my_app() {
if (my_app supports Spanish)
return Spanish;
if (my_app supports French
return French;
if my_app supports English
return English;

return default_language_of_my_app;
}

(Note: The default language of any app is defined by the app itself. One app’s default language could be Polish, another one’s could be German. This is the language that the values/strings.xml file is in. Unfortunately, there’s no way to tell Android what the default language is for a given app. This will bite on us later on as you’ll see).

In theory, the underlying logic of this list is simple. However, sometimes an app doesn’t use the correct language and is falling back to its default language when you would expect otherwise.

To illustrate this problem, update your phone language settings as shown in the screenshot above. Then create a new app with an Empty Activity template in Android Studio. Open res/values/strings.xml and set the app_name to “App In Default Language”.

<resources>
<string name="app_name">App In Default Language</string>
</resources>

Now run the app to make sure it’s working as expected. You should see the app name in the top-left corner of the screen.

Now we’re going to create two additional values.xml files:

values-fr/strings.xml:

<resources>
<string name="app_name">App In French</string>
</resources>

values-en/strings.xml:

<resources>
<string name="app_name">App In English</string>
</resources>

We’re ready to run the app again. But before running it, answer this question: Which value of app_name will be displayed?

An intelligent answer would be the French title. The rationale is as follows: Spanish is the preferred language of the user. But there’s nostrings.xml in Spanish. So the system tries the next preferred language, which is French. There happens to be a French strings.xml, so that would be used it. Makes sense, right?

Run the app again:

Holly cow…What just happened? Why isn’t it in French? Are we going nuts?

No. We’re not going nuts. Let me explain what’s going on here.

When you create a project in Android Studio, it comes with some default libraries. Look at your app/build.gradle file:

These libraries contain their own resource files in many languages. They are probably defining their own values-es/strings.xml files. So when your app is built and everything is bundled up, it ends up having some strings.xml files in Spanish.

Because of this, Android assumes that the app is available in Spanish, and tries to look up the app_name in Spanish. But because we didn’t define one, it falls back to the default strings.xml. That’s why it displays “App In Default Language”.

This is called resource contamination. Android is shooting itself in its own foot. You can learn more about this here and here.

If you want to overcome this, you need to declare the languages your app supports. You do that by adding the following to your app/build.gradle.

android {
defaultConfig {
resConfigs "fr", "en" // the order of this list doesn't matter
}
}

This basically strips away all resources, except those in French or English. Not only does this avoid resource contamination, it also makes the resulting APK smaller.

So update your app/build.gradle as shown above, and re-run your app:

Great. What if you remove French from the list of supported languages?

This works as expected…except for one case.

Gotcha

Let’s change a little bit our setup of our project. This is now our new preferred language list:

Now we’re going to delete the resources/values-en folder from our project, and add a new values-es/strings.xml file:

<resources>
<string name="app_name">App in Spanish</string>
</resources>

So now we have a default strings.xml file (which keeps saying “App In Default Language”), a French version of it, and a Spanish version of it.

Let’s remove English from our list of supported languages and add Spanish:

android {
defaultConfig {
resConfigs "fr", "es"
}
}

Before running the app, ask yourself: What language should the app be in?

If we follow the logic that we have followed up to now, the answer should be Spanish.

Run the app again:

Holly crap… again?? Didn’t we just solve this?

I observed this behavior, and after several hours of experimenting, I came up with this conclusion:

Somewhere in the Android codebase, there’s the assumption that default resources are in English. In my opinion, this is an erroneous conclusion, but that’s the way it is.

I previously said the resConfig clause strips away all the resources except the ones of the languages you declared. Well…that’s a lie. The resConfig clause WILL NEVER strip the default resources.

So the user settings now specify that he prefers English. The app bundle contains some default string files (because these are never stripped away), and Android considers them as being in English. Therefore, it resolves to use the default resource files for an app.

Key-Takeaway #1: Always declare the languages your app supports with resConfigs.

Key-Takeaway#2: If you want your app to support English, then always use the default resource files for English. Android makes this assumption in its codebase, and it’s the happiest path to take.

(If you don’t need to support English in your app, ignore key takeaway #2).

Problem #2: How do you get the app’s current locale?

This is one question that should be easy to answer. Unfortunately, it isn’t… just do a quick Google search and you’ll see that question repeated numerous times on Stack Overflow.

In fact, if you check Stack Overflow, you might encounter several misleading answers, such as:

Locale.getDefault()
getResources().getConfiguration().getLocales().get(0);
getResources().getConfiguration().locale

I’m going to explain each and every of these method calls. But before that, we need to clarify something rather obvious that the folks at Stack Overflow are confusing:

Device language != App language

The former is the language that the user chose through the Settings. The latter is the language that Android resolved to use for a given app.

It’s simple to get the former. Just do this:

Configuration config = Resources.getSystem().getConfiguration();
String locale = config.getLocales().get(0);
// or if you're under API level 24
String locale = config.locale;

Now that we got the device language out of the way, let’s talk about the app language. And I am sorry to say this, but there’s no official 100% bug-free of knowing the language of the app at runtime.

I’m going to repeat this: There’s no official trustworthy way of knowing the language of the app at runtime.

Why is this? There are 2 cases to consider:

Case 1: Android found a matching strings.xml file and it’s going to use that.

Case 2: Android didn’t find a matching strings.xml file and will use the default one.

For Case 1, you can do this, and it will always return the correct value:

getResources().getConfiguration().getLocales().get(0);
// or getResources().getConfiguration().locale if you're under API
// level 24

[[[[ START OF BIG SIDE NOTE: Folks at Stack Overflow also suggest doing Locale.getDefault(). What are the differences between this and the method I just described?

I recommend reading the Locale class documentation. In a nutshell, the Locale.getDefault() is used for JVM purposes. There are operations which might be locale-sensitive (such as formatting dates). If you don’t provide a Locale for this operations, the JVM will use the value returned from Locale.getDefault().

But the most important difference is that Locale.getDefault() is defined at app start-up time. And the only way that value can change is by calling Locale.setDefault(newLocale) yourself. In other words, if the user changes his default language settings while running the app, Locale.getDefault() will keep returning the value that was defined at start-up time.

On the other hand, getResources().getConfiguration().getLocales().get(0) always the updated value (assuming you didn’t cache it).

END OF BIG SIDE NOTE ]]]]

Ok. That’s Case 1. What about Case 2?

Unfortunately, there’s no way to get the app current locale for Case 2. There’s no official way to know whether Android is using the default strings.xml or a localized one. Don’t try to use what we used for Case 1. It will return the device locale.

I will repeat these: If you’re in case 2, and try to do getResources().getConfiguration().getLocales.get(0), it will end up returning the device locale, instead of the app locale.

However, there’s a hacky workaround to overcome this. You need to have a custom string (let’s call it current_locale) in each and every one of your strings.xml files. The Spanish version of strings.xml will have current_locale = 'es', the Italian version will have current_locale = 'it', and the default one will have current_locale = 'en' (because of Key takeaway #2). In your app code, just call: getString(R.strings.current_locale)

Key-Takeaway #3: To get the current locale of the device: Resources.getSystem().getConfiguration().getLocales().get(0) . If you’re in API Level 23 or below, use Resources.getSystem().getConfiguration().locale

Key-Takeaway #4: There’s no trustworthy official way to get an app’s current locale programmatically. If you really need to know this, then read the hacky workaround one paragraph above.

Problem #3: How to get the preferred list of languages of the device?

If you read the Android guides, they will tell you that there’s a new API called LocaleList from Android API Level 24 onwards. When calling LocaleList.getDefault() , in theory you should obtain the list of preferred languages of the user as defined in the settings. This list should be independent of any app. Or at least that’s what the Android guides say...

I’ve made some experiments with LocaleList.getDefault()and it doesn’t always return the list of preferred languages as defined in the Settings.

In which cases it doesn’t return the list of preferred languages as defined in the Settings? It’s better to exemplify it: let’s assume that your phone DOESN’T have German as a preferred language, and your app doesn’t support it either. Then run this:

Locale.setDefault(new Locale('de'));
LocaleList.getDefautlt(); // this list will contain German

Somehow, the list returned by LocaleList.getDefault() contains German…even though neither your phone or your app supports it. Checking the documentation of LocaleList.getDefault():

What?? I’ve read those paragraphs three times and I still don’t understand them.

Anyway, I will tell you how to really get the preferred list of locales in the device as the last key takeaway of this article:

Key-Takeaway #5: To get the list of preferred locales of the device (as defined in the Settings), call Resources.getSystem().getConfiguration().getLocales() . This only applies from API level 24 onwards, since before, the user could only select a single language as his preferred one.

Conclusion

Android documentation (and also its underlying design) lacks to consider many import use cases for localization. This might be frustrating for developers who need to account for edge-cases that aren’t necessarily the happiest paths.

I hope that what I explained here can help you to understand how Android handles localization and to choose the best API for your use case.

--

--