国际化(i18n)

源: https://blog.ximinghui.org/e9b09f41/index.html

以 Java 21 为例,简单探索i18n。

一、什么是国际化 / i18n?

国际化是中文名,i18n是国际化英文单词“internationalization”的缩写,因为第一个字母i和最后一个字母n中间有18个字母。

应用程序国际化就是应用程序适应不同的地区/国家和语言。比如中国人打开软件看到的就是简体中文和符合中国人习惯格式,法国人看到的则是法语和符合他们习惯的格式。

二、地区/国家和语言

因为不同国家对某些地方是否属于独立国家的认同不一致,这里提醒尽量不用“国家(Country)”而是使用“地区(Region/Locale)”称呼来避免带来不必要的麻烦。本文将使用“地区”来称呼。

i18n主要以“地区”+“语言”为基础来进行适配。例子如“简体中文,新加坡”、“英文,香港”。

语言和地区更多信息见“Java中的语言和地区标准”。

三、尝试使用i18n打印本地化的欢迎语

小提示:可以使用更加符合本土习惯的“翻译”而不是机械地直译。比如,西方人看到的问候语是“Hey there! You look very smart today.”,台湾人看到的是“好久不見,歡迎回來!”,藏族人看到的是“བཀྲ་ཤིས་བདེ་ལེགས(大意为‘祝你好运’或‘愿所有吉祥的征兆到来’)”。相反,生硬的直译如某软:“请坐和放宽,好东西就要来了。”。

1. 创建i18n项目

项目结构:

i18n
 |- pom.xml
 |- src
     |- main
         |- java
             |- org.ximinghui
                 |- Test.java
         |- resource

pom.xml内容:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <!-- 项目信息 -->
    <groupId>org.ximinghui</groupId>
    <artifactId>i18n</artifactId>
    <version>1.0-SNAPSHOT</version>

    <!-- 属性定义 -->
    <properties>
        <!-- Java源代码版本 -->
        <maven.compiler.source>21</maven.compiler.source>
        <!-- Java字节码版本 -->
        <maven.compiler.target>21</maven.compiler.target>
        <!-- Java发行版本 -->
        <maven.compiler.release>21</maven.compiler.release>
        <!-- Java源码和资源文件编码 -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <!-- 依赖项 -->
    <dependencies>
        <!-- lombok:提供自动化管理类构造器、Setter方法、Getter方法、日志对象等能力的Java类管理库 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.34</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

</project>

Test.java内容:

package org.ximinghui.test;

public class Test {

    public static void main(String[] args) {
    }

}

2. 体验i18n

提示:这里使用Java标准的i18n而不是Spring的i18n。

创建资源捆:在resource目录创建messages.propertiesmessages_zh_CN.propertiesmessages_zh_TW.properties文件。

提示:properties文件编码为 iso 8859-1,且不支持Unicode(即非ASCII字符),但自Java 9开始,用于国际化的properties文件支持UTF-8且默认为UTF-8。需要注意的是,仅指用于i18n的properties文件。Properties类保持ISO 8859-1不变),而i18n文件读取是由PropertyResourceBundle类完成的且该类默认UTF-8)。

messages.properties内容:

greetings=Hey there! You look very smart today.

messages_zh_CN.properties内容:

greetings=看,星星在朝你眨眼,一切美好如你所愿。

messages_zh_TW.properties内容:

greetings=好久不見,歡迎回來!

提示:请确保IDE/编辑器支持UTF-8的properties文件编辑。常见的如 IntelliJ IDEA 默认强制properties文件使用ISO 8859-1且限制修改编码,应在Settings->Editor->File Encodings中更改(更改后请注意确保非i18n的properties文件依然为8859-1编码)。

打印问候语:

public static void main(String[] args) {
    // 获取资源捆
    ResourceBundle chinaResourceBundle = ResourceBundle.getBundle("messages", Locale.CHINA);
    // 获取国际化的问候语
    String greetings = chinaResourceBundle.getString("greetings");
    // 打印问候语:看,星星在朝你眨眼,一切美好如你所愿。
    System.out.println(greetings);
}

调整上述代码的Locale.CHINALocale.TAIWAN,再次运行则打印:“好久不見,歡迎回來!”

3. 解释

a. 资源捆(ResourceBundle)

资源捆是一组资源文件,比如语言包、图像或配置文件。上面的 messages.propertiesmessages_zh_CN.propertiesmessages_zh_TW.properties 文件就是一个名为“message”的资源捆,其后跟上语言和地区后缀。资源捆名字根据情况自行命名。

每个properties文件中都由一行行的键值对组成,key用来定位消息,value则为对应的本地化消息。

key的命名没有固定标准,根据团队情况保持统一就好,常见的如全字母小写 display.button.create=创建,或者全小写加下划线 display.button_text.save_change=Save change,或Spring Security Core中的风格AbstractUserDetailsAuthenticationProvider.credentialsExpired=User credentials have expired

语言和地区后缀部分解释见“Java中的语言和地区标准”。

b. 代码

代码ResourceBundle.getBundle("messages", Locale.CHINA)获取一个名为message,地区为CHINA的资源捆。

代码chinaResourceBundle.getString("greetings")获取键 “greetings” 对应的消息。

