國際化(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));
}