app-language-change-in-android-a-study-in-problem-solving
Engineering
Sep 13, 2024

App Language Change in Android: A Study in Problem Solving

Alireza Rahmaty
Mobile Engineer

Our Android Engineer, Alireza Rahmaty, shares innovative approaches and practical tips to enhance your app’s localization capabilities, unlocking new opportunities for engaging a global audience.

{{divider}}

When talking about Localization, we might only consider having string resources in different languages and regions, but we can adapt every app resource to locals. We have many resources in the app, such as drawables, sounds, fonts, animations, colors, etc. All of them can be subcategorized for different locales.

Making the app responsive to different languages and regions is called localization. This customization in resources helps to achieve more users, revenue and growth. The OS handles Switching between localized resources, but applications should have the appropriate implementation to handle configuration changes.

Changing the app’s language on Android, regardless of the OS language, has been an ongoing challenge and has raised some complex problems. However, it brings massive business value in the long term since many users are multilingual and tend to apply a specific language to each app rather than the default OS language.

Before Android 13, there was no official recommendation to change the app language. Still, we have been able to change the app's locale by creating or updating the configuration instance with the desired language and attaching it to the context. 

1. Until Android 16, updateConfiguration method was used to modify the configuration, and it has been deprecated since SDK 17 (but still working 😈).

An image of an Android Kotlin code snippet that overrides the onCreate method. It sets the content view, retrieves the current configuration, sets the locale to French, and updates the configuration.

2. Using createConfigurationContext of ContextWrapper, introduced on Android SDK 17. This way, we create a localized context and pass it to activities and application classes as a new base.

Image of an Android Kotlin code snippet that overrides the attachBaseContext method. It initializes a new configuration, sets the locale to French, updates the locale list, creates a new context with the updated configuration, and calls the superclass's attachBaseContext method with the new context.

3. Using applyOverrideConfiguration: This method is also a bit tricky to ensure is called before any call to getAsset() or getResource(), making the implementation super delicate.

Image of an Android Kotlin code snippet that overrides the attachBaseContext method. It initializes a new configuration, sets the locale to Italian, updates the configuration with the new locale, applies the override configuration, and calls the superclass's attachBaseContext method with the new base context.

Problems using these approaches: Imagine we have created a method called changeLanguage with one of the implementations above

  1. We should always fight the system by changing the locale of a configuration instance during callbacks such as onConfigurationChanged and onCreate since the OS always resets the Configuration to defaults. 
  2. If we have multiple activities or services, we should keep all of them in sync. 
  3. Most of the time, activity toolbar titles are not in sync with the applied locale, so we should sync the activity title after its creation to display it in the desired locale. 

One more thing to note is Locale.setDefault(), which we have to call with all three approaches.

Leveraging Per App Language on Android 13+ 👏  

A new API is providing an innovative solution by helping us remove lots of boilerplate code. A new service called Locale service has been added recently, and LocaleManager is a bridge to access it. With this new service, regardless of OS language, all apps could have specific languages, either through the OS setting or a language picker feature. We can even have both implementations on our apps, the problem is solved! Now, we focus on forward-thinking approaches and ensuring they stay in sync.

Enabling App-specific language change option on the OS setting:

This option only exists on Android 13+. First, we need to determine the list of languages our app supports by setting up the locale configuration. This can be set up in two ways: through a config XML file or by setting it up dynamically in the code.

Locale Configuration file

We can manually create a locale config file in the XML folder and reference it in the application tag in the manifest file.

Image of an XML file named locale_config.xml. The file defines a locale configuration for an Android application, specifying three locales: Afrikaans (af), Amharic (am), and Arabic (ar).
Image of an Android project directory and a segment of the AndroidManifest.xml file. The directory includes files such as backup_rules.xml, data_extraction_rules.xml, and locale_config.xml. The manifest segment shows the application tag with attributes for locale configuration, label, backup allowance, and data extraction rules.

