2020-04-25

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),货拉拉资深客户端工程师