20、联系人国际化:处理不同语言姓名格式、电话号码格式化、地址本地化、RTL布局适配
各位同学,今天我们来聊聊国际化。说实话,我早年做联系人模块时,觉得这玩意儿不就是存个名字、号码、地址嘛,有啥难的?直到我遇到了阿拉伯语用户的名字、日本的地址格式、还有从右往左的布局……嗯,那会儿我才意识到,国际化不是「翻译一下」那么简单,它是一整套数据结构和UI的适配工程。
这一章,我会把我在实际项目中踩过的坑、总结的经验,掰开揉碎了讲给你听。你想想看,一个联系人系统如果连名字都显示不对,用户会怎么想?
20.1 姓名格式的国际化处理
不同文化对姓名的排列顺序完全不同。西方人习惯「名在前,姓在后」,东亚人(中日韩)则是「姓在前,名在后」。更麻烦的是,有些国家的人有中间名,有些没有。
核心思路:不要硬编码姓名显示格式。Android 提供了 android.icu.text.PersonName 和 android.icu.util.ULocale 来辅助处理,但说实话,这套 API 在低版本上兼容性一般。我个人习惯的做法是:
- 在数据层,把姓(family name)、名(given name)、中间名(middle name)分开存储。
- 在展示层,根据当前用户的 Locale 动态拼接。
避坑指南:我曾经遇到过一个日本用户,他的姓氏是「佐藤」,名字是「太郎」,但系统把他的姓氏显示在了后面。用户直接投诉说「你们连我的名字都写不对」。从那以后,我强制要求所有联系人表单必须区分姓和名,并且根据 Locale 调整显示顺序。
// 根据 Locale 决定姓名显示顺序
public static String formatDisplayName(String familyName, String givenName, Locale locale) {
if (locale == null) {
locale = Locale.getDefault();
}
// 东亚地区:姓在前,名在后
if (locale.equals(Locale.CHINA) || locale.equals(Locale.JAPAN) || locale.equals(Locale.KOREA)) {
return familyName + " " + givenName;
} else {
// 西方:名在前,姓在后
return givenName + " " + familyName;
}
}
你可能会问:「那中间名怎么办?」嗯,我的建议是:中间名单独存,显示时放在 givenName 后面,familyName 前面。比如「John Michael Doe」,显示为「John Michael Doe」即可。
20.2 电话号码格式化:PhoneNumberUtils 的正确用法
电话号码格式化,说白了就是让一串数字变得「好读」。不同国家的号码长度、区号格式、分隔符都不一样。Android 自带的 PhoneNumberUtils 其实已经很强大了,但很多人不会用。
我建议你记住两个核心方法:
PhoneNumberUtils.formatNumber():根据当前 Locale 格式化号码。PhoneNumberUtils.isGlobalPhoneNumber():判断是否是有效的全球号码。
String rawNumber = "8613800138000";
String formatted = PhoneNumberUtils.formatNumber(rawNumber, "CN");
// 输出:+86 138 0013 8000
String usNumber = "12125551234";
String usFormatted = PhoneNumberUtils.formatNumber(usNumber, "US");
// 输出:+1 212-555-1234
小技巧:如果你需要格式化用户正在输入的号码(实时格式化),可以用 PhoneNumberUtils.formatNumber() 配合 TextWatcher。但注意,不要在用户输入过程中频繁格式化,否则光标位置会乱跳。我一般会在用户输入完成后(失去焦点时)统一格式化一次。
还有一个坑:PhoneNumberUtils 在 Android 10 以上已经标记为部分废弃,推荐使用 libphonenumber 库(Google 开源)。如果你要支持全球号码的严格校验,建议直接上这个库。
// 使用 libphonenumber 库(推荐)
PhoneNumberUtil util = PhoneNumberUtil.getInstance();
Phonenumber.PhoneNumber number = util.parse("13800138000", "CN");
String formatted = util.format(number, PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL);
// 输出:+86 138 0013 8000
20.3 地址本地化:从「省市区」到「都道府县」
地址格式化比姓名更复杂。中国的地址是「国-省-市-区-街道-门牌号」,从大到小。日本的地址是「邮编-都道府县-市-区-町-番地」,也是从大到小,但写法不同。美国的地址是「门牌号-街道-城市-州-邮编」,从小到大。
我的经验是:不要试图用一套模板适配所有国家。Android 提供了 android.location.Address 类,它包含了 getAdminArea()(省/州)、getLocality()(城市)、getSubLocality()(区)、getThoroughfare()(街道)等字段。但注意,这些字段在不同国家的映射关系不一样。
| 国家 | 地址格式 | 示例 |
|---|---|---|
| 中国 | 国家 + 省 + 市 + 区 + 街道 + 门牌号 | 中国 北京市 海淀区 中关村大街 1号 |
| 日本 | 邮编 + 都道府县 + 市 + 区 + 町 + 番地 | 〒100-0001 東京都 千代田区 千代田 1-1 |
| 美国 | 门牌号 + 街道 + 城市 + 州 + 邮编 | 1 Infinite Loop, Cupertino, CA 95014 |
// 根据国家代码格式化地址
public static String formatAddress(Address address, String countryCode) {
StringBuilder sb = new StringBuilder();
switch (countryCode) {
case "CN":
sb.append(address.getCountryName()).append(" ");
sb.append(address.getAdminArea()).append(" ");
sb.append(address.getLocality()).append(" ");
sb.append(address.getSubLocality()).append(" ");
sb.append(address.getThoroughfare());
break;
case "US":
sb.append(address.getThoroughfare()).append(", ");
sb.append(address.getLocality()).append(", ");
sb.append(address.getAdminArea()).append(" ");
sb.append(address.getPostalCode());
break;
// 其他国家的格式...
}
return sb.toString();
}
注意:地址字段可能为空。比如日本有些地址没有「区」这一级,美国有些地址没有「子区域」。一定要做空值判断,否则用户看到的地址会是一堆「null」。
20.4 RTL布局适配:从右往左的UI世界
阿拉伯语、希伯来语等语言的阅读方向是从右往左(RTL)。如果你的 App 只适配了从左往右(LTR),那在这些用户眼里,整个界面都是反的。
Android 的 RTL 支持其实很成熟了,核心就三点:
- 在 Manifest 中声明支持 RTL:
<application
android:supportsRtl="true">
...
</application>
- 使用「start」和「end」代替「left」和「right」:
<!-- 错误写法 -->
<TextView
android:layout_marginLeft="16dp"
android:paddingRight="8dp" />
<!-- 正确写法 -->
<TextView
android:layout_marginStart="16dp"
android:paddingEnd="8dp" />
- 测试时切换 RTL 模式:
在开发者选项中,开启「强制使用从右到左的布局方向」。这样你就能看到所有界面在 RTL 下的表现。
我曾经踩过的坑:有一个联系人详情页,电话号码的图标在左边,文字在右边。在 LTR 下没问题,但切换到 RTL 后,图标跑到了右边,文字在左边,看起来非常奇怪。解决方案是:图标使用 drawableStart 而不是 drawableLeft,这样系统会自动根据布局方向调整位置。
另外,联系人列表中的姓名对齐也要注意。阿拉伯语的名字是从右往左写的,如果你用 gravity="left",那名字会紧贴左边框,看起来像「悬空」了一样。应该用 gravity="start"。
20.5 知识体系总览
下面这张图,是我整理的本章节知识结构。你可以把它当作一个检查清单,看看自己有没有遗漏。
你看,国际化其实就四个维度:名字怎么排、号码怎么读、地址怎么写、界面怎么翻。每个维度都有对应的 Android API 或第三方库。我的建议是:不要等到产品上线了才考虑国际化,那会儿改起来成本太高了。从项目第一天起,就把这些规则写进代码里。
好了,这一章的内容就到这里。如果你在实际开发中遇到了其他国际化的问题,欢迎随时交流。