JAVA国际化教程【转载】

news/2024/6/18 19:57:17
  JAVA国际化教程


1.关于本教程
2.简介
3.Java平台I18N支持概述
4.Unicode与Java字符
5.提供本地化的资源
6.使用日期、数字和货币
7.将各部分放到一起
8.结束语与参考资料
9.完整的代码清单

1.1 本教程是关于什么的?
本教程向您介绍了 Java 编程语言对多语言和多国环境的支持。教程从对国际化原理和概念的一般性讨论开始,然后对 Java 国际化支持的特定领域做了一个概述。最后几节针对任何国际化 Java 应用程序的基本领域(Unicode 与 Java 字符;语言环境与资源束以及格式化日期、数字和货币)提供了更具实践性的讨论,包括主要讨论领域的示例程序,以及一个将它们连接在一起的最终的较完整应用程序。
一旦读完本教程,您将对国际化的元素及 Java 平台提供支持的领域有一个牢固的理解。您也应该能够编写使用资源束并且能够格式化并解析日期、数字和货币的应用程序。

1.2 本教程适合我吗?
如果您是一名中级 Java 程序员,对 I/O 和 Swing 有所了解并且对构建国际化 Java 应用程序感兴趣,那么Java 国际化基础知识非常适合于您。然而,从初级到高级的所有开发人员也能够从中收集有用的信息和复习材料。特别是,每一位 Java 程序员都应该理解Unicode 支持和 Java 字符与 char 数据类型 节中讨论的内容。虽然对所有的示例代码做了解释,但是重点集中在同国际化有密切关系的领域,而不是一般的 Java 编程。以前对国际化的任何了解对学习本教程都是十分有帮助的,但是这里并不假定任何特殊的背景知识。
作者注:虽然我有一些德语和俄语的基础,但是示例中的单词和短语主要是因特网字典研究的结果。我希望,对于任何差劲或不恰当的用词,您会觉得可笑而不是生气。如有任何改正、语言或其它方面的问题,请毫无拘束地和我联系。
请参阅参考资料以获取根据这里给出的材料展开的教程、文章和其它参考资料清单。

1.3 代码样本与安装需求
Item 5: 虽然撰写本教程时已经可以获得 JDK 1.4 的一个评估版,但 JDK 1.3 还是应用最广泛的版本。明确地讲,这些示例是使用在 Windows NT 4.0, Service Pack 6a 上运行的 J2SE v1.3.1_02 进行测试的。在 1.4 中,只有几个同国际化有关的新项,在适当的节中会提到这些变更或增加项。您当然应该有任何 JDK/JRE 的国际化版本。
注:代码示例只是用来促进对基础知识的理解,并没有为生产使用而进行过优化。
本教程中使用的示例的类和源代码在参考资料中可以作为可下载的 JAR 文件获得。在附录 A:完整的代码清单中还列出了单独的源文件。

1.4 关于作者
有关本教程内容的技术问题,请同作者联系。
Joe Sam Shirah 是 conceptGO 的负责人和开发人员,该公司提供远程咨询和软件开发服务以及产品,专长是 JDBC、I18N、AS/400、RPG、金融、库存以及后勤方面。 Joe Sam 于 1998 年 JavaOne 上获得 Java 社区奖,他也是 Java Developer Connection 上的 JDBC 2.0 Fundamentals 短期课程的作者。他是 developerWorks“Java filter”论坛的主持人,还是 jGuru 的 JDBC、国际化和 Java400 FAQ 的管理人员。Joe Sam 拥有经济学工商管理学士学位以及国际管理硕士学位。可以通过 joesam@conceptgo.com 和 Joe Sam 联系。

2.1 国际化
同计算机编程有关的国际化是设计和编写应用程序以便可以在全球或多国环境中使用的过程。国际化程序能够支持不同的语言以及不同格式的日期、时间、货币和其它值,而无须软件修改。这通常涉及“软编码”或将文本组件同程序代码分离并且可能涉及可插入代码模块。
从业者通常将国际化(internationalization)简写为 I18N。原因是 internationalization 一词开始的 I 和最后的 N 之间有 18 个字母。试着多说、多写“internationalization”一词几次,您就会欣赏这个较短版本的价值。此外,您可能会看到“I18N'ed”作为“国际化的(internationalized)”的缩写形式。虽然这在语法上不准确,在技术上也不正确,但是 “I18N'ed”很有用,您将在包括本教程在内的文献里经常看到它。
关系数据库管理系统和操作系统可能也对国际化的某些方面提供基本支持,通常使用术语“本地语言支持(National Language Support)”或 NLS 来表示。

2.2 本地化
本地化是设计和编写能够处理特定区域、国家或地区、语言、文化、企业或政治环境的应用程序的过程。从某种意义上说,为特定地区编写的所有应用程序都本地化了,虽然这些应用程序大多数只支持一种语言环境。然而,真正的本地化通常是由访问语言环境、位置、政治或其它特定组件和模块的核心代码,以及将文本翻译成适合于用户的版本来实现的。适当国际化的程序使本地化更为便利,并为本地化提供了基础。
与将“internationalization”缩写成“I18N”相同的原因和逻辑,本地化(localization)通常缩写为 L10N。
可以将处理多个国家或地区(比如说美国、加拿大、墨西哥和巴西)的税收或会计软件包国际化,使得无需对每个国家或地区重复定制显示、报表生成和其它程序。然后本地化该软件包以处理适合于特定国家或地区甚至可能是州或省的不同会计和报表生成过程。

