Android 国际化与本地化探索

Posted 乐翁龙

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 国际化与本地化探索相关的知识,希望对你有一定的参考价值。

android 国际化与本地化探索

缘起公司项目国际化与本地化的需求,本篇文章将从 语言翻译UI设计代码规范 ,这三个方面来进行阐述,其中不免包含本人的主观偏见,如果有任何宝贵的意见,欢迎指正。

1、翻译注意事项

由于我们现在使用的是 腾讯文档 协同处理的翻译文本,而腾讯文档导出后的Excel文件会有些奇怪的小问题,比如说标红文字前后的空格导出后空格会丢失,导致英语等其他语言显示异常。

同时,这么多的文字,加上多达十几种语言的处理,靠人工来转换成App所需资源实在是效率低下,所以脚本工程请参见:http://192.168.1.28:8888/dragon/ExcelProcessor。该工程会根据Excel文件转换为Android和ios所需的多语言资源,具体使用方式如有需要请参阅项目README文档及源码注释。

在项目中有些文字是不确定的,需要根据情况动态改变。例如说:“小时之前”,这个,是会根据你当前时间自动计算的,现在看是“1小时之前”,那么1小时后再看就是“2小时之前”了,所以我们需要一个占位符来表示这个几,Android中会选择使用 %s ,或者 %d ,也就是 %d小时之前 。那么翻译的时候这个%d就要留出来不翻译了,我们可以考虑提供该段文字的语境,这样让翻译人员也不会一头雾水到底要怎么翻译“小时之前”了。

