Issue
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:
- If locale is set in the application context, regardless of whether you set activity context or not, locale will be set to application context only and not to activity (or any other) context.
- If locale isn't set in the application context but set in the activity context, the locale will be set to the activity context.
The workarounds that I can think of are:
- Set locale in the activity context and use them everywhere. But notifications, etc. will not work if there isn't any opened activity.
- Set locale in the application context and use it everywhere. But it means that you cannot take advantage of
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);
}
}
Solution
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 (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 init(@NonNull Application application) {
application.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {
private final HashMap<ComponentName, Locale> mLastLocales = new HashMap<>();
@Override
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
mLastLocales.put(activity.getComponentName(), applyLocaleToActivity(activity));
}
@Override
public void onActivityStarted(@NonNull Activity activity) {
if (!Objects.equals(mLastLocales.get(activity.getComponentName()), getFromPreference(activity))) {
Log.d("LangUtils", "Locale changed in activity " + activity.getComponentName());
ActivityCompat.recreate(activity);
}
}
@Override
public void onActivityResumed(@NonNull Activity activity) {
}
@Override
public void onActivityPaused(@NonNull Activity activity) {
}
@Override
public void onActivityStopped(@NonNull Activity activity) {
}
@Override
public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {
}
@Override
public void onActivityDestroyed(@NonNull Activity activity) {
mLastLocales.remove(activity.getComponentName());
}
});
application.registerComponentCallbacks(new ComponentCallbacks() {
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
applyLocale(application);
}
@Override
public void onLowMemory() {
}
});
applyLocale(application);
}
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) {
String language = // TODO: Fetch current language from the shared preferences
getAppLanguages(context);
Locale locale = sLocaleMap.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;
}
public static Locale applyLocaleToActivity(Activity activity) {
Locale locale = applyLocale(activity);
// Update title
try {
ActivityInfo info = activity.getPackageManager().getActivityInfo(activity.getComponentName(), 0);
if (info.labelRes != 0) {
activity.setTitle(info.labelRes);
}
} catch (Exception e) {
e.printStackTrace();
}
// Update menu
activity.invalidateOptionsMenu();
return locale;
}
private static Locale applyLocale(Context context) {
return applyLocale(context, LangUtils.getFromPreference(context));
}
private static Locale applyLocale(@NonNull Context context, @NonNull Locale locale) {
updateResources(context, locale);
Context appContext = context.getApplicationContext();
if (appContext != context) {
updateResources(appContext, 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.init(this);
}
}
In your preference where you are changing the language, you can simply restart the preferences activity like this:
ActivityCompat.recreate(activity);
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.applyLocaleToActivity(this);
Answered By - Muntashir Akon
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.