2.3 I18N 存在的理由(I18N raison d'etre)
这一页的标题本身就为国际化提供了一个理由:没有接触过“I18N”或不懂法语(raison d'etre 的大致意思是“存在的理由”)的人将搞不清这一节是关于什么的。有时,缺乏知识是一个优点,这可以由人造钻石的成功来证明。然而,如果软件不能被人理解,那么不论开发人员多么陶醉于自己的智慧,它也是无用的。不便或惹人生气的软件用处也不大,不适于销售。
最开始时使用 ASCII。直至今日,大多数编译器仍要求 ASCII 输入。随着计算机的发展,需要额外的语言支持已为人们所公认,通常包含 ASCII 和本地语言的特定于国家或地区的字符集也随操作系统一起提供。即便如此,一般也只支持一种“其它”语言,大多数开发人员都根据他们自己的国家和当地文化来设计程序。由于历史和实际的原因,因特网和 Web 上的应用程序,通常遵循相同的模式。虽然重点通常在英语语言上,但是很容易找到各种单语言应用程序和网站。
另外一个考虑是经济上的;在您的国家之外存在着巨大的市场。随着那些以前贫穷的国家或地区 GNP 的增长,对计算机的广泛接受以及上网人口的增加,市场正在改变。根据 Global Reach 的全球因特网统计信息(Global Internet Statistics)页面提供的信息,到 2001 年 12 月,大约 45% 的因特网人口讲英语。接下来是日语,大约占 9 %,紧随其后的顺序是中文、德语、西班牙语、韩国语、意大利语、法语以及其它语言。假定计算机访问跟踪类似的比例是合理的。虽然从 2001 年到 2005 年在线人口预期会翻一翻(在线商业预计从大约 1 万亿美元增长到 6 万亿美元),但整体讲英语的比例预期会持续下降到 39% 左右。
其它关注国际化问题的理由可能更接近于自身的利益:您的公司可能在其它国家或地区开设办事处,或者从位于另外一个国家或地区的潜在客户收到一个投标请求(Request For Proposal (RFP))。

3.1 国际化和 Java 编程语言
和大多数使用其它语言的程序员不同,Java 程序员是大量构建在 JDK 中提供 I18N 支持的标准代码的受益者。大部分代码最初来自 IBM 的 Taligent 子公司(自从合并进 IBM 以后),代表了许多人年的工作成果,比大多数公司独自在其产品中提供的代码要切实可行得多。
这些代码及其远见并不总是完美的;例如,请看一下 java.util.Date 类中许多弃用的(deprecated)方法。我们中的许多人可能记得太平洋标准时间(Pacific Standard Time)显然也是 Java 世界时间(Java World Time)。然而,即使在“错误的旧时代”,其它语言也没有能与这种内置的功能匹敌的东西,即便有,也很少。这一节的页面简要地讨论了 Java 平台支持的一般国际化领域。

3.2 Unicode 支持
Java 语言字符集是 Unicode,而且相应地,原始 char 数据类型的长度是两个字节(16 位),以容纳 Unicode 值。由于大家熟悉的 String 由 char 组成,因此 String 也是基于 Unicode 的。Unicode 本身是这样定义的:值 0 到 127 匹配标准 ASCII,0 到 255 匹配 ISO 8859-1 (Latin-1) 标准。由于这一起始值的一致性,不使用 I18N 功能或不需要面对 I18N 问题的程序员可以编写他们的 Java 程序而无需理解或知道 Unicode。然而,考虑到 Windows 的普遍使用,该平台的程序员应该知道标准 ISO 8859-1 和 Windows Latin-1 (cp1252) 之间的差异。
16 位 char 长度允许 0 到 65535 之间的值。提供了 Unicode 转义以在本地平台不支持实际字符时仍然允许输入。其格式是“\u”后跟 0000 到 FFFF 的四个十六进制数字。例如,下面两行代码是等价的:

char c1 = 'a';
char c2 = '\u0061';

JDK/JRE 的 1.3 版本支持 Unicode 2.1;1.4 版本支持 Unicode 3.0。更多关于 Unicode 和称为 UniBook 的 Unicode 显示程序的信息,请参阅参考资料中到 Unicode Consortium 的链接。

3.3 字符集转换和流输入/输出
上一页提到过 Java 字符集是 Unicode,但并不是所有平台都支持 Unicode。那么,这个戏法是怎么完成的呢?答案是:所有支持字符的输入和输出流 ? 即 java.io.Reader 和 java.io.Writer 层次结构 ? 自动调用在平台的本地编码和 Unicode 之间执行转换的隐藏代码层。请注意,本地编码是假设的。如果数据不是缺省编码的,您将不得不自己转换数据。幸运的是, java.io.InputStreamReader、 java.io.OutputStreamWriter 和 java.lang.String 类具有允许使用受支持的编码的转换规范的方法。您可以在 JDK 文档(可以从参考资料访问)的 Internationalization 节中的 Supported Encodings 下面找到它们。请注意,JDK 1.4 现在对泰国语和印地语提供支持。

有趣的是,Java 对数字的大尾数格式提供保证,而对于 char 数据类型却不支持这一保证。缺省格式同平台有关。例如,在 NT 4.0 上,系统特性“sun.io.unicode.encoding”被设置成 “UnicodeLittle”。如果因为某种原因您想自己指定该格式,那么您可以根据文档来选择 UnicodeBig、 UnicodeBigUnmarked、UnicodeLittle、UnicodeLittleUnmarked、UTF8 或 UTF-16。

3.4 字符分类与 Character 类
除了以标准方式为多种语言定义字符之外,Unicode 也为每个字符定义了几个特性。这些特性标识诸如一般类别、双向性、大写、小写以及该字符是数字还是控制字符等事情。在可以从 Unicode Consortium 网站上获得的 UnicodeData 文件中定义了这些特性。
Java Character 类提供获取这些特性的方法。虽然特定实例是不变的,但是许多方法是静态的,允许实时访问字符的特性。
该类有用性的一个示例来自一个典型的 ASCII 编程算法:许多程序员利用了这样一个事实,如果字符值在 0x41 和 0x5A 之间,那么它是大写字母(A-Z)。加上 0x20,您就得到小写字母(a-z)。遗憾的是,如果处理的语言包含有超出 ASCII 范围的字符时,该算法会失效。 解决方案是使用 Character.isUpperCase() 和 Character.toLowerCase(),它们在任何情况下都起作用。另外一个示例是 Character.isDigit(),它也用于表示 ASCII‘0’到‘9’以外的数字的字符。

3.5 语言环境
在 Java 语言中,语言环境(locale)仅仅是一个标识符,而不是一组本地化的属性。java.util.Locale 类的一个实例表示一个特定的地理政治区域,使用表示语言、区域以及国家或地区的参数创建。每个与语言环境相关的类都维护着它自己的一组本地化属性,并且确定如何对含有 Locale 参数的方法请求做出响应。
按照以前的陈述,很明显,没有关于程序员可能怎样对含有 Locale 参数的方法请求做出响应的约束。然而,在 Sun 的参考 Java 2 平台和其它一致实现中,有一组一致的受支持的本地化实现。更多信息,请参阅 JDK 文档(可以从参考资料访问)中的 Internationalization 一节中 Supported Locales。应该注意,该文档将多种语言环境列为“也提供了,却未测试(also provided, but not tested)”。我个人看见这一“未测试”问题出现在 JDK 1.3.1 中的 Finnish (fi_FI) 语言环境;买主自行当心。

3.6 AWT/Swing Name 和 Locale 属性
java.awt.Component 类包含 Name 和 Locale 属性的读方法和写方法。虽然文档也讨论了 Component 的构造器及其使用 Name 参数的子类,但我显然需要倍加小心,因为我以前从未找到它们。Component 位于大多数 Swing 类的层次结构中,它们也自动支持这些属性。
Name 属性是一个您可以通过编程进行赋值的不可本地化的 String。这有助于国际化 ? 听起来可能有些奇怪,但是随着大多数数据根据语言环境改变时,Name 提供了一个标识组件的设置锚点。当然,在一个给定的类里,为对象等同性测试对象引用可以达到相同的目的。虽然每种技术都有极好的理由,但我通常在 actionPerformed() 方法中使用对象等同性测试,如同您在代码示例中看到的那样。文档声明:如果不通过编程设置 Name,那么将赋予一个缺省值,但不给出值或模式。在我编写的代码中,如果在调用 Component.setName("aName") 之前调用了 Component.getName(),它将返回 null。当然,作为未在文档中记录的行为,结果可能不一致,并且可能会在将来发生改变。因此,当将使用 Name 属性时,良好的编程实践要求将所有组件的 Name 属性设置成标准值(也就是“取消设置”),然后适当地设置想要的组件。
Locale 属性允许组件跟踪它自己的语言环境,即便是应用程序的其余部分正在使用不同的语言环境。在某些情况下,该项技术非常有用,虽然对于具有文本值的 Component,可以在将文本发送给 Component 之前对它执行本地化,而无需设置特定的 Component Locale。

3.7 本地化的资源
java.util.ResourceBundle 是一个为存储和定位由应用程序使用的资源提供机制的抽象类。资源通常是本地化的 String,但也可以是任何 Java 对象。 ResourceBundle 以一种层次结构建立,它以一个具有基础名称的一般 ResourceBundle 开始,然后通过向另外的 ResourceBundle 的基础名添加语言和国家或地区标识(它们在 JDK 文档 Internationalization 一节的 Supported Locales 中有定义,可以从参考资料访问这一节),使这种层次结构变得更为特定。ResourceBundle 的三大优点是:
● 类装入器机制用于定位 ResourceBundle,因此无需额外的 I/O 代码。
● ResourceBundle“知道”如何通过使用 static getBundle(String baseName) 或 getBundle(String baseName, Locale locale) 方法,按照从特定到一般的顺序,搜索层次结构以寻找适合于语言环境的实例。
● 如果在特定实例中没有找到资源,那么将使用来自更一般实例的资源。
好消息/坏消息是:ResourceBundle 实例一旦被装入,将被以性能优化的名义进行高速缓存;这一高速缓存从不会被更新,并且没有操作该高速缓存的正式方法。

ResourceBundle 有两个子类:
● ListResourceBundle,它是另一个抽象类,因此您必须提供自己的实现。首先,您必须覆盖 getContents(),它返回二维 Object 数组(Object[] [])。这种 ResourceBundle 可以返回任何类型的 Object。
● PropertyResourceBundle,它是一个由 java.util.Properties 文件支持的具体类,它只能返回 String。
您也可以提供您自己的定制子类。在这种情况下,您必须覆盖并实现 handleGetObject() 和 getKeys(String key)。

ResourceBundle 使用键/值对,并提供 getString(String key) 和 getObject(String key) 方法。您也可以使用 getKeys() 来获得可用键的 Enumeration。


3.8 日历与时区支持
最初打算将 java.util.Date 用来处理日期与时间操作,但是内在的缺陷导致其只能以时间的形式表示具体时刻。JDK 1.1 中引入了抽象类 java.util.Calendar 及其具体子类 java.util.GregorianCalendar 来处理 java.util.Date 的不足。Calendar 类具有获取所有日期与时间字段以及执行日期与时间运算的方法。
抽象 java.util.TimeZone 类及其具体子类 java.util.SimpleTimeZone 维护全球统一时间(Universal Coordinated Time(缩写为 UTC,而不是您期待的 UCT;由于历史原因这一缩写取自法语形式))的标准时及夏令时的偏差值。此外,TimeZone 也含有获取本机及本地化时区显示名称的方法。

3.9 格式化与解析
数字、货币、日期、时间以及程序消息都受到文化及地区差异的影响,并且对于本地化需要大量的格式化与解析工作。创建了抽象类 java.text.Format 及其子类来处理这一 I18N 领域的问题。所有这些子类都有与语言环境相关的 format() 和 parse() 方法来以与语言环境相关的方式操作值。遇到非法值,parse() 方法将抛出 ParseException。具体子类 java.text.SimpleDateFormat 和 java.text.DecimalFormat 允许模式及对实例的适当符号的访问。通常,抽象父类拥有返回适当本地化的对象的 getInstance() 和 getXXXInstance() 静态工厂方法。

下面是 java.text.Format 的直接子类的列表:
● 抽象 java.text.DateFormat 类及其具体子类 java.text.SimpleDateFormat,由 java.text.DateFormatSymbols 类支持,用于处理日期与时间值。
● 抽象 java.text.NumberFormat 类及其具体子类 java.text.ChoiceFormat 和 java.text.DecimalFormat,由 java.text.DecimalFormatSymbols 类支持,用于处理数字、货币及百分数。
● java.text.MessageFormat 允许“软编码的”位置及格式化要插入本地化的消息的值。
对于 JDK/JRE 1.4,已经添加了 java.util.Currency 以使得可以独立于语言环境使用货币。java.text.NumberFormat 拥有处理货币和整数的新方法。

3.10 与语言环境相关的 String 操作
作为开发人员,我们经常需要操作、搜索 String 以及对其排序。当涉及多种语言,这项工作的难度简直令人难以置信。Java 平台提供下列类以供帮助:
● 抽象 java.text.Collator 类及其具体子类 java.text.RuleBasedCollator 允许对与语言环境相关的 String 进行比较。
● java.text.CollationElementIterator 类以给定的整理顺序遍历 String 的每个字符并返回其有序的优先级。
● java.text.CollationKey 类表示一个由特定 Collator 管理的 String,它允许相对较快的排序比较。
● java.text.BreakIterator 类以与语言环境相关的方式实现了定位断行、断句、断词和断字符的位置的约定。
● java.text.StingCharacterIterator 类对 Unicode 字符提供双向遍历,用于搜索 String 内的字符。

3.11 输入法
实际上,以上所有讨论都涉及操作或显示数据。然而,必须以某种方式输入数据。对于最终用户,最常用的是键盘。但是,如果键盘不支持某种语言输入所需的字符,您该怎么办呢?

输入法(Input method)是允许数据输入的软件组件的一个技术术语。Java 平台既允许使用主机 OS 输入法也允许使用基于 Java 语言的输入法。如果您需要实现输入法,您可以使用输入法框架(Input Method Framework)。您可以在 JDK 文档中 Internationalization 一节中的 Input Method Framework(可以从参考资料中访问该文档)中找到输入法客户机 API(Input Method Client API)及输入法引擎 SPI(Input Method Engine SPI)的规范、参考和教程。

4.1 Java 字符与 char 数据类型
Java 程序员的一个最知名的抱怨是“我只看到程序输出是问号(或方块),我的数据是怎么被破坏的呢?”通常,作为 Java 开发人员,您应该理解实际发生了什么以及这一表面问题后面的原因,而这种知识在处理国际化问题时尤为重要。
Java 语言规范(Java Language Specification)将 char 定义为原始的、数值型和整型的类型。此外,char 是唯一的无符号(unsigned)数字类型,它允许一些有趣的(或讨厌的,这取决于您的观点)窍门。char 在另一方面也十分特殊,因为将它们送到诸如显示器或打印机的输出设备时,会将其值从字符映射或字体映射成字形。然而,从根本上来说,char 是数值类型,支持所有整数运算。因此 Unicode 支持 注释道:可以使用字母或 Unicode 转义符设置 char。因为 char 是数值型,所以您也可以使用八进制、十进制或十六进制表示法甚至反转位来赋值。
假设出现上述情况并假定没有程序错误,上面问题的答案是:字符映射或字体不支持该字符,显示问号或方块来作为替代。该 char 本身的值仍然有效。但是,这样您就不能可视地验证数据;您不得不核对数值。下面的示例显示了这一行为。
这幅图像显示了日本象形文字中的“Go”或 5,以 Unicode 表示为‘\u4E94’。该字符导致在下面的 charExample 程序中显示成问号和方块:

import javax.swing.*;

public class charExample
{
public static void main( String[] args )
{
boolean bFirst = true;
char aChar[] = {
'A', // character
65, // decimal
0x41, // hex
0101, // octal
'\u0041' // Unicode escape
};

char myChar = 256;

for( int i = 0; i < aChar.length; i++ )
{
System.out.print( aChar[i]++ + " " );
if( i == (aChar.length - 1) )
{
System.out.println( "\n---------" );
if( bFirst )
{
i = -1;
bFirst = !bFirst;
}
}
} // end for
// the result of adding two chars is an int
System.out.println( "aChar[0] + aChar[1] equals: " +
(aChar[0] + aChar[1]) );
System.out.println( "myChar at 256: " + myChar );
System.out.println( "myChar at 20116 or \\u4E94: " +
( myChar = 20116 ) );
// show integer value of the char
System.out.println( "myChar numeric value: " +
(int)myChar );

JFrame jf = new JFrame();
JOptionPane.showMessageDialog( jf,
"myChar at 20116 or \\u4E94: " +
( myChar = 20116 ) +
"\nmyChar numeric value: " +
(int)myChar,
"charExample", JOptionPane.ERROR_MESSAGE);

jf.dispose();
System.exit(0);

} // end main

} // End class charExample


首先,程序用字母“A”的各种表示法初始化一个 char 数组,并将一个 char 变量设置成 256(‘\u0100’)。程序在一个循环中打印两次数组的值。打印之后递增每个元素的值(char 是数值型,记得吗?)。接下来,将头两个元素加到一起,然后打印其结果(int)。然后,打印 char 变量,首先用其初始值,然后用值 20116 或‘\u4E94’,它是 5 的日本象形文字“Go”。如预期的那样在使用代码页 cp1252 的 Windows NT 上,会在显示器上将这两个值打印成问号。根据您的系统使用的代码页,显示可能略微有些不同。要核查其值,接下来将变量作为 int 打印。最后,JOptionPane 显示其值,对于不受支持的 char‘\u4E94’,它显示一个方块。

下面是 charExample 的输出:


A A A A A
---------
B B B B B
---------
aChar[0] + aChar[1] equals: 134
myChar at 256: ?
myChar at 20116 or \u4E94: ?
myChar numeric value: 20116

JOptionPane 显示:


4.2 字体、字体特性及 Lucida 字体
Java 平台既识别逻辑字体也识别物理字体。
逻辑字体是那些被映射到主机系统字体的字体。比如人们熟悉的 Serif、Sans-serif、Monospaced、Dialog 以及 DialogInput 字体。还有四种逻辑字体样式:普通、粗体、斜体及粗斜体。使用一个位于 JRE/lib 目录下的 font.properties 文件来实现主机字体到逻辑字体的映射。尽管细节因系统而异,但是缺省 font.properties 文件通常设置成英语环境,虽然 JDK 也有一个本地化日语版本可用。还提供了另外的 font.properties 文件;JDK 1.3.1 Windows 版包含阿拉伯语、希伯来语、日语、韩国语、俄语、泰国语字体文件以及中文的几种版本的字体文件。就象命名约定一样,搜索适当的 font.properties 同用于 ResourceBundle 的方法类似(但不全相同)。如果特定于语言的 font.properties 文件同您的系统的语言环境相匹配,并且安装了期望的字体(通常随 OS 的那一版本一起提供),那么就会对该语言进行自动映射。否则,就使用缺省的(通常是英语)文件映射。
如果您安装了适当的字体并且在调用 Java 应用程序时传入了相应的语言及国家或地区代码,那么也会进行自动映射。如果期望的 font.properties 文件存在,那么这种行为对开发非常有用。也可以将最初的缺省 font.properties 文件拷贝到别的地方,然后将特定文件重命名为“font.properties”,通过这样来将该语言/字体有效地设置成缺省情况。虽然对于开发人员来说很容易,但这显然不是最终用户非得做的事情。
如果您必须亲自定制或创建一个新的 font.properties 文件,那就完全是另一回事,而且更难。可以在 JDK 文档 Internationalization 一节中的 Font Properties 中找到处理 font.properties 文件的指示信息。
物理字体是我们始终在使用的正常字体。基于 ASCII 和 ISO 8859-1 的字体不会有问题。然而,一旦超出这一范围,主机平台显然必须理解它们,并且必须对它们进行 Unicode 编码以使之能够用于您的 Java 程序。找到这些字体不象以前那么难了。例如,Windows MS Mincho TrueType 字体(主要是日语)是 Unicode 编码的,可以以标准方式立即投入使用。当在系统上装入了适当的物理字体时,您可以让用户选择他们想要的字体并保存他们的首选项,或者将该字体设置成整个软件包的标准字体,而不用进入 font.properties 文件。
Java 2 SDK 还提供三种物理字体系列:Lucida Sans、 Lucida Bright 和 Lucida Sans Typewriter。每一系列都含有四种字体 ? 分别用于普通、斜体、粗体以及粗斜体样式 ? 总计 12 种字体。虽然关于这些字体的确切功能的信息极为稀少,但 Lucida Sans 字体处理大多数欧洲和中东语言。不包括亚洲语言。由于该字体随 JDK 一起提供,因此教程中的所有图形应用程序示例都使用 Lucida Sans 字体。更多信息,请参阅 JDK 文档 Internationalization 一节中的 Physical Fonts(可以从参考资料访问)。

5.1 创建语言环境
提供任何类型的本地化资源时,您应该做的第一件事情是创建一个适当的语言环境(参阅语言环境)。虽然有一个包含平台/浏览器变体的构造器,但通常您将使用Locale l = new Locale(String language, String country);
其中,language 是由 ISO-639 定义的小写、两字母代码,country 是一个由 ISO-3166 定义的大写、两字母代码。
下面是用于特定于德国的德语语言环境:
Locale l = new Locale( "de", "DE");
Locale 有一个返回受支持的语言环境数组的 static getAvailableLocales() 方法。实际上,所有与语言环境相关的 Java 2 Platform API 都有一个 getAvailableLocales() 方法,您可以期望它返回一致的值。其它有用的方法是 static getDefault(),它返回缺省的语言环境,以及 getDisplayName() 和 getDisplayName(Locale inLocale) 方法,它们分别用缺省或所请求的语言环境语言来返回适于显示的名称。您也可以获取国家或地区以及语言的代码和名称。这些方法允许没有特定语言知识的程序员为最终用户提供读、选择以及返回本地化的语言环境信息的能力。

5.2 使用资源束
ResourceBundle 包含键/值结合。键总是 String,而值总是 PropertyResourceBundle 中的 String,但可以是 ListResourceBundle 中的任何对象或定制子类。如果没有找到请求的资源,那么 ResourceBundle 访问方法会抛出一个 MissingResourceException。
参阅本地化的资源以获得更多常规信息。本教程将集中于 PropertyResourceBundle,因为它们适合于大多数情形,并且易于生成和修改而无须编写任何新代码。
ResourceBundle.getBundle(String baseName) 和 ResourceBundle.getBundle(String baseName, Locale locale) 提供一种内置的搜索机制,当这些束的结构恰当时,这种机制工作得非常好。正常的搜索从 base_language_country_variant 到 base_language_country,再到 base_language,最后到 base。注:如果请求一个特定的、非缺省的语言环境,并且同资源一起存在一个缺省的语言环境束,那么搜索将会停在那儿而不是继续搜索到基础束。我们的示例程序(参阅 PropertyResourceBundle 代码示例)支持英语、法语、德语和俄语并使用 PropertyResourceBundle。支持的 .properties 文件被命名为:
ByTheNumbersrb.properties
ByTheNumbersrb_de.properties
ByTheNumbersrb_en.properties
ByTheNumbersrb_fr.properties
ByTheNumbersrb_ru.properties
所有文件都含有全部所需资源。英语用作缺省值,ByTheNumbersrb.properties 和 ByTheNumbersrb_en.properties 是相同的。这种做法略微有些偏离传统认知,即:对于基础缺省语言,不需要专门命名的 .properties 文件,因此我们不需要 ByTheNumbersrb_en.properties。然而,当一段特定信息使用非缺省语言环境时,这种设置却是必需的,我们的示例程序就是这种情形。假定一个英语语言环境将被用来在一台法语为缺省语言环境机器上显示某项。如果 _fr 束中存在相同的键,_en 搜索失败时,将会选择该值。这完全不是所请求的或所期待的那样。如果在程序的任意一次给定运行中只使用一种语言环境,那么专门命名的副本就不是必需的。但无论如何,在任何情况下这种做法都不需要新的代码并且起作用。
如果我们需要更特定的语言环境支持,例如奥地利语、瑞士语和德语(分别是 _de_AT、 _de_CH 和 _de_DE),那么只将国家或地区细节置于以适当的国家或地区命名的特性文件(例如,myprops_de_CH.properties)中,而将更一般的元素置于 _de 束级别,这样做会很有意义。在那种情形下,需要其它元素时,将总能找到 _de 束。
您也应该为束实现几种命名约定。我们的示例使用这种通用格式:Object.getClass().getName() + "rb"。主要规则是:对于 .properties 文件,不要只使用类名称作为其基础名称。忽视该规则在有些平台上照样能行,但在其它一些平台上您会大吃一惊。记入文档的准则是:如果类和具有相同名称的 .properties 文件同时存在,那么被选中和装入的将是类。就是这样。这一行为的一个好结果是:使用适当命名的束,您可以在 ListResourceBundle 和 PropertyResourceBundle 之间转换,而不用更改代码;只要将期望的类型移到类路径即可。
您可能会发现让不同的信息类型具有多个 ResourceBundle 更加合适。它们可以为许多不同的程序提供资源。特定的前缀或后缀约定对于避免类名冲突仍然有用。

5.3 使用 PropertyResourceBundle
PropertyResourceBundle 的语义同其父束 ResourceBundle 的语义相同。不同之处在于数据存储的位置。PropertyResourceBundle 由符合 Properties 约定的 .properties 文件支持。下面是创建文件所要知道的内容:
文件被格式化为 ISO 8859-1 编码的基本文本,因此您可以使用任何编辑器来创建和编辑文件。
以 # 开头的行是注释。
每个资源以 key=value 的形式被设置成键/值对。
文件扩展名必须是 .properties。名称必须遵守下列格式,其中 language 由 ISO-639 定义,country 由 ISO-3166 定义(参阅创建语言环境):
baseName.properties
baseName_language.properties
baseName_language_country.properties
baseName_language_country_variant.properties
下面是 ByTheNumbersrb_en.properties 的一个示例项:
1=One:
下面是 ByTheNumbersrb_ru.properties 的一个示例项:
1=\u041E\u0434\u0438\u043D:
上面两个示例中的冒号实际上是值的一部分而不是所需的项。注:一旦我们超越了 ISO 8859-1 而进入其它 Unicode 范围,我们必须使用 Java Unicode 转义。您可以使用 JDK native2ascii 工具来从不同编码进行转换。

5.4 PropertyResourceBundle 代码示例
显示在右侧的 ByTheNumbers 示例使用俄语语言环境 ? ru_RU。
ByTheNumbers.java(参阅 ByTheNumbers.java:PropertyResourceBundle 示例)以几种不同的语言显示数字 0 到 10 的名称。进入时,将缺省的语言环境同支持的语言环境(英语、法语、德语和俄语)相比较。如果缺省语言环境与其中的某一种不匹配,那么就将英语选做缺省,将基础 ResourceBundle 用于资源;否则,使用缺省语言环境 ResourceBundle。通过使用缺省语言环境获得支持的语言的语言环境显示名称(Locale Display Name),并将其装入 JComboBox。用户可以键入适当名称的号码然后按 OK。程序验证这些输入项并显示祝贺消息或重试消息。我们提供一个按钮以随机顺序显示号码名称。用户可以从 JComboBox 选择任何语言,并且字段初始将以数值顺序显示选中的语言。程序使用 Lucida Sans 字体,因此可以正确地显示所有受支持的语言。遗憾的是,我们的翻译还没有返回我们对标题翻译的请求,因此 “title=Key in numbers to match the words:”键/值对仅仅出现在基础名称文件中,它给了我们一个机会,让我们明白不位于层次结构较低位置的键可以在祖先文件中找到。
要运行该程序,使用下列任意一条命令:
java ByTheNumbers // 如果支持缺省语言环境,就使用它,否则,就使用英语。
java -Duser.language=de -Duser.region=DE ByTheNumbers // 德语
java -Duser.language=en -Duser.region=US ByTheNumbers // 英语
java -Duser.language=fr -Duser.region=FR ByTheNumbers // 法语
java -Duser.language=ru -Duser.region=RU ByTheNumbers // 俄语
下面显示了五个 .properties 文件中的两个:

ByTheNumbersrb.properties (与 ByTheNumbersrb_en.properties 相同)

# Default properties in English
0=Zero:
1=One:
2=Two:
3=Three:
4=Four:
5=Five:
6=Six:
7=Seven:
8=Eight:
9=Nine:
10=Ten:
random=Random
title=Key in numbers to match the words:

ByTheNumbersrb_ru.properties

# Default properties in Russian
0=\u041D\u0443\u043B\u044C:
1=\u041E\u0434\u0438\u043D:
2=\u0414\u0432\u0430:
3=\u0422\u0440\u0438:
4=\u0427\u0435\u0442\u044B\u0440\u0435:
5=\u041F\u044F\u0442\u044C:
6=\u0428\u0435\u0441\u0442\u044C:
7=\u0441\u0435\u043C\u044C:
8=\u0412\u043E\u0441\u0435\u043C\u044C:
9=\u0414\u0435\u0432\u044F\u0442\u044C:
10=\u0414\u0435\u0441\u044F\u0442\u044C:
random=\u041D\u0430\u0443\u0433\u0430\u0434



5.5 PropertyResourceBundle 代码示例:I18N 详细信息
让我们看一看同 I18N 有关的代码部分。首先,建立支持的语言环境和 ResourceBundle 基础名称。

Locale[] alSupported = {
Locale.US,
Locale.FRANCE,
Locale.GERMANY,
new Locale( "ru", "RU" )
};
...

String sRBName = getClass().getName() + "rb";

接下来,使用与 OK 按钮的字体相同的样式和大小创建 Lucida Sans 字体,然后获得缺省语言环境语言所支持的语言的显示名称(Display Names)。此外,对缺省语言环境进行比较以确定是否支持它。如果不支持,英语数字将是首先被显示的集合。
Font fJB = jbOK.getFont();
fLucida = new Font("Lucida Sans",
fJB.getStyle(),
fJB.getSize() );

...

asDNames = new String[ alSupported.length ];
Locale lDefault = Locale.getDefault();
for( i = 0; i < alSupported.length; i++ )
{
asDNames[i] =
alSupported[i].getDisplayName();

if( iSelIndex == 0 &&
lDefault.equals( alSupported[i] ) )
{ iSelIndex = i; }
} // end for

接下来,在一个循环中创建 JLabel 和 JTextField 并将它们装入数组。设置每个 JLabel 的 Font 和 Name。一旦构建了数组,就调用 loadFromResourceBundle() 来设置每个 JLabel 的文本值。接下来设置本地化 jbRandom 按钮和标题文本。请注意,这两个组件的属性只设置了一次,这是典型程序中所有组件的正常情况,在那里,语言环境在给定的运行期间不会改变。
jlTemp.setFont( fLucida );
jlTemp.setName( i + "" ); // set Name
...
loadFromResourceBundle(); // get localized labels
...
jbRandom.setFont( fLucida );
jbRandom.setText( rb.getString( "random" ) );
...
jlTemp = new JLabel( rb.getString( "title" ) );
jlTemp.setFont( fLucida );


下面是 loadFromResourceBundle() 方法,它使用选中的语言环境访问适当的 ResourceBundle。使用 JLabel.Name 属性作为 getString(String key) 的键来设置 JLabel 的文本。如果没有找到特别的资源,就显示一个错误对话框。从 JComboBox 选择语言时,也会调用这一方法。


public void loadFromResourceBundle()
{
try
{ // get the PropertyResourceBundle
rb = ResourceBundle.getBundle(
sRBName,
alSupported[iSelIndex] );
// get data associated with keys
for( int i = 0; i < sfiSIZE; i++ )
{
aiOrder[i] = i;
ajl[i].setText( rb.getString( ajl[i].getName() ) );
}
bRandomize = false;
} // end try
catch( MissingResourceException mre )
{
JOptionPane.showMessageDialog( this,
"ResourceBundle problem;\n" +
"Specific error: " + mre.getMessage(),
"", JOptionPane.ERROR_MESSAGE);
}
} // end loadFromResourceBundle

同样,有关完整的程序清单及所有 .properties 文件的内容,请参阅 ByTheNumbers.java:PropertyResourceBundle 示例。

6.1 日期、数字和货币
对于任何那些从未出过国,或从未接触过日期、数字和货币的“外国”用法的人来说,格式化和解析日期、数字和货币显得很简单。毕竟,所有人都能理解 lundi 1 avril 2002 或至少 4.1.02 的月和日部分,对吗?虽然我们极少有人能实际以 150,75 购买 32 1500,7 项东西,但我们能够很容易地理解以欧元表示的价格有多少项。或者可能不是这样。这些示例看起来可能不典型,但他们确实发生了,并且表示了为什么非本国人在理解本国的日期、数字和货币格式方面经常会有问题。

结果是,全世界使用的日期有各种各样的顺序和符号。数字和货币也是如此。此外,货币符号可能不止一个字符,它可能出现在值的前面或后面,和值之间有或没有空格。在大多数编程语言中,您几乎总是靠自己来处理这些情形。但 Java API 却能够处理每个受支持语言环境的所有的不同格式。而且,通过使用 DateFormatSymbols 和 DecimalFormatSymbols 类,您可以获得诸如这样的信息:本地化的长短月日名称、十进制与货币分隔符以及货币与百分比符号。

API 文档鼓励您为 I18N 应用程序使用抽象父类 DateFormat 和 NumberFormat 的 getInstance() 和 getXXXInstance() 方法。从 1.3(和 1.4)参考实现起,分别返回 SimpleDateFormat 和 DecimalFormat 的实例。两个类都有缺省的模式与符号用于格式化和解析,并且还允许定制。

下面几页中的示例程序都使用缺省模式来帮助您理解它们是怎样工作的。您将看到:由于 API 设计的缘故,三个示例中的代码都非常相似。从最终用户的观点,它们也非常相似:以本机语言环境提供一个输入域。当用户按下 OK 按钮时,就在表示用户选择的语言环境和标准解析的“原始”值的单独域中显示值。这三个示例都将处理由 JDK API 支持的所有语言环境。Lucida Sans 字体用于所有显示。“Toggle Display Names”按钮将语言环境名称的显示从用户的本机语言切换为特定语言环境的本机语言。当字体中没有用于本地化显示名称的第一个字符的字形时,“ - font can't display.”就被附加到下拉框中的语言环境名称上。程序仍然会工作,但在那种情况下,您可能会看到输出的某些部分是您熟悉的方框或问号。

使用下面的命令调用程序:


java AppName


由于支持所有 API 语言环境,您也可以使用下列命令调用它们


java -Duser.language=lc -Duser.region=cc AppName


其 lc 是 API 支持的语言环境的 ISO-639 语言代码,cc 是 API 支持的语言环境的 ISO-3166 国家或地区代码,以使输入格式化成该语言环境的样式。

注:由于要访问整个语言环境显示名称集,这些应用程序将比正常情况要花费更长的时间来启动。

6.2 日期格式化示例
这个 JIBDateGUI 示例使用德语作为缺省语言环境 ? de_DE。

JIBDateGUI(参阅 JIBDateGUI.java: DateFormat 示例)允许用户以其本地的格式输入日期。输入时确定本地语言环境,并且该语言环境显示在 OK 按钮的旁边。当用户按下 OK 时,则解析输入数字并以选中的语言环境显示该数字。还以 ISO 格式对该值进行单独解析和显示。可能使用参数 “full”、“long”、“medium”或“short”调用程序。如果没有发送参数或者发送了这四个参数之外的其它参数,那么就使用 “short”。这些值对应于 DateFormat.FULL、DateFormat.LONG、 DateFormat.MEDIUM 和 DateFormat.SHORT,并且被用来以选中的样式创建 DateFormats。

程序通过定义缺省和选中的 DateFormats 和 locales 开始。java.sql.Date 被初始化成显示标准 ISO 日期值的当前日期(注:没有为该示例对日期进行标准化),然后定义 Lucida 字体、缺省语言环境、支持的语言环境的数组以及本机和本地化的语言环境显示名称。


DateFormat dfLocal,
dfSelected;

java.sql.Date jsqlDate = new java.sql.Date(
System.currentTimeMillis() );

Font fLucida;
...
Locale lDefault = Locale.getDefault();
Locale[] alSupported;

String[] asDNames,
asLDNames;


在构造器中,创建了 Lucida Sans 字体并将其赋给显示域。捕获请求的样式并创建缺省 DateFormat。接下来,同时以缺省和本地化格式收集所有可用的显示名称。由 Font.canDisplay() 检查每个本地化显示名称的第一个字符;如果返回 false,那么 “ - font can't display.”就被附加到该名称后面。如果 Java API 支持缺省语言环境,那么相应的显示名称就会被选中;否则就选中第零行。此外,使用 java.sql.Date 的值设置和格式化输入域。DateFormat.setLenient(false) 被应用到缺省 DateFormat 并且获取缺省显示名称以供显示。


Font fJCB = jbToggle.getFont();
fLucida = new Font("Lucida Sans",
fJCB.getStyle(),
fJCB.getSize() );

iFormat = argiFormat;
dfLocal = DateFormat.getDateInstance(
iFormat );

alSupported = Locale.getAvailableLocales();
asDNames = new String[ alSupported.length ];
asLDNames = new String[ alSupported.length ];
for( int i = 0; i < alSupported.length; i++ )
{
asDNames[i] =
alSupported[i].getDisplayName();

s1 =
alSupported[i].getDisplayName( alSupported[i] );
if( fLucida.canDisplay( s1.charAt( 0 ) ) )
{ asLDNames[i] = s1; }
else
{ asLDNames[i] = s1 + " - font can't display."; }

if( iSelIndex == 0 &&
lDefault.equals( alSupported[i] ) )
{ iSelIndex = i; }
} // end for
...
jtI.setText( dfLocal.format( jsqlDate ) );
...
dfLocal.setLenient( false );
...
JLabel jlTemp = new JLabel("Default = " +
lDefault.getDisplayName() );
jlTemp.setFont( fLucida );


在 ActionListener(actionPerformed() 方法)中为显示名称 JComboBox(jcb)处理所有其它 I18N 功能:根据选择项,创建新的 DateFormat 并清空显示域。如果在下一节中出现了任何错误,对话框将显示 ParseException 消息。代码试图从输入解析 java.util.Date 并使用缺省 DateFormat 对其重新进行格式化以供输出。接下来,格式化选中的 DateFormat 的显示。最后,解析该值并用它来创建 java.sql.Date,java.sql.Date 被用来显示 ISO 值。


if( oSource == jcb )
{
dfSelected = DateFormat.getDateInstance(
iFormat,
alSupported[ jcb.getSelectedIndex() ] );
} // end if jcb, continue on

jtD.setText( "" );
jtP.setText( "" );

try
{
java.util.Date d = dfLocal.parse(
jtI.getText() );
jtI.setText( dfLocal.format( d ) );
jtI.setCaretPosition(0);
jtD.setText( dfSelected.format( d ) );
jtD.setCaretPosition(0);
d = dfSelected.parse( jtD.getText() );
// get new java.sql.Date
jsqlDate = new java.sql.Date( d.getTime() );

jtP.setText( jsqlDate.toString() );
}
catch( ParseException pe )
{
JOptionPane.showMessageDialog( this,
pe.getMessage(), "", JOptionPane.ERROR_MESSAGE);
}


同样,JIBDateGUI.java: DateFormat 示例上列出了完整的程序。

6.3 数字格式化示例
这个 JIBNumberGUI 示例使用法语作为缺省语言环境 ? fr_FR。

JIBNumberGUI(参阅 JIBNumberGUI.java: NumberFormat 示例)有意地以非常类似于日期格式化示例的方式运行。该应用程序允许用户以本地格式输入数字或百分数(如果选择的话)。输入时确定本地语言环境,并且该语言环境被显示在 OK 按钮的旁边。当用户按下 OK 时,则解析输入数字并以选中的语言环境显示该日期。该值也被单独作为标准数值型值进行解析和显示。

代码也非常相似。程序以定义 Lucida 字体、缺省语言环境以及缺省的和选中的 NumberFormat 开始。定义了支持的语言环境的数组以及本机的和本地化的语言环境显示名称。还定义了一个数组以显示数字或百分比下拉框。


Font fLucida;
...
Locale lDefault = Locale.getDefault();
Locale[] alSupported;

NumberFormat nfLocal = NumberFormat.getNumberInstance(),
nfSelected;

String[] asDNames,
asLDNames,
asDP = { "Number", "Percent"};


在构造器中,除了将输入域初始化成数字并为 Number/Percent 输入添加了一个额外的 JComboBox(jcbDP)之外,代码几乎同 JIBDateGUI.java 中构造器的代码相同。


jtI.setText( nfLocal.format( 123456.7 ) );
...
jcbDP = new JComboBox( asDP );


同样,在 ActionListener 中处理了另一个 I18N 功能。如果 Number 输入和 Percent 输入之间有变化,那么就设置跟踪输入类型的标记,并使用现有的本地 NumberFormat 来解析当前的输入值。然后通过使用 NumberFormat.getNumberInstance() 或 NumberFormat.getPercentInstance() 适当地创建一个新的 NumberFormat。通过使用新的本地 NumberFormat 重新格式化输入值,并且代码继续为选中的 NumberFormat 做相同的工作。


if( oSource == jcbDP )
{
if( jcbDP.getSelectedIndex() == 0 )
{
bNumberFormat = true;
try { n = nfLocal.parse( jtI.getText() ); }
catch( ParseException pe ) {}
nfLocal = NumberFormat.getNumberInstance();
}
else
{
bNumberFormat = false;
try { n = nfLocal.parse( jtI.getText() ); }
catch( ParseException pe ) {}
nfLocal = NumberFormat.getPercentInstance();
}
jtI.setText( nfLocal.format( n ) );
// set to perform jcb operation
oSource = jcb;
}


如果 Display Names 下拉框更改了,那么就为选中的语言环境创建一个适当的新 NumberFormat。代码然后继续将新 NumberFormat 应用于输入和显示值。


if( oSource == jcb )
{
if( bNumberFormat )
{
nfSelected = NumberFormat.getNumberInstance(
alSupported[ jcb.getSelectedIndex() ] );
}
else
{
nfSelected = NumberFormat.getPercentInstance(
alSupported[ jcb.getSelectedIndex() ] );
}
} // end if jcb, continue on


无论是否从由组合框的更改或直接由 OK 按钮引起操作继续,显示域都将被清空。代码试图从输入解析 Number 并使用缺省的本地 NumberFormat 对其进行格式化以供输出。接下来,格式化选中的 NumberFormat 的显示。最后,解析该值并用它来创建 Number,Number 接下来被用来显示原始值。


jtD.setText( "" );
jtP.setText( "" );

try
{
n = nfLocal.parse( jtI.getText() );
jtI.setText( nfLocal.format( n ) );
jtD.setText( nfSelected.format( n ) );
n = nfSelected.parse( jtD.getText() );
jtP.setText( n.toString() );
}
catch( ParseException pe )
{
JOptionPane.showMessageDialog( this,
pe.getMessage(), "", JOptionPane.ERROR_MESSAGE);
}


同样,JIBNumberGUI.java: NumberFormat 示例中列出了完整的程序。

6.4 货币格式化示例
这个 JIBCurrencyGUI 示例使用俄语作为缺省语言环境 ? ru_RU。

同样,JIBCurrencyGUI(参阅 JIBCurrencyGUI.java: CurrencyFormat 示例)非常象以前的示例那样运行。该应用程序允许用户以本地格式输入货币值。输入时确定本地语言环境,并且该语言环境显示在“Require Symbol”复选框的旁边。当用户按下 OK 时,解析输入值并使用选中的语言环境的货币符号显示输入值。这是纯粹机械的格式化;在两种货币之间没有自动值转换。在现实生活中,您将不得不自己处理汇率。还对选中的格式文本进行了解析并将其作为标准数值型值单独显示。

至此,这些示例已据实地使用了标准格式化和解析。如果您使用这些程序中的值,您会发现这些值偶尔很不灵活或让人恼火。这个示例处理了一个主要的不便之处:虽然我们期望有一个 NumberFormat CurrencyInstance 来显示货币符号,但我们通常不想强迫最终用户在键入时包括它。如果选中 “Require Symbol”复选框,那么用户就必须在输入时包含货币符号来避免 ParseException。这是标准行为。如果没有选中该复选框,就只能输入数值型值。在提供这一行为期间,我们将简单地研究一下 DecimalFormatSymbols。

首先,定义到目前为止您已很熟悉的同 I18N 有关的数据类型。主要变化是缺省的和选中的 NumberFormat 现在含有 CurrencyInstance。我们还定义了一个标准 NumberFormat 来处理不含货币符号的项,还定义了 String 来包含当前的货币符号。这是一个 String 而不是 char,因为符号中可能不止一个字符,例如“DM”? 德国马克。


NumberFormat cfLocal = NumberFormat.getCurrencyInstance(),
cfSelected,
nfLocal = NumberFormat.getInstance();
...
String sCurSymbol = "";


在构造器中,除了添加了新复选框之外,最后一个代码块之前的所有代码都与 JIBNumberGUI 中相同。首先,代码确保 NumberFormat.getCurrencyInstance() 请求返回一个 DecimalFormat。如果没有返回,我们就不能获取所需的信息,复选框也被禁用,意味着程序运行期间“Symbol Required”将为真。否则,我们就获得 DecimalFormat 的相关的 DecimalFormatSymbols 并获得缺省货币符号。代码还检查 MonetaryDecimalSeparator 和 DecimalSeparator 是否相同。如果不同,就将 DecimalSeparator 设置成 MonetaryDecimalSeparator。然后将用于备用的 NumberFormat 的 DecimalFormatSymbols 设置成 CurrencyInstance。


if( cfLocal instanceof DecimalFormat )
{
DecimalFormatSymbols dfs =
((DecimalFormat)cfLocal).getDecimalFormatSymbols();
sCurSymbol = dfs.getCurrencySymbol();

char chMDS = dfs.getMonetaryDecimalSeparator();
if( chMDS != dfs.getDecimalSeparator() )
{
dfs.setDecimalSeparator( chMDS );
}

if( nfLocal instanceof DecimalFormat )
{
((DecimalFormat)nfLocal).setDecimalFormatSymbols(
dfs );
}
else
{ jchkb.setEnabled( false ); }
} // end if cfLocal instanceof DecimalFormat
else
{ jchkb.setEnabled( false ); }


在 actionPerformed() 中,如果选中的语言环境改变了,那么就获得一个适当的新的 CurrencyInstance,然后就应用用于 OK 按钮的代码。如果用户按下 OK,就清空显示域。


if( oSource == jcb )
{
cfSelected = NumberFormat.getCurrencyInstance(
alSupported[ jcb.getSelectedIndex() ] );
} // end if jcb, continue on


接下来是使接受不键入货币符号的输入成为可能的代码:代码检查是否需要货币符号。如果需要,则使用本地 CurrencyInstance 来解析输入;否则,代码确定输入中是否包含货币符号。如果包含,就使用 CurrencyInstance;否则,本地 DecimalFormat 被用于解析。除了 CurrencyInstance 被用于格式化之外,代码的余下部分类似于前面的示例。


jtD.setText( "" );
jtP.setText( "" );

try
{
if( bRequireSymbol )
{
n = cfLocal.parse( sText );
}
else
{ // currency symbol may still be present, check
if( sText.indexOf( sCurSymbol ) == -1 )
{
n = nfLocal.parse( sText );
}
else
{
n = cfLocal.parse( sText );
}
}

jtI.setText( cfLocal.format( n ) );
jtD.setText( cfSelected.format( n ) );
n = cfSelected.parse( jtD.getText() );
jtP.setText( n.toString() );
}
catch( ParseException pe )
{
JOptionPane.showMessageDialog( this,
pe.getMessage(), "", JOptionPane.ERROR_MESSAGE);
}

参阅在 JIBCurrencyGUI.java: CurrencyFormat 示例中的完整清单。

7.1 清洁工程师维护(Sanitation Engineer Maintenance)概述
这个 JIBSEM 示例使用德语作为缺省语言环境 ? de_DE。

最后这个示例将所有的部分放在一起。

在我们的人造方案中,我们涉及一种职业其从业者实际上是在做清理工作 ? 清洁工程(Sanitation Engineering)。对于我们来说幸运的是,在许多国家或地区老板希望工程师们能够用任何语言谈天论地。实际需要两个最终程序:1) 一个被限制成从用户的特定国家或地区选择工程师的查询,供低层经理使用;以及 2) 一个显示所有工程师并允许使用特定于选中的工程师的语言环境格式编辑薪水、雇佣日期和负责的吨位的程序,只有高层管理人员有权访问该程序。