还有一些特殊的字符,例如英文单引号(’)、And符号(&)、英文双引号("),这些在资源文件中都有特殊含义,所以建议尽量少使用这些符号来进行翻译,如果使用了也无伤大雅,上述脚本工程会自动转义掉这些特殊符号。

一份合格、标准的多语言文档真的相当重要,所以概述注意事项如下。

1.1、尽量避免使用简写!

例如:距离通话结束还剩30秒。

错误示例:距离通话结束还剩30s。

1.2、严禁回车、换行等!

例如:1、这是第1条内容。\\n\\n2、这是第2条内容。

错误翻译:

  1. This is the first content. \\n\\n
  2. This is the second content.(iOS上会使用原格式,导致多换行)

正确翻译:1. This is the first content. \\n\\n2. This is the second content.

1.3、尽量避免使用&、’、”等特殊符号!

例如:建议和反馈

错误翻译:suggestions & feedback(单引号、双引号、And符号都有特殊含义)
正确翻译:suggestions and feedback

1.4、注意空格的使用!

例如:%s天之前

错误翻译:%sdays ago(英语、俄罗斯语、阿拉伯语等需要有空格)
正确翻译:%s days ago(中文、日语等不需要空格)

2、设计探讨事项

此处的设计包括了需求设计和UI设计两方面,直接来看几个实例吧!

2.1、标题栏

关于标题栏,如下图这种设计,左边返回按钮,中间标题区域,右侧子标题。这种设计有个问题就是当换用其他语言,例如俄罗斯语、马来语等,这种语言长度会很长,容易导致标题栏和子标题栏遮挡或者显示不全等问题。


当把右侧的文字标题设计为能明确表示意义的图标后,右侧区域长度完全可控了,至于标题换成其他语言后长度过长的问题,可能还是需要使用省略号等来进行处理。或者根据文字长度设置动态改变其大小。


下面的这种设计标题靠近返回按钮,也可采用,具体看App的设计了。但是第一种的设计建议千万不要采用。

2.2、输入框

关于输入框,一般在设计的时候可能会有如下第一种设计,文字类似这样:【群组名:请输入群组的名称】。中文情况下没有问题,因为文字大部分情况下都不会很长,UI基本无需适配。而换用俄罗斯语后,上述文字翻译后的长度会将UI完全挤变形,或者根本无法完全展示其提示信息。

我们可以考虑输入框中直接展示【群组名】,这样就表明这个输入框输入的是一个群组名了,省去“请输入群组的名称”这样的提示信息,这样的处理多了,还能省下一笔翻译费用。


实在需要提示语,那么建议采用上下的设计,如下图,这样保证上下两行文字不会互相影响,以免造成横向上排列不开的问题,这种情况下就算翻译后提示语过长,也可以换行处理,不会特别影响UI的显示效果(目前Google的Material设计输入框的默认效果就类似这样)。

2.3、标签栏

如下标签栏,一般我们可能会展示用户的排行榜,日榜、周榜、月榜,然后就这样居中均分设计了三个Tab标签,但是在翻译成其他语言后,可能【日榜】的翻译可能就占了大半屏幕,这样的话UI效果就大打折扣了。


我们可以考虑从左到右进行设计布局,如果需求之后添加【年榜】,那么我们可以直接布局到最右侧,当tab标签长度过长的时候,甚至可以让Tab横向滚动展示,这样可以最佳的去适配任何语言。

2.4、状态视图

如下,例如我们经常可能需要展示一些用户的状态信息,例如设计了如下在线状态,将状态信息展示在了用户昵称后。由于用户昵称本身长短不一,多语言情况下翻译后的状态信息文字也长短不一,虽然状态信息很详细的展示了出来,这样的显示效果其实很差,昵称和状态信息肯定会比较拥挤。


所以我们是否可以考虑使用表示状态的各色的点,来代替“在线”、“online”这种具体的文字呢?采用下图的设计显示效果就完全可控了,唯一的缺点就是颜色点无法让用户准确的理解到底是什么状态,可能需要培养一段时间的用户习惯。

还有的情况下,用户离线了我们可能会展示离线时间,这种翻译出来后也是长短不一,所以是否可以考虑如下的纵向设计,而不是将其显示在屏幕的最右侧。

2.5、其他

总之目前就一条:慎用横向排列的组件设计,横向空间有限,竖向可以无限的滚动。如果一个普通用户信息列表页面,既要用户横向滚动,又需要纵向滚动才能展示完全信息,那这个页面的用户使用率肯定大打折扣了!

3、代码设计规范

3.0、支持国际化和本地化

首先android支持切换多种语言,只需在资源文件夹中新建对应语言的values-xx文件夹,然后保证strings.xml中的字符串资源一一对应即可。

Android Studio为我们提供了很方便的方式,新建资源文件夹,然后在可选的限定符内选择相关内容即可,如上图所示,可以创建支持阿拉伯语的values-ar资源文件夹。

然后还需要注意的一点就是,阿拉伯语是从右向左的,也就是RTL。如果要适配阿拉伯语,在app的清单文件中声明支持RTL即可,剩下的就是布局中的适配了,请继续重点阅读下文:

android:supportsRtl="true"

如果我们需要进行app内手动更换语言,那么直接配置相关的Local即可,注意7.0前后版本的适配问题即可,如下,直接定义了切换为阿拉伯语(ar)。

public static Context attachBaseContext(Context context) 
    String language = getCurrentLanguage();
    Resources resources = context.getResources();
    Configuration configuration = resources.getConfiguration();
    Locale locale = new Locale(language);
    mLocale = locale;
    Locale.setDefault(locale);
    //注意7.0以上的处理方式
    if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) 
        configuration.setLocales(new LocaleList(locale));
        return context.createConfigurationContext(configuration);
     else 
        configuration.setLocale(locale);
        configuration.locale = locale;
        resources.updateConfiguration(configuration, resources.getDisplayMetrics());
        return context;
    

然后7.0以上重写Activity的attachBaseContext(Context newBase)即可:

@Override
protected void attachBaseContext(Context newBase) 
    super.attachBaseContext(LanguageUtil.attachBaseContext(newBase));

但是在7.0及以下的手机需要在ActivityonCreate()中setContentView()之前调用:

