网站国际化实现(1)—JDK的国际化支持
一、背景
很多网站的用户分布在世界各地,因此网站需要针对不同国家的用户展示不同语言的内容,因此就有了国际化实现的需求,大多数网站都会在网站的头部或尾部设置语言切换链接,这样就可以直接切换成相应的内容。其中有些网站是通过网站地址或参数进行区分,有些是通过设置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项目代码。
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);
}
}
对于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)根据模板和动态参数进行格式化处理。下面是简单示例:
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官方国际化教程,里面讲解得非常详细,而且有更多示例,笔者的一些示例也是在官方示例上面做的修改。
上一篇: Restful API的设计与实践
下一篇: 如何做一名出色的技术管理者?