英文:
Android - Change application locale programmatically
问题
在 Android 应用内部更改语言环境从来都不是一件容易的事情。随着 androidx.appcompat:appcompat:1.3.0-alpha02
的引入,似乎在应用程序中更改语言环境变得比我想象的要困难得多。看起来活动上下文(activity context)和应用程序上下文(application context)的行为差异很大。如果我使用一个通用的 BaseActivity
来更改活动的语言环境(如下所示),那么它将对应的活动起作用。
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();
}
}
但是我还需要更改应用程序上下文的语言环境,因为这仅限于活动。为了做到这一点,我可以很容易地重写 Application#attachBaseContext()
方法,就像上面的代码一样。
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));
}
}
虽然这成功地更改了应用程序上下文的语言环境,但活动上下文不再遵循自定义的语言环境(无论我是否从 BaseActivity
扩展每个活动)。很奇怪。
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) {
// 从共享偏好中获取语言
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);
}
}
因此,我的结论是:
- 如果在应用程序上下文中设置了语言环境,无论您是否设置活动上下文,语言环境都将仅设置为应用程序上下文,而不会设置为活动(或任何其他)上下文。
- 如果应用程序上下文中未设置语言环境但在活动上下文中设置了语言环境,语言环境将设置为活动上下文。
我能想到的解决方法有:
- 在活动上下文中设置语言环境并在所有地方使用它。但是如果没有打开的活动,通知等将无法工作。
- 在应用程序上下文中设置语言环境并在所有地方使用它。但这意味着您无法利用
Context#getResources()
方法来为活动获取资源。
编辑(2020年10月30日): 有些人建议使用 ContextWrapper
。我尝试过使用一个(如下所示),但问题仍然存在。一旦我使用上下文包装器包装应用程序上下文,语言环境就不再适用于活动和片段。没有任何变化。
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
<details>
<summary>英文:</summary>
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**
```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);
}
}
答案1
得分: 5
一个博客文章,如何在Android运行时更改语言而不发疯,解决了这个问题(以及其他问题),作者创建了一个叫做Lingver的库来解决这些问题。
编辑(2023年2月13日): AndroidX Appcompat库1.6.0引入了一个选项来动态更改区域设置。然而,截至今日,它只能应用于活动。我已经重新编写了支持该库的类,但请注意,您不能使用库提供的自动语言处理,因为它需要首先初始化应用程序。那么,为什么要使用这个库呢?有两个原因:
- 与Android 13兼容
- 它大大简化了一些事情,因为库可以访问代理类。
编辑(2022年6月3日): Lingver库在解决一些问题上彻底失败,并且似乎已经有一段时间没有活动了。经过彻底的调查,我自己想出了一个实现方法:(您可以根据Apache-2.0或GPL-3.0-or-later许可将下面的代码复制下来)
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) {
// 代码省略...
}
@NonNull
public static ArrayMap<String, Locale> getAppLanguages(@NonNull Context context) {
// 代码省略...
}
@NonNull
public static Locale getFromPreference(@NonNull Context context) {
// 代码省略...
}
private static Locale applyLocale(Context context) {
// 代码省略...
}
public static Locale applyLocale(@NonNull Context context, @NonNull Locale locale) {
// 代码省略...
}
private static void updateResources(@NonNull Context context, @NonNull Locale locale) {
// 代码省略...
}
// 代码省略...
}
MyApplication.java
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
LangUtils.applyLocale(this);
}
}
在更改语言的首选项中,您可以像这样简单地应用区域设置:
LangUtils.applyLocale(context, newLocale);
使用Android WebView的活动
在通过Activity.findViewById()
加载Web视图后,您可以立即添加以下行:
// 修复由于WebView引起的区域设置问题(https://issuetracker.google.com/issues/37113860)
LangUtils.applyLocale(context);
英文:
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:
- Compatibility with Android 13
- It greatly simplifies a few things as the library can access the delegate class.
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);
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论