Android 国际化总结

题外话

我们经常看微信公众号的文章,手机看挺方便,但是电脑上要搜索,要看某一篇就比较费劲了。电脑上,我使用10条传送门 看的。一般收录的是一些比较热门的账号文章,有其他好的方式,欢迎推荐哈。

概述

本篇对近期 Android 海外版项目用到的国际化知识做了些总结。不对之处,欢迎指正哈。如若转载,请注明出处。

Hard Code

硬编码提取部分,QQ音乐技术团队的这篇文章已经讲得非常详细了,我就不重复造轮子了。里面讲述了通过 Lint 找出硬编码,自定义一份 profile,整理成 Excel,语言切换,创建多语言文件夹等,很值得一看。

文章好,但是有需要补充的地方。

添加地区配置文件

除了文章介绍的方法,还可以通过:右键点击 string.xml ,选中 【Open translate Editor
,打开编辑界面。


点击左边的蓝色圆圈可以增加国家区域,勾选【Show only keys needing translations】可以把没有翻译完成的字符串都找出来。比如图中的 all_picture 就是在其他国家的 string.xml 里面没有声明的,这样可以很方便的知道哪些字符串是漏写的。

语言切换

上面的文章介绍的语言切换方法,并不能很好的适应 Android 7.0 ,语言切换建议按照这篇文章的实现 ,其中封装有 updateResources 适用于 7.0 及以上,7.0 以下的用 updateResourcesLegacy 方法。此外还有一点要注意,7.0以上的更新了方法,所以必须要 ApplicationActivityattachBaseContext返回更新的 Context。

string 相关

%1$s 、%1$d 用法

比如用到一个字符串,已经扫描x本书。这个 x 值不是固定的,会随着我扫描的增加而增加。

一种原始的做法是,把这个字符串分解成,已经扫描x本书,三个部分。这种做法可以实现功能,但是多了这么多字符串,太麻烦了,明显不够优雅。

优雅的做法是这样的:
<string name="scan_books">已扫描%1$d本书</string>

用的时候是这样的,

1
2
int bookCounts = 5;	
String text = context.getString(R.string.scan_books, bookCounts);

其中 %1$d代表可以改变部分。

  • %1 代表第一个参数,如果有多个参数,就是 %2 , %3这样以此类推,比如,我想表达 已经扫描了x本书,覆盖y种学科,正确的表达方式是,

    1
    <string name="scan_books_example">已经扫描了%1$d本书,覆盖%2$d种学科</string>

    外面使用和原来一样,只是后面多了个参数。

    1
    2
    3
    int bookCounts = 5;
    int type = 2;
    String text = context.getString(R.string.scan_books, bookCounts, type);
  • $d 表示类型,其中 d 表示:整形 int , s 表示:字符串 String,f 表示浮点型。
    这部分和以前学 C 语言语法是一致的。

    %n$ms:设置m的值可以在输出之前放置m个空格

    %n$md:设置m的值可以在输出之前放置m个空格

    %n$mf:设置m的值可以控制小数位数,如一个数是 343.22634 , 但m = .2 时,截取小数点后面两位,因为第三位数值为 6,满五进一,所以小数点第二位为 3 ,输出结果为 343.23 。

复数

选择 x 本书,2本书,中文都用本修饰。但是英文则不一样,有复数的不同, one book , two books

这种情况下,我们应该使用 plurals 修饰。

values-en 下面的 strings.xml (英文环境)

1
2
3
4
<plurals name="choose_book">
<item quantity="one">Choose %1$d book</item>
<item quantity="other">Choose %1$d books</item>
</plurals>

values-zh-rHK 下面的 strings.xml (中国香港)

1
2
3
4
<plurals name="choose_book">
<item quantity="one">选择 %1$d 本</item>
<item quantity="other">选择 %1$d 本</item>
</plurals>

外面是这样使用的,

1
String titleText = getResources().getQuantityString(R.plurals.choose_book, size, size);

转义字符

比如 I’m ,Tom’s ,如果你在 strings.xml 输入,会出现编译不过的情况,这种情况下这个时候我们需要加入转义字符,I'mTom's

我不需要这样也可以在 strings.xml 里面输入呀?

你很可能用错成了中文字符 ,变成了 Im。

其他常用转义字符

符号 转义表示
&#34;&quot;
&#39;&apos;
& &#38;&amp;
< &#60;&lt;
> &#62;&gt;
换行 \n
空格 &#160;

国家区域列表

国家区域列表,包括国家名称还有国家区号。什么是国家区号?但你打电话的时候,细心的你会发现,有时候前面有个 +86 ,这个就是区号,86 代表的就是中国大陆,美国的是 1

