关于App国际化,之前有讲到国际化资源、字符换、布局相关,想要了解的猛戳用力抱一下APP国际化 。借着本次重构多语言想跟大家聊一下多语言切换,多语言切换对于一款国际化App来讲是重中之重,并非难事,但是若要做好也是一件不容易的事情。
问题
Android N版本适配问题
AndroidX不同版本兼容问题
一些界面局部适配突然失效
切换系统导航,更改深色模式导致多语言无法适配
系统授权弹窗导致ApplicationContext中的Local被还原
切换语言,系统通知栏显示未翻译,重启后正常
Service服务中Toast不适配
系统Local.getDefault()之伤,如何正确获取系统当前语言
WebView第一次加载多语言不适配
系统广播中的获取context中的Local信息显示异常
上面我随手列出了项目中常见遇到的问题,有一些是随着Android版本升级而未做出相应兼容性调整造成的,有一些则是局部失效寻找原因所得。我们先了解下应用中一般多语言切换适配的方案,从中会提到这些问题相应的解决方案。
Android 13 语言偏好设置 最近Android 13发布了,讲多语言切换之前,我们先了解一下这个新平台多语言新特性。Android 13 在手机设置页面中新增了一个集中设置应用语言的选项,用于设置各个应用语言首选语言,如果你的应用存在多语言,谷歌强烈建议在设置中进行多语言切换,这样就无须在应用中去做多语言选择切换的功能,页面由设置中的应用语言界面统一管理,具体使用如下:
1. 如何让应用app显示在设置中的应用语言中
创建一个名为 res/xml/locales_config.xml
的文件,并指定您的应用的语言,如下所示:
1 2 3 4 5 6 <?xml version="1.0" encoding="utf-8" ?> <locale-config xmlns:android ="http://schemas.android.com/apk/res/android" > <locale android:name ="ja" /> <locale android:name ="fr" /> <locale android:name ="en" /> </locale-config >
1 2 3 4 5 6 7 <manifest ... <application ... android:localeConfig ="@xml/locales_config" > </application > </manifest >
2. 如何处理设置中的语言偏好 对于具有或想要使用应用内语言选择器的应用,请使用这些新 API(而非自定义应用逻辑)来处理相关设置和获取用户对应用的首选语言设置。
1 2 3 4 LocaleListCompat appLocale = LocaleListCompat.forLanguageTags("xx-YY" );AppCompatDelegate.setApplicationLocales(appLocale);
1 2 3 4 5 mContext.getSystemService(LocaleManager.class).setApplicationLocales(newLocaleList(Locale.forLanguageTag("xx-YY" )));
1 2 3 4 LocaleList currentAppLocales = mContext.getSystemService(LocaleManager.class).getApplicationLocales();
举个栗子,手机系统如果是中文,设置界面中的应用语言如果首选英文,app如果已启动,那么需要自己监听 onConfigurationChanged
来切换应用内部语言,如果未启动,第一次启动时候需要先去读取设置中的语言然后设置给当前应用。亦或是如果你的应用保留了内部切换语言的方案,那么语言切换是也应该调用以上API把当前语言刷到系统设置应用语言中,以保持同步。
讲完Android 13 多语言新特性,想要了解更多猛戳 ,我们继续回到本文的重点,应用内多语言切换如何去做适配。
多语言适配整体部分 1. application适配 我们为什么要适配Application,原因很简单,对于多语言来讲,我们其实最关心的是切换语言后,界面或者Toast等等显示是否已经翻译成所选择的语言,但是一般我们项目中都会直接或者间接用到ApplicationContext,比如Application中一些三方控件的初始化,还有一些项目中封装的工具类,为了方便全局一次行初始化,有可能甚至用到单例模式,当我们用到ApplicationContxt去getString(@StringRes int id),在切换语言后,如果不重启整个应用或者刷新ApplicaitonContext的local,那么肯定是无效的。 我们在启动APP时候,应该对Application中的context进行当前应用语言Local适配。
1 2 3 4 @Override protected void attachBaseContext (Context newBase) { super .attachBaseContext(LanguageUtil.attachBaseContext(newBase)); }
当我们做了系统的配置更改,比如说切换了系统导航或者说更改了深色模式,那么我们一般的处理是也是要对Application作出处理。
1 2 3 4 5 6 @Override public void onConfigurationChanged (Configuration newConfig) { super .onConfigurationChanged(newConfig); LanguageUtil.attachBaseContext(this ); }
如果项目中有用到ApplicationContext去getString(@StringRes int id)实现加载的提示语,那么如果只是单纯的重启界面则无法让当前的提示语跟随当前切换的语言,所以我们要么重启整个应用,要么对ApplicationContext中的Local也作出相应的更新方可,这里有一点问题,虽然Android N之后updateConfiguration是过时方法,官方给出使用createConfigurationContext代替,但是更新ApplicationContext的Local发现无效使用老版本updateConfiguration正常。
1 2 3 4 5 6 7 8 9 10 11 Resources resources = context.getResources();Configuration configuration = resources.getConfiguration();Locale locale = getLanguageLocale(newLanguage);if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { configuration.setLocales(new LocaleList (locale)); } else { configuration.setLocale(locale); } DisplayMetrics dm = resources.getDisplayMetrics();resources.updateConfiguration(configuration, dm);
如果你发现你的应用广播通知栏适配无效,那就是context中的Local在切换语言是并未及时更新Local,这里调试一下便知,如果是Applicaiton注册的广播,那么多半情况下是没有更新ApplicationContext的Local所导致的。
2. Service适配 如果你的Service有用到Toast提示或者UI相关的东西,你必须要对Service也进行适配,这时候Service中也需要重写attachBaseContext进行语言适配,否则语言适配也是无效的。
1 2 3 4 @Override protected void attachBaseContext (Context newBase) { super .attachBaseContext(LanguageUtil.getNewLocalContext(newBase)); }
3. Activity适配 Activity是我们最主要的适配的界面,正常的情况下我们直接在基类BaseActivity中去处理即可,但是值得注意的一点是如果我们使用的是Androidx而非support库,那么不同的版本适配有点区别,这也是官方组件的问题.记得一些第三方界面如果不是继承我们的BaseActivity需要单独处理即可。
1 2 3 4 5 6 7 8 9 @Override protected void attachBaseContext (Context newBase) { if (isSupportMultiLanguage()){ super .attachBaseContext(LanguageUtil.getNewLocalContext(newBase)); }else { super .attachBaseContext(newBase); } }
多语言适配基本步骤大概就是如此了,下面看一下适配的细节问题。
适配部分细节 1. Android N 适配 Android N开始,由于系统的API变更,updateConfiguration已经被沦为过时的方法。但是有一点需要大家注意,网上几乎全部的判断都是有问题的,API已经明确说明是在API25过时的,不等价于Build.VERSION_CODES.N
,所以你的项目用对了嘛,详情可参考下图。
还有一点Android N之后,手机系统的语言配置选项已经不是单选了,改为一个列表了,具体可以参考手机设置中的语言和输入法,所以setLocal(@Nullable Locale loc)
方法建议不要再使用了,我相信很多人还在用,正确的用法应该是setLocals(@Nullable LocaleList locales)
,需要传递一个集合。
1 2 3 4 5 6 7 8 public static Context attachBaseContext (Context context) { String language = LanguageSp.getLanguage(context); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { return createConfigurationContext(context, language); } else { return updateConfiguration(context, language); } }
1 2 3 4 5 6 7 8 9 10 11 12 @RequiresApi(api = Build.VERSION_CODES.N_MR1) private static Context createConfigurationContext (Context context, String language) { Resources resources = context.getResources(); Configuration configuration = resources.getConfiguration(); Locale locale = getLanguageLocale(language); Log.d(TAG, "current Language locale = " + locale); LocaleList localeList = new LocaleList (locale); configuration.setLocales(localeList); return context.createConfigurationContext(configuration); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 private static Context updateConfiguration (Context context, String language) { Resources resources = context.getResources(); Configuration configuration = resources.getConfiguration(); Locale locale = getLanguageLocale(language); Log.e(TAG, "updateLocalApiLow==== " + locale.getLanguage()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { configuration.setLocales(new LocaleList (locale)); } else { configuration.locale = locale; DisplayMetrics dm = resources.getDisplayMetrics(); resources.updateConfiguration(configuration, dm); } return context; }
2. 关于AndroidX版本兼容问题 当你的应用使用的是androidx.appcompat:appcompat:1.1.0时,BaseActivity中需要实现下面方法
1 2 3 4 5 6 7 8 9 10 @Override public void applyOverrideConfiguration (Configuration overrideConfiguration) { if (overrideConfiguration != null ) { int uiMode = overrideConfiguration.uiMode; overrideConfiguration.setTo(getBaseContext().getResources().getConfiguration()); overrideConfiguration.uiMode = uiMode; } super .applyOverrideConfiguration(overrideConfiguration); }
当你的应用使用的是androidx.appcompat:appcompat:1.2.0及以上时,BaseActivity中需要实现下面方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Override protected void attachBaseContext (Context newBase) { if (isSupportMultiLanguage()) { String language = LanguageSp.getLanguage(newBase); Context context = LanguageUtil.attachBaseContext(newBase, language); final Configuration configuration = context.getResources().getConfiguration(); final ContextThemeWrapper wrappedContext = new ContextThemeWrapper (context, R.style.Theme_AppCompat_Empty) { @Override public void applyOverrideConfiguration (Configuration overrideConfiguration) { if (overrideConfiguration != null ) { overrideConfiguration.setTo(configuration); } super .applyOverrideConfiguration(overrideConfiguration); } }; super .attachBaseContext(wrappedContext); } else { super .attachBaseContext(newBase); } }
3. 系统授权弹框导致Local失效 我们惊奇的发现,当我们首次进入APP选择语言后,当首页检查系统权限弹框的时候,Local被莫名其妙的重置了,我在想,可能因为google授权弹框他有自己的多语言翻译,所以不会采取我们的,所以把ApplicationContext中的Local给重置了,所以当我们点击允许或者仅在使用此应用时允许后需要再次把Application中的Local修改掉。
1 2 3 4 5 override fun onRequestPermissionsResult (requestCode: Int , permissions: Array <out String >, grantResults: IntArray ) { super .onRequestPermissionsResult(requestCode, permissions, grantResults) LanguageUtil.updateApplicationLocale(AppApplication.getAppContext(),LanguageSp.getLanguage(mContext)) }
4. 如何真正的获取系统语言 我们有可能会存在这个场景,当我们的APP不跟随系统语言的时候,使用的APP内部语言,我们去检测系统语言的时候如何去判断,是不是很多人在此跌倒了,无论是Local.getDefault()
还是LocalList.get(0)
始终获取的语言是错误的,应该通过以下渠道获取当前的系统语言。
1 2 3 4 5 6 7 8 9 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {return Resources.getSystem().getConfiguration().getLocales().get(0 ).getLanguage();} else { return Locale.getDefault().getLanguage();} return ConfigurationCompat.getLocales(Resources.getSystem().getConfiguration()).get(0 ).getLanguage();
5. 关于WebView适配 在原来的低版本切换语言中,我们会发现WebView第一次加载时,适配是无效的,再次加载则正常适配,所以网上也有了一道方案如下:
1 2 3 4 5 @Override public void onCreate (Bundle savedInstanceState) { ~~new WebView (this ).destroy(); ~~ super .onCreate(savedInstanceState); }
这套方案目前不在推荐,直接去替换attatchBaseContext()中的context则可,经过测试是完全正常的。
工具类 以下则是多语言操作的工具类,现在提供出来,需要的朋友可以自行进行改造。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 public class LanguageUtil { private static final String TAG = "LanguageUtil" ; private static HashMap<String, Locale> supportLanguage = new HashMap <String, Locale>(4 ) {{ put(Language.ENGLISH, Locale.ENGLISH); put(Language.FRANCE, Locale.FRANCE); put(Language.ARABIC, new Locale ("ar" , "" , "" )); }}; public static Context attachBaseContext (Context context) { String language = LanguageSp.getLanguage(context); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { return createConfigurationContext(context, language); } else { return updateConfiguration(context, language); } } public static Context attachBaseContext (Context context, String language) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { return createConfigurationContext(context, language); } else { return updateConfiguration(context, language); } } private static Locale getLanguageLocale (String language) { if (supportLanguage.containsKey(language)) { return supportLanguage.get(language); } else { Locale systemLocal = getSystemLocal(); for (String languageKey : supportLanguage.keySet()) { if (TextUtils.equals(supportLanguage.get(languageKey).getLanguage(), systemLocal.getLanguage())) { return systemLocal; } } } return Locale.ENGLISH; } public static Locale getCurrentLocale (Context context) { String language = LanguageSp.getLanguage(context); if (supportLanguage.containsKey(language)) { return supportLanguage.get(language); } else { Locale systemLocal = getSystemLocal(); for (String languageKey : supportLanguage.keySet()) { if (TextUtils.equals(supportLanguage.get(languageKey).getLanguage(), systemLocal.getLanguage())) { return systemLocal; } } } return Locale.ENGLISH; } private static Locale getSystemLocal () { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { return Resources.getSystem().getConfiguration().getLocales().get(0 ); } else { return Locale.getDefault(); } } private static Context updateConfiguration (Context context, String language) { Resources resources = context.getResources(); Configuration configuration = resources.getConfiguration(); Locale locale = getLanguageLocale(language); Log.e(TAG, "updateLocalApiLow==== " + locale.getLanguage()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { configuration.setLocales(new LocaleList (locale)); } else { configuration.locale = locale; DisplayMetrics dm = resources.getDisplayMetrics(); resources.updateConfiguration(configuration, dm); } return context; } @RequiresApi(api = Build.VERSION_CODES.N_MR1) private static Context createConfigurationContext (Context context, String language) { Resources resources = context.getResources(); Configuration configuration = resources.getConfiguration(); Locale locale = getLanguageLocale(language); Log.d(TAG, "current Language locale = " + locale); LocaleList localeList = new LocaleList (locale); configuration.setLocales(localeList); return context.createConfigurationContext(configuration); } public static void switchLanguage (String language, Activity activity, Class<?> cls) { LanguageSp.setLanguage(activity, language); Intent intent = new Intent (activity, cls); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); activity.startActivity(intent); activity.finish(); } public static void switchLanguage (String language, Activity activity, Class<?> cls, Bundle bundle) { LanguageSp.setLanguage(activity, language); Intent intent = new Intent (activity, cls); if (bundle != null ) { intent.putExtras(bundle); } intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); activity.startActivity(intent); activity.finish(); } public static Context getNewLocalContext (Context newBase) { try { Context context = LanguageUtil.attachBaseContext(newBase); final Configuration configuration = context.getResources().getConfiguration(); return new ContextThemeWrapper (context, R.style.Theme_AppCompat_Empty) { @Override public void applyOverrideConfiguration (Configuration overrideConfiguration) { if (overrideConfiguration != null ) { overrideConfiguration.setTo(configuration); } super .applyOverrideConfiguration(overrideConfiguration); } }; } catch (Exception e) { e.printStackTrace(); } return newBase; } public static void updateApplicationLocale (Context context, String newLanguage) { Resources resources = context.getResources(); Configuration configuration = resources.getConfiguration(); Locale locale = getLanguageLocale(newLanguage); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { configuration.setLocales(new LocaleList (locale)); } else { configuration.setLocale(locale); } DisplayMetrics dm = resources.getDisplayMetrics(); resources.updateConfiguration(configuration, dm); } }