提示:具体传递的Locale对象,服务器端可以提供根据请求的Accept-Language相关标头来决定。或者应用程序也可以在数据库或前端存储用户时区、语言、日期时间格式等偏好信息,服务器根据存储的偏好来决定。或者以用户设备系统的当前所在地区来决定等等。

四、Java中的语言和地区标准

语言和地区是多对多的关系。一语言可以在多个地区存在,美国、新加坡、香港等也有简体中文;一地区也有多种语言,如中国除了简体中文还有蒙文、藏文、维吾尔文和壮文(字样见纸质人民币)。

JDK的Locale类中只定义了常见的语言地区,如果我们要支持小众些的语言,就需要自定义Locale了。

java.util.Locale的Javadoc文档有许多说明,这里简单说下两个部分:

1. language

language应是表示语言的代码,可以是ISO 639 alpha-2 语言代码ISO 639 alpha-3 语言代码IANA 注册的语言子标签

这里以IANA 注册的语言子标签为例,其中Type为language的都是可用的语言代码,取Subtag即可。如Type: language & Subtag: zh

提示:可以借助 https://glosbe.com/zh 网站查询语言信息,将url路径中的zh改成想要查询的Subtag值,比如查询藏语:https://glosbe.com/bo。该网站还可以链接到查询语言对应的维基百科和 Ethnologue站点(全球语言信息在线数据库,提供语言使用情况、分布、方言、相关的文化、历史背景等信息)。

提示:维基百科 - ISO_639:m页面也可用于语言Subtag的对照。

2. country / region

country / region应是ISO 3166 alpha-2 国家代码或 UN M.49 数字-3 区域代码。同样可以查询IANA 注册的语言子标签,但是要看类型为地区的,即Type: region

五、尝试添加藏语支持

参考IANA 注册的语言子标签,找到或Google搜藏语的Subtag / Language code,得知藏语为 bo。

有了语言,还得有地区。藏语使用主要分布在中国西藏自治区,但西藏并没有像港澳台一样有自己的Region子标签,所以区域上应选中国,即藏语, 中国。现属印度的锡金或一些别的地放(主要是喜马拉雅地区)也有使用藏语的藏族,因此可以创建 藏语, 印度来对应印度的藏语。

于是我们就可以构建出藏语Locale对象:

// 藏语, 中国
Locale chinaTibetan = new Locale.Builder().setLanguage("bo").setRegion("CN").build();
// 藏语, 印度
Locale indiaTibetan = new Locale.Builder().setLanguage("bo").setRegion("IN").build();

添加对应的资源文件:

messages_bo_CN.properties内容:

greetings=བཀྲ་ཤིས་བདེ་ལེགས

messages_bo_IN.properties内容:

greetings=བཀྲ་ཤིས་བདེ་ལེགས

传递自定义的Locale对象来打印对应的问候语:

String chinaTibetanGreetings = ResourceBundle.getBundle("messages", chinaTibetan).getString("greetings");
String indiaTibetanGreetings = ResourceBundle.getBundle("messages", indiaTibetan).getString("greetings");

System.out.println(chinaTibetanGreetings);
System.out.println(indiaTibetanGreetings);

六、默认资源文件

在"message"资源捆中,资源文件清单如下:

  • messages.properties
  • messages_bo_CN.properties
  • messages_bo_IN.properties
  • messages_zh_CN.properties
  • messages_zh_TW.properties

除了带语言和地区的文件外,还有message.propertires这样的后面什么都不带的文件,这就是默认资源文件,用于没有提供/找不到语言或地区时作为通用消息。

演示:

// 随意创建一个不存在的语言代码
Locale unsupportedLocale = new Locale.Builder().setLanguage("ij").setRegion("CN").build();

// 获取国际化消息
String greetings = ResourceBundle.getBundle("messages", unsupportedLocale).getString("greetings");

// 由于找不到指定的资源文件,故回退到默认资源文件
// 输出:Hey there! You look very smart today.
System.out.println(greetings);

七、尝试枚举值国际化

创建性别枚举类:

@AllArgsConstructor
public enum PersonSex {

    MALE("display.sex.male"),

    FEMALE("display.sex.female"),

    UNKNOWN("display.sex.unknown");

    private final String i18nKey;

    /**
     * 获取性别展示名,默认区域为China
     *
     * @return 性别展示名
     */
    public String displayName() {
        return displayName(Locale.CHINA);
    }

    /**
     * 获取国际化的性别展示名
     *
     * @param locale 语言环境,即展示名称的语言
     * @return 性别展示名
     */
    public String displayName(Locale locale) {
        ResourceBundle bundle = ResourceBundle.getBundle("messages", locale);
        return bundle.getString(i18nKey);
    }

}

在所有资源文件中添加 display.sex.male 等键值对。以messages_zh_CN.properties为例(其他文件省略):

greetings=看,星星在朝你眨眼,一切美好如你所愿。
display.sex.male=男
display.sex.female=女
display.sex.unknown=未知

测试:

public static void main(String[] args) {
    System.out.println(PersonSex.MALE.displayName(Locale.TAIWAN));
}

Xi_Minghui
1 声望0 粉丝

一枚普普通通的程序猿