0 前言
全是干货的技术殿堂
文章收录在我的 GitHub 仓库,欢迎Star/fork:
开发一个支持多国语言的Web应用程序,要求系统能够根据客户端的系统的语言类型返回对应的界面:英文的操作系统返回英文界面,而中文的操作系统则返回中文界面——这便是典型的i18n国际化问题。
对于有国际化要求的应用系统,我们不能简单地采用硬编码的方式编写用户界面信息、报错信息等内容,而必须为这些需要国际化的信息进行特殊处理。简单来说,就是为每种语言提供一套相应的资源文件,并以规范化命名的方式保存在特定的目录中,由系统自动根据客户端语言选择适合的资源文件。
1 简介
“国际化信息”也称为“本地化信息”,一般需要两个条件才可以确定一个特定类型的本地化信息
- “语言类型”
- “国家/地区的类型”
如中文本地化信息既有中国大陆地区的中文,又有中国台湾、中国香港地区的中文,还有新加坡地区的中文。Java通过java.util.Locale类表示一个本地化对象,它允许通过语言参数和国家/地区参数创建一个确定的本地化对象。
语言参数使用ISO标准语言代码表示,这些代码是由ISO-639标准定义的,每一种语言由两个小写字母表示。在许多网站上都可以找到这些代码的完整列表
国家/地区参数也由标准的ISO国家/地区代码表示,这些代码是由ISO-3166标准定义的,每个国家/地区由两个大写字母表示
- 查看ISO-3166的标准代码
http://www.iso.ch/iso/en/prods-services/iso3166ma/02iso-3166-code-lists/list-en1.html - 一些语言和国家/地区的标准代码
2 Locale 类
java.util.Locale是表示语言和国家/地区信息的本地化类,是创建国际化应用的基础。下面给出几个创建本地化对象的示例:
//①带有语言和国家/地区信息的本地化对象 Locale locale1 = new Locale("zh","CN"); //②只有语言信息的本地化对象 Locale locale2 = new Locale("zh"); //③等同于Locale("zh","CN") Locale locale3 = Locale.CHINA; //④等同于Locale("zh") Locale locale4 = Locale.CHINESE; //⑤获取本地系统默认的本地化对象 Locale locale 5= Locale.getDefault();
用户既可以同时指定语言和国家/地区参数定义一个本地化对象①
也可以仅通过语言参数定义一个泛本地化对象②
Locale类中通过静态常量定义了一些常用的本地化对象,③和④处就直接通过引用常量返回本地化对象
用户还可以获取系统默认的本地化对象,如⑤
在测试时,如果希望改变系统默认的本地化设置,可以在启动JVM时通过命令参数指定:
java -Duser.language=en -Duser.region=US MyTest。
本地化工具类
JDK的java.util包中提供了几个支持本地化的格式化操作工具类:NumberFormat、DateFormat、MessageFormat。下面,我们分别通过实例了解它们的用法:
Locale locale = new Locale("zh", "CN"); NumberFormat currFmt = NumberFormat.getCurrencyInstance(locale); double amt = 123456.78; System.out.println(currFmt.format(amt));
上面的实例通过NumberFormat按本地化的方式对货币金额进行格式化操作,运行实例,输出以下信息:
Locale locale = new Locale("en", "US"); Date date = new Date(); DateFormat df = DateFormat.getDateInstance(DateFormat.MEDIUM, locale); System.out.println(df.format(date));
通过DateFormat#getDateInstance(int style,Locale locale)方法按本地化的方式对日期进行格式化操作。该方法第一个入参为时间样式,第二个入参为本地化对象。运行以上代码,输出以下信息:
Jan 8, 2007
MessageFormat在NumberFormat和DateFormat的基础上提供了强大的占位符字符串的格式化功能,它支持时间、货币、数字以及对象属性的格式化操作。下面的实例演示了一些常见的格式化功能:
//①信息格式化串 String pattern1 = "{0},你好!你于{1}在工商银行存入{2} 元。"; String pattern2 = "At {1,time,short} On{1,date,long},{0} paid {2,number, currency}."; //②用于动态替换占位符的参数 Object[] params = {"John", new GregorianCalendar().getTime(),1.0E3}; //③使用默认本地化对象格式化信息 String msg1 = MessageFormat.format(pattern1,params); //④使用指定的本地化对象格式化信息 MessageFormat mf = new MessageFormat(pattern2,Locale.US); String msg2 = mf.format(params); System.out.println(msg1); System.out.println(msg2);
pattern1是简单形式的格式化信息串,通过{n}占位符指定动态参数的替换位置索引,{0}表示第一个参数,{1}表示第二个参数,以此类推。
pattern2格式化信息串比较复杂一些,除参数位置索引外,还指定了参数的类型和样式。从pattern2中可以看出格式化信息串的语法是很灵活的,一个参数甚至可以出现在两个地方:如 {1,time,short}表示从第二个入参中获取时间部分的值,显示为短样式时间;而{1,date,long}表示从第二个入参中获取日期部分的值,显示为长样式时间。关于MessageFormat更详细的使用方法,请参见JDK的Javadoc。
在②处,定义了用于替换格式化占位符的动态参数,这里,我们使用到了JDK5.0自动装包的语法,否则必须采用封装类表示基本类型的参数值。
在③处,通过MessageFormat的format()方法格式化信息串。它使用了系统默认的本地化对象,由于我们是中文平台,因此默认为Locale.CHINA。而在④处,我们显式指定MessageFormat的本地化对象。
运行上面的代码,输出以下信息:
John,你好!你于07-1-8 下午9:58在工商银行存入1,000元。 At 9:58 PM OnJanuary 8, 2007,John paid $1,000.00.
资源文件/属性文件
应用系统中某些信息需要支持国际化功能,则必须为希望支持的不同本地化类型分别提供对应的资源文件,并以规范的方式进行命名。国际化资源文件的命名规范规定资源名称采用以下的方式进行命名:
<资源名>_<语言代码>_<国家/地区代码>.properties
其中,语言代码和国家/地区代码都是可选的。<资源名>.properties命名的国际化资源文件是默认的资源文件,即某个本地化类型在系统中找不到对应的资源文件,就采用这个默认的资源文件。<资源名>_<语言代码>.properties命名的国际化资源文件是某一语言默认的资源文件,即某个本地化类型在系统中找不到精确匹配的资源文件,将采用相应语言默认的资源文件。
举一个例子:假设资源名为resource,则语言为英文,国家为美国,则与其对应的本地化资源文件命名为resource_en_US.properties。信息在资源文件以属性名/值的方式表示:
引用
greeting.common=How are you! greeting.morning = Good morning! greeting.afternoon = Good Afternoon!
对应语言为中文,国家/地区为中国大陆的本地化资源文件则命名为resource_zh_ CN.properties,资源文件内容如下:
greeting.common=\u60a8\u597d\uff01 greeting.morning=\u65e9\u4e0a\u597d\uff01 greeting.afternoon=\u4e0b\u5348\u597d\uff01
本地化不同的同一资源文件,虽然属性值各不相同,但属性名却是相同的,这样应用程序就可以通过Locale对象和属性名精确调用到某个具体的属性值了。
上面中文的本地化资源文件内容采用了特殊的编码表示中文字符,这是因为资源文件对文件内容有严格的要求:只能包含ASCII字符。所以必须将非ASCII字符的内容转换为Unicode代码的表示方式。如上面中文的resource_zh_CN.properties资源文件的三个属性值分别是“您好!”、“早上好!”和“下午好!”三个中文字符串对应的Unicode代码串。
如果在应用开发时,直接采用Unicode代码编辑资源文件是很不方便的,所以,通常我们直接使用正常的方式编写资源文件,在测试或部署时再采用工具进行转换。JDK在bin目录下为我们提供了一个完成此项功能的native2ascii工具,它可以将中文字符的资源文件转换为Unicode代码格式的文件,命令格式如下:
native2ascii [-reverse] [-encoding 编码] [输入文件 [输出文件]]
resource_zh_CN.properties包含中文字符并且以UTF-8进行编码,假设将该资源文件放到d:\目录下,通过下面的命令就可以将其转换为Unicode代码的形式:
D:\>native2ascii -encoding utf-8 d:\resource_zh_CN.properties d:\resource_zh_CN_1.properties
由于原资源文件采用UTF-8编码,所以必须显式通过-encoding指定编码格式。
通过native2ascii命令手工转换资源文件,不但在操作上不方便,转换后资源文件中的属性内容由于采用了ASCII编码,阅读起来也不方便。很多IDE开发工具都有属性编辑器的插件,插件会自动将资源文件内容转换为ASCII形式的编码,同时以正常的方式阅读和编辑资源文件的内容,这给开发和维护带来了很大的便利。对于MyEclipse来说,使用MyEclipse Properties Editor编辑资源属性文件;对于Intellij IDEA来说,无须安装任何插件就自然支持资源属性文件的这种编辑方式了。
如果应用程序中拥有大量的本地化资源文件,直接通过传统的File操作资源文件显然太过笨拙。Java为我们提供了用于加载本地化资源文件的方便类java.util.ResourceBoundle。
ResourceBoundle为加载及访问资源文件提供便捷的操作,下面的语句从相对于类路径的目录中加载一个名为resource的本地化资源文件:
ResourceBundle rb = ResourceBundle.getBundle("com/baobaotao/i18n/resource", locale)
通过以下的代码即可访问资源文件的属性值:
rb.getString("greeting.common")
来看下面的实例:
ResourceBundle rb1 = ResourceBundle.getBundle("com/baobaotao/i18n/resource", Locale.US); ResourceBundle rb2 = ResourceBundle.getBundle("com/baobaotao/i18n/resource", Locale.CHINA); System.out.println("us:"+rb1.getString("greeting.common")); System.out.println("cn:"+rb2.getString("greeting.common"));
rb1加载了对应美国英语本地化的resource_en_US.properties资源文件;而rb2加载了对应中国大陆中文的resource_zh_CN.properties资源文件。运行上面的代码,将输出以下信息:
us:How are you! cn:您好!
加载资源文件时,如果不指定本地化对象,将使用本地系统默认的本地化对象。所以,在中文系统中,ResourceBundle.getBundle(“com/baobaotao/i18n/resource”)语句也将返回和代码清单5-14中rb2相同的本地化资源。
ResourceBundle在加载资源时,如果指定的本地化资源文件不存在,它按以下顺序尝试加载其他的资源:本地系统默认本地化对象对应的资源→默认的资源。上面的例子中,假设我们使用ResourceBundle.getBundle(“com/baobaotao/i18n/resource”,Locale.CANADA)加载资源,由于不存在resource_en_CA.properties资源文件,它将尝试加载resource_zh_CN.properties的资源文件,假设resource_zh_CN.properties资源文件也不存在,它将继续尝试加载resource.properties的资源文件,如果这些资源都不存在,将抛出java.util.MissingResourceException异常。
在资源文件中使用格式化串
在上面的资源文件中,属性值都是一般的字符串,它们不能结合运行时的动态参数构造出灵活的信息,而这种需求是很常见的。要解决这个问题很简单,只须使用带占位符的格式化串作为资源文件的属性值并结合使用MessageFormat就可以满足要求了。
上面的例子中,我们仅向用户提供一般性问候,下面我们对资源文件进行改造,通过格式化串让问候语更具个性化:
greeting.common=How are you!{0},today is {1} greeting.morning = Good morning!{0},now is {1 time short} greeting.afternoon = Good Afternoon!{0} now is {1 date long}
将该资源文件保存在fmt_resource_en_US.properties中,按照同样的方式编写对应的中文本地化资源文件fmt_resource_zh_CN.properties。
下面,我们联合使用ResourceBoundle和MessageFormat得到美国英文的本地化问候语:
//①加载本地化资源 ResourceBundle rb1 = ResourceBundle.getBundle("com/baobaotao/i18n/fmt_ resource",Locale.US); ResourceBundle rb2 = ResourceBundle.getBundle("com/baobaotao/i18n/fmt_ resource",Locale.CHINA); Object[] params = {"John", new GregorianCalendar().getTime()}; String str1 = new MessageFormat(rb1.getString("greeting.common"),Locale. US).format(params); ② String str2 =new MessageFormat(rb2.getString("greeting.morning"),Locale. CHINA).format(params); String str3 =new MessageFormat(rb2.getString("greeting.afternoon"),Locale. CHINA).format(params); System.out.println(str1); System.out.println(str2); System.out.println(str3);
运行以上的代码,将输出以下信息:
How are you!John,today is 1/9/07 4:11 PM 早上好!John,现在是下午4:11 下午好!John,现在是2007年1月9日