Reputation: 9441
Changing locale inside an Android app was never been easy. With androidx.appcompat:appcompat:1.3.0-alpha02
, it seems that changing locale in an application has become much more difficult than I imagined. It appears that activity context and application context behaves very differently. If I change the locale of activities using a common BaseActivity
(like below), it will work for the corresponding activity.
BaseActivity.java
public class BaseActivity extends AppCompatActivity {
private Locale currentLocale;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
currentLocale = LangUtils.updateLanguage(this);
super.onCreate(savedInstanceState);
}
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(LangUtils.attachBaseContext(newBase));
}
@Override
protected void onResume() {
super.onResume();
if (currentLocale != LangUtils.getLocaleByLanguage(this)) recreate();
}
}
But I need to change the locale of application context as well as this is only limited to activities. To do that, I can easily override Application#attachBaseContext()
to update locale just as above.
MyApplication.java
public class MyApplication extends Application {
private static MyApplication instance;
@NonNull
public static MyApplication getInstance() {
return instance;
}
@NonNull
public static Context getContext() {
return instance.getBaseContext();
}
@Override
public void onCreate() {
instance = this;
super.onCreate();
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(LangUtils.attachBaseContext(base));
}
}
While this successfully changes the locale of the application context, the activity context no longer respects the custom locale (regardless of whether I extend each activity from BaseActivity
or not). Weird.
LangUtils.java
public final class LangUtils {
public static final String LANG_AUTO = "auto";
private static Map<String, Locale> sLocaleMap;
private static Locale sDefaultLocale;
static {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
sDefaultLocale = LocaleList.getDefault().get(0);
} else sDefaultLocale = Locale.getDefault();
}
public static Locale updateLanguage(@NonNull Context context) {
Resources resources = context.getResources();
Configuration config = resources.getConfiguration();
Locale currentLocale = getLocaleByLanguage(context);
config.setLocale(currentLocale);
DisplayMetrics dm = resources.getDisplayMetrics();
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N){
context.getApplicationContext().createConfigurationContext(config);
} else {
resources.updateConfiguration(config, dm);
}
return currentLocale;
}
public static Locale getLocaleByLanguage(Context context) {
// Get language from shared preferences
String language = AppPref.getNewInstance(context).getString(AppPref.PrefKey.PREF_CUSTOM_LOCALE_STR);
if (sLocaleMap == null) {
String[] languages = context.getResources().getStringArray(R.array.languages_key);
sLocaleMap = new HashMap<>(languages.length);
for (String lang : languages) {
if (LANG_AUTO.equals(lang)) {
sLocaleMap.put(LANG_AUTO, sDefaultLocale);
} else {
String[] langComponents = lang.split("-", 2);
if (langComponents.length == 1) {
sLocaleMap.put(lang, new Locale(langComponents[0]));
} else if (langComponents.length == 2) {
sLocaleMap.put(lang, new Locale(langComponents[0], langComponents[1]));
} else {
Log.d("LangUtils", "Invalid language: " + lang);
sLocaleMap.put(LANG_AUTO, sDefaultLocale);
}
}
}
}
Locale locale = sLocaleMap.get(language);
return locale != null ? locale : sDefaultLocale;
}
public static Context attachBaseContext(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return updateResources(context);
} else {
return context;
}
}
@TargetApi(Build.VERSION_CODES.N)
private static Context updateResources(@NonNull Context context) {
Resources resources = context.getResources();
Locale locale = getLocaleByLanguage(context);
Configuration configuration = resources.getConfiguration();
configuration.setLocale(locale);
configuration.setLocales(new LocaleList(locale));
return context.createConfigurationContext(configuration);
}
}
Therefore, my conclusions are:
The workarounds that I can think of are:
Context#getResources()
for an activity.EDIT(30 Oct 2020): Some people have suggested using a ContextWrapper
. I've tried using one (like below) but still the same issue. As soon as I wrap the application context using the context wrapper, locale stops working for activities and fragments. Nothing changes.
public class MyContextWrapper extends ContextWrapper {
public MyContextWrapper(Context base) {
super(base);
}
@NonNull
public static ContextWrapper wrap(@NonNull Context context) {
Resources res = context.getResources();
Configuration configuration = res.getConfiguration();
Locale locale = LangUtils.getLocaleByLanguage(context);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
configuration.setLocale(locale);
LocaleList localeList = new LocaleList(locale);
LocaleList.setDefault(localeList);
configuration.setLocales(localeList);
} else {
configuration.setLocale(locale);
DisplayMetrics dm = res.getDisplayMetrics();
res.updateConfiguration(configuration, dm);
}
configuration.setLayoutDirection(locale);
context = context.createConfigurationContext(configuration);
return new MyContextWrapper(context);
}
}
Upvotes: 6
Views: 9217
Reputation: 9441
A blog article, how to change the language on Android at runtime and don’t go mad, addressed the issue (along with others) and the author created a library called Lingver to solve the issues.
EDIT (13 Feb 2023): AndroidX Appcompat library 1.6.0 introduced an option to change locale dynamically. However, as of today, it can only apply locale for activities only. I've rewritten the class to support the library, but be aware that you can't use the automatic handling of languages provided with the library because it requires the app to initialise first. So, why use the library? For two reasons:
EDIT (3 Jun 2022): Lingver library has completely failed to address a few issues and appears to be inactive for some time. After a thorough investigation, I have came up with my own implementation: (You can copy the code below under the terms of either Apache-2.0 or GPL-3.0-or-later license)
LangUtils.java
public final class LangUtils {
public static final String LANG_AUTO = "auto";
public static final String LANG_DEFAULT = "en";
private static ArrayMap<String, Locale> sLocaleMap;
public static void setAppLanguages(@NonNull Context context) {
if (sLocaleMap == null) sLocaleMap = new ArrayMap<>();
Resources res = context.getResources();
Configuration conf = res.getConfiguration();
// Assume that there is an array called language_key which contains all the supported language tags
String[] locales = context.getResources().getStringArray(R.array.languages_key);
Locale appDefaultLocale = Locale.forLanguageTag(LANG_DEFAULT);
for (String locale : locales) {
conf.setLocale(Locale.forLanguageTag(locale));
Context ctx = context.createConfigurationContext(conf);
String langTag = ctx.getString(R.string._lang_tag);
if (LANG_AUTO.equals(locale)) {
sLocaleMap.put(LANG_AUTO, null);
} else if (LANG_DEFAULT.equals(langTag)) {
sLocaleMap.put(LANG_DEFAULT, appDefaultLocale);
} else sLocaleMap.put(locale, ConfigurationCompat.getLocales(conf).get(0));
}
}
@NonNull
public static ArrayMap<String, Locale> getAppLanguages(@NonNull Context context) {
if (sLocaleMap == null) setAppLanguages(context);
return sLocaleMap;
}
@NonNull
public static Locale getFromPreference(@NonNull Context context) {
if (BuildCompat.isAtLeastT()) {
Locale locale = AppCompatDelegate.getApplicationLocales().getFirstMatch(getAppLanguages(context).keySet()
.toArray(new String[0]));
if (locale != null) {
return locale;
}
}
// Fall-back to shared preferences
String language = // TODO: Fetch current language from the shared preferences
Locale locale = getAppLanguages(context).get(language);
if (locale != null) {
return locale;
}
// Load from system configuration
Configuration conf = Resources.getSystem().getConfiguration();
//noinspection deprecation
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ? conf.getLocales().get(0) : conf.locale;
}
private static Locale applyLocale(Context context) {
return applyLocale(context, LangUtils.getFromPreference(context));
}
public static Locale applyLocale(@NonNull Context context, @NonNull Locale locale) {
AppCompatDelegate.setApplicationLocales(LocaleListCompat.create(locale));
updateResources(context.getApplicationContext(), locale);
return locale;
}
private static void updateResources(@NonNull Context context, @NonNull Locale locale) {
Locale.setDefault(locale);
Resources res = context.getResources();
Configuration conf = res.getConfiguration();
//noinspection deprecation
Locale current = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ? conf.getLocales().get(0) : conf.locale;
if (current == locale) {
return;
}
conf = new Configuration(conf);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
setLocaleApi24(conf, locale);
} else {
conf.setLocale(locale);
}
//noinspection deprecation
res.updateConfiguration(conf, res.getDisplayMetrics());
}
@RequiresApi(Build.VERSION_CODES.N)
private static void setLocaleApi24(@NonNull Configuration config, @NonNull Locale locale) {
LocaleList defaultLocales = LocaleList.getDefault();
LinkedHashSet<Locale> locales = new LinkedHashSet<>(defaultLocales.size() + 1);
// Bring the target locale to the front of the list
// There's a hidden API, but it's not currently used here.
locales.add(locale);
for (int i = 0; i < defaultLocales.size(); ++i) {
locales.add(defaultLocales.get(i));
}
config.setLocales(new LocaleList(locales.toArray(new Locale[0])));
}
}
MyApplication.java
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
LangUtils.applyLocale(this);
}
}
In your preference where you are changing the language, you can simply apply the locale like this:
LangUtils.applyLocale(context, newLocale);
Activites that use Android WebView
After loading the webview via Activity.findViewById()
you can add the following line immediately:
// Fix locale issue due to WebView (https://issuetracker.google.com/issues/37113860)
LangUtils.applyLocale(context);
Upvotes: 6