网站国际化实现(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项目代码

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


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就非常实用。简单示例如下:
<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)根据模板和动态参数进行格式化处理。下面是简单示例:


定义模板.png

<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官方国际化教程,里面讲解得非常详细,而且有更多示例,笔者的一些示例也是在官方示例上面做的修改。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,082评论 5 464
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,231评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 145,047评论 0 327
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,977评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,893评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,014评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,976评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,605评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,888评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,906评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,732评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,513评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,980评论 3 301
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,132评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,447评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,027评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,232评论 2 339

推荐阅读更多精彩内容