JIBSEM.java 是一个原型,设计成用来处理这两方面以确保我们能够提供希望的功能。开始时,应用程序将特别处理美国英语、法语、德语、和俄语语言环境,缺省是美国英语。进入时,程序中的描述性的标签将用本地语言环境(如果受支持的话)显示。应该用特定于工程师的语言环境显示数据,解析数据并格式化数据。由于数据正常情况下是在特定地点以当地格式输入,并且在数据库中也是那样保存的,所以这是必需的。数据库包括每位工程师的语言环境信息以支持这种功能。为简单起见,我们的原型仅仅将四行匹配到四个受支持的语言环境。当然我们需要修改并优化该程序以供生产,但现在它已经可以满足我们的用途了。

JIBSEM 为每种受支持的语言环境显示具有模拟数据的 JTable。选中一行时,屏幕底部两次显示适当的数据:一次在一个可编辑域,另一次在一个非编辑域中,这样用户就可以跟踪其当前值了。用户可以更改这些值并按 OK 来应用它们。如果值通过编辑,那么新值显示在两个域中,并且支持的存储也被更新;否则,会显示一个错误对话框。

您会注意到这一示例中有好些 Swing 代码,但与 I18N 相关的部分应该已经熟悉了。提供了支持类 JIBSEMATM.java(AbstractTableModel 实现)和 JIBSEMRow.java(包含行数据),分别是下列 properties 文件:

JIBSEMrb.properties(缺省用美国值)


JIBSEMrb_de_DE.properties(德语)


JIBSEMrb_fr_FR.properties(法语)


JIBSEMrb_ru_RU.properties(俄语)
请注意,在这种情形下无需重复的缺省文件,虽然这种重复不会有什么坏处;在启动时我们只查询 ResourceBundle,并且对于 ResourceBundle 访问,语言环境从不改变。

7.2 清洁工程师维护:I18N 详细信息
定义的 I18N 有关的数据类型是:


DateFormat[] aDF;
DateFormat dfSelected;

Font fLucida,
fLucidaNormal,
fLucidaTitle;
...
Locale lDefault = Locale.getDefault();
Locale[] alSupported = {
Locale.US,
Locale.FRANCE,
Locale.GERMANY,
new Locale( "ru", "RU" )
};
NumberFormat[] aCF,
aNF;
NumberFormat cfSelected,
nfSelected;
...
ResourceBundle rb;

String[] asHeaders = new String[2];
String sRBName = getClass().getName() + "rb";


在构造器中,为支持的语言环境装入含有货币、日期及数字的格式化器的数组。代码还确定是否支持缺省语言环境;如果不支持,则美国语言环境用于缺省语言环境。接下来,通过对 loadFromResourceBundle() 的调用将标签从适当的 ResourceBundle 装入。注:我们还为表显示设置了 Lucida Sans 字体。


