国际化(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内容:
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.propertiesmessages_bo_CN.propertiesmessages_bo_IN.propertiesmessages_zh_CN.propertiesmessages_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));
}
评论留言