@Override
protected void onCreate(Bundle savedInstanceState) 
    super.onCreate(savedInstanceState);
	//7.0及以下手机还需要在这里进行处理
    LanguageUtil.attachBaseContext(this);
    
    setContentView(R.layout.activity_main);

3.1、XML相关规范

这里的代码规范我们以阿拉伯语为例来展示RTL这种XML布局方式。
(注意:相关代码仅展示了必须的属性,并非完整代码。)

3.1.1、代码规范

之前大家可能没有注意到过以下这些属性的区别,我们也是习惯了直接去书写控件的left和right属性,这在开发国内的App的时候完全没有问题,但是只要适配RTL布局,那么这就是灾难了(夸张的手法)。

所以按照Google推荐的方式,我们可以使用start和end属性来替代left、right。由于我们现在适配一般都是从Android 5.0甚至更高版本开始的,所以也无需考虑start和end在低版本系统上的兼容性了。

仅支持LTR的属性支持LTR、RTL的属性
android:gravity=“left”android:gravity=“start”
android:gravity=“right”android:gravity=“end”
android:layout_gravity=“left”android:layout_gravity=“start”
android:layout_gravity=“right”android:layout_gravity=“end”
android:paddingLeftandroid:paddingStart
android:paddingRightandroid:paddingEnd
android:drawableLeftandroid:drawableStart
android:drawableRightandroid:drawableEnd
android:layout_alignLeftandroid:layout_alignStart
android:layout_alignRightandroid:layout_alignEnd
android:layout_marginLeftandroid:layout_marginStart
android:layout_marginRightandroid:layout_marginEnd
android:layout_alignParentLeftandroid:layout_alignParentStart
android:layout_alignParentRightandroid:layout_alignParentEnd
android:layout_toLeftOfandroid:layout_toStartOf
android:layout_toRightOfandroid:layout_toEndOf
app:layout_constraintLeft_toLeftOfapp:layout_constraintStart_toStartOf
app:layout_constraintLeft_toRightOfapp:layout_constraintStart_toEndOf
app:layout_constraintRight_toRightOfapp:layout_constraintEnd_toEndOf
app:layout_constraintRight_toLeftOfapp:layout_constraintEnd_toStartOf

强烈建议以后的xml的开发规范全部按照右侧方式书写,无论是否需要适配RTL布局,这样的写法无疑是最优解!

目前项目中的可以全局替换处理,也可以使用AS提供的工具,点击Refactor->Add RTL Support Where Possible,然后运行即可。

3.1.2、TextView

当TextView中设置的文本会根据语言环境自行切换时,按照正常的TextView去设置gravity = "start"属性没有问题。LTR的布局会自行从左向右展示,RTL的布局会自行从右向左展示。

但是,在显示不本地化处理的文字时,情况就不对了,如下代码,在阿拉伯语的环境下,需要显示中文:

<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="这是一段不本地化处理的示例文本"/>

来看下中文环境下的渲染结果:


然后切换到阿拉伯语环境,效果如下所示:

所以你看出来问题了吗?不本地化处理的文本还是按照它自己的中文环境进行了处理,即从左到右展示。但是这时候测试发难了,切换到阿拉伯语时候不应该从右到左展示吗,那不应该右对齐吗,你这样的布局也太难看了,改改吧!

没问题,加一行代码设置下TextView的对齐方式就好:

android:textAlignment="viewStart"

textAlignment属性支持的有:inherit、gravity、textStart、textEnd、center、viewStart、viewEnd。我们就看下viewStart的含义:

Align to the start of the view, which is ALIGN_LEFT if the view’s resolved layoutDirection is LTR, and ALIGN_RIGHT otherwise.

也就是说当设置了这个属性时,文本的布局会对齐到视图开始的部分,如果视图是LTR,那么对齐到左侧。否则对齐到右侧。所以再来看下设置后的渲染效果:

