APP为什么要做多语言?
首先如果APP的用户量超级多,并且不只在内地使用,海外也有市场。那么来自各个不同国家和地区的人使用的时候,肯定想把这个APP设置成他所熟悉的语言,比如微信,微博,支付宝等这些APP都支持多语言设置的。
此外还有一些股票类的APP,因为股票类的APP所提供的行情报价服务包含了各大证券市场的,有内地的上证指数,深圳指数,还有香港的恒生指数,以及美股的纳斯达克和道琼斯指数等等。这些不同市场的指数涵盖了中文简体和繁体,还有英文的使用人群。所以股票类APP为了更符合当地人的语言习惯,多语言(国际化)则是必然要做的事情。这类APP有大智慧,同花顺,富途牛牛等。
笔者所在公司的产品也是做股票类的,主要面向的用户是香港地区,所以支持繁体和英文多语言切换也是刚需了。
说到要做多语言,刚开始我们原生这边就觉得这不是Android系统自带就支持的东西吗?在res资源目录下新建不同语言的values文件名,Android系统就会自动的去找对应的语言包下的string显示出来,这应该不难吧?
于是在网上看了一些教程之后,就开始干了起来。结果在实际的开发过程中遇到了很多的问题,比如说Android7.0 的兼容性问题,以及在试用微信的Tinker热修复中遇到语言切换失效的问题等。接下来就来谈一谈笔者在APP多语言过程中的一些经验分享,希望能够帮到大家。
示例项目的效果图
APP多语言需要原生和后台都要做改动
后台的话,需要跟随客户端的语言来返回对应的语言数据,比如在客户端设置成简体的情况下,返回简体的数据,繁体的情况,则返回繁体数据。这就需要客户端在每次初始化和切换语言之后把请求接口中的语言参数改成对应的语言code,后台根据客户端的请求中所带的语言参数来决定返回什么样的语言。后台返回的语言数据,除了某些特别的词语为了符合本地的语言习惯,手动配置的,其他的简繁体词语,英文都是通过机器翻译的。
重点说一下原生Android的多语言的做法
1. 因为以前我们的字符串都是写死在Java代码中或者layout布局文件中的,这样的话就不可以根据多语言设置来变化,同时也比较难维护。
所以首先我们要把Java代码和layout布局文件中的写死的字符串硬编码抽取出来,统一的放到values/strings.xml的文件里面,这样就能为不同语言的values文件的strings.xml打下基础,这是前提条件。这也告诉我们在平时的开发中要注意到这一点,养成把字符串配置在strings.xml文件里面的习惯,维护起来方便,将来如果要做多语言的话也不用费时费力的把那些写死的字符串抽出来了,省了很多无脑的操作。
2. 新建一个你需要支持的语言包资源文件,比如values目录可以放默认的中文简体strings.xml文件,values-en则是放置英文版的strings,values-zh-rHK和values-zh-tTW这两个资源文件夹分别代表香港和*,*默认的语言是繁体,香港也有繁体的使用习惯,所以这两个文件里面放的strings.xml几乎都是一样的,但其实地方不一样,本地的语言习惯也是有一些区别的。比如我们写的博客用香港的繁体写也是博客,但是*人习惯称之为部落格(blog)。
res下的多语言资源文件如下图:
3. 多语言资源包配置好了之后,就是语言切换的代码编写了,
package com.finddreams.languagelib;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Build;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Log;
import org.greenrobot.eventbus.EventBus;
import java.util.Locale;
/** * 多语言切换的帮助类 * http://blog.csdn.net/finddreams */
public class MultiLanguageUtil {
private static final String TAG = "MultiLanguageUtil";
private static MultiLanguageUtil instance;
private Context mContext;
public static final String SAVE_LANGUAGE = "save_language";
public static void init(Context mContext) {
if (instance == null) {
synchronized (MultiLanguageUtil.class) {
if (instance == null) {
instance = new MultiLanguageUtil(mContext);
}
}
}
}
public static MultiLanguageUtil getInstance() {
if (instance == null) {
throw new IllegalStateException("You must be init MultiLanguageUtil first");
}
return instance;
}
private MultiLanguageUtil(Context context) {
this.mContext = context;
}
/** * 设置语言 */
public void setConfiguration() {
Locale targetLocale = getLanguageLocale();
Configuration configuration = mContext.getResources().getConfiguration();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
configuration.setLocale(targetLocale);
} else {
configuration.locale = targetLocale;
}
Resources resources = mContext.getResources();
DisplayMetrics dm = resources.getDisplayMetrics();
resources.updateConfiguration(configuration, dm);//语言更换生效的代码!
}
/** * 如果不是英文、简体中文、繁体中文,默认返回简体中文 * * @return */
private Locale getLanguageLocale() {
int languageType = CommSharedUtil.getInstance(mContext).getInt(MultiLanguageUtil.SAVE_LANGUAGE, 0);
if (languageType == LanguageType.LANGUAGE_FOLLOW_SYSTEM) {
Locale sysType = getSysLocale();
if (sysType.equals(Locale.ENGLISH)) {
return Locale.ENGLISH;
} else if (sysType.equals(Locale.TRADITIONAL_CHINESE)) {
return Locale.TRADITIONAL_CHINESE;
} else if (TextUtils.equals(sysType.getLanguage(), Locale.CHINA.getLanguage())) { //zh
if (TextUtils.equals(sysType.getCountry(), Locale.CHINA.getCountry())) { //适配华为mate9 zh_CN_#Hans
return Locale.SIMPLIFIED_CHINESE;
}
} else {
return Locale.SIMPLIFIED_CHINESE;
}
} else if (languageType == LanguageType.LANGUAGE_EN) {
return Locale.ENGLISH;
} else if (languageType == LanguageType.LANGUAGE_CHINESE_SIMPLIFIED) {
return Locale.SIMPLIFIED_CHINESE;
} else if (languageType == LanguageType.LANGUAGE_CHINESE_TRADITIONAL) {
return Locale.TRADITIONAL_CHINESE;
}
Log.e(TAG, "getLanguageLocale" + languageType + languageType);
getSystemLanguage(getSysLocale());
return Locale.SIMPLIFIED_CHINESE;
}
private String getSystemLanguage(Locale locale) {
return locale.getLanguage() + "_" + locale.getCountry();
}
//7.0以上获取方式需要特殊处理一下
public Locale getSysLocale() {
if (Build.VERSION.SDK_INT < 24) {
return mContext.getResources().getConfiguration().locale;
} else {
return mContext.getResources().getConfiguration().getLocales().get(0);
}
}
/** * 更新语言 * * @param languageType */
public void updateLanguage(int languageType) {
CommSharedUtil.getInstance(mContext).putInt(MultiLanguageUtil.SAVE_LANGUAGE, languageType);
MultiLanguageUtil.getInstance().setConfiguration();
EventBus.getDefault().post(new OnChangeLanguageEvent(languageType));
}
public String getLanguageName(Context context) {
int languageType = CommSharedUtil.getInstance(context).getInt(MultiLanguageUtil.SAVE_LANGUAGE,LanguageType.LANGUAGE_FOLLOW_SYSTEM);
if (languageType == LanguageType.LANGUAGE_EN) {
return mContext.getString(R.string.setting_language_english);
} else if (languageType == LanguageType.LANGUAGE_CHINESE_SIMPLIFIED) {
return mContext.getString(R.string.setting_simplified_chinese);
} else if (languageType == LanguageType.LANGUAGE_CHINESE_TRADITIONAL) {
return mContext.getString(R.string.setting_traditional_chinese);
}
return mContext.getString(R.string.setting_language_auto);
}
/** * 获取到用户保存的语言类型 * @return */
public int getLanguageType() {
int languageType = CommSharedUtil.getInstance(mContext).getInt(MultiLanguageUtil.SAVE_LANGUAGE, LanguageType.LANGUAGE_FOLLOW_SYSTEM);
if (languageType == LanguageType.LANGUAGE_CHINESE_SIMPLIFIED) {
return LanguageType.LANGUAGE_CHINESE_SIMPLIFIED;
} else if (languageType == LanguageType.LANGUAGE_CHINESE_TRADITIONAL) {
return LanguageType.LANGUAGE_CHINESE_TRADITIONAL;
} else if (languageType == LanguageType.LANGUAGE_FOLLOW_SYSTEM) {
return LanguageType.LANGUAGE_FOLLOW_SYSTEM;
}
Log.e(TAG, "getLanguageType" + languageType);
return languageType;
}
}
- 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
上面最关键的切换语言的代码是这个setConfiguration()是切换到对应的语言环境的方法,方法中通过Configuration对象来把语言对象locale设置进去,最后通过Resources对象来实现语言的更换生效。
/** * 设置语言 */
public void setConfiguration() {
Locale targetLocale = getLanguageLocale();
Configuration configuration = mContext.getResources().getConfiguration();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
configuration.setLocale(targetLocale);
} else {
configuration.locale = targetLocale;
}
Resources resources = mContext.getResources();
DisplayMetrics dm = resources.getDisplayMetrics();
resources.updateConfiguration(configuration, dm);//语言更换生效的代码!
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
getLanguageLocale()方法先获取到用户保存在手机中的语言是什么,如果没有设置过则是默认跟随系统的语言设置来,getSysLocale()来获取到本地的语言locale对象,通过语言的判断来返回对应的语言locale对象,默认返回简体的locale对象,如果用户选择了英文则是返回英文的Locale.ENGLISH,同理返回其他对应语言的Locale对象。
/** * 如果不是英文、简体中文、繁体中文,默认返回简体中文 * * @return */
private Locale getLanguageLocale() {
int languageType = CommSharedUtil.getInstance(mContext).getInt(LanguageUtil.SAVE_LANGUAGE, 0);
if (languageType == LanguageType.LANGUAGE_FOLLOW_SYSTEM) {
Locale sysType = getSysLocale();
if (sysType.equals(Locale.ENGLISH)) {
return Locale.ENGLISH;
} else if (sysType.equals(Locale.TRADITIONAL_CHINESE)) {
return Locale.TRADITIONAL_CHINESE;
} else if (TextUtils.equals(sysType.getLanguage(), Locale.CHINA.getLanguage())) { //zh
if (TextUtils.equals(sysType.getCountry(), Locale.CHINA.getCountry())) { //适配华为mate9 zh_CN_#Hans
return Locale.SIMPLIFIED_CHINESE;
}
} else {
return Locale.SIMPLIFIED_CHINESE;
}
} else if (languageType == LanguageType.LANGUAGE_EN) {
return Locale.ENGLISH;
} else if (languageType == LanguageType.LANGUAGE_CHINESE_SIMPLIFIED) {
return Locale.SIMPLIFIED_CHINESE;
} else if (languageType == LanguageType.LANGUAGE_CHINESE_TRADITIONAL) {
return Locale.TRADITIONAL_CHINESE;
}
Log.e(TAG, "getLanguageLocale" + languageType + languageType);
getSystemLanguage(getSysLocale());
return Locale.SIMPLIFIED_CHINESE;
}
- 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
在多语言切换选择的时候,调用updateLanguage方法来实现切换到用户选择的语言类型
/** * 更新语言 * * @param languageType */
public void updateLanguage(int languageType) {
CommSharedUtil.getInstance(mContext).putInt(LanguageUtil.SAVE_LANGUAGE, languageType);
MultiLanguageUtil.getInstance().setConfiguration();
EventBus.getDefault().post(new OnChangeLanguageEvent(languageType));
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
语言切换成功事件可以通过发生EventBus来通知想要知道这个事件的页面,来实现语言变化后刷新UI和更改请求接口中的语言参数重新请求服务端获取对应语言的数据来填充布局。
4. 多语言切换功能一般出现在二三级页面,选择其他语言确认之后跳转到哪里? Android 中很多的APP的做法是直接跳转到首页,比如微信都是直接删除所有的activity同时跳转到首页,这样再次打开其他二级页面的时候就会重新初始化页面,重新加载语言当然也会跟着变化成用户选择的语言。当然返回上一级页面也是可以的,只是处理起来会比较麻烦,所以推荐还是微信的切换语言之后的做法。
选择完语言确认的跳转逻辑如下:
@Override
public void onClick(View view) {
int id = view.getId();
int selectedLanguage = 0;
switch (id) {
case R.id.rl_followsytem:
setFollowSytemVisible();
selectedLanguage = LanguageType.LANGUAGE_FOLLOW_SYSTEM;
break;
case R.id.rl_simplified_chinese:
setSimplifiedVisible();
selectedLanguage = LanguageType.LANGUAGE_CHINESE_SIMPLIFIED;
break;
case R.id.rl_traditional_chinese:
setTraditionalVisible();
selectedLanguage = LanguageType.LANGUAGE_CHINESE_TRADITIONAL;
break;
case R.id.rl_english:
setEnglishVisible();
selectedLanguage = LanguageType.LANGUAGE_EN;
break;
}
LanguageUtil.getInstance().updateLanguage(selectedLanguage);
Intent intent = new Intent(SetLanguageActivity.this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
if (selectedLanguage == LanguageType.LANGUAGE_FOLLOW_SYSTEM) {
System.exit(0);
}
}
- 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
通过intent的 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_CLEAR_TASK)来清除activity的栈,来打开MainActivity看起来像是重启的效果。这个跳转过程要做到尽量的平滑,不然会被产品经理刁飞的。另外设置跟随系统有些特别,如果不先退出整个APP再回到首页的话,则不会有切换语言的效果。
多语言开发过程中的遇到的问题
1. 横竖屏切换(屏幕旋转)导致多语言(国际化)的设置失效
原因:当屏幕旋转横竖屏切换时,Activity的onConfigurationChanged方法使用了系统的语言设置,Activity中onConfigurationChanged的源码如下:
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
getDelegate().onConfigurationChanged(newConfig);
if (mResources != null) {
// The real (and thus managed) resources object was already updated
// by ResourcesManager, so pull the current metrics from there.
final DisplayMetrics newMetrics = super.getResources().getDisplayMetrics();
mResources.updateConfiguration(newConfig, newMetrics);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
从上面的代码中我们可以看到newConfig取的是系统的,这就解释了为什么APP明明设置成了繁体,但是打开一个横屏页面之后,发现APP又重置成了简体,就是这个方法导致的。
所以我们只需要在onConfigurationChanged方法中,重新设置为我们用户选择的语言配置就可以了。因为要在所有的Activity中的onConfigurationChanged中设置会有些麻烦,我们可以在application中的onConfigurationChanged方法中统一设置,这样就可以解决屏幕旋转造成的多语言失效问题。
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
MultiLanguageUtil.getInstance().setConfiguration();
}
- 1
- 2
- 3
- 4
- 5
2. 打开有 webview 的Activity会导致多语言失效,发生在Android7.0以上
原因:可能是webview在加载的过程中也会重新设置系统的语言,这样因为打开网站可能有英文版的和中文的。
解决方案,既然很多情况下都会造成多语言失效,不如我们统一在BaseActivity的onCreate方法中都设置
LanguageUtil.getInstance().setConfiguration();
- 1
- 2
当然如果有些Activity不是继承子BaseActivity,我们依然可以在application中的onCreate方法中使用这样的方式来拿到整个应用的所有activity的生命周期方法,然后在这样设置:
registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(Activity activity, Bundle bundle) {
}
@Override
public void onActivityStarted(Activity activity) {
}
@Override
public void onActivityResumed(Activity activity) {
MultiLanguageUtil.getInstance().setConfiguration();
}
@Override
public void onActivityPaused(Activity activity) {
}
@Override
public void onActivityStopped(Activity activity) {
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {
}
@Override
public void onActivityDestroyed(Activity activity) {
}
});
- 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
3. 使用微信开源的热修复框架Tinker,打了包含资源的补丁之后会导致多语言失效
笔者在试用的过程中遇到如果打了包含资源string文件的补丁之后,会导致多语言失效,本来选的繁体变成了简体语言,同时无论你怎么切换语言,都没有生效。这属于Tinker的bug,已经有人在Tinker的github主页上反馈了,但是这个issue 任然没有关闭:https://github.com/Tencent/tinker/issues/302 。 后来因为担心热修复技术可能会存在其他兼容性问题,所以还没有用在公司的app上。但是“热修复不是请客吃饭”,感谢Tinker开发者的努力。
多语言(国际化)过程虽然繁琐,充满了重复劳动,但总体来说不算难。最后附示例项目地址,欢迎大家提问
https://github.com/finddreams/AndroidMultiLanguage