J2SE 6 在国际化方面的增强

原文出处: http://java.sun.com/developer/technicalArticles/javase/i18n_enhance
翻译:铁针

对国际化和本地化的支持是Java标准版一个长处。JavaSE6一如既往地为那些注重本地
化资源访问和操作的应用程序开发者提供支持。JavaSE6在以下几方面为本地化作了加强:

.资源的访问和控制
.针对本地化的服务
.归一化文本
.国际域名
.日本国的日历
.新增locales


资源访问和控制
----------------
编程人员用java.util.ResourceBundle类中提供的方法来为应用程序提供本地化资源。
使用这个类中的静态方法getBundle来定位以及装载本地化资源,调用后得到
ResourceBundle的实例,那么这个实例就表征了要被使用的本地化了的文本,图片以及其他
针对本地化的资源。locale是由语言和地理区域的不同而形成的文化标志。

尽管在缺省方式下,定位和装载资源绑定已经为我们做了很多工作,JavaSE6版本里还
提供缓存以及让编程人员能对本地化资源进行更好地操纵。我们仍然使用ResourceBundle
类获取本地化的资源,但是JavaSE6新增加的功能让编程人员能更灵活地来为应用程序
的本地化资源内容决定如何存储以及怎样存储。

JavaSE6先前的版本中,编程人员通常是用属性文件(properies)和ListResourceBundle
的一个子类来存储本地化资源。现在,编程人员可以为资源文件指定不同的格式了。举例来
说,使用基于XML格式的资源文件,编程人员也可能改动本地化资源文件的缺省命名规范,
像这样的特定是ResourceBundle.Control类来做工作。

ResourceBundle.Control类负责资源装载过程中的主要步骤,每个步骤对应类中的一个独立
的方法。通过覆盖这些方法来定制策略,以期对资源实现特殊定位,装载和缓存。Control
类里定义的方法是实现现有的缺省策略,所以子类化来实现特定的功能。在Control的子类
里的getBundle方法里定制你自己的功能,甚至能自己决定如何让应用程序找到以及怎样
使用本地化资源。

当然可以直接使用缺省的Control类而不是非得去实现自定义的Control类。缺省的Control
类里的方法是为编程人员提供了缺省的功能实现,下面的代码给出的是使用Control类里缺省
的功能
LocaletargetLocale=newLocale("fr","FR");//Frenchlanguage,Frenchregion
ResourceBundlemyResources=getBundle("com.sun.demo.intl.AppResource",targetLocale);

假如,你正在使用以en_US为缺省local的环境,那么Control对象在默认情况下就会搜索
如下列出的那些本地化的AppResource名称:
com.sun.demo.intl.AppResource_fr_FR
com.sun.demo.intl.AppResource_fr
com.sun.demo.intl.AppResource_en_US
com.sun.demo.intl.AppResource_en
com.sun.demo.intl.AppResource

对于以上列出的每个“绑定”名称,Control默认地会去搜索两种实现格式:一种是ResourceBundle
的子类(.class文件格式);另一种是PropertyResulrceBundle的属性文件(.properties文件格式)。
假如能找到这两种格式的文件之一,那么就能知道“绑定”的链级关系,从而得到ResourceBundle
的实例。“绑定名”也是以本地化专用的后缀名来区分,比如,fr_FR,fr以及en_US,这就是
使用相同的本地化“基准名”再配上用于区分具体“绑定”对象的名。此外,AppResource的缺省行为
是会为“绑定”(bundles)进行缓存的,也就是说,即使在对同一个“绑定”(bundle)反复地调用
getBundle方法也只是得到被缓存过了的资源。在JAVA平台的文档里详细讲述了
getBundle方法的行为方式( http://java.sun.com/javase/6/docs/api/java/util/ResourceBundle.html#getBundle%28java.lang.String,java.util.Locale,java.lang.ClassLoader,java.util.ResourceBundle.Control%29

除了缺省的方式外,也许会用到不同于缺省行为的“绑定”装载的方式。接下来就来说说在
哪些场景下装载绑定是不同于缺省方式的,以下就列写这些场景:

用属性文件(properties),而不是用class绑定
把资源配置存放到与local对应的文件目录中
在经过一段时间后似的缓存资源失效


仅搜索Properties文件
--------------------
有些资源绑定的装载并不需要一个完整的自定义的Control子类,只需要用Control类里
的静态方法getControl再配以标准的选项,与使用缺省的方式稍有不同就能达到我们的目的。
假如应用程序在使用属性文件时候有排它性,为了避免遍历搜索所有的ResourceBundle子类,
我们只需要一个仅搜索Properties文件的Control对象就可以了。

调用Control.getControl方法,并且用一个List<String>来指定需要的文件格式。这个预
定义的字符串的值有两:java.class和java.properties。三个静态的,不可被修改的常量
List<String>列出了可指定的文件格式:

FORMAT_CLASS包含“java.class”的仅允许class文件格式的列表(java.util.List<String>)
FORMAT_PROPERTIES包含“java.properties”的,仅允许properties文件格式的列表(java.util.List<String>)
FORMAT_DEFAULT既包含“java.class”也包含“java.properties”的列表(java.util.List<String>)

选用常量Control.FORMAT_PROPERTIES使得Control对象仅搜索properties文件,代码如下:
ControlpropOnlyControl=Control.getControl(Control.FORMAT_PROPERTIES);
ResourceBundlebundle=ResourceBundle.getBundle("com.sun.demo.intl.res.Warnings",propOnlyControl);

使用propOnlyControl变量(Control的实例),getBundle方法就忽略以class结尾的文件,
而只搜索以properties结尾的文件。

Locales是包名称的一部分
-----------------------
基本名相同的各种本地化绑定,通常是以后缀名来区分。缺省的警告信息的"绑定"
(Warningsbundle)仅是简单地用Warnings.properties配置文件实现。然而,当你需要
用法文来显示这些警告消息的时候,就要用Warnings_fr_FR.properties文件了。使用缺省
的Control,这些本地化绑定名都是存在于同一个包里的,但是可以改变这些本地化绑定
的命名。假设有这样的情形:你想要把同一个绑定的不同的本地化版本放置在各自对应的子
目录或者是包里,那么就需要按照如下的样子创建这些properties并把它们放到各自对应
的文件路径或者是包里:

com/sun/demo/intl/res/root/Warnings.properties
com/sun/demo/intl/res/fr_FR/Warnings.properties
com/sun/demo/intl/res/ja_JP/Warnings.properties

通过子类化Control,并且在子类中覆盖以下方法来达成这样的目的。需要被覆盖的方法是:

*getFormats
*toBundleName

覆盖getFormats方法是因为应用程序仅需要properties文件作资源绑定;覆盖toBundleName
方法是因为应用程序需要使用指定的locale作为新的“绑定”的包名称的一部分,而不是在
“绑定”后面追加locale的名称。

看看示例代码是怎样通过定制Control子类来指定locale包名

classSubdirControlextendsControl{

//仅搜索properties格式的文件
publicList<String>getFormats(){
returnControl.FORMAT_PROPERTIES;
}

publicStringtoBundleName(StringbundleName,Localelocale){
StringBufferlocalizedBundle=newStringBuffer();
//Findthebasebundlename.
intnBaseName=bundleName.lastIndexOf('.');
StringbaseName=bundleName;
//Createanewnamestartingwiththepackagename.
if(nBaseName>=0){
localizedBundle.append(bundleName.substring(0,nBaseName));
baseName=bundleName.substring(nBaseName+1);
}
StringstrLocale=locale.toString();
//Nowappendthelocaleidentificationtothepackagename.
if(strLocale.length()>0){
localizedBundle.append("."+strLocale);
}else{
localizedBundle.append(".root");
}
//Nowappendthebasenametothefullyqualifiedpackage.
localizedBundle.append("."+baseName);
returnlocalizedBundle.toString();
}
}

下面的代码演示了如何来调用上面子定义的getBundle方法:

StringbundleName="com.sun.demo.intl.res.Warnings";
SubdirControlcontrol=newSubdirControl();
Localelocale=newLocale("fr","FR");
ResourceBundlebundle=ResourceBundle.getBundle(bundleName,locale,control);

假如缺省的locale是en_US,那么getBundle方法就用Control类去搜索侯选项并返回
如下列出的那些包名
com.sun.demo.intl.res.fr_FR.Warnings
com.sun.demo.intl.res.fr.Warnings
com.sun.demo.intl.res.en_US.Warnings
com.sun.demo.intl.res.en.Warnings
com.sun.demo.intl.res.root.Warnings


缓存Control对象的实例
--------------------------
装载资源绑定的时候,默认地就会对每个“绑定”检查,判断它是否已经被装载过。我们也可以
对此方式作点改变。假如,想在加载一个绑定前,简单地清除掉缓存,可以调用ResourceBundle
类的clearCache方法来实现。

ResourceBundle.clearCache();
ResourceBundlemyBundle=ResourceBundle.getBundle("com.sun.demo.intl.res.Warnings");

甚至能为缓存设置一个“过期”数值来控制缓存的“生存周期”。在Control的子类里覆盖方法
getTimeToLive,这个方法返回以毫秒值代表的“生命周期”。缺省情况下,这个方法返回的是
预定义的两个值中的一个,这两个值是:TTL_DONT_CACHE和TTL_NO_EXPIRATION_CONTROL

Control缺省情况时返回TTL_NO_EXPIRATION_CONTROL,这个值表示:缓存永不过期。
而TTL_DONT_CACHE表示:根本就不对绑定进行缓存。假如,想让“绑定”每过4个小时就要
进行更新,而且不是重新启动程序的话,那么需要像如下代码那样来覆盖getTimeToLive方法:
publiclonggetTimeToLive(){
return4L*60*60*1000;//14,400,000millisecondsisfourhours.
}

Control对象里有很多方法来为绑定的搜索和控制进行细致地设置。本文仅列举了其中的一些,
其他的,如下所列的方法也可通过覆盖来实现定制:
*getCandidateLocales
*getFallbackLocale
*newBundle
*needsReload

请参阅详细的文档中对这些方法的说明( http://java.sun.com/javase/6/docs/api/java/util/ResourceBundle.Control.html



针对locale的服务
----------------
在java.text和java.util包里支持超过100个的locale。这些locale可以为世界上大多数
地区的人所使用,然而对某些地区的支持仍然未被支持。为了让JAVA支持这些locale,需要
做很多调查工作,比如说研究和确定数字和日期的格式,国家名称的翻译,排列的次序。某些
情形下,设置是政治上的冲突都会影响到locale的内容。事实上,JAVA平台上的locale做不到
“与时具进”。

有一种解决办法就是提供新的编程接口(API)让编程人员使用任意的locale数据。
JavaSE6里提供给编程人员一个新的接口,可让定制的locale插到应用程序上或者是关联
到服务。幸运的是,当前正在进行的一个项目CommonLocaleDataRepository(CLDR
通用区域数据仓库)正在努力地跟踪研究现今世界上所有的区域数据并且维护这些数据。
Unicode组织主持这个项目。借助新的“区域相关的服务提供接口”,就可让应用程序
使用任意的与区域相关的数据。

为了使用区域相关的数据和服务,先要确定应用程序需要什么样的功能。可以为以下列出的类
应用与区域相关的数据:
*java.text.BreakIterator
*java.text.Collator
*java.text.DateFormat
*java.text.DateFormatSymbols
*java.text.DecimalFormatSymbols
*java.text.NumberFormat
*java.util.Currency
*java.util.Locale
*java.util.TimeZone

确定了需要使用区域相关数据的功能后,就要遵照服务提供接口来实现,这些接口是定义在
java.text.spi和java.util.spi包里。

比如说,想要为一个新的区域相关的数据提供DateFormat对象,可以通过实现抽象类
java.text.spi.DateFormatProvider里的
*getAvailableLocales
*getDateInstance
*getDateTimeInstance
*getTimeInstance
这些方法达到目的。

注意,getAvailableLocales方法是继承自父类LocaleServiceProvider,因此所有的SPI
提供者必须实现此方法来声明它们所支持的区域对象。其他三个方法是被映射到相关类上的
工厂方法,譬如getDateInstance方法就是在java.text.DateFormat类上的。

实现了以上的这些必要的方法后需要把服务包装起来以便部署到JAVA运行环境。区域相关
的服务是基于标准的JAVA扩展机制( http://java.sun.com/javase/6/docs/technotes/guides/extensions/
把它们打包进JAR文件并放到JRE的扩展目录中,运行环境就能提供当初那些不被支持的
区域相关的数据了。


归一化文本
---------------
Unicode标准允许用户以不同的方式创建等效的文本。比方讲,é这个字符是带重音的拉丁
小字母,它在Unicode编码表上的码值是U+00E9。基本字符e和重音符号被合成到一个编码
上来表示。

也能把小写字母e和重音符号合并起来展现同样的可见字符。比如,1/2这样的字符串就有
3个字符,它与作为单个字符的1/2(unicode字符U+00BD)其实表示了同样的意思。类似地,
上标字符2与常规字符2是同样的意思,仅是在外观表现上有点不同罢了,正因为字符的这些
特点,我们可以使用很多方式来进行文本的输入。你可能想到,像文本搜索和排序是不是会
因为同样的字符而表象变得复杂起来呢?

Java中的java.text.Collator能正确领会Unicode编码并且会将文本进行“归一化”以实现正确
的搜索功能。为了执行文本的正确操作,需要把各不相同的文本转化到单一的形式。在JDK1.6
前,java.text.Collator使用私有的API执行文本归一化,在1.6版本中,这些API成为了
公开方法了。

用java.text.Normalizer来对文本执行归一化操作,在进行文本处理,串行化,转换甚至是
数据库存储操作前进行归一化工作。java.text.Normalizer类里只有两个静态方法:
normalize和isNormalized。正如所想,normalize方法对文本执行归一化,isNormalized
方法检查文本是否已经被归一化。

枚举型Normalizer.Form表示了每个unicode归一化形式:
*NFD(NormalizationFormD)
*NFC(NormalizationFormC)
*NFKD(NormalizationFormKD)
*NFKC(NormalizationFormKC)

NFD是规范分解,按照unicode标准把合并了的字符分解成合并了的序列。拿unicode码U+00F1
(ñ),会被分解成U+006EU+0303,这个分解后的序列其实就是字符n和发音符号。

NFC是紧随规范合成后的规范分解。把文本分解后,接下来把字符序列合并到标准代码。比如
把字符序列U+0065U+0300并成单个字符编码U+00E8,也就是字符è。NFC是W3C组织推荐
的用于互联网上文本传输和处理的归一化方式。

NFKD是一种兼容性的分解。这种方式把某些字符转换到兼容的形式。通过预定义的字符映射
来实现兼容。常见的商标字符TM,其unicode码是U+2122,也就是大写的拉丁字符T(U+0054)
和M(U+004D)

NFKC是一种兼容的分解方式。这种归一化方式试图创建兼容原始字符的合并字符。等效的
兼容字符由unicode标准定义。把NFKC应用到U+1E9B(拉丁小写长音字符s,带有圆点上标),
分解的时候就创建U+017FU+0307两个字符序列,最后,进行合并而成为单个字符U+1E61

示例代码演示了如何使用Mormalizer类来把文本归一化到NFD形式:
StringstrName="Jos/u00E9";//usingacomposedé
StringstrNFD=Normalizer.normalize(strName,Normalizer.Form.NFD);

变量strNFD现在有五个码:Jose'这五个码是:U+004AU+006FU+0073U+0065U+0301
接着看看文本是否已经被归一化了:
booleanbNormalized=Normalizer.isNormalized(strNFD,Normalizer.Form.NFD);
System.out.printf("NFD?%b/n",bNormalized);


国际域名
------------------------
RFC3490为应用程序里的国际域名(IDNA)做定义,事实上,国际域名不在仅限于ASCII码了,根据
unicode3.2规范,域名定义的限制比原先要少了:unicode3.2规范中的所有字符都能用来
作域名。很遗憾,域名服务器以及定位服务对于unicode3.2还不能妥善的存储和使用非ASCII
字符的信息。IDNA的解决方式是:定义一个用ASCII编码来表示非ASCII字符的方法,这就使
得DNS和名称定位服务的应用软件继续提供兼容ASCII码的服务,通过使用扩展的unicode字
符,集终端用户还能使用国际化域名。

JavaSE6里提供了java.util.IDN类来自持IDNA,此类中提供了一些方法来把unicode编码
的域名转换成与ASCII码兼容的域名,这些方法是:toASCII和toUnicode。应用程序在
和DNS或者是名称定位服务打交道前,需用toASCII方法把域名转换到ASCII码;反过来,
使用方法toUnicode创建用户可见的unicode文本。

如果在应用程序里输入非ASCII字符集的域名,程序在把数据发送到国际互联网前需要做如下
工作:
//获取应用程序界面上的输入
StringstrUnicodeName=txtUnicodeName.getText();
//转换成与ASCII兼容的编码
StringstrACEName=IDN.toASCII(strUnicodeName);


如图,使用日本语的域名,变量strACEName保存了文本“xn--wgv71a119e.jp”

“xn--wgv71a119e.jp”这样的文本谁也读不懂,因为这是对字符编码后的样子,只有对计算机
和应用程序有用。可以用下面代码演示如何把这样的字符转变为人能读懂的文本:
StringstrACEName=txtACEName.getText();
StringstrUnicodeName=IDN.toUnicode(strACEName);


日本国的日历
---------------
日本人常使用两种纪年方式:国际上的公元纪年法和他们本国的年号纪年法。几乎所有人都
会用公元纪年法,然而日本政府在日常习俗和文件中还常使用年号纪年法。年号纪年法依据
天皇在位的时间来定义。

JAVA编程中使用java.util.Calendar.getInstance方法来获取日历对象的实例。通过像
演示代码中那样的方式来使用日本国的年号纪年法:
CalendarcalJapanese=Calendar.getInstance(newLocale("ja","JP","JP"));

在创建了Calendar对象实例后就可在其上使用基于年号纪年法的日期设置,获取以及组装。

公元纪年法和年号纪年法一个明显区别就是在格式化日期上。java.text.SimpleDateFormat
和java.text.DateFormat类为新的日历格式提供了支持,可向下面代码演示的那样来对日
期格式化和显示:

Datenow=newDate();
LocalelocaleJapanese=newLocale("ja","JP");
LocalelocaleImperialJapanese=newLocale("ja","JP","JP");
DateFormatdfGregorian=DateFormat.getDateInstance(DateFormat.FULL,localeJapanese);
DateFormatdfImperial=DateFormat.getDateInstance(DateFormat.FULL,localeImperialJapanese);
StringstrGregorianDate=dfGregorian.format(now);
StringstrImperialDate=dfImperial.format(now);
txtGregorianDate.setText(strGregorianDate);
txtImperialDate.setText(strImperialDate);


对locale设置了使用“ja_JP”后,DateFormat就用日文字符来表示年月日的格式化结果。
如果对locale设置了使用“ja_JP_JP”,DateFormat就会输出年号纪年法格式化的日期
字符串。如下图所示:



新增locales
--------------
在JavaSE6里,在现有支持的LOCALE基础上又新添了许多locale以支持不同的区域相关
类。区域相关的数据来源于CLDR(http://unicode.org/cldr/),尽管新的区域相关数据
被引入了很多,但是不影响以前就存在的那些区域相关的对象。下表列出了JavaSE6里新
添加了的区域对象

Chinese(Simplified)Singaporezh_SG
EnglishMaltaen_MT
EnglishPhilippinesen_PH
EnglishSingaporeen_SG
GreekCyprusel_CY
IndonesianIndonesiain_ID
IrishIrelandga_IE
Japanese(JapaneseImperialcalendar)Japanja_JP_JP
MalayMalaysiams_MY
MalteseMaltamt_MT
SerbianBosniaandHerzegovinasr_BA
SerbianSerbiaandMontenegrosr_CS
SpanishUnitedStateses_US

小结
------------
JavaSE6向开发者敞开大门,让他们能对资源的定位和装载实现更多的控制,这样就使得
JAVA平台对国际化的支持更广泛了,同样,也能使用区域相关的服务接口来支持未在JAVASE6
中提供的区域数据。Normalizer类不再是私有的了,它能把文本归一化到四种unicode标准
格式:NFC,NFD,NFKC,NFKD。不用再把域名限制在ASCII编码集了,IDN类提供API来转换非ASCII
域名到可用的兼容ASCII编码的域名服务和名称定位服务。新添加了支持日本国的年号纪年法
的日期格式化。结束本文前,超过“一打”的新的区域对象已经可以被拿来使用了,这些区域
对象的数据是从CLDR项目上获得的,这些新添加了的区域对象不会与现存对象冲突。

你可能感兴趣的:(J2SE)