Why switching language inside the app sometimes does nothing: setApplicationLocales only recreates activities that are on screen
"I switched to English and it stayed Korean until I restarted the app." The cause is that AppCompat recreates only the activities that are started at that moment; screens that were stopped behind it come back in the old language. The fix I applied to 8 apps, and five traps that came with it.
The symptom
In the remote app's settings, switching the language to English turned the settings screen English, but going back to the main screen sometimes showed Korean. Killing and relaunching the app fixed it. I wanted to know what "restart fixes it" actually meant.
The cause
AppCompatDelegate.setApplicationLocales() recreates only the activities that are in the started state at that moment. The settings screen is recreated in the new language, but the main screen, stopped behind it, resumes with its old Configuration. On a full restart, Application.onCreate re-applies the locale, so everything looks fine.
That means the bug only exists in apps with more than one activity. A single-activity app is fully refreshed when that one activity is recreated. Of mine, the four remote apps (6 to 7 activities each), the benefits calendar, the bookmark manager, the screen-time app and the lottery app were affected.
The fix
In onActivityResumed of Application.registerActivityLifecycleCallbacks, compare the global selection (AppCompatDelegate.getApplicationLocales()) with the locale in that activity's own Configuration. If they differ, recreate().
Two things matter:
- Checking the global value alone is useless. It is already the new language; you can only detect a mismatch by comparing against the activity's own configuration.
- Prevent infinite recreation. A
WeakHashMap<Activity, Boolean>allows one recreate per instance. A recreated activity is a new instance, so the normal path ends after one.
The same class went into 8 apps.
Also found: the locale-persistence switch was missing in 10 apps
Below API 33, AppCompat stores and restores the selected locale only if the manifest declares AppLocalesMetadataHolderService with the autoStoreLocales metadata. Ten apps did not have it. That is why some apps kept the language after restart and others fell back to the system language.
Five traps (every one actually happened)
- Hard-coded Korean ignores the language setting. 97 remote-control button labels were literals like
android:text="외부입력". All 131 English translations existed invalues-enand the screen still showed Korean. - Do not search for Hangul with
grep -P '[\x{AC00}-\x{D7A3}]'. In Git Bash under a non-UTF-8 locale it fails with "supports only unibyte and UTF-8 locales" and returns zero matches. A false negative. Use Python. - Korean in the code is not necessarily Korean on screen. Some strings were assigned to variables nobody read and only went to logs. Trace the actual display path to the end before translating.
- Do not look resources up by name with
getResources().getIdentifier(). R8 strips the names and it breaks in release only. - Read the warnings in the original comments. Above
NO_NAME = "(no name)"it said "used for text comparison, do not move to resources"; I moved only the constant and broke the build. To move it, the logic must move too so that display and comparison use the same string.
In-app language applies to activities only
One more. AppCompat's in-app language applies to activity resources. Notifications, home-screen widgets and date formatting done in the background still use the system language, so you end up with an English app sending Korean notifications. Where notifications and widgets are built, wrap a Context with the stored language tag (createConfigurationContext) and read resources from that.
Verification
This is a timing bug, so a passing compile proves nothing. On a real device, walk screen A → settings → change language → back → A by hand.
Summary
setApplicationLocalesrecreates only on-screen activities. Stopped screens come back in the old language.- In
onActivityResumed, compare the activity's own configuration with the global value andrecreate()once on mismatch. - Without the
autoStoreLocalesmetadata, the selection is not persisted below API 33. - Hard-coded strings, false-negative grep, display paths, getIdentifier, original comments.
- Notifications and widgets do not get the in-app language; wrap the context separately.