網站國際化實現(1)—JDK的國際化支持

一、背景

很多網站的用戶分佈在世界各地,因此網站需要針對不同國家的用戶展示不同語言的內容,因此就有了國際化實現的需求,大多數網站都會在網站的頭部或尾部設置語言切換鏈接,這樣就可以直接切換成相應的內容。其中有些網站是通過網站地址或參數進行區分,有些是通過設置cookie值進行進行區分。

這裏先不講網站具體的實現,先介紹下網站國際化需要的基礎知識,即JDK本身對國際化的支持。這裏說明下JDK本身的國際化只是網站國際化實現的基礎,其本身還可以支持GUI程序或其它應用程序的國際化實現。

二、簡介

JAVA官方國際化教程

國際化(Internationalization )用於便捷地支持不同語言或區域的處理,國際化有時簡稱爲 i18n,取Internationalization單詞的首字母和尾字母,中間因爲還有18個字母,用18代替,故簡寫爲i18n。

一般需要國際化處理的數據有時間、數字、金額、文本等。國際化一般有本地化的數據,而且通常都不是硬編碼的,不需要每次修改都重新編譯,而且還需要處理非常便捷。

國際化的整個過程可以大致分爲三步:本地化、數據獲取、格式化。下面再詳細說明下。

三、本地化

既然要做到國際化,那麼首先肯定得知道是哪個語言或區域,這個如何去獲取或設置呢?JDK提供了Locale類去抽象本地化實現,Locale對象表示了特定的地理、政治和文化地區。

Locale有幾個重要的編碼這裏先介紹下:

  1. 語言編碼(Language Code): 兩到三位符合ISO 639 標準的字母。這個編碼比較好理解,主要用作不同語言的定義。語言編碼參照錶鏈接
  2. 腳本編碼(Script Code):由一個大寫首字母+三個小寫字母組成,符合ISO 15924標準的編碼。這個編碼JDK7以後才引入,主要用於區分同一語言同一國家地區使用不同的書寫系統的情形,例如uz-Cyrl-UZ表示使用西裏爾字母的烏茲別克語。腳本編碼參照錶鏈接
  3. 區域編碼(Region Code):由兩個或者三個大寫符合ISO 3166標準的字母組成。 這個編碼主要用於表示國家或者地區。區域編碼參照錶鏈接
  4. 多樣編碼(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項目代碼

 

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);
}
}

 


Paste_Image.png

對於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就非常實用。簡單示例如下:
NumberFormat nf = NumberFormat.getNumberInstance(locale);
DecimalFormat df = (DecimalFormat)nf;
df.applyPattern("###,###.###");
String output = df.format(value);

上面可以看到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舉例,:
SimpleDateFormat formatter = new SimpleDateFormat(pattern, currentLocale);
Date today = new Date();
String output = formatter.format(today);
System.out.println(pattern + " " + output);

這裏同樣有個格式化語法

符號 含義 類型 示例
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)根據模板和動態參數進行格式化處理。下面是簡單示例:


定義模板.png

 

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);

詳細代碼示例可以看我上傳的github項目代碼

通過上面的示例可以看到,MessageFormat類會自動將傳爲的參數,按照ResourceBundle類獲取的模板要求做相應的格式化處理,這樣就可以滿足動態數據的展示了。上面在定義文本模板時用到了類似{3,number,currency}這樣的寫法,表示第三個參數格式類型爲數字,形式用金額形式。這裏也可以用{3}或者{3,nmuber}這樣就會相應的默認形式格式化。具體語法詳細講解鏈接

另外在有些語言環境下,複數的表現形式不同,比如英語環境下,one file、two files,這個時候的模板直接定義成{0}file這種形式就不太合適,這個時候就可以用到ChoiceFormat類進行處理。

通過上面的三個步驟(本地化—數據獲取—格式化),整個國際化的過程就完成了。當然簡單情況下本地化—數據獲取兩步也可能

最後還囉嗦一句,由於上面的每個點展開講都可以寫一篇甚至幾篇博文,限於篇幅,筆者主要把概念和常用部分重點做了強調,有了清晰的概念介紹與示例,對於大家的理解應該還是很有幫助的。不過這裏還是強烈建議大家仔細閱讀下JAVA官方國際化教程,裏面講解得非常詳細,而且有更多示例,筆者的一些示例也是在官方示例上面做的修改。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章