The interesting part is you don't need to create the aforementioned file and reference it in the manifest if you set autogenerate config for Gradle; Gradle will do the job for you. However, be careful, as you may not have control over the list of displayed languages, and there could be a language you don't want to appear in the settings.

Image of a Gradle build script configuring the defaultConfig block. Inside the android block, within androidResources, the generateLocaleConfig property is set to true.

Setting locales programmatically!

The other innovative solution is to set the locale config dynamically by calling the setOverrideConfig method on the app start-up time, which has been available recently on Android 14(SDK 34). We are interested in this method because we can set up the language change feature for a specific set of users under an A/B experimentation and also change the list of available languages without an app release. However, the drawback is that we can use it only on Android 14, and it's not backward compatible.

Screenshot of an Android Kotlin code snippet defining the MainActivity class that extends AppCompatActivity. In the onCreate method, it sets the content view to activity_main and initializes a LocaleManager service. It then overrides the locale configuration with a list of language tags: German (de), English (en), French (fr), Italian (it), Spanish (es), Portuguese (pt), Russian (ru), and Japanese (ja).

The last step is to save the selected local so that the OS will know which language to use for your app. On Android 13+, this is automatically saved, but for lower versions, you can use the locale metadata service at the app-level manifest file by using the following snippet. This will cause strict mode violations.

Screenshot of an XML snippet defining a service in an Android manifest file. The service is named androidx.appcompat.app.AppLocalesMetadataHolderService, with attributes enabled set to false and exported set to false. Inside the service, there is meta-data with the name autoStoreLocales and the value set to true.

⛳Up until here, we have enabled OS-level language change for our app only using an XML locale file, and we can't create it dynamically unless we set the minimum SDK to 14.

As a result, the language menu should appear on Android 13+; unfortunately, this menu doesn't exist on lower versions. You can stop at this point since, with this configuration, the language change is possible through the OS setting! 🎉

The latest Android launcher offers improved functionality and a modern design for enhanced user interaction.
Source: Android
Setting up a language picker

Implementing the UI of the language picker should be straightforward. It could be a BottomSheetFragment with a list of languages, which we could either fetch from the backend or use a static list.

Another thing about UI implementation is that you can make a language name unique in all locales. 😕

If you choose to use a specific language name, like Deutsch, in all translations, you can either create a string resource with the translatable property set to false (so the IDE ignores it) or use the displayName property of the Locale class to show the language name based on the context.

Screenshot of an XML snippet defining string resources in an Android project. Each string has a name attribute and is marked as non-translatable. The strings represent language names in their native scripts: Czech (Čeština), Danish (Dansk), German (Deutsch), and Greek (Ελληνικά).
Screenshot of a Kotlin code snippet demonstrating the use of the Locale class to display language names based on the app's current language. It shows how to get the display names for German and Italian when the app's language is set to English, German, or French. Comments indicate the expected display names in each case.

The next step is to apply the selected language to the app by calling

AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags("it")).

The method triggers a configuration change and calls onDestroy, attachBaseContext, and onCreate on the MainActivity, allowing for side effects like tracking analytics or updating HTTP request headers. Important: Don’t call this method before the MainActivity's onCreate, or the app will crash!

Pro Tip: If using a DialogFragment to show the language list, it's easier to call setApplicationLocales after the fragment closes to avoid potential configuration issues.

Until this point, everything should be working smoothly when changing the language in the Debug APK using the app’s language setting or picker. However, the complex problem arises when switching to the release app bundle.

Challenging part! Language resource Installation, Dynamic delivery

The new language content won’t be available right away after the app is released. Unlike the debug version, the language change in the release app won’t happen instantly. This is because the selected language resources might not be immediately accessible in the release version.

While releasing the app bundle, Google Play split it based on the languages, density, etc. If we have 4 densities and 10 languages, then there would be 40 different APK versions, and the final APK installation would be based on the device's language and density specs. This really helps optimize app download size, but it hinders the language change functionality!