比如有个需求,注册手机账号的时候,要供用户选择区号,比如完成如下列表,那么应该如何实现呢?

前提:Android API

我们先看 Android 系统 API 提供了我们什么功能。

系统都是通过 Local 这个类获取国家区域相关信息的,比如国家名称,对应语言等。但是要注意:不能获取对应的国家区号(86这些)

获取当前国家编号

1
2
3
4
5
6
7
8
/**
* 获取当前系统设置的国家编号,比如中国大陆 CN
* @return
*/
public static String getCurrentCountry() {
String country = Locale.getDefault().getCountry();
return country;
}

获取当前系统语言

提供两种方法,都是通过 Local 获取的。

1
2
3
4
5
6
7
8
9
/**
* 获得当前系统语言
* @return zh/en..
*/
public static String getCurrentLauguage(){
String mCurrentLanguage = Locale.getDefault().getLanguage();
//设置成简体中文的时候,getLanguage()返回的是zh
return mCurrentLanguage;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 获得当前系统语言(第二种方法,这种方法需要判断设备是否大于 Android 7.0)
* @param context
* @return zh/en..
*/
public static String getCurrentLauguageUseResources(Context context){
if (null != context && null != context.getResources() && null != context.getResources().getConfiguration()) {
Locale locale; //en_US
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
locale = context.getResources().getConfiguration().getLocales().get(0);
} else {
locale = context.getResources().getConfiguration().locale;
}

if (null != locale) {
String language = locale.getLanguage(); // 获得语言码 en
return language;
}
}
return null;
}

实例化 Local

local 有多个构造方法

1
2
3
public Locale(String language) {
this(language, "", "");
}

参数 language 表示获取的 Local 对象里面的字符串显示的语言类型,如果选择中文,locale 返回的就是中文,其他语言同理。

1
2
3
public Locale(String language, String country) {
this(language, country, "");
}

相比第一种构造方法,多了个 country (国家编码)参数,就是可以指定获取 country 对应的 local 信息。第一种构造方法,默认是获取当前系统选择的区域信息。

获取 Local 列表

1
2
3
4
5
6
7
8
/**
* 获取 Local 列表
* @return
*/
public static Locale[] getSystemLanguageList(){
Locale mLanguagelist[] = Locale.getAvailableLocales();
return mLanguagelist;
}

下面我们看看国家区号的获取方法,

方法 1:xml 文件

在网上找到各个国家的名称和对应的区号,之后写入 arrays.xml 文件,之后在用到的地方读取这个 xml 文件,显示出来。国家资源,可以参考这几篇资料。(资料1资料2资料3)。

这种写死的方式,在支持语言多的时候会比较麻烦,而且容易出错,所以推荐方法 2 。

方法 2:libphonenumber

使用 googlei18n/libphonenumber 。该库支持获取国家列表,对应的国家区号,号码是否合法等功能,支持 Java, C++ and JavaScript ,除此之外还收录了 C#, Objective-c, PHP, PostgreSQL, Python, Ruby 的方法。

获取国家区号

本质就是调用 phoneNumberUtil.getCountryCodeForRegion 其中参数是 getCurrentCountry 就是上面介绍的,代表国家编号。传入当前国家编号,就是获取当前国家区号。传入其他国家编码,就能获取其他国家编号。

1
2
3
4
5
6
7
8
9
10
11
/**
* 获取当前系统对应的国家区号,比如中国大陆 CN 对应 86
* @return
*/
public static int getCurrentCountryCode() {
PhoneNumberUtil phoneNumberUtil = PhoneNumberUtil.getInstance();
if (null != phoneNumberUtil) {
return phoneNumberUtil.getCountryCodeForRegion(getCurrentCountry());
}
return 0;
}

拼接 item 数据