到这里相信各位小伙伴肯定对这个属性留下了深深的印象。但是,难道我们要给代码中的所有TextView手动添加这行代码吗?这也太累了吧,想偷懒!

我想说:可以!大家对style肯定不陌生吧,我们定义一个通用的TextView的style,设置它的textAlignment属性,然后在App的主题style中应用这个通用的style,自定义TextView的style代码很简单:

<style name="Text.Alignment" parent="@android:style/Widget.TextView">
    <item name="android:textAlignment">viewStart</item>
</style>

添加到你自己App的主题中去,记得是android:textViewStyle而不是android:textStyle,两个含义不一样哦:

<style name="AppTheme" parent="Theme.MaterialComponents.NoActionBar">
   <item name="android:textViewStyle">@style/Text.Alignment</item>
</style>

但是还有些文本是需要居中显示的,那你要单独再进行处理了,至于全局处理还是单个处理,请自行斟酌。

3.1.3、EditText

输入框和上述TextView也有同样的问题,按照上述方法自定义style即可,最后添加到自己App主题的时候记得属性是editTextStyle:

<item name="editTextStyle">@style/EditTextStyle.Alignment</item>

你以为这就解决问题了吗?是的,其实大部分问题都解决了,但是在部分vivo的手机上输入时候居然有半个光标的情况,如下所示:


起初测试直接给提了个bug,我也以为是个bug,后来多次尝试才觉得:这可能是vivo这个机型对阿拉伯语环境的“优化”?因为输入纯数字的时候EditeText无法判断这个到底是阿拉伯语还是其他语言,所以展示前后两个光标。当继续输入英文字符后,此时光标变为一个,位于英文字符之后。干脆回复测试:不予处理!!!

【请仔细阅读下文,思考为何会出现这种情况,且如何解决】

3.1.4、ImageView

关于ImageView其实在于RTL布局需不需要镜像的问题,这个问题说简单也简单说复杂也可以做的很复杂,我们先说最最最简单的方法。

  • 1、scaleX属性(推荐)

首先了解下scaleX属性,这个属性我们一般来处理X轴方向上视图的缩放,大于1时放大,小于1时缩小,那么小于0的时候呢?一开始的时候我从来没想过,但是国际化的时候翻阅官网各种镜像View的资料【见这里】,很神奇的就发现了这么个方式:

android:scaleX="-1"

X轴方向缩放到负值的时候,居然就是水平方向上的镜像效果了,牛批牛批,真的长见识了!!!(那么竖直方向的镜像效果你也应该知道怎么设置了,但是RTL布局不需要竖直方向上的镜像效果,不要多此一举哦)

正常LTR布局效果如下所示:


当设置以上属性的时候,布局效果如下所示,水平镜像:


关于是否需要ImageView的镜像效果,看你具体的图片了,以上图片只是为了演示镜像效果选择的,其实这个图片不需要镜像效果,因为上面有非本地化的文字显示,镜像之后反而不合适了。

好的,知道了怎么镜像图片,那么我们怎么根据语言环境设置相应的值呢,可以根据RTL的规范,我们在values文件夹同级目录建立values-ldrtl资源文件夹,这就表示RTL布局会使用该资源文件夹下的资源,然后建立integer.xml文件,内容如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <integer name="rtl_scale_x">-1</integer>
</resources>

同样,在values文件夹下也需要建立integer.xml文件,内容同上述xml代码类似,值需要改为1。

然后给ImageView设置应用如下属性值即可,表示在LTR布局中水平缩放是1,也就是正常显示原图片,在RTL布局中则会镜像图片进行显示:

android:scaleX="@integer/rtl_scale_x"
  • 2、rotation属性

说完了以上最简单的方法,我们再来说一个属性,rotation,这个属性是控制视图的旋转属性的,默认是0,当我们设置:

android:rotation="180"

此时效果图如下所示:


