国际化英文单词为:Internationalization,又称I18N,I为因为单词的第一个字母,18为这个单词的长度,而N代表这个单词的最后一个字母。国际化又称本地化(Localization,L10N)。
Java国际化主要通过如下3个类完成
- java.util.ResourceBundle:用于加载一个资源包
- java.util.Locale:对应一个特定的国家/区域、语言环境。
- java.text.MessageFormat:用于将消息格式化
为实现程序的国际化,必须提供程序所需要的资源文件。资源文件的内容由key-value对组成,资源文件的命名可以有3种格式:
- basename_language_country.properties
- basename_language.properties
- basename_properties
若资源文件包含非西方字符,则需要用JDK自带的工具来处理:native2ascii,这个工具的语法格式如下:
native2ascii 资源文件名 目标资源文件名
如:
native2ascii mess_zh_XXX.proerties mess_zh_CN.proerties
Locale类可获取各国区域环境(如:Locale.ENGLISH、Locale.CHINESE,这些常量返回一个Locale实例),也可以获取当前系统所使用的区域语言环境,也可以使用语言和区域来创建Locale对象,Java的本地语言使用的时国际化标准组织(ISO)所定义的编码,本地语言由小写的两个字母的代码表示,遵循ISO-639-1;国家代码由大写的两个字母的代码组成,遵循ISO-3166-1,下表为常用的代码:
语言 | 代码 | 国家 | 代码 |
Chinese | zh | China | CN |
English | en | United States | US |
Japanese | ja | Japan | JP |
可以使用Locale的静态方法 getAvailableLocales 来获取所支持的语言和国家,该方法返回一个Locale数组,该数组里包含了java所支持的语言和国家,代码如下:
Locale[] availableLocales = Locale.getAvailableLocales();
for (Locale l : availableLocales) {
System.out.println("Locale DisplayName=" + l.getDisplayName() + " Country=" + l.getCountry() + " Language="
+ l.getLanguage());
}
- 数字格式
数字和货币的格式时高度依赖与Locale的,Java类库提供了一个格式器(formatter)对象的集合,可以对java.text包中的数字值进行格式化和解析,可以通过下面的步骤来对特定的Locale的数字进行格式化:
- 获取Locale对象
- 使用工厂方法获取格式器对象,工厂方法时 NumberFormat 类的静态方法,接受一个Locale 类型的参数,有三个工厂方法:getNumberInstance(数字)、getCurrencyInstance(货币) 和 getPercentInstance(百分比)
- 使用这个格式器对象来完成格式化和解析工作
格式化示例代码:
Locale deLocale = new Locale("de", "DE");
NumberFormat currFmt = NumberFormat.getCurrencyInstance(deLocale);
double amt = 123878.34;
String formatResult = currFmt.format(amt);
System.out.println("amt=" + amt + " Format=" + formatResult);
如果想要读取一个按照某个Locale的惯用法而输入或存储的数字,那边就需要使用 parse 方法。
解析示例代码:
Localede Locale=newLocale("de","DE");
NumberFormat currFmt=NumberFormat.getCurrencyInstance(deLocale);
Number input=currFmt.parse(formatResult);
double parseAmt=input.doubleValue();
System.out.println("FormatAmt="+formatResult+"ParseAmt="+parseAmt);
- 日期和时间
当格式化日期和时间时,需要考虑4个与Locale相关的问题,例如:月份和星期应该用本地语言来表示;年月日的顺序要符合本地习惯;公历可能不是本地首选的日期表示方法;必须要考虑本地时区。Java 使用 DateFormat 类来处理这些问题,和 NumberFormat 类很类似,调用 DateFormat 类的静态方法,并传入 Locale 来实例化,还需要设置日期或时间的格式化值,DateFormat 有如下三个工厂方法:
DateFormat.getDateInstance(dateStyle,loc);
DateFormat.getTimeInstance(timeStyle,loc);
DateFormat.getDateTimeInstance(dateStyle,timeStyle,loc);
其日期和时间的风格使用 DateFormat 的静态常量来表示,常用的静态常量如下:
DateFormat.DEFAULT
DateFormat.FULL
DateFormat.LONG
DateFormat.MEDIUM
DateFormat.SHORT
示例代码如下:
String fmt = "";
Date nowDate = new Date();
DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, zhLocale);
fmt = dateFormat.format(nowDate);
System.out.println("Short style date string " + fmt);
dateFormat = DateFormat.getTimeInstance(DateFormat.MEDIUM, zhLocale);
fmt = dateFormat.format(nowDate);
System.out.println("MEDIUM style time string " + fmt);
dateFormat = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL, zhLocale);
fmt = dateFormat.format(nowDate);
System.out.println("FULL style date FULL style time string " + fmt);
如果要解析一个用户输入的日期,可以使用 DateFormat 类的 parse 方法,但其输入的格式必须和创建 DateFormat 对象设置的风格一致,如果不一致则会抛出 IllegalArgumentException 异常,默认支持宽松的转换,比如日期 2017年2月30日,会被解析为 2017年3月2日,如果希望关闭宽松的转换,需要设置 lenient 标识,示例代码如下:
dateFormat.setLenient(false);
Date convertDate = dateFormat.parse(fmt);
System.out.println("parse string " + fmt + " convertDate " + convertDate);
- 字符串排序
Java 中的排序时用Unicode字符来决定顺序的,比如小写字母的Unicode值比大写的大,有重音符的字母的值甚至更大,这样将使结果失去意义,如果需要定义排序的强度,可以使用Locale对象来创建 Collator 类,该类继承了 Comparator接口,因此可以将该对象传递给 Collections.soft 方法来进行排序,示例代码如下:
List<String> list = new LinkedList<>();
list.add("America");
list.add("Zulu");
list.add("able");
list.add("zebra");
Collator collator = Collator.getInstance(zhLocale);
// 设置排序强度
collator.setStrength(Collator.PRIMARY);
Collections.sort(list, collator);
可以设置排序强度来选择不同的排序行为,字符间的差别可以被分为首要的(primary)、其次的(secndary)和再次的(tertiary)。比如,在英语中,A 和 Z 之间的差异被归类为首要的;A 和 Å (重音符)之间的差异是其次的;A 和 a 之间是再次的,可以使用方法 setStrength 来设置排序强度,Collator.PRIMARY 表示首要的、Collator.SECONDARY 表示其次的、 Collator.TERTIARY 表示再次的,而Collator.IDENTICAL 则表示不允许有任何差异。
偶尔我们会碰到一个字符或字符序列在描述成Unicode时,可以有多种方式,例如,字母序列"ffi"可以用代码U+FB03描述成单个字符"拉丁小连字ffi",Unicode 标准对字符串定义了四种范式形式:D、KD、C和KC,其中 D 和 KD 时用于排序的,在范化形式 D 重,重音字符被分解为基字符和组合重音符;范化形式 KD 更进一步将兼容性字符也进行了分解,例如 ffi 连字符或商标符号TM,我们可以选择排序器所使用的范化程度:Collator.NO_DECOMPOSITION表示不对字符串做任何范化;Collator.CANONICAL_DECOMPOSITION 使用范化形式 D;Collator.FULL_DECOMPOSITION 使用范化形式 KD。可以使用方法 setDecomposition 来设置分解模式
- 消息格式化
Java 类库中有一个 MessageFormat 类,用来格式化带变量的文本,就像这样:"今天 {0} 是个好日子,{1} 公司给我发了工资 {2} 元",括号中的数字是一个占位符,可以用实际的名字和值来替换他,使用静态的 MessageFormat.format 方法,该方法使用当前系统的 Locale 对值进行格式化,如果需要用指定的 Locale 来进行格式化,需要使用 MessageFormat 实例的 format 方法,示例代码如下:
String mfString = "今天 {0} 是个好日子,{1} 公司给我发了工资 {2} 元";
String formatString = MessageFormat.format(mfString, new Date(), "汇元1", 50000);
System.out.println(formatString);
MessageFormat mf = new MessageFormat(mfString, zhLocale);
formatString = mf.format(new Object[]{new Date(), "汇元2", 50000});
System.out.println(formatString);
如果还想在指定占位符的同时设置类型和样式,可以按照如下格式:
String mfString = "今天 {0,date,long} 是个好日子,{1} 公司给我发了工资 {2,number,currency} 元";
mf = new MessageFormat(mfString, zhLocale);
formatString = mf.format(new Object[]{new Date(), "汇元3", 50000});
System.out.println(formatString);
输出内容
今天 2017年5月26日 是个好日子,汇元3 公司给我发了工资 ¥50,000.00 元
占位符索引后面可以跟一个类型和样式,之间用逗号隔开,类型可以是number、time、date、choice,如果类型是 number 则样式有 integer、currency、percent;如果类型是 time 或 date,那么样式有 short、medium、long、full 或者是一个日期模式(yyyy-MM-dd);choice 表示希望消息跟随占位符的值而变化,选项格式是由一个序列对构成的,每个序列对包含一个下限和一个格式化字符串,下限和字符串使用#号分隔,对于对之间用 | 分隔,示例如下:
mfString = "今天 {0,date,yyyy-MM-dd} 是个好日子,{1,choice,0#汇元|1#汇元1|2#汇元2} 公司给我发了工资 {2,number,currency} 元";
mf.applyPattern(mfString);
formatString = mf.format(new Object[]{new Date(), 1, 50000});
System.out.println(formatString);
可以使用 < 符号或 ≤ 符号 来替换 # ,则表示值小于或者小于等于下限值,示例代码如下:
// choice说明: 这个表示 小于 1000 并且 1001-5000 的使用"可怜" ,5001-50000 的使用"不够",50001 以上为"正好"
mfString = "今天 {0,date,yyyy-MM-dd} 是个好日子,{1} 公司给我发了工资 {2,number,currency} 元,{2,choice,1000<可怜|5000<不够|50000<正好}";
mf.applyPattern(mfString);
formatString = mf.format(new Object[]{new Date(), "汇元4", 60000});
System.out.println(formatString
输入内容
今天 2017-05-26 是个好日子,汇元4 公司给我发了工资 ¥60,000.00 元,正好
- 资源包
当本地化一个应用时,可能会有大量的消息字符串、按钮标签和其他的东西需要被翻译,为了能灵活的完成这项任务,你会希望外部定义消息字符串,通常称之为资源(resource),翻译人员不需要接触程序源代码就可以很容易的编辑资源文件,在Java 中使用属性文件来设定字符串资源,并未其他类型的资源实现相应的类。
当本地化一个应用时,会制造出很多资源包(resource bundle),每一个包都是一个属性文件或者一个描述了玉locale相关的项的类,对于每一个包,都要为所有你想要支持的locale提供相应的版本,并需要对这些包使用一种统一的命名规则,例如,为中国定义的资源放在一个名为"包名_zh_CN"的文件中,而为所有使用中文简体的国家所共享的资源则放在名为"包名_zh"的文件中,一般来说,使用
包名_语言_国家
来命名所有和国家相关的资源,使用
包名_语言
来命名所有和语言相关的资源,最后,作为后备,可以把默认资源放到一个没有后缀的文件中,可以使用下面的代码加载一个包:
ResourceBundle resourceBundle = ResourceBundle.getBundle(bundleName, locale);
getBundle 方法加载包的顺序如下:
- 包名_当前locale的语言_当前locale的国家_当前locale的变量
- 包名_当前locale的语言_当前locale的国家
- 包名_当前locale的语言
- 包名_默认locale的语言_默认locale的国家_默认locale的变量
- 包名_默认locale的语言_默认locale的国家
- 包名_默认locale的语言
- 包名
- 抛出 MissingResourceException 异常
一旦getBundle 方法定位了一个包,比如,"包名_zh_CN" ,他还会继续查找"包名_zh"和"包名"这二个包,如果这些包也存在,他们在资源层次中就称为了"包名_zh_CN"的父包,以后查找资源的时候,如果在当前包中没有找到,就会去查找其父包。
- 属性文件
属性文件是为了提供字符串资源常用的文件,每行存放一个键-值对的文本文件,比如,MyProgramStrings.properties 就是一个属性文件,存储属性文件都是ASCII文件,然后可以使用 native2ascii 工具来产生MyProgramStrings_语言_国家.properties 文件,示例如下:
native2ascii MyProgramStrings.properties MyProgramStrings_zh_CN.properties
要查找一个具体的字符串,可以调用:
String resourceText= resourceBundle.getString("show.text");
示例资源文件内容:
show.text=u8fd9u4e2au662fu8d44u6e90u6587u4ef6u7684u5185u5bb9
- 包类
为了提供字符串以外的资源,需要定义类,必须继承 ResourceBundle 类(简单的方法是继承 ListResourceBundle类),应该使用标准的命名规则来命名类,比如:
MyProgramResource.java
MyProgramResource_zh.java
MyProgramResource_zh_CN.java
可以使用与加载属性文件相同的 getBundle 方法来加载这个类,其 bundleName 为 资源包类的完整命名(包名和类名):
ResourceBundle resourceBundle1Class =
ResourceBundle.getBundle("locale.MyProgramResource", Locale.SIMPLIFIED_CHINESE);
要查找一个字符串或其他类型资源可以调用:
resourceText = resourceBundle1Class.getString("resourceClass.show.text");
Object resourceObj = resourceBundle1Class.getObject("resourceClass.show.obj");
如果一个Key同时存在属性文件和包类,则包类的优先。