一、背景
很多網(wǎng)站的用戶分布在世界各地仰冠,因此網(wǎng)站需要針對(duì)不同國家的用戶展示不同語言的內(nèi)容乏冀,因此就有了國際化實(shí)現(xiàn)的需求,大多數(shù)網(wǎng)站都會(huì)在網(wǎng)站的頭部或尾部設(shè)置語言切換鏈接辆沦,這樣就可以直接切換成相應(yīng)的內(nèi)容蔚晨。其中有些網(wǎng)站是通過網(wǎng)站地址或參數(shù)進(jìn)行區(qū)分累舷,有些是通過設(shè)置cookie值進(jìn)行進(jìn)行區(qū)分析蝴。
這里先不講網(wǎng)站具體的實(shí)現(xiàn)佑菩,先介紹下網(wǎng)站國際化需要的基礎(chǔ)知識(shí),即JDK本身對(duì)國際化的支持谭确。這里說明下JDK本身的國際化只是網(wǎng)站國際化實(shí)現(xiàn)的基礎(chǔ)禀梳,其本身還可以支持GUI程序或其它應(yīng)用程序的國際化實(shí)現(xiàn)郊艘。
二、簡介
國際化(Internationalization )用于便捷地支持不同語言或區(qū)域的處理唯咬,國際化有時(shí)簡稱為 i18n纱注,取Internationalization單詞的首字母和尾字母,中間因?yàn)檫€有18個(gè)字母胆胰,用18代替狞贱,故簡寫為i18n。
一般需要國際化處理的數(shù)據(jù)有時(shí)間蜀涨、數(shù)字瞎嬉、金額、文本等厚柳。國際化一般有本地化的數(shù)據(jù)氧枣,而且通常都不是硬編碼的,不需要每次修改都重新編譯别垮,而且還需要處理非常便捷便监。
國際化的整個(gè)過程可以大致分為三步:本地化、數(shù)據(jù)獲取、格式化烧董。下面再詳細(xì)說明下毁靶。
三、本地化
既然要做到國際化逊移,那么首先肯定得知道是哪個(gè)語言或區(qū)域预吆,這個(gè)如何去獲取或設(shè)置呢?JDK提供了Locale類去抽象本地化實(shí)現(xiàn)胳泉,Locale對(duì)象表示了特定的地理啡浊、政治和文化地區(qū)。
Locale有幾個(gè)重要的編碼這里先介紹下:
- 語言編碼(Language Code): 兩到三位符合ISO 639 標(biāo)準(zhǔn)的字母胶背。這個(gè)編碼比較好理解,主要用作不同語言的定義喘先。語言編碼參照表鏈接
- 腳本編碼(Script Code):由一個(gè)大寫首字母+三個(gè)小寫字母組成钳吟,符合ISO 15924標(biāo)準(zhǔn)的編碼。這個(gè)編碼JDK7以后才引入窘拯,主要用于區(qū)分同一語言同一國家地區(qū)使用不同的書寫系統(tǒng)的情形红且,例如uz-Cyrl-UZ表示使用西里爾字母的烏茲別克語。腳本編碼參照表鏈接
- 區(qū)域編碼(Region Code):由兩個(gè)或者三個(gè)大寫符合ISO 3166標(biāo)準(zhǔn)的字母組成涤姊。 這個(gè)編碼主要用于表示國家或者地區(qū)暇番。區(qū)域編碼參照表鏈接
- 多樣編碼(Variant Code):這個(gè)編碼在JDK7以前常用于定義語言或者區(qū)域之外的區(qū)別,比如計(jì)算平臺(tái)Windows或UNIX思喊。但是IETF BCP 47標(biāo)準(zhǔn)不建議這么使用壁酬。所以JDK7之后,多樣編碼(Variant Code)主要用來定義一門語言后者方言的多樣性恨课。多樣編碼參照表鏈接
而前面說到的非語言的多樣性舆乔,比如平臺(tái)的區(qū)別(Windows, UNIX, Linux)或者發(fā)布信息(6u23 or JDK 7)等,JDK7引入Unicode Locale Extensions支持來符合IETF BCP 47標(biāo)準(zhǔn)剂公。
JDK8支持的本地化一覽鏈接(Supported Locales欄)
當(dāng)然希俩,在實(shí)際Locale使用中可能用不到所有的編碼定義或拓展,大多數(shù)情況下語言編碼和區(qū)域編碼就足夠區(qū)分定義纲辽,不過了解這些編碼的含義與作用對(duì)使用上還是有好處的颜武。實(shí)際上Locale對(duì)象的創(chuàng)建就是根據(jù)上述的編碼和拓展定義出來的。
這里以JDK8為例拖吼,Locale的創(chuàng)建可以通過Locale.Builder類鳞上、Locale本身的構(gòu)造方法、forLanguageTag方法绿贞、或者預(yù)先定義好的常量進(jìn)行創(chuàng)建因块。當(dāng)然getDefault方法也可以得到基于當(dāng)前環(huán)境默認(rèn)的Locale對(duì)象。這里方法上各有差異籍铁,本質(zhì)還是設(shè)置前面說到的編碼或拓展值涡上。
四趾断、數(shù)據(jù)獲取
得到了本地化信息,那么下一步就是要獲取對(duì)應(yīng)的數(shù)據(jù)吩愧。前面提到過國際化需要信息不是硬編碼的芋酌,這樣就不要每次修改都重新編譯,而且也易于維護(hù)雁佳。
在JDK中脐帝,數(shù)據(jù)隔離和獲取一般使用ResourceBundle類配合properties文件使用,實(shí)際使用中糖权,一般會(huì)定義一些properties文件堵腹,文件名前綴相同,后綴跟一些本地化的信息星澳,這樣不同的文件就可以存儲(chǔ)不同本地化對(duì)應(yīng)的數(shù)據(jù)疚顷。
這里說得太抽象,直接上結(jié)合官網(wǎng)示例修改的代碼禁偎,為了便于閱讀腿堤,下面列個(gè)大概,具體請(qǐng)看我上傳的github項(xiàng)目代碼如暖。
<pre><code>public class ResourceBundleDemo {
public static void main(String[] args) {
// 這里用到的i18n下面的文件名都以下劃線分隔笆檀,RBControl_語言編碼_區(qū)域編碼的形式
String baseName = "i18n/RBControl";
// 演示Locale常量解析RBControl_zh_cn.properties數(shù)據(jù)
Locale l = Locale.CHINA;
ResourceBundle rs = ResourceBundle.getBundle(baseName, l);
String result = rs.getString("region");
System.out.println("示例1結(jié)果:" + result);
// 演示Locale.Builder解析RBControl_zh_hk.properties數(shù)據(jù)
l = new Locale.Builder().setLanguage("zh").setRegion("hk").build();
rs = ResourceBundle.getBundle(baseName, l);
result = rs.getString("region");
System.out.println("示例2結(jié)果:" + result);
// 演示Locale構(gòu)造函數(shù)解析RBControl_zh_tw.properties數(shù)據(jù)
l = new Locale("zh", "tw");
rs = ResourceBundle.getBundle(baseName, l);
result = rs.getString("region");
System.out.println("示例3結(jié)果:" + result);
// 演示Locale構(gòu)造函數(shù)解析RBControl_en_US.properties數(shù)據(jù)
l = Locale.forLanguageTag("en-US");
rs = ResourceBundle.getBundle(baseName, l);
result = rs.getString("region");
System.out.println("示例4結(jié)果:" + result);
// 演示Locale解析RBControl_zh.properties數(shù)據(jù),但是對(duì)應(yīng)數(shù)據(jù)不存在時(shí),會(huì)取默認(rèn)RBControl.properties
l = new Locale("zh");
rs = ResourceBundle.getBundle(baseName, l);
result = rs.getString("region");
System.out.println("示例5結(jié)果:" + result);
}
}</pre></code>
對(duì)于ResourceBundle盒至,在指定的locale找不到的時(shí)候酗洒,getBundle方法會(huì)找最相近的
值。例如官網(wǎng)中舉例ButtonLabel_fr_CA_UNIX是文件名枷遂,Locale默認(rèn)是en_US寝蹈,getBundle方法會(huì)按照如下的順序查找ButtonLabel_fr_CA_UNIX、ButtonLabel_fr_CA登淘、ButtonLabel_fr箫老、ButtonLabel_en_US、ButtonLabel_en黔州、ButtonLabel耍鬓,如果getBundle在列表中找不到匹配,會(huì)拋出MissingResourceException異常流妻,所以為了避免這個(gè)異常牲蜀,最好每次都使用沒有后綴的文件,在前面示例中就是ButtonLabel文件名绅这。
五涣达、格式化
上次已經(jīng)可以獲取到數(shù)據(jù)了,有些時(shí)候數(shù)據(jù)獲取到之后可以直接展示,但是如果涉及到時(shí)間度苔、數(shù)字匆篓、金額、動(dòng)態(tài)文本等數(shù)據(jù)時(shí)寇窑,又需要額外做下處理了鸦概,因?yàn)楸旧磉@些數(shù)據(jù)就是本地化敏感的,那么這個(gè)時(shí)候怎么辦呢甩骏?這時(shí)就需要對(duì)相應(yīng)的數(shù)據(jù)進(jìn)行格式化操作窗市。下面詳細(xì)做下說明。
5.1 數(shù)字與金額
數(shù)字與金額其實(shí)都是數(shù)值相關(guān)的處理饮笛,JDK提供了NumberFormat類進(jìn)行處理咨察,處理過程可以大致分為兩步:(1)getInstance方法得到實(shí)例;(2)format方法格式化數(shù)據(jù)福青。
比如long扎拣、long可以使用NumberFormat.getNumberInstance(Locale inLocale)方法獲得相應(yīng)本地化的對(duì)象實(shí)例,比如int可以使用getIntegerInstance(Locale inLocale)方法獲得對(duì)應(yīng)實(shí)例素跺,金額可以調(diào)用getCurrencyInstance(Locale inLocale)方法得到實(shí)例,還有百分比的情況可以調(diào)用getPercentInstance(Locale inLocale)得到實(shí)例誉券;最后再調(diào)用format方法即可指厌。
這里額外還說下DecimalFormat類,這個(gè)類主要做小數(shù)的格式化處理踊跟。比如有不少場景對(duì)于123456.789這樣的數(shù)字要格式化成123,456.789 踩验;這個(gè)時(shí)候DecimalFormat就非常實(shí)用。簡單示例如下:
<code>
NumberFormat nf = NumberFormat.getNumberInstance(locale);
DecimalFormat df = (DecimalFormat)nf;
df.applyPattern("###,###.###");
String output = df.format(value);
</code>
上面可以看到DecimalFormat格式化時(shí)會(huì)需要有個(gè)格式化的模式"###,###.###"商玫,而這個(gè)模式還可以支持更多靈活的語法箕憾。基本如下:
符號(hào) | 含義 |
---|---|
0 | 阿拉伯?dāng)?shù)字 |
# | 阿拉伯?dāng)?shù)字,0如果無效的話就不顯示 |
. | 小數(shù)的分隔符 |
, | 分組的分隔符 |
E | 分隔科學(xué)計(jì)數(shù)法中的尾數(shù)和指數(shù) |
; | 格式化分隔符拳昌,分隔正數(shù)和負(fù)數(shù)子模式 |
- | 默認(rèn)的負(fù)數(shù)前綴 |
% | 乘以100袭异,百分?jǐn)?shù)展示 |
? | 乘以1000,千分?jǐn)?shù)展示 |
¤ | 貨幣記號(hào)炬藤,由貨幣符號(hào)替換御铃。如果兩個(gè)同時(shí)出現(xiàn),則用國際貨幣符號(hào)替換沈矿。如果出現(xiàn)在某個(gè)模式中上真,則使用貨幣小數(shù)分隔符,而不使用小數(shù)分隔符 |
X | 任意可以用在前綴或后綴的字符 |
' | 用于在前綴或或后綴中為特殊字符加引號(hào)羹膳,例如 "'#'#" 將 123 格式化為 "#123"睡互;如果要?jiǎng)?chuàng)建單引號(hào)本身,就使用兩個(gè)單引號(hào)"# 9''123" |
這里有兩個(gè)不太常用到的點(diǎn)做下說明:(1)格式里面有分號(hào)作分隔符,其實(shí)完整的模式應(yīng)該是subpattern;subpattern就珠,前一個(gè)subpattern是正數(shù)的格式化模式寇壳,后一個(gè)subpattern是負(fù)數(shù)的格式化模式,每一個(gè)subpattern的形式都可以用前面表格的去定義表示嗓违,不過負(fù)數(shù)的格式化模式是可選的九巡,通常情況下不會(huì)用;(2)前面表格的分隔符還可以定制化蹂季,使用DecimalFormatSymbols類就可以自定義分隔符冕广,具體使用時(shí)調(diào)用含DecimalFormatSymbols參數(shù)的DecimalFormat構(gòu)造方法,再進(jìn)行格式化處理即可偿洁。
5.2 日期與時(shí)間
日期與時(shí)間的處理撒汉,以前主要用到SimpleDateFormat這個(gè)實(shí)現(xiàn)類,JDK8新引進(jìn)了java.time包下的DateTimeFormatter類也可以進(jìn)行格式化處理涕滋。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è)格式化語法:
符號(hào) | 含義 | 類型 | 示例 |
---|---|---|---|
G | 紀(jì)元 | Text | AD |
y | 年份 | Number | 2009 |
M | 月(在一年中的月分) | Text & Number | July & 07 |
d | 日(在一個(gè)月中的天數(shù)) | Number | 10 |
h | 小時(shí)(12小時(shí)制宾肺,1-12) | Number | 12 |
H | 小時(shí)(24小時(shí)制溯饵,0-23) | Number | 0 |
m | 分 | Number | 30 |
s | 秒 | Number | 55 |
S | 毫秒 | Number | 978 |
E | 日(在一周中的天數(shù)) | Text | Tuesday |
D | 日(在一年中的天數(shù)) | Number | 189 |
F | 第幾周(這一天在這一個(gè)月的第幾周) | Number | 2 (2nd Wed in July) |
w | 第幾周(在一年的第幾周) | Number | 27 |
W | 第幾周(這個(gè)月的第幾周) | Number | 2 |
a | 上午/下午(am/pm) | Text | PM |
k | 小時(shí)(24小時(shí)制,1-24) | Number | 24 |
K | 小時(shí)(12小時(shí)制锨用,0-11) | Number | 0 |
z | 時(shí)區(qū) | Text | Pacific Standard Time |
' | 文本分隔(格式化內(nèi)容中插入文本時(shí)用到) | Delimiter | (none) |
' | 單引號(hào) | Literal | ' |
5.3 文本
在網(wǎng)站應(yīng)用里面丰刊,文本國際化應(yīng)該是最常用到的了。而且復(fù)雜情況下增拥,文本可能還是是固定不變的啄巧,可能是動(dòng)態(tài)數(shù)據(jù),還可能包含前面講的金額或時(shí)間等信息掌栅。比如文本是“我在xxx時(shí)間秩仆,在xxx網(wǎng)站,花費(fèi)了xxx錢猾封,購買了xxx東西”澄耍,這個(gè)時(shí)候時(shí)間、站名晌缘、金額逾苫、東西都不一樣。不過JDK的MessageFormat類提供了簡便的實(shí)現(xiàn)枚钓。
主要的步驟可以分為三步:(1)定義文本模板铅搓;(2)初始化MessageFormat類;(3)根據(jù)模板和動(dòng)態(tài)參數(shù)進(jìn)行格式化處理搀捷。下面是簡單示例:
<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>
詳細(xì)代碼示例可以看我上傳的github項(xiàng)目代碼星掰。
通過上面的示例可以看到多望,MessageFormat類會(huì)自動(dòng)將傳為的參數(shù),按照ResourceBundle類獲取的模板要求做相應(yīng)的格式化處理氢烘,這樣就可以滿足動(dòng)態(tài)數(shù)據(jù)的展示了怀偷。上面在定義文本模板時(shí)用到了類似{3,number,currency}這樣的寫法,表示第三個(gè)參數(shù)格式類型為數(shù)字播玖,形式用金額形式椎工。這里也可以用{3}或者{3,nmuber}這樣就會(huì)相應(yīng)的默認(rèn)形式格式化。具體語法詳細(xì)講解鏈接
另外在有些語言環(huán)境下蜀踏,復(fù)數(shù)的表現(xiàn)形式不同维蒙,比如英語環(huán)境下,one file果覆、two files颅痊,這個(gè)時(shí)候的模板直接定義成{0}file這種形式就不太合適,這個(gè)時(shí)候就可以用到ChoiceFormat類進(jìn)行處理局待。
通過上面的三個(gè)步驟(本地化—數(shù)據(jù)獲取—格式化)斑响,整個(gè)國際化的過程就完成了。當(dāng)然簡單情況下本地化—數(shù)據(jù)獲取兩步也可能
最后還啰嗦一句钳榨,由于上面的每個(gè)點(diǎn)展開講都可以寫一篇甚至幾篇博文舰罚,限于篇幅,筆者主要把概念和常用部分重點(diǎn)做了強(qiáng)調(diào)薛耻,有了清晰的概念介紹與示例营罢,對(duì)于大家的理解應(yīng)該還是很有幫助的。不過這里還是強(qiáng)烈建議大家仔細(xì)閱讀下JAVA官方國際化教程昭卓,里面講解得非常詳細(xì),而且有更多示例瘟滨,筆者的一些示例也是在官方示例上面做的修改候醒。