aCF = new NumberFormat[ alSupported.length ];
aDF = new DateFormat[ alSupported.length ];
aNF = new NumberFormat[ alSupported.length ];

boolean bLocaleMatched = false;
Locale lTemp;
for( i = 0; i < alSupported.length; i++ )
{
lTemp = alSupported[i];
aCF[i] = NumberFormat.getCurrencyInstance(
lTemp );

aDF[i] = DateFormat.getDateInstance(
DateFormat.SHORT,
lTemp );
aDF[i].setLenient( false );

aNF[i] = NumberFormat.getNumberInstance(
lTemp );
if( lDefault.equals( lTemp ) )
{
bLocaleMatched = true;
}
} // end for
if( !bLocaleMatched ) { lDefault = Locale.US; }
...
loadFromResourceBundle(); // get localized labels
...
jtbl.setFont( fLucidaNormal );
jtbl.getTableHeader().setFont( fLucidaNormal );


定义了三种格式化方法来处理三种值:formatCurrency(double dSalary)、formatDate(java.util.Date d) 和 formatNumber(double dTonnage)。请注意代码的相似处。


public String formatCurrency( double dSalary )
{
cfSelected = aCF[iRowIndex];
return cfSelected.format( dSalary );
} // end formatCurrency


public String formatDate( java.util.Date d )
{
dfSelected = aDF[iRowIndex];
return dfSelected.format( d );
} // end formatDate


