一、背景
很多網站的用戶分布在世界各地,因此網站需要針對不同國家的用戶展示不同語言的內容,因此就有了國際化實現的需求,大多數網站都會在網站的頭部或尾部設置語言切換鏈接,這樣就可以直接切換成相應的內容。其中有些網站是通過網站地址或參數進行區分,有些是通過設置cookie值進行進行區分。
這里先不講網站具體的實現,先介紹下網站國際化需要的基礎知識,即JDK本身對國際化的支持。這里說明下JDK本身的國際化只是網站國際化實現的基礎,其本身還可以支持GUI程序或其它應用程序的國際化實現。
二、簡介
國際化(Internationalization )用于便捷地支持不同語言或區域的處理,國際化有時簡稱為 i18n,取Internationalization單詞的首字母和尾字母,中間因為還有18個字母,用18代替,故簡寫為i18n。
一般需要國際化處理的數據有時間、數字、金額、文本等。國際化一般有本地化的數據,而且通常都不是硬編碼的,不需要每次修改都重新編譯,而且還需要處理非常便捷。
國際化的整個過程可以大致分為三步:本地化、數據獲取、格式化。下面再詳細說明下。
三、本地化
既然要做到國際化,那么首先肯定得知道是哪個語言或區域,這個如何去獲取或設置呢?JDK提供了Locale類去抽象本地化實現,Locale對象表示了特定的地理、政治和文化地區。
Locale有幾個重要的編碼這里先介紹下:
- 語言編碼(Language Code): 兩到三位符合ISO 639 標準的字母。這個編碼比較好理解,主要用作不同語言的定義。語言編碼參照表鏈接
- 腳本編碼(Script Code):由一個大寫首字母+三個小寫字母組成,符合ISO 15924標準的編碼。這個編碼JDK7以后才引入,主要用于區分同一語言同一國家地區使用不同的書寫系統的情形,例如uz-Cyrl-UZ表示使用西里爾字母的烏茲別克語。腳本編碼參照表鏈接
- 區域編碼(Region Code):由兩個或者三個大寫符合ISO 3166標準的字母組成。 這個編碼主要用于表示國家或者地區。區域編碼參照表鏈接
- 多樣編碼(Variant Code):這個編碼在JDK7以前常用于定義語言或者區域之外的區別,比如計算平臺Windows或UNIX。但是IETF BCP 47標準不建議這么使用。所以JDK7之后,多樣編碼(Variant Code)主要用來定義一門語言后者方言的多樣性。多樣編碼參照表鏈接
而前面說到的非語言的多樣性,比如平臺的區別(Windows, UNIX, Linux)或者發布信息(6u23 or JDK 7)等,JDK7引入Unicode Locale Extensions支持來符合IETF BCP 47標準。
JDK8支持的本地化一覽鏈接(Supported Locales欄)
當然,在實際Locale使用中可能用不到所有的編碼定義或拓展,大多數情況下語言編碼和區域編碼就足夠區分定義,不過了解這些編碼的含義與作用對使用上還是有好處的。實際上Locale對象的創建就是根據上述的編碼和拓展定義出來的。
這里以JDK8為例,Locale的創建可以通過Locale.Builder類、Locale本身的構造方法、forLanguageTag方法、或者預先定義好的常量進行創建。當然getDefault方法也可以得到基于當前環境默認的Locale對象。這里方法上各有差異,本質還是設置前面說到的編碼或拓展值。
四、數據獲取
得到了本地化信息,那么下一步就是要獲取對應的數據。前面提到過國際化需要信息不是硬編碼的,這樣就不要每次修改都重新編譯,而且也易于維護。
在JDK中,數據隔離和獲取一般使用ResourceBundle類配合properties文件使用,實際使用中,一般會定義一些properties文件,文件名前綴相同,后綴跟一些本地化的信息,這樣不同的文件就可以存儲不同本地化對應的數據。
這里說得太抽象,直接上結合官網示例修改的代碼,為了便于閱讀,下面列個大概,具體請看我上傳的github項目代碼。
<pre><code>public class ResourceBundleDemo {
public static void main(String[] args) {
// 這里用到的i18n下面的文件名都以下劃線分隔,RBControl_語言編碼_區域編碼的形式
String baseName = "i18n/RBControl";
// 演示Locale常量解析RBControl_zh_cn.properties數據
Locale l = Locale.CHINA;
ResourceBundle rs = ResourceBundle.getBundle(baseName, l);
String result = rs.getString("region");
System.out.println("示例1結果:" + result);
// 演示Locale.Builder解析RBControl_zh_hk.properties數據
l = new Locale.Builder().setLanguage("zh").setRegion("hk").build();
rs = ResourceBundle.getBundle(baseName, l);
result = rs.getString("region");
System.out.println("示例2結果:" + result);
// 演示Locale構造函數解析RBControl_zh_tw.properties數據
l = new Locale("zh", "tw");
rs = ResourceBundle.getBundle(baseName, l);
result = rs.getString("region");
System.out.println("示例3結果:" + result);
// 演示Locale構造函數解析RBControl_en_US.properties數據
l = Locale.forLanguageTag("en-US");
rs = ResourceBundle.getBundle(baseName, l);
result = rs.getString("region");
System.out.println("示例4結果:" + result);
// 演示Locale解析RBControl_zh.properties數據,但是對應數據不存在時,會取默認RBControl.properties
l = new Locale("zh");
rs = ResourceBundle.getBundle(baseName, l);
result = rs.getString("region");
System.out.println("示例5結果:" + result);
}
}</pre></code>
對于ResourceBundle,在指定的locale找不到的時候,getBundle方法會找最相近的
值。例如官網中舉例ButtonLabel_fr_CA_UNIX是文件名,Locale默認是en_US,getBundle方法會按照如下的順序查找ButtonLabel_fr_CA_UNIX、ButtonLabel_fr_CA、ButtonLabel_fr、ButtonLabel_en_US、ButtonLabel_en、ButtonLabel,如果getBundle在列表中找不到匹配,會拋出MissingResourceException異常,所以為了避免這個異常,最好每次都使用沒有后綴的文件,在前面示例中就是ButtonLabel文件名。
五、格式化
上次已經可以獲取到數據了,有些時候數據獲取到之后可以直接展示,但是如果涉及到時間、數字、金額、動態文本等數據時,又需要額外做下處理了,因為本身這些數據就是本地化敏感的,那么這個時候怎么辦呢?這時就需要對相應的數據進行格式化操作。下面詳細做下說明。
5.1 數字與金額
數字與金額其實都是數值相關的處理,JDK提供了NumberFormat類進行處理,處理過程可以大致分為兩步:(1)getInstance方法得到實例;(2)format方法格式化數據。
比如long、long可以使用NumberFormat.getNumberInstance(Locale inLocale)方法獲得相應本地化的對象實例,比如int可以使用getIntegerInstance(Locale inLocale)方法獲得對應實例,金額可以調用getCurrencyInstance(Locale inLocale)方法得到實例,還有百分比的情況可以調用getPercentInstance(Locale inLocale)得到實例;最后再調用format方法即可。
這里額外還說下DecimalFormat類,這個類主要做小數的格式化處理。比如有不少場景對于123456.789這樣的數字要格式化成123,456.789 ;這個時候DecimalFormat就非常實用。簡單示例如下:
<code>
NumberFormat nf = NumberFormat.getNumberInstance(locale);
DecimalFormat df = (DecimalFormat)nf;
df.applyPattern("###,###.###");
String output = df.format(value);
</code>
上面可以看到DecimalFormat格式化時會需要有個格式化的模式"###,###.###",而這個模式還可以支持更多靈活的語法。基本如下:
符號 | 含義 |
---|---|
0 | 阿拉伯數字 |
# | 阿拉伯數字,0如果無效的話就不顯示 |
. | 小數的分隔符 |
, | 分組的分隔符 |
E | 分隔科學計數法中的尾數和指數 |
; | 格式化分隔符,分隔正數和負數子模式 |
- | 默認的負數前綴 |
% | 乘以100,百分數展示 |
? | 乘以1000,千分數展示 |
¤ | 貨幣記號,由貨幣符號替換。如果兩個同時出現,則用國際貨幣符號替換。如果出現在某個模式中,則使用貨幣小數分隔符,而不使用小數分隔符 |
X | 任意可以用在前綴或后綴的字符 |
' | 用于在前綴或或后綴中為特殊字符加引號,例如 "'#'#" 將 123 格式化為 "#123";如果要創建單引號本身,就使用兩個單引號"# 9''123" |
這里有兩個不太常用到的點做下說明:(1)格式里面有分號作分隔符,其實完整的模式應該是subpattern;subpattern,前一個subpattern是正數的格式化模式,后一個subpattern是負數的格式化模式,每一個subpattern的形式都可以用前面表格的去定義表示,不過負數的格式化模式是可選的,通常情況下不會用;(2)前面表格的分隔符還可以定制化,使用DecimalFormatSymbols類就可以自定義分隔符,具體使用時調用含DecimalFormatSymbols參數的DecimalFormat構造方法,再進行格式化處理即可。
5.2 日期與時間
日期與時間的處理,以前主要用到SimpleDateFormat這個實現類,JDK8新引進了java.time包下的DateTimeFormatter類也可以進行格式化處理。DateTimeFormatter可以看我前面寫的JDK8新特性一覽里面的介紹,下面以SimpleDateFormat舉例,:
<code>
SimpleDateFormat formatter = new SimpleDateFormat(pattern, currentLocale);
Date today = new Date();
String output = formatter.format(today);
System.out.println(pattern + " " + output);</code>
這里同樣有個格式化語法:
符號 | 含義 | 類型 | 示例 |
---|---|---|---|
G | 紀元 | Text | AD |
y | 年份 | Number | 2009 |
M | 月(在一年中的月分) | Text & Number | July & 07 |
d | 日(在一個月中的天數) | Number | 10 |
h | 小時(12小時制,1-12) | Number | 12 |
H | 小時(24小時制,0-23) | Number | 0 |
m | 分 | Number | 30 |
s | 秒 | Number | 55 |
S | 毫秒 | Number | 978 |
E | 日(在一周中的天數) | Text | Tuesday |
D | 日(在一年中的天數) | Number | 189 |
F | 第幾周(這一天在這一個月的第幾周) | Number | 2 (2nd Wed in July) |
w | 第幾周(在一年的第幾周) | Number | 27 |
W | 第幾周(這個月的第幾周) | Number | 2 |
a | 上午/下午(am/pm) | Text | PM |
k | 小時(24小時制,1-24) | Number | 24 |
K | 小時(12小時制,0-11) | Number | 0 |
z | 時區 | Text | Pacific Standard Time |
' | 文本分隔(格式化內容中插入文本時用到) | Delimiter | (none) |
' | 單引號 | Literal | ' |
5.3 文本
在網站應用里面,文本國際化應該是最常用到的了。而且復雜情況下,文本可能還是是固定不變的,可能是動態數據,還可能包含前面講的金額或時間等信息。比如文本是“我在xxx時間,在xxx網站,花費了xxx錢,購買了xxx東西”,這個時候時間、站名、金額、東西都不一樣。不過JDK的MessageFormat類提供了簡便的實現。
主要的步驟可以分為三步:(1)定義文本模板;(2)初始化MessageFormat類;(3)根據模板和動態參數進行格式化處理。下面是簡單示例:
<pre><code>ResourceBundle messages = ResourceBundle.getBundle("i18n/Message",currentLocale);
Object[] messageArguments = {new Date(), messages.getString("goods"),"taobao",65.00};
MessageFormat formatter = new MessageFormat(messages.getString("template"),currentLocale );
String output = formatter.format(messageArguments);
System.out.println(output);</pre></code>
詳細代碼示例可以看我上傳的github項目代碼。
通過上面的示例可以看到,MessageFormat類會自動將傳為的參數,按照ResourceBundle類獲取的模板要求做相應的格式化處理,這樣就可以滿足動態數據的展示了。上面在定義文本模板時用到了類似{3,number,currency}這樣的寫法,表示第三個參數格式類型為數字,形式用金額形式。這里也可以用{3}或者{3,nmuber}這樣就會相應的默認形式格式化。具體語法詳細講解鏈接
另外在有些語言環境下,復數的表現形式不同,比如英語環境下,one file、two files,這個時候的模板直接定義成{0}file這種形式就不太合適,這個時候就可以用到ChoiceFormat類進行處理。
通過上面的三個步驟(本地化—數據獲取—格式化),整個國際化的過程就完成了。當然簡單情況下本地化—數據獲取兩步也可能
最后還啰嗦一句,由于上面的每個點展開講都可以寫一篇甚至幾篇博文,限于篇幅,筆者主要把概念和常用部分重點做了強調,有了清晰的概念介紹與示例,對于大家的理解應該還是很有幫助的。不過這里還是強烈建議大家仔細閱讀下JAVA官方國際化教程,里面講解得非常詳細,而且有更多示例,筆者的一些示例也是在官方示例上面做的修改。