我们每个 item 的数据是这样的 Afghanistan(+93) ,这样划分: Afghanistan(国家的显示名称)、 93(国家区号)、**(+ )** 修饰部分字符串。getSupportedRegions 获取集合,之后遍历集合,提现想要字段加入到 bean 里面。

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
/**
* 获取 item 数据集合
* 耗时操作,后台线程调用
* @return
*/
private List<CountryCodeBean> getCountryData() {
List<CountryCodeBean> datatList = new ArrayList<CountryCodeBean>();
//获取国家代码数据
PhoneNumberUtil phoneNumberUtil = PhoneNumberUtil.getInstance();
Set<String> set = phoneNumberUtil.getSupportedRegions();
Iterator<String> iterator = set.iterator();
while (iterator.hasNext()) {
String compactCountry = iterator.next();
//根据当前系统语言,输出 compactCountry(国家编码)对应的 local 对象。
Locale locale = new Locale(LanguageUtil.getCurrentLauguage(), compactCountry);
String displayCountry = locale.getDisplayCountry(); //获取详细的国家名称 China
int countryCode = phoneNumberUtil.getCountryCodeForRegion(compactCountry); //获取国家代码 86
CountryCodeBean bean = new CountryCodeBean();
if (null != bean) {
bean.setCompactCountry(compactCountry);
bean.setCountryCode(countryCode);
bean.setDisplayCountry(displayCountry);
datatList.add(bean);
}
}

sort(datatList);

return datatList;
}

默认是没有按照字母的顺序排列的,我们要做下排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 按照字母(国际化)排序数据集合
* 根据不同国家排序,中国就按拼音,英文就按英文字母排序
* @param datatList
*/
private void sort(List<CountryCodeBean> datatList) {
final Locale locale = new Locale(LanguageUtil.getCurrentLauguage());
Collections.sort(datatList, new Comparator<CountryCodeBean>() {
@Override
public int compare(CountryCodeBean lhs, CountryCodeBean rhs) {
String display1 = lhs.getDisplayCountry();
String display2 = rhs.getDisplayCountry();
//根据不同国家排序,中国就按拼音,英文就按英文字母排序
return Collator.getInstance(locale).compare(display1, display2);
//return display1.compareTo(display2);
}
});
}

最后再加入一些修饰的字符串(比如 (+ ) 即可),显示成需求想要的效果即可。

手机号码

判断手机号码是否合法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 判断号码是否合法
* @param number
* @param region 国家编码
* @return
*/
public static boolean isValidNumber(String number, String region) {
PhoneNumberUtil phoneNumberUtil = PhoneNumberUtil.getInstance();
try {
Phonenumber.PhoneNumber swissNumberProto = phoneNumberUtil.parse(number, region);
return phoneNumberUtil.isValidNumber(swissNumberProto);
} catch (NumberParseException e) {
}
return false;
}

phoneNumber 为未包括区号的号码,region 代表国家编码。方法里面会根据传入的国家编码,拼接号码,最终验证号码是否合法。

号码格式化显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
String swissNumberStr = "044 668 18 00";
PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();
try {
Phonenumber.PhoneNumber swissNumberProto = phoneUtil.parse(swissNumberStr, "CH");
if (null != swissNumberProto) {
//+41 44 668 18 00
String internationalStr = phoneUtil.format(swissNumberProto, PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL);
//044 668 18 00
String nationnalStr = phoneUtil.format(swissNumberProto, PhoneNumberUtil.PhoneNumberFormat.NATIONAL);
//+41446681800
String e164Str = phoneUtil.format(swissNumberProto, PhoneNumberUtil.PhoneNumberFormat.E164);
}

} catch (NumberParseException e) {
System.err.println("NumberParseException was thrown: " + e.toString());
}

初始化的时候可以知道区域比如 ‘’CH’’,使用过程也可以转到其他区域,不然下面为转到美国号码的显示。

1
2
//011 41 44 668 1800
phoneUtil.formatOutOfCountryCallingNumber(swissNumberProto, "US");

时区转换

时间戳

Unix 时间戳(Unix timestamp) 是指:从协调世界时(UTC)1970年1月1日0时0分0秒起至现在的总秒数,不考虑闰秒。

对于大多数用途来说: UTC 可以认为等价于 格林尼治标准时间 GMT

在 Android 中获取时间戳的方式,就是按 Java 的方式:System.currentTimeMillis() 获取的就是13位的时间戳。10位的时间戳表示秒级13位的表示毫秒级

那么比如一个10位时间戳:1503904245 。在北京时间 GMT+8:00 (位于东八区)就表示为: 2017-08-28 15:10:45。在纽约(北美东部夏令时间)GMT-4:00,则表示为:2017-08-28 3:10:45

实现

上面的方式如何用代码体现?方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 根据时区格式化输出
* @param unixTimeStamp unix 时间戳
* @param timeZone 输出时间所在时区
* @return timeZone 时区下,对应的时间
*/
private String fomatByTimeZone(long unixTimeStamp, TimeZone timeZone) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
dateFormat.setTimeZone(timeZone);
String formatStr = dateFormat.format(new Date(unixTimeStamp));
return formatStr;
}

如下方式调用,可以获取当前 unix 时间戳下,对应的东八区时间。