public String formatNumber( double dTonnage )
{
nfSelected = aNF[iRowIndex];
return nfSelected.format( dTonnage );
} // end formatDate


下面是 loadFromResourceBundle() 方法。就象大多数 I18N 程序一样,该方法(和资源装入)只在启动时调用一次:


public void loadFromResourceBundle()
{
try
{ // get the PropertyResourceBundle
rb = ResourceBundle.getBundle( sRBName,
getLocale() );
// get data associated with keys
jlTitle.setText( rb.getString( "title" ));
asHeaders[0] = rb.getString( "Engineer" );
asHeaders[1] = rb.getString( "Name" );

jlE.setText( asHeaders[0] + ":" );
jlEdit.setText( rb.getString( "Edit" ));
jlCurrent.setText( rb.getString( "Current" ));
jlCI.setText( rb.getString( "Salary" ));
jlDI.setText( rb.getString( "Date" ));
jlNI.setText( rb.getString( "Tons" ));
} // end try
catch( MissingResourceException mre )
{
JOptionPane.showMessageDialog( this,
"ResourceBundle problem;\n" +
"Specific error: " + mre.getMessage(),
"", JOptionPane.ERROR_MESSAGE);
}
} // end loadFromResourceBundle


和通常一样,大多数操作在 actionPerformed() 中。当用户按下 OK 时,代码试图使用选中的格式化器解析货币、日期和数字值。如果抛出一个异常,那么会显示一个错误对话框并且在方法返回之前不再做任何工作。否则,数据被更新,两组域都显示新值,并且我们也准备处理新行。当在 valueChanged() 中选中了特定行时,用于建立索引的 iRowIndex 字段被捕获。同前面提到的一样,此时原型的行和数组元素匹配。


