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条内容。
错误翻译:
- This is the first content. \\n\\n
- 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:paddingLeft | android:paddingStart |
android:paddingRight | android:paddingEnd |
android:drawableLeft | android:drawableStart |
android:drawableRight | android:drawableEnd |
android:layout_alignLeft | android:layout_alignStart |
android:layout_alignRight | android:layout_alignEnd |
android:layout_marginLeft | android:layout_marginStart |
android:layout_marginRight | android:layout_marginEnd |
android:layout_alignParentLeft | android:layout_alignParentStart |
android:layout_alignParentRight | android:layout_alignParentEnd |
android:layout_toLeftOf | android:layout_toStartOf |
android:layout_toRightOf | android:layout_toEndOf |
app:layout_constraintLeft_toLeftOf | app:layout_constraintStart_toStartOf |
app:layout_constraintLeft_toRightOf | app:layout_constraintStart_toEndOf |
app:layout_constraintRight_toRightOf | app:layout_constraintEnd_toEndOf |
app:layout_constraintRight_toLeftOf | app: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的属性 |
---|---|
leftMargin | setMarginStart() |
rightMargin | setMarginEnd() |
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 本地化适配:RTL(right-to-left) 适配清单
- Android阿拉伯适配rtl
- Android官方指导
- Material Icons Guide(不仅提供了Android的方案,还提供了iOS、Web的适配方案)
- 支持不同的语言和文化
- iOS官方指导
以上是关于Android 国际化与本地化探索的主要内容,如果未能解决你的问题,请参考以下文章