国际化(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.properties
、messages_zh_CN.properties
、messages_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.CHINA
为Locale.TAIWAN
,再次运行则打印:“好久不見,歡迎回來!”
3. 解释
a. 资源捆(ResourceBundle)
资源捆是一组资源文件,比如语言包、图像或配置文件。上面的 messages.properties
、messages_zh_CN.properties
、messages_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));
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。