安卓 - 如何通过编程更改应用程序语言

6

在Android应用程序中更改语言环境从来都不是一件容易的事情。使用androidx.appcompat:appcompat:1.3.0-alpha02,似乎在应用程序中更改语言环境比我想象的要困难得多。它似乎表明活动上下文和应用程序上下文的行为非常不同。如果我使用一个通用的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) {
        // 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);
    }
}

因此,我的结论是:
  1. 如果在应用程序上下文中设置了locale,则无论是否设置了活动上下文,locale都将仅设置为应用程序上下文,而不是活动(或任何其他)上下文。
  2. 如果在应用程序上下文中未设置locale但在活动上下文中设置了,则会将locale设置为活动上下文。
我可以想到的解决方法是:
  1. 在活动上下文中设置语言环境并在所有地方使用它。但是,如果没有打开的活动,则通知等功能将无法工作。
  2. 在应用程序上下文中设置语言环境并在所有地方使用它。但这意味着您不能利用Context#getResources()来获取活动的资源。
编辑(2020年10月30日):有人建议使用ContextWrapper。我尝试过使用它,但是问题依旧。一旦我使用上下文包装器包装应用程序上下文,活动和片段的locale就停止工作了。

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引入了一个选项来动态更改语言环境。然而,截至今天,它只能应用于活动。我已经重新编写了该类以支持该库,但请注意,你无法使用该库提供的语言自动处理功能,因为它需要应用先初始化。那么为什么使用该库呢?有两个原因:
  1. 与Android 13兼容
  2. 库可以访问委托类,从而大大简化了一些事情。

编辑(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) {
        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);
    }
}

在您更改语言的偏好设置中,您可以像这样简单地应用区域设置:
LangUtils.applyLocale(context, newLocale);

使用Android WebView的活动 通过Activity.findViewById()加载WebView后,您可以立即添加以下行:

// Fix locale issue due to WebView (https://issuetracker.google.com/issues/37113860)
LangUtils.applyLocale(context);

我在堆栈中有许多活动,Lingver 是否与这些活动一起工作? - famfamfam
@famfamfam 是的,如果它们位于与更改语言的堆栈相同的堆栈中。 - Muntashir Akon
重要提示,请务必使用 ActivityCompat.recreate(activity)。否则,在 Android 8 以下的系统中,recreate() 后键盘将停止工作。 - Zakhar Rodionov
如果您需要通过应用程序上下文在API <33上解析字符串,则这是您唯一的选择。不幸的是,新的AppCompatDelegate.setApplicationLocales(locale) API仅适用于活动上下文:https://issuetracker.google.com/issues/243457462。 - digrec
1
@digrec:在最后一次更新之后,我不得不实现AppCompatDelegate的许多功能,因为为一个本应帮助开发人员的类编写解决方法并没有真正起到帮助作用。您可以在这里找到它:https://github.com/MuntashirAkon/AppManager/blob/master/app/src/main/java/io/github/muntashirakon/AppManager/utils/appearance/AppearanceUtils.java。如果有任何人需要,我可以添加可允许的许可证。 - Muntashir Akon
@MuntashirAkon 感谢您的更新和所有研究!我分享了这整件事情的相同沮丧。我仍在使用 AppCompatDelegate 来更改语言环境并在用户登录时重新创建活动(当用户的语言变更时),并将其与您的应用程序上下文配置更新(来自此答案)相结合。幸运的是,我不需要主题或方向配置,只需要语言环境。 - digrec

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接