这个效果并不符合我们的预期,因为它还上下颠倒了。但是有些图片,比如我们上文演示的返回按钮,它旋转180度之后显示效果是跟我们预期相符的。

  • 3、rtl文件夹

再来一种方式,根据不同的资源文件夹,放置多种方向的图片,这种方式无疑会增加APK的体积(使用bundle的方式不受影响)。

针对所有RTL设置,则命名方式为:
• drawable -> drawable-ldrtl
• drawable-xhdpi -> drawable-ldrtl-xhdpi

如果只适配阿拉伯语这么一种语言,那么也可以是这样:
• drawable-xhdpi -> drawable-ar-xhdpi

同理drawable、values、layout等资源文件夹都可以如法炮制。

3.1.5、自定义Shape

我们一般自定义shape属性如下所示:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">

    <corners
        android:bottomLeftRadius="20dp"
        android:topLeftRadius="20dp" />

    <gradient
        android:angle="0"
        android:endColor="@color/mainColorEnd"
        android:startColor="@color/mainColorStart" />
</shape>

效果如下所示:


此时处理方法也可以类似上文的ImageView一样,给使用该Shape的View设置scaleX来做镜像处理,这样做最简便。

如果想在shape中直接处理的话,部分是可以的,但是还有点限制。
先看gradient属性,它支持设置angle,0表示从左到右,180则表示从右到左,所以可以根据上文一样,使用不同的integer.xml中的值进行处理。

但是corners这个属性只支持bottomLeftRadius、topLeftRadius,并不支持bottomStartRadius、topStartRadius,所以我们无法直接在该shape文件中处理。只能建立drawable-ldrtl文件夹,建立同名文件,然后修改bottomLeftRadius、topLeftRadius为bottomRightRadius、topRightRadius进行处理了。

3.1.6、不支持RTL的控件

目前ViewPager不支持RTL的布局方向,由于时间紧任务重,本项目中还未适配该组件,可以参考GitHub上的项目 RtlViewPager 进行处理,该项目已经5年未维护了,参考下原理即可。
同时建议使用ViewPager2逐步替换ViewPager。
TabLayout现在已经支持。

3.2、Java相关规范

最重要的一点:禁止任何形式的字符串硬编码!!!

3.2.1、代码规范

同xml一样,自定义View的时候也需要注意相关left、right的问题,其他未标明的也需要自己多注意:

仅支持LTR的属性支持LTR、RTL的属性
leftMarginsetMarginStart()
rightMarginsetMarginEnd()
setMargins(int left, int top, int right, int bottom)需自行处理left、right
setPadding(int left, int top, int right, int bottom)setPaddingRelative(int start, int top, int end, int bottom)

3.2.2、自定义View位置

由于自定义View的时候可能会使用到位置属性,而layoutParams中没有Start、End这种属性,只有Left、Right。所以当我们布局视图的位置时就要考虑到RTL和LTR布局了。

比如说:视频通话页面,LTR环境下是,默认远端视频是全屏展示的,自己的视频会显示在右上角。而到了RTL环境下,那么自己的视频就需要显示在左上角了。所以定位自己视频的位置时候就需要根据不同的环境进行处理。默认情况下我们计算出正常LTR布局下小视频流的中心位置,然后判断当前的布局环境,如下所示:

Configuration config = getResources().getConfiguration();
if (config.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) 
    mSmallCenterX = ScreenUtils.getScreenWidth() - mSmallCenterX;

如果是RTL布局环境,那么镜像下处理方式就是:屏幕宽度减去LTR布局下的X坐标的位置。

然后在切换本地视图和远端视图大小的时候,动画效果也可以正常执行了。

3.2.3、Throwable、Exception

一般项目中我们会有些自定义的异常等信息,所以经常会直接使用其构造函数直接硬编码message参数。在国际化项目中建议一开始就先定制基类,传递String资源ID进去。对于一些三方框架可能也要做下兼容处理,例如如下BaseException:

public class BaseException extends Exception 

    private final int strId;
    private final int errorCode;

    /**
     * 建议使用资源ID的方式
     */
    public BaseException(int errorCode, @StringRes int strId) 
        super("请使用资源ID的方式获取错误信息,获取String资源ID请使用getStrId()函数!");
        this.strId = strId;
        this.errorCode = errorCode;
    

    /**
     * 为兼容三方框架,还是保留了String类型的参数信息
     */
    public BaseException(int errorCode, String msg) 
        super(msg);
        this.strId = 0;
        this.errorCode = errorCode;
    

然后一般展示错误信息的话是在Activity,Fragment等进行展示,可以直接使用context.getString()来获取相应的信息:

public String getMessageFromThrowable(Throwable throwable) 
    if (throwable instanceof BaseException) 
        BaseException exception = (BaseException) throwable;
        return getString(exception.getStrId());
     else 
        return throwable.getMessage();
    

还有些情况可能是在工具类中直接吐司展示错误信息的,那么建议获取顶层Activity等然后使用getString()的方式来进行处理。

3.2.4、Toast

关于Toast一般项目中都会自定义统一的Toast,单说普通情况下,如下这两种方式都会跟随你设置后的语言进行展示:

Toast.makeText(getContext().getApplicationContext(), getString(R.string.sample_text), Toast.LENGTH_SHORT).show();
Toast.makeText(getContext(), getString(R.string.sample_text), Toast.LENGTH_SHORT).show();

有的项目可能还集成了blankj作者的工具箱utilcodex,在该工具箱中有一个工具类ToastUtils,它支持使用资源ID的形式,如下:

public static void showShort(@StringRes final int resId) 
		show(UtilsBridge.getString(resId), Toast.LENGTH_SHORT, DEFAULT_MAKER);

但是使用该方式获取的始终是跟随系统的语言,如果系统语言不在App支持列表内那就会使用App默认的语言,具体原因请查看源码。

3.2.5、文字格式化

一般我们在拼接文字的时候都会用到占位符来进行处理,例如:地址是:%s?,我们把地址固定为:688 号大街(注意688后有一个空格),那么使用String.format()函数处理过后,中文情况下显示效果为:

然而阿拉伯语显示情况却有点异常,如下:

为什么688跑到后面去了,正常情况下这段文字应该从左到右显示的,然而688却显示到了右边。这是因为我们给的固定地址中包含了数字这个特殊文本(同上前文的EditText输入数字时部分手机光标的情况),系统无法确定它是属于LTR的部分还是RTL的部分,所以显示就出了问题。

那么怎么处理呢?需要使用BidiFormatter类,该类unicodeWrap()函数会检测字符串方向,并封装该字符串,官方示例如下,直接使用了BidiFormatter.getInstance()

String.format(
		getString(R.string.did_you_mean), 
		BidiFormatter.getInstance().unicodeWrap(mySuggestion)
)

这种情况适合系统设置的语言为阿拉伯语的情况,如果我们是应用内切换语言,那么此时BidiFormatter.getInstance()默认的处理方式就不对了,我们需要使用其**BidiFormatter getInstance(Locale locale)**函数了,把我们本地的环境传递进去,例如:new Locale(“ar”)。这样处理完后才会正常显示,效果如下:

3.3、固定方向

加入有时候我们的控件不需要根据RTL布局来改变方向怎么处理呢?使用layoutDirection就可以强制该控件按照LTR或者RTL等方向来布局了。

android:layoutDirection="ltr"

参考文档

以上是关于Android 国际化与本地化探索的主要内容,如果未能解决你的问题,请参考以下文章

Compose中的国际化与本地化暗黑模式与夜间模式

Compose中的国际化与本地化暗黑模式与夜间模式

Compose中的国际化与本地化暗黑模式与黑夜模式

Android-语言国际化

android继续探索Fresco

Android 编译优化探索2 Hack字节码