20、联系人国际化:处理不同语言姓名格式、电话号码格式化、地址本地化、RTL布局适配

各位同学,今天我们来聊聊国际化。说实话,我早年做联系人模块时,觉得这玩意儿不就是存个名字、号码、地址嘛,有啥难的?直到我遇到了阿拉伯语用户的名字、日本的地址格式、还有从右往左的布局……嗯,那会儿我才意识到,国际化不是「翻译一下」那么简单,它是一整套数据结构和UI的适配工程。

这一章,我会把我在实际项目中踩过的坑、总结的经验,掰开揉碎了讲给你听。你想想看,一个联系人系统如果连名字都显示不对,用户会怎么想?

20.1 姓名格式的国际化处理

不同文化对姓名的排列顺序完全不同。西方人习惯「名在前,姓在后」,东亚人(中日韩)则是「姓在前,名在后」。更麻烦的是,有些国家的人有中间名,有些没有。

核心思路:不要硬编码姓名显示格式。Android 提供了 android.icu.text.PersonNameandroid.icu.util.ULocale 来辅助处理,但说实话,这套 API 在低版本上兼容性一般。我个人习惯的做法是:

  1. 在数据层,把姓(family name)、名(given name)、中间名(middle name)分开存储。
  2. 在展示层,根据当前用户的 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 支持其实很成熟了,核心就三点:

  1. 在 Manifest 中声明支持 RTL:
<application
    android:supportsRtl="true">
    ...
</application>
  1. 使用「start」和「end」代替「left」和「right」:
<!-- 错误写法 -->
<TextView
    android:layout_marginLeft="16dp"
    android:paddingRight="8dp" />

<!-- 正确写法 -->
<TextView
    android:layout_marginStart="16dp"
    android:paddingEnd="8dp" />
  1. 测试时切换 RTL 模式:

在开发者选项中,开启「强制使用从右到左的布局方向」。这样你就能看到所有界面在 RTL 下的表现。

我曾经踩过的坑:有一个联系人详情页,电话号码的图标在左边,文字在右边。在 LTR 下没问题,但切换到 RTL 后,图标跑到了右边,文字在左边,看起来非常奇怪。解决方案是:图标使用 drawableStart 而不是 drawableLeft,这样系统会自动根据布局方向调整位置。

另外,联系人列表中的姓名对齐也要注意。阿拉伯语的名字是从右往左写的,如果你用 gravity="left",那名字会紧贴左边框,看起来像「悬空」了一样。应该用 gravity="start"

20.5 知识体系总览

下面这张图,是我整理的本章节知识结构。你可以把它当作一个检查清单,看看自己有没有遗漏。

联系人国际化 姓名格式 姓/名分开存储 Locale 动态拼接 东亚 vs 西方顺序 电话号码格式化 PhoneNumberUtils libphonenumber 库 实时格式化 vs 失焦格式化 地址本地化 Address 类字段映射 国家特定格式模板 空值处理 RTL 布局适配 supportsRtl="true" start/end 代替 left/right 开发者选项强制 RTL 测试 核心原则:数据层分离 + 展示层适配

你看,国际化其实就四个维度:名字怎么排、号码怎么读、地址怎么写、界面怎么翻。每个维度都有对应的 Android API 或第三方库。我的建议是:不要等到产品上线了才考虑国际化,那会儿改起来成本太高了。从项目第一天起,就把这些规则写进代码里。

好了,这一章的内容就到这里。如果你在实际开发中遇到了其他国际化的问题,欢迎随时交流。


公众号:蓝海资料掘金营,微信deep3321