The new language API is built so that when a new language is applied, Google Play automatically installs the language resources for the app. While this is a great feature, the process typically takes a few days to complete.

To speed up the process, we can use the SplitInstallManager API. This allows us to request the language resource download from Google Play immediately when a new language is applied, ensuring faster availability.

First, let’s enable the SplitInstall API in our application. This will allow the activity/app access to the installed resources.

If you have only one activity, override attachBaseContext 

Screenshot of an Android Kotlin code snippet that overrides the attachBaseContext method. It calls the superclass's attachBaseContext method with the new base context and then calls a private method enableAccessToDownloadedLanguageResources, which uses SplitCompat.installActivity to enable access to downloaded language resources.

In the case of a custom application class, simply inherit SplitCompatApplication

Then, use the SpilitManager API to make a language installation request. The SpilitManager applies a “fire-and-forget” approach, so we must observe the request status using the SplitInstallStateUpdatedListener class. Remember to unregister the listener.

Screenshot of an Android Kotlin code snippet demonstrating the use of SplitInstallManager for downloading language resources. It initializes a download session ID, creates a SplitInstallManager, and builds a SplitInstallRequest to add a language. The download task is started, and a success listener updates the session ID. A listener for SplitInstallStateUpdatedListener handles various states such as CANCELED, FAILED, INSTALLED, REQUIRES_USER_CONFIRMATION, and UNKNOWN, with corresponding actions for each state. Finally, the listener is unregistered from the SplitInstallManager.

PS: here is the proper dependency to add spilitManager to your app

 
implementation "com.google.android.play:feature-delivery:2.1.0"

Sync Locale!

Most of us use the locale class for date formatting, sending the correct language header to the backend, etc. The locale is not the same as the app language. When the app’s language changes or the new language is installed, we need to set the default locale Locale.setDefault so that the formatting and header we send to the backend are correct.

Otherwise, we might have the backend content in language A and app string resources in language B 💣

👿 It’s crucial to remember that the Locale class must be synchronized each time the app starts, as it doesn’t automatically update to reflect the app’s language.

The proper flow for changing apps’ Language

This applies when language resources are not included within the app itself.

  • To check if a language is installed in the app, we can use the splitInstallManager.installedLanguages property. This property will not work on the Debug APK but only on the release bundle. 
  • Calling setApplicationLocales will recreate the activity so we can do the follow-up actions.
  • Depending on the device and brand, the new language may not be installed immediately after activity recreation. In such cases, you may need to either retry the process or wait for Google Play to complete the installation.
  • To retry, first cancel the previous language installation request using the downloadSessionId with splitInstallManager.cancelInstall(downloadSessionId).
Flowchart illustrating the process of selecting and setting a language in an app. The steps include: User selects a language, checks if the language resource is already available, makes a request to Google Play to download if not available, observes download status, handles success or failure, calls per app language API to set the language, recreates the activity, checks if the language is installed, syncs the locale if installed, or retries/waits for the next app restart if not.

How to test the language change

To test, we should have the app project on Google Play. Then, we must generate a signed app bundle in AS and upload it to the internal app-sharing page in the Google Play Store. You can then share the link with testers or install it on your device. The uploaded app bundles will be deleted in two months, and you can see the errors/info logs in Crashlytics to check if everything is working correctly. 

If you are adding all the language resources to the app, there is no need to build a bundle; simply test everything in the debug APK!

Other articles from this series
No items found.

Featured roles

Marketing Executive
Berlin
Full-time / Permanent
Marketing Executive
Berlin
Full-time / Permanent
Marketing Executive
Berlin
Full-time / Permanent

Join the journey.

Our 800+ strong team is changing the way millions experience the world, and you can help.

Keep up to date with the latest news

Oops! Something went wrong while submitting the form.