1
fomatByTimeZone(System.currentTimeMillis(), TimeZone.getTimeZone("GMT+08:00"));

特别注意

美国

美国实行夏令时 DST,美国夏令时始于每年4月的第1个周日,止于每年10月的最后一个周日。夏令时比正常时间早一小时,这样人们就可以早起早睡,充分利用日照时间。
举个例子,美国纽约在1月份,所在时区是 GMT-5:00 北美东部标准时间。在7月份,所在时区是 GMT-4:00 北美东部夏令时间

所以我们在 TimeZone.getTimeZone("GMT-04:00") 传入的参数,不应该写死值 GMT-04:00,而应该输入区域位置,比如纽约就是:America/New_York ,这样系统才能动态的根据不同时间段,给予我们不同的时区。

国内因为没有夏令时的区分,所以用 GMT+08:00Asia/Shanghai 是一样的。

区域列表,可以查看此处,需要注意的是,单词间空格部分,需要用下划线 _ 代替

服务端交互

服务端传给我们的时间,一般是时间戳的形式(10位/13位),我们前端获取时间戳后,将它转化为此时手机对应的时区。

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
/**
* 将时间戳转化为当前系统的时间
* @param timestam
* @param format 自定义输出格式:yyyy.MM.dd HH:mm:ss
* @return
*/
public static String fomatTimeZoneByQfq(String timestamp, String format) {
String result = "";
if (null != timestamp) {
long time = 0;
try {
if (timestamp.length() == 10) {
//PHP返回位10位
time = new Long(timestamp) * 1000L;
} else if (timestamp.length() == 13) {
//JAVA位13位
time = new Long(timestamp);
}

if (0 != time) {
result = fomatByTimeZone(time, TimeZone.getDefault(), format);
}

} catch (Exception e) {

}

}
return result;
}

/**
* 根据时区格式化输出
* @param unixTimeStamp unix 时间戳
* @param timeZone TimeZone.getTimeZone("GMT+08:00")
* @param format yyyy.MM.dd HH:mm:ss
* @return timeZone 时区下,对应的时间
*/
public static String fomatByTimeZone(long unixTimeStamp, TimeZone timeZone, String format) {
//SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
SimpleDateFormat dateFormat = new SimpleDateFormat(format);
dateFormat.setTimeZone(timeZone);
String formatStr = dateFormat.format(new Date(unixTimeStamp));
return formatStr;
}

手机上选择了个时间,要提交给服务端,这个时候可以和后端的商议,是用标准的时间戳,还是统一提交东八区的时间。我们是选择后者:

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
/**
* 将一个data字符串转化成东八区的时间
* @param data 2017-10-31 12:30:00 (在系统设置的时区下的原始时间)
* @param format yyyy-MM-dd HH:mm:ss 或者 yyyy/MM/dd HH:mm:ss 由外部指定格式
* @return 2017-10-31 12:30:00
*/
public static String fomatDataStringToGMT8ByQfq(String data, String format) {
long timeStamp = date2TimeStamp(data, format);
return fomatToGMT8ByQfq(String.valueOf(timeStamp), format);
}

/**
* 把时间戳转化成东八区的时间 (业务方法:传入值为null,或者为"0",或者非10位,13位的时候,返回"")
* @param timestamp
* @param format yyyy.MM.dd HH:mm:ss
* @return
*/
public static String fomatToGMT8ByQfq(String timestamp, String format) {
String result = "";
if (null != timestamp) {
long time = 0;
try {
if (timestamp.length() == 10) {
//PHP返回位10位
time = new Long(timestamp) * 1000L;
} else if (timestamp.length() == 13) {
//JAVA位13位
time = new Long(timestamp);
}

if (0 != time) {
//指定为东八区
result = fomatByTimeZone(time, TimeZone.getTimeZone("GMT+08:00"), format);
}

} catch (Exception e) {

}

}
return result;
}

更新/勘误

更新

2017.11.16

时间戳-与服务端交互

勘误

2017.11.16

修改部分单词拼写错误。

相关参考

[1]Android App国际化 - QQ音乐技术团队
[2]android中string.xml中%1$s、%1$d等的用法
[3]不可不知的 Android strings.xml 那些事
[4]在线中文简体转繁体
[5]Change Language Programmatically in Android
[6]country-list
[7]android 国际区号注册手机号编码 以及常用城市列表
[8]Android 选择国家对应区号 中英双版
[9]googlei18n/libphonenumber
[10]维基百科
[11]epochconverter.
[12]时间戳转化

坚持原创技术分享,您的支持将鼓励我继续创作!