Letting Users Switch the App's Language Without Touching Their Device Language — Notes on Android Per-App Language
How to implement in-app language switching on Android with AppCompatDelegate.setApplicationLocales and locales_config, plus the gotchas around pre-API-33 compatibility, activity recreation, and AdMob, from an implementation point of view.
A request to put a language switch in the settings screen tends to arrive sooner for apps published abroad. With the wallpaper and utility apps I maintain on Android, I once heard from an English-speaking user who said, "I want to keep my phone in English but read this one app in Japanese." Asking them to switch the entire device language doesn't solve that.
Android gained a proper mechanism for switching only the app's language in API 33 (Android 13), called Per-App Language Preferences. When you actually wire it up, though, a string of quiet snags appears: how to handle devices below API 33, a flicker the moment the language changes, and settings that vanish after a restart. This article is my implementation notes, written while letting an Antigravity agent handle the research and a first draft of the code.
Overriding the device language and switching the app language are two different things
The long-standing approach was to override locale in Configuration and rebuild the Context. Many of you have probably seen code that swaps the locale in attachBaseContext. It works, but it has two weaknesses.
First, the OS has no idea about your app's language setting. Your app won't appear in the system Settings list of "App languages." Second, you have to reapply the locale yourself on every recreation, and libraries and system dialogs tend to stay in the device language.
setApplicationLocales, from API 33 onward, hands this setting to the OS. The OS remembers the per-app language, so it shows up in Settings, and on every launch the OS prepares a Context with the correct locale. Here is how the two compare.
New project or existing one, my conclusion is to build around AppCompatDelegate.setApplicationLocales. AppCompat 1.7 backports this API to below API 33, so you can write the same code even when your minimum version is old.
Declaring supported languages to the OS with locales_config
First, tell the OS which languages your app supports. Create res/xml/locales_config.xml and reference it from the manifest via android:localeConfig. With this in place, the system Settings "App language" screen shows your language list.
Here is the first pitfall. The language tags listed in locales_config.xml must match the languages that actually have a values-xx folder. For region or script tags like zh-Hans, prepare the resource folder as values-b+zh+Hans rather than values-zh-rCN so they line up. When the listed languages and the resources diverge, you get a confusing state where the language appears in Settings but selecting it changes nothing.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦The proper Android 13+ way to switch only the app's language without changing the device language, and how to back it down to API levels below 33
✦Where activity recreation, process recreation, and persistence trip you up when changing language, with the code that avoids it
✦How to diagnose why AdMob or a WebView won't follow the app language, and what actually worked
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
The autoStoreLocales setting for backward compatibility
To make setApplicationLocales work below API 33, you specify where AppCompat stores the chosen language. The easiest path is to enable automatic storage. Add the following metadata and service to the manifest.
With autoStoreLocales set to true, below API 33 AppCompat remembers the language in an internal store and restores it automatically on launch. On API 33 and above the OS holds it, so this setting is ignored. In other words, the same code runs on both old and new devices.
This automatic storage carries a small cost. AppCompat reads the stored locale before the first Activity comes up, so a tiny synchronous I/O lands on cold start. On my Pixel 6a it measured a few milliseconds, with no perceptible impact. If you want to manage the store yourself, skip autoStoreLocales and call setApplicationLocales explicitly at launch instead.
The one line that switches language, and a path to the settings screen
The actual switch is one line. When the user picks a language, wrap that tag in a LocaleListCompat and pass it in.
import androidx.appcompat.app.AppCompatDelegateimport androidx.core.os.LocaleListCompatfun applyAppLanguage(languageTag: String) { // e.g. "ja" / "en" / "zh-Hans". Empty string returns to system default. val locales = if (languageTag.isEmpty()) { LocaleListCompat.getEmptyLocaleList() } else { LocaleListCompat.forLanguageTags(languageTag) } AppCompatDelegate.setApplicationLocales(locales)}
Always provide a "follow the device language" option. Passing getEmptyLocaleList() returns to the system default. Without it, a user who once chose Japanese will be stuck with a Japanese-only app even after switching their device to English.
On API 33 and above, you can also send users to the OS language setting without a screen of your own. The Settings.ACTION_APP_LOCALE_SETTINGS intent opens the system's app-language screen directly. Whether to make your in-app UI or the OS setting the primary path is a matter of policy; I keep the switch in the app while adding a secondary link to the OS setting.
Flicker at the moment of switching, and double recreation
Calling setApplicationLocales recreates the Activity and redraws it in the new language. I ran into two behaviors here.
First, depending on how you call it, the Activity recreates twice. If you select a language inside a settings screen and also call recreate() yourself, the API-triggered recreation and your own recreation overlap. After calling setApplicationLocales, do not call your own recreate(). Leave the redraw to the API.
Second, you can briefly see the previous language during recreation. Applying the language after closing the settings dialog makes this less noticeable. I dismiss the dialog first and call setApplicationLocales after a slight delay to keep the visual flicker down.
// inside the language selection dialogfun onLanguageSelected(tag: String) { dismiss() // close the dialog first view?.post { applyAppLanguage(tag) // applying on the next frame makes the switch smoother }}
When libraries or resources don't follow the app language
The most common report is that the switch clearly took effect, yet some part stays in the device language. The cause was usually one of these.
First, using the wrong Context. If you read strings from applicationContext, you may reference the app-wide base rather than the Activity's locale. Read display strings from the Context of that screen's Activity or Fragment.
Next, the WebView. Content inside a WebView is a separate track from Android's resource language; it depends on the Accept-Language header or the locale of the URL you load. To match the app language, pull the tag from the current AppCompatDelegate.getApplicationLocales() and pass it explicitly at load time.
And ad SDKs such as AdMob. The creative's language is generally based on the device language or the user's inferred region, so it does not follow the in-app switch. That is by design. Rather than forcing it, it was more realistic to match only the UI text you own (such as the parts of a consent banner you draw yourself) and leave the ad itself to the SDK. The table below is the order I use to triage this.
Won't follow
Main cause
Fix
Only some screens stay old
Strings read from applicationContext
Read from the Activity/Fragment Context
WebView content
Separate track from resource language
Pass the tag via Accept-Language or the URL
Ad creatives
SDK decides by device language and region
Don't force it; match only your own UI text
Notification text
Locale at notification build time
Resolve strings in the current app language when building
What I left to the agent, and what I kept
For this implementation, I left the Antigravity agent the boilerplate for locales_config.xml, the mapping table between supported tags and resource folders, and the research into a minimal setup that makes setApplicationLocales work below API 33. Getting a full set of boilerplate out quickly made the initial direction much faster to settle.
On the other hand, behaviors you only learn on a real device, like the double recreation; the product decision to give up on the ad following the language; and the requirement to always offer a "back to device default" option, were the parts a human had to hold. The agent writes code that works correctly, but whether a flow leaves users stranded is something you decide while picturing your own readers. Spending time with one physical device, flipping languages as you go, still felt like the most reliable check.
After adding a language switch to a published app, you notice the language of reviews and emails shift little by little. It is a small feature, but it was a satisfying one that widened who the app reaches. I hope it helps anyone growing a multilingual app the same way.
Share
Thank You for Reading
Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.