public void actionPerformed(ActionEvent ae)
{
Object oSource = ae.getSource();

boolean bError = false;
java.util.Date d = null;
Number n = null,
nCur = null;

try
{
nCur = cfSelected.parse( jtCI.getText() );
d = dfSelected.parse( jtDI.getText() );
n = nfSelected.parse( jtNI.getText() );
}
catch( ParseException pe )
{
JOptionPane.showMessageDialog( this,
pe.getMessage(), "", JOptionPane.ERROR_MESSAGE);
bError = true;
}

if( bError == false )
{
aRows[iRowIndex].setSalary( nCur.floatValue() );
jtCD.setText( jtCI.getText() );
aRows[iRowIndex].setHireDate( d );
jtDD.setText( jtDI.getText() );
aRows[iRowIndex].setTonnage( n.doubleValue() );
jtND.setText( jtDI.getText() );
jtbl.requestFocus();
}
} // End actionPerformed


请参阅 JIBSEM.java: Sanitation Engineer Maintenance 示例中完整应用程序的清单。

8 结束语
虽然本教程仅仅只在编程级别触及了处理 I18N 这一问题的冰山一角,但是至此您应该有了足够的信息和材料来处理 I18N 程序员通常要面对的大多数问题:
Java 字符与 char 数据类型
字体、字体特性及 Lucida 字体
创建语言环境
使用资源束
日期、数字和货币
我们也在国际化中大体上简单地讨论了 I18N 并在国际化和 Java 编程语言中描述了 Java API 支持。我们提供了参考资料以供您进一步研究。最后,我们完成了几个示例,包括清洁工程师维护(Sanitation Engineer Maintenance)概述,它将所有特定元素集中到一个应用程序中。

