Android国际化方案在货拉拉的实践
背景
海外Lalamove产品的功能需尽快升级,经海内外的产研团队评估后,确定以国内货拉拉系统为基础,国际化后用于海外市场。因此,12月起,海外产研与大陆产研中心合并项目整合资源,称产研国际化项目,通过这个项目,让我们具备全球范围内快速开城、具备跨区域工作的能力,从技术上变成真正国际化互联网企业。目前国际化项目已经在海外上线。国内货拉拉原有Android各个客户端(本次国际化涉及用户和司机)由于早期产品并没有规划国际化的需求,当然也有早期研发工程师的一些历史原因,在我们的项目工程中有很多Hard code的资源。
国际化需要做的事情
- 资源(字符串、图片等)提取与回填
- 地址 时区 货币国际化
- 地图改造
- 推送改造
- 第三方SDK改造(支付 社交分享及登录等 )
- 功能裁剪及UI调整等
- 上架google play
资源(字符串、图片等)提取与回填
创建语言文件夹
要想支持不同的语言和文化,那么APP就需要包含专门针对特定文化而设计的资源,Andriod会根据系统语言区域设置来解析特定的语言和图片资源,所以我们需要在Android project资源目录中创建不同的语言文件夹来提供支持,这也是Android国际化的处理方案。
多语言文件举例:具体可以查询i18n的一个对照表。
语言和地区 | 对应的语言文件夹 |
---|---|
中文 (中国) | values-zh-rCN |
英文(美国) | values-en-rUS |
英文(英国) | values-en-rGB |
英文(澳大利亚) | values-en-rAU |
英文 (爱尔兰) | values-en-rIE |
英文(印度) | values-en-rIN |
要添加对更多语言区域的支持,请在 res/
内创建额外的目录,创建完后如下