原文http://www.javaresearch.org/forum/thread.jsp?column=16&thread=42239

 


http://www.niftyadmin.cn/n/2970602.html

相关文章

JVMTM Tool Interface

http://java.sun.com/j2se/1.5.0/docs/guide/jvmti/jvmti.html

matlab求两向量夹角_10373 高中立体几何向量法全攻略

向量法是解高中立体几何题的神器。只要能建立空间直角坐标系的题&#xff0c;都可以用向量法来解&#xff0c;而这样的题目可以占到所有立体几何题的 95% 以上。与传统方法相比&#xff0c;向量法的计算量稍微大一些&#xff0c;但它的优点是不需要费脑筋做辅助线&#xff0c;而…

c语言算法 - 分而治之算法 - 残缺棋盘

残缺棋盘&#xff08;defective chessboard&#xff09;是一个有2k2k 个方格的棋盘&#xff0c;其中恰有一个方格残缺。图2 - 3给出k≤2时各种可能的残缺棋盘&#xff0c;其中残缺的方格用阴影表示。注意当k 0时&#xff0c;仅存在一种可能的残缺棋盘&#xff08;如图1 4 - 3 a…

转储控制文件信息

当前控制文件的内容可以通过CONTROLF dump以文本形式转储到实例参数指定的目录下。 各级别CONTROLF转储的区别: 转储级别转储内容1仅包含文件头信息2包括文件头&#xff0c;数据库信息记录&#xff0c;检查点进程记录3所有记录类型&#xff0c;针对循环重用的记录类型仅保留最早…

Visual Studio 2008 远程调试工具详解

远程调试工具用于在开发平台与实际平台间执行一些调试工作。利用远程调试工具可以在开发平台运行&#xff0c;得到实际平台上的文件、监视实际平台上进程或线程的状况、测试实际平台上应用程序的性能等。一部分远程调试工具是我们在开发应用程序时常用的&#xff0c;还有一部分…

硬盘安装xp(初学者)

http://www.jz5u.com/十九. 想不想安装&#xff1f;想就回车。二十. 按F8。不按&#xff1f;你不是还想回到解放前吧&#xff1f;(http://www.jz5u.com/)二十一. 没的说&#xff0c;全新安装&#xff0c;谁愿意修复安装呀&#xff1f;&#xff01;二十二. 不说了&#xff0c;打…

Oracle PL\SQL操作(五)其他技术

1.在表之间传输数据 1&#xff09;利用INSERT传输数据insert into test1 (select name2,age2 from test2); 从上面的操作可以看出&#xff0c;可通过SELECT向一个表中成批地添加数据&#xff0c;但应注意&#xff1a;数据类型要一致&#xff0c;所选择的列数应一致。此语句的…

DataWindow.net应用介绍(一)

Datawindow中文译名为数据窗口&#xff0c;是开发工具PowerBuilder(PB)的一个王牌组件&#xff0c;使用PB作为开发工具的&#xff0c;基本上都是冲着Datawindow 来的&#xff0c;它不仅可以用来显示数据&#xff0c;还可以实现数据录入、报表打印等功能&#xff0c;是Sybase 的…