提取Hard Code
先来看一波正常的流程
在AndroidStudio中,选中要提取的Hard Code,快捷键(mac:option+enter),自动将代码中的字符串转成String 资源。
java脚本和正则表达式
我们统计了一下,用户端和司机端的字符串平均有3.5K个,如果全部采用这种方式来提取,显而是不合适的,最终我们通过java的脚本将布局文件中 含有 “android:textx=” 和 “android:hint=” 的内容全部自动抽离到默认的string.xml中。我们不得不说这个速度是比较快,但是也是“ 牺牲 ”了代码可读性来换取的,因为我们无法像正常资源文件那样做到见名知义的命名方式,取而代之的是“模块名+数字”的方式。
布局文件里的Hard Code通过上面的方式处理后虽然有“不香”的成份,但也算是完成了目标,接下来就是业务逻辑中Hard Code了,这块我们是采用正则的方式来提取的,正则表达式为:^((?!(*|//)).)+[\u4e00-\u9fa5],匹配到后,再将其抽离到String资源中。
所有的Hard code都抽取到了string.xml后,我们需要将其翻译成对应的语言。这里我们用到了一个翻译平台—crowdin,我们只需要上传默认的中文文件到crowdin平台,然后由翻译人员翻译成对应的语言,待所有的翻译完成后将语言文件放到工程对应的目录下即可.由于我们是多人协作开发,后续所有的语言更新都需要先从crowdin平台先更新再提交,否则就有被覆盖的风险,推荐使用crowdin的插件来获取更新。

crowdin后台翻译后的语言
代码中应用
在xml布局文件中,我们需要使用 @string/xxxx
去引用文字资源。
在java文件中,我们需要使用 getString(R.string.xxxx)
去引用文字资源。
当然还有一些特殊的需要引入占位符来进行格式化,例如:
<string name="module_freight_lat_lng" translatable="false">%1$f,%2$f</string>
String myIntAsString = String.format("module_freight_lat_lng", lat,lng);
语言切换
APP如果没有设置语言,我们会跟随系统语言为当前语言,如果用户通过APP更改了语言设置,本地会维护一个系统语言的环境变量,这里我们是通过SP来存储这个变量的值。
BaseActivity是所有Activity的基类
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(AppUtils.setLocal(newBase));
}
APPUtils.java
public static Context setLocal(Context context) {
return updateResources(context, getSetLanguageLocale(context));
}
private static Context updateResources(Context context, Locale locale) {
Locale.setDefault(locale);
Resources res = context.getResources();
Configuration config = new Configuration(res.getConfiguration());
config.setLocale(locale);
context = context.createConfigurationContext(config);
return context;
}
功能改造 与第三方替换
文化的差异,对APP的功能需求也是不一样的,有些功能我们需要隐藏,而有些功能我们需要增加。然而让我们最头疼的是国内和国外google服务的问题,地图方面国内我们基本上用的诸如baidu、 AMap等,推送就更多了,而在国外,地图基本上是google,推送就是Fcm。这一部份我们主要通过productFlavors和 配置来加载不同的功能 。
sea {
}
india {
}
huolala {
}
/**
* 是否为全球化版本
*
*/
public static boolean isGlobalization() {
if (BuildConfig.is_globalization) {
return true;
}
return false;
}
上架google play
由于大部分时间我们都是在国内发布版本,对于google的审核标准不是很熟悉,导致我们在这里踩了很多坑。
密钥托管

意思是要使用Android App Bundle,并要从Google Play的动态投放中受益,必须允许Google Play管理您的应用签名密钥,这是一个巨坑,我们差点踩进去了。
一旦你申请了APP Signing, google会给我们创建一个.jks(我们暂且命名google.jks),文件并存在google的服务器上,而我们第一次上传的应用签名.jks(暂且命名为app.jks)会被当作为应用上传的签名,如果你上传的包不是app.jks就会报错,校验失败。当你用app.jks上传后,google会抹去我们的app.jks签名,用google的google.jks为我们的应用签名,到这一步,你应该知道接下来要发生了什么。意味着应用只能通过google市场来更新,第三方的服务比如分享 推送 地图都不将不能使用。
解决方法:
- 去第三方服务后台配置成google.jks的密钥
- 更换包名
- 联系google更改(这个能不能过不好说)
WebViewSSL问题

What’s happening

意思是您的一个或多个应用包含onReceivedSslError处理程序的不安全实现,这使该应用容易受到中间人攻击。这里我们的做法是加一个弹窗,把决定权交给用用户,然后就可以了。
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
final SslErrorHandler mHandler;
mHandler = handler;
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setMessage("这是提示信息");
builder.setPositiveButton("继续打开", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mHandler.proceed();
}
});
builder.setNegativeButton("取消就关闭", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mHandler.cancel();
}
});
builder.setOnKeyListener(new DialogInterface.OnKeyListener() {
@Override
public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
mHandler.cancel();
dialog.dismiss();
return true;
}
return false;
}
});
AlertDialog dialog = builder.create();
dialog.show();
}
权限问题
对于权限这一块google管的是很严格的。我们在国内可能很多开发者对权限有“滥用”的习惯,就是不管APP有没有用到,先申请一波再说。在这里我们也入坑了,由于我们在国内有接入第三方的支付,其中有一个权限是这样的。
<uses-permission android:name="android.permission.NFC" />
这个在国内即使你没有使用,申请了也不会有问题,但是在国外,如果某个手机没有NFC芯片,那么该机器就不能安装这个APP,意味着支持的设备将大大减少。还有其他的一些敏感权限(例如 READ_SMS、SEND_SMS、WRITE_SMS、RECEIVE_SMS、RECEIVE_WAP_PUSH、RECEIVE_MMS),也要特别注意。
动态更新
对于通过 Google Play 分发的应用,不得采用 Google Play 更新机制以外的其他任何方式修改、替换或更新应用本身。同样地,应用不得从 Google Play 以外的其他来源下载可执行代码(例如 dex、JAR 和 .so 文件)。这项限制不适用于在虚拟机上运行且只具备有限的 Android API 使用权限的代码(例如 WebView 或浏览器中的 JavaScript),以下这些是国内用的比较多的,都需要做特殊处理。
Geitui
Tinker
X5Webview
作者介绍
朱体亮(liam.zhu),货拉拉资深客户端工程师