关于本教程

本教程是关于什么的?

本教程向您介绍Java编程语言对多语言和多国家/地区环境的支持。 本课程首先对国际化原则和概念进行一般性讨论,然后继续对Java国际化支持的特定领域进行概述。 最后几节提供了关于任何国际化Java应用程序基本领域的更多动手讨论,包括主要讨论区域的示例程序以及将它们联系在一起的最终更完整的应用程序。 语言环境和资源包; 并格式化日期,数字和货币。

完成本教程后,您将对国际化的元素以及Java平台提供支持的领域有深刻的了解。 您还应该能够编写使用资源束的应用程序,并且可以格式化和解析日期,数字和货币。

我应该学习本教程吗?

如果您是对I / O和Swing有一定了解的中级Java程序员,并且对构建国际Java应用程序感兴趣,那么Java国际化基础知识将非常适合您。 但是,从入门到高级的开发人员也将能够收集有用的信息并查看材料。 特别是,每个Java程序员都应该对Unicode支持和Java字符以及char数据类型部分中的讨论有所了解。 解释了所有示例代码,但重点是与国际化密切相关的领域,而不是通用的Java编程。 在学习本教程时,任何以前接触过国际化的内容都将有所帮助,但是不假定具有特定背景。

作者注释 :尽管我有一些德语和俄语的背景,但是示例中使用的单词和短语主要是互联网词典研究的结果。 我希望您会被任何可怜或不合适的单词选择逗乐而不是烦恼。 如有任何语言或其他方面的更正,请随时与我联系 。

请参阅相关的主题为扩大对材料提出了在这里的教程,文章和其他参考文献的列表。

代码样本和安装要求

尽管在编写本教程时可以使用JDK 1.4的候选版本,但JDK 1.3仍然适用于最广泛的读者。 具体地说,使用Windows NT 4.0 Service Pack 6a上的J2SE v1.3.1_02来测试示例。 1.4中只有几项关于国际化的新项目,这些更改或增加在相应的部分中进行了提及。 您当然应该拥有任何JDK / JRE的国际版本。

请注意,这些代码示例旨在促进对基础的理解,并且尚未针对生产用途进行优化。

在本教程中使用的示例中的类和源代码都可以作为一个可下载的JAR文件相关主题 。 附录A:完整的代码清单中也列出了各个源文件。

国际化

与计算机编程有关的国际化是设计和编写应用程序的过程,以便可以在全球或跨国环境中使用它。 国际化程序无需软件修改即可支持不同的语言以及日期,时间,货币和其他值。 这通常涉及“软编码”或将文本组件与程序代码分离,并且可能涉及可插拔的代码模块。

从业人员通常将国际化缩短为I18N。 理由是在国际化中 ,开头I和结尾N之间有18个字母。 尝试多次说出并写出“国际化”字样,您将体会到较短版本的价值。 此外,您可能会看到“国际化”的缩写形式是“国际化”。 尽管语法上不精确且技术上不正确,但“ I18N'ed”很有用,您会在包括本教程在内的文献中经常看到它。

关系数据库管理系统和操作系统通常还可以使用术语国家语言支持或NLS为国际化的某些方面提供基础支持。

本土化

本地化是设计和编写能够处理特定区域,国家,语言,文化,商业或政治背景的应用程序的过程。 从某种意义上说,为特定区域编写的每个应用程序都已本地化,尽管其中大多数实际上仅支持一个语言环境。 不过,通常情况下,真正的本地化是通过访问区域设置,位置,政治或其他特定组件和模块的核心代码,以及翻译适合受众的文本来实现的。 适当的国际化计划将为本地化提供便利和基础。

出于将“国际化”转换为“ I18N”所用的相同理由和逻辑,本地化通常缩短为L10N。

可以对涉及美国,加拿大,墨西哥和巴西的税收或会计程序包进行I18N编辑,这样就不会因每个国家/地区的自定义项而重复显示,报告和其他程序。 然后,将对L10N进行打包,以处理适用于该国家,甚至可能适用于州或省的不同会计和报告程序。

I18N存在理由

本部分的标题本身提供了进行国际化的原因之一:从未接触过“ I18N”或法语(“ raison d'etre”的意思是“ 存在的理由 ”)的人不会知道本部分的含义。关于。 有时,缺乏知识是一种优势, 人造钻石的成功证明了这一点。 但是,如果无法理解软件,那么无论从开发人员的角度上如何取悦软件,它都是没有用的。 不便或烦人的软件也不太有用,也不太有市场。

最初是ASCII。 即使在今天,大多数编译器仍希望使用ASCII输入。 随着计算机的发展,人们认识到需要额外的语言支持,并且操作系统随附了通常包含ASCII和本地语言的特定于国家/地区的字符集。 即使这样,通常也仅支持一种“其他”语言,并且大多数开发人员根据自己的国家或地方文化来设计程序。 由于历史和实践原因,Internet和Web上的应用程序通常遵循相同的模式。 尽管通常重点放在英语上,但是很容易找到各种语言的单语言应用程序和网站。

另一个考虑因素是经济。 在您国家的边界​​之外,还有一个巨大的大市场。 随着原先贫穷国家的国民生产总值上升,计算机的广泛接受以及网络人口的增加,市场正在发生变化。 根据“全球覆盖”的“ 全球互联网统计”页面,到2001年12月,互联网人口可能细分为讲英语的人口约占45%。 其次是日语,大约占9%,其次是中文,德文,西班牙文,韩文,意大利文,法文等等。 合理地假设计算机访问跟踪相似的比例。 从2001年到2005年,在线人口预计将翻一番(在线商务将从大约1万亿美元增长到6万亿美元以上),而英语占总人口的百分比预计将持续下降到39%。

关注国际化问题的其他原因可能离家很近:您的公司可能在其他国家/地区开设办事处或收到来自另一个国家/地区的潜在客户的投标申请书(RFP)。

Java平台对I18N的支持概述

国际化和Java编程语言

与大多数其他语言的程序员不同,Java程序员是JDK中为I18N支持而内置的大量标准代码的受益者。 大部分代码最初来自IBM的Taligent子公司(自合并为IBM后),并且代表了许多人年的工作量,远远超出了大多数公司独立提供产品的可行性。

代码和愿景并不总是完美的。 例如,看看java.util.Date类中许多不赞成使用的方法。 而且,我们许多人都记得太平洋标准时间显然也是Java世界时间。 但是,即使在“糟糕的过去”中,很少有其他语言(如果有的话)具有(或具有)与该内置功能相比的任何东西。 本节简要讨论Java平台支持的常规I18N区域。

Unicode支持

Java语言字符集是Unicode,因此,原始char数据类型的长度为两个字节(16位),以容纳Unicode值。 因为熟悉的Stringchars组成,所以String也是基于Unicode的。 定义Unicode本身是为了使0到127的值与标准ASCII匹配,0到255的值与ISO 8859-1(Latin-1)标准匹配。 由于起始值具有这种一致性,因此,不使用I18N工具或遇到I18N问题的程序员可以在不了解或不了解Unicode的情况下编写Java程序。 但是,考虑到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协会相关主题 。

字符集转换和流输入/输出

上一节提到Java字符集是Unicode,但并非所有平台都支持Unicode。 那么,这种魔术是如何实现的呢? 答案是所有支持字符的输入和输出流(即java.io.Readerjava.io.Writer层次结构)都会自动调用隐藏的代码层,该层代码从平台的本机编码转换为Unicode,然后返回。 注意,假定使用本机编码。 如果数据不是默认编码,则必须自己转换数据。 幸运的是, java.io.InputStreamReaderjava.io.OutputStreamWriterjava.lang.String类具有允许使用支持的编码进行转换说明的方法。 您可以在在JDK文档(从访问的国际化部支持的编码找到这些相关主题 )。 请注意,JDK 1.4现在提供了对泰文和印地文编码的支持。

有趣的是,对于char数据类型,Java的数字大端顺序格式保证不成立。 默认格式取决于平台。 例如,在NT 4.0上,系统属性“ sun.io.unicode.encoding”设置为“ UnicodeLittle”。 如果出于某种原因要自己指定格式,则可以选择使用UnicodeBig,UnicodeBigUnmarked,UnicodeLittle,UnicodeLittleUnmarked,UTF8或UTF-16的文档。

字符分类和字符类

除了以标准方式为多种语言定义字符外,Unicode还为每个字符定义了几个属性。 这些属性标识诸如通用类别,双向性,大写,小写,字符是数字字符还是控制字符之类的东西。 这些属性在Unicode Consortium网站上可用的UnicodeData文件中定义。

Java Character类提供了获取这些属性的方法。 虽然特定实例是不可变的,但许多方法都是静态的,从而允许即时访问角色的属性。

此类有用的一个例子来自典型的ASCII编程算法:许多程序员利用以下事实:如果字符的值在0x41到0x5A范围内,则它是一个大写字母(AZ)。 通过添加0x20,您将获得小写字母(az)。 不幸的是,当处理包含超出ASCII范围的字符的语言时,该算法将失败。 解决方案是使用在任何情况下都可以使用的Character.isUpperCase()Character.toLowerCase() 。 另一个示例是Character.isDigit() ,它也适用于表示ASCII从'0'到'9'范围之外的数字的字符。

语言环境

在Java语言中, 语言环境只是一个标识符, 而不是一组本地化的属性。 java.util.Locale类的实例代表特定的地缘政治区域,并使用语言和地区或国家/地区的参数创建。 每个对语言环境敏感的类都维护自己的一组本地化属性,并确定如何响应包含Locale参数的方法请求。

给定前面的语句,应该清楚的是,对于程序员如何响应包含Locale参数的方法请求,没有任何限制。 但是,在Sun的参考Java 2平台和其他符合标准的实现中,有一组一致的受支持的本地化版本。 请参阅JDK文档(从访问的国际化部支持的语言环境 相关信息 )了解更多信息。 您应该注意,文档将许多语言环境列为“也已提供,但未经测试”。 我亲眼看到JDK 1.3.1中芬兰(fi_FI)语言环境出现了这个“未经测试”的问题; 告诫买主 。

AWT / Swing名称和区域设置属性

java.awt.Component类包含NameLocale属性的getter和setter。 尽管文档还讨论了带有Name参数的Component及其子类的构造函数,但我显然比我想象的要多得多,因为我从未能找到它们。 对于大多数Swing类, Component都位于层次结构中,并且它们也自动支持这些属性。

Name属性是一个非本地化的String ,您可以通过编程方式分配它。 这可能有助于国际化,这听起来很奇怪,但是随着大多数数据根据区域设置而变化, Name提供了一个固定的锚来标识组件。 当然,在给定的类中,测试对象引用的对象相等性可以达到相同的目的。 尽管有两种方法都有很好的理由,但是我通常在actionPerformed()方法中使用对象相等性测试,如代码示例所示。 文档指出,如果未通过编程设置,则会分配默认Name ,但不会提供任何值或模式。 在我编写的代码中,如果在Component.setName("aName")之前调用Component.getName()返回null 。 当然,由于未记录的行为,结果可能会不一致,并且将来可能会更改。 因此,当使用Name属性时,良好的编程习惯将要求将所有组件的Name属性设置为表示“未设置”的标准值,然后适当地设置所需的组件。

即使应用程序的其余部分使用不同的语言环境,“ Locale属性也允许组件跟踪其自己的语言环境。 尽管对于具有文本值的Component ,可以在将文本发送到Component之前对其进行本地化,而无需设置特定的Component Locale ,但是该技术在某些情况下非常有用。

本地化资源

java.util.ResourceBundle是一个抽象类,提供用于存储和查找应用程序使用的资源的机制。 这些资源通常是本地化的Strings ,但是可以是任何Java对象。 ResourceBundle s的成立在某种层次的,与一般的开头ResourceBundle用的基本名称,然后通过添加语言和国家标识符(如支援的地区在JDK文档Internationalization部分,这是访问的定义变得更具体的相关主题 )到其他ResourceBundle的基本名称。 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

日历和时区支持

java.util.Date最初旨在处理日期和时间操作,但是固有的缺陷已将其简化为代表特定的时间点。 JDK 1.1中引入了抽象类java.util.Calendar及其具体子类java.util.GregorianCalendar来处理java.util.Date的缺陷。 Calendar类具有获取所有日期和时间字段以及执行​​日期和时间算术的方法。

抽象的java.util.TimeZone类及其具体的子类java.util.SimpleTimeZone保持标准时间和夏时制与世界标准时间的时差(缩写UTC,而不是您期望的UCT;由于历史原因,该缩写来自法文形式) )。 此外, TimeZone还包含用于获取本地和本地时区显示名称的方法。

格式化和解析

数字,货币,日期,时间和程序消息都受文化和地区差异的影响,并且需要大量的格式化和解析工作才能实现本地化。 创建抽象类java.text.Format及其子类来处理此I18N区域。 所有子类都具有对语言环境敏感的format()parse()方法,以对语言环境敏感的方式操作值。 parse()方法将对无效值抛出ParseException 。 具体的子类java.text.SimpleDateFormatjava.text.DecimalFormat允许使用模式并访问实例的适当符号。 通常,抽象父类具有返回适当本地化对象的静态工厂方法getInstance()getXXXInstance()

以下是java.text.Format的直接子类的列表:

  • 抽象java.text.DateFormat类及其具体子java.text.SimpleDateFormat ,由支持java.text.DateFormatSymbols类,用于处理日期和时间值。
  • 抽象java.text.NumberFormat类及其具体子类java.text.ChoiceFormatjava.text.DecimalFormat ,通过备份java.text.DecimalFormatSymbols类,用于处理数字,货币和百分比。
  • java.text.MessageFormat允许将“软编码”位置和值的格式插入到本地化消息中。

对于JDK / JRE 1.4,添加了java.util.Currency以便可以独立于语言环境使用货币。 java.text.NumberFormat具有处理货币和整数的新方法。

区域设置敏感的字符串操作

作为开发人员,我们经常需要对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搜索字符。

输入法

几乎所有前面的讨论都涉及操纵或显示数据。 但是,必须通过某种方式输入数据。 对于最终用户,这通常是键盘。 但是,如果键盘不支持语言输入所需的字符,您该怎么办?

输入法是允许输入数据的软件组件的技术术语。 Java平台允许使用主机OS输入法以及基于Java语言的输入法。 如果需要实现输入法,则可以使用输入法框架。 你可以找到规范,参考,并为输入法客户端API和JDK文档(从访问的国际部分下输入法框架的输入法引擎SPI教程相关主题 )。

Unicode和Java字符

Java字符和char数据类型

Java程序员最著名的抱怨之一是“我只在程序输出中看到问号(或块)。我的数据如何被破坏?” 通常,作为Java开发人员,您应该了解实际发生的情况以及该看似问题背后的原因,但是在处理国际化问题时,这些知识尤其重要。

Java语言规范将char定义为原始,数字,整数类型。 另外, char是唯一的无符号数字类型,它允许一些有趣的(或讨厌的,取决于您的视图)技巧。 chars也以另一种方式是特殊的,因为当将其值发送到显示器或打印机等输出设备时,它们的值会从字符映射或字体映射为字形。 但是, char是数字类型,它支持所有整数运算。 支持Unicode指出,一个char可以使用字母或Unicode转义来设置。 由于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',     // character65,     // decimal0x41,   // hex0101,   // 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 intSystem.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 charSystem.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”。这两个值将按预期方式作为问号打印在显示屏上在Windows NT上使用代码页cp1252。 根据系统的代码页,显示可能会略有不同。 要检查该值,然后将该变量打印为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显示:

字体,字体属性和Lucida字体

Java平台可以识别逻辑和物理字体。

逻辑字体是自动映射到主机系统字体的字体。 这些是熟悉的Serif,Sans-serif,Monospaced,Dialog和DialogInput字体。 还有四种逻辑字体样式:普通,粗体,斜体和粗体。 从主机到逻辑字体的映射是通过位于JRE / lib目录中的font.properties文件完成的。 尽管具体情况因系统而异,但通常会为讲英语的人设置默认的font.properties文件,尽管有JDK的日语本地化版本。 附带了其他font.properties文件; Windows的JDK 1.3.1包含阿拉伯文,希伯来文,日文,韩文,俄文,泰文的文件以及中文的多个版本。 搜索适当的font.properties的方式与命名约定类似(但不完全相同),用于ResourceBundle 。 如果特定于语言的font.properties文件与系统的语言环境匹配,并且安装了预期的字体(通常随该版本的OS一起提供),则将对该语言进行自动映射。 否则,将使用默认的文件映射(通常为英语)。

如果您安装适当的字体并在调用Java应用程序时传递相应的语言和国家/地区代码,也会发生自动映射。 如果存在所需的font.properties文件,则此行为对于开发非常有用。 您还可以通过将初始的默认font.properties文件复制到其他文件中并将特定文件重命名为“ font.properties”,来有效地使该语言/字体成为默认语言/字体。 虽然对于开发人员来说足够容易,但这显然不是最终用户要做的事情。

如果您必须自己定制或创建新的font.properties文件,则情况完全不同,而且难度更大。 JDK文档的“ 国际化”部分的“ 字体属性”中提供了有关处理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文档(从访问的国际节相关主题 )。

提供本地化资源

创建语言环境

提供任何类型的本地化资源时,您应该做的第一件事是创建适当的语言环境(请参阅Locales )。 虽然有一个包含平台/浏览器变体的构造函数,但通常您会使用

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) ,它们分别返回适合以默认或请求的语言环境显示的名称。 您还可以获取国家和语言的代码和名称。 这些方法允许程序员在不了解特定语言的情况下为最终用户提供读取,选择和返回本地化语言环境信息的能力。

使用资源包

ResourceBundle包含键/值组合。 键始终是String ,而值始终是PropertyResourceBundleString ,但可以是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搜索失败时将选择该值。 That's not exactly what was requested or expected. If only one locale is used in any given run of the program, then the specifically named duplicate is not necessary. But in any event, this approach doesn't require any new code and works in any situation.

If we required more specific locale support, say Austria, Switzerland, and Germany (_de_AT, _de_CH and _de_DE, respectively), then it would make sense to put only the country specifics in the appropriate country-named property file (myprops_de_CH.properties, for instance), and the more general elements at the _de bundle level. In that case, the _de bundle would always be found when the other elements were needed.

You should also implement some sort of naming convention for the bundles. Our examples use this general format: Object.getClass().getName() + "rb" . The main rule is this: do not use just the class name for the base name with .properties files . Ignoring that rule will work on some platforms, but on others you will get quite a surprise. The documented guideline is: if a class and a .properties file with the same name both exist, the class wins and will be loaded. 期。 One good result of this behavior is that, with properly named bundles, you can shift between ListResourceBundle s and PropertyResourceBundle s with no code changes; just move the desired type to the classpath.

You may find it more appropriate to have multiple ResourceBundles for separate types of information. These could provide resources for many different programs. Specific prefix or suffix conventions remain useful to avoid class name clashes.

Using PropertyResourceBundles

The semantics of PropertyResourceBundle s are the same as the parent, ResourceBundle . The difference is where the data is stored. PropertyResourceBundle s are backed up by .properties files that meet Properties conventions. Here's what you need to know to create the files:

  • The file is formatted as basic text with ISO 8859-1 encoding, so you can use just about any editor to create and edit the file.
  • Lines starting with # are comments.
  • Each resource is set up as a key/value pair, in the form key=value .
  • The file extension must be .properties. The name must adhere to the following format where language is defined by ISO-639, and country is defined by ISO-3166 (see Creating locales ):
    • baseName.properties
    • baseName_ language .properties
    • baseName_language_ country .properties
    • baseName_language_country_ variant .properties

Here is an example entry from ByTheNumbersrb_en.properties:

1=One:

And here is an example entry from ByTheNumbersrb_ru.properties:

1=\u041E\u0434\u0438\u043D:

The colon in these examples is actually part of the value and not a required entry. Note that once we are past ISO 8859-1 into other Unicode ranges, we must use Java Unicode escapes. You can use the JDK native2ascii tool to convert from different encodings.

PropertyResourceBundle code example

The ByTheNumbers example shown on the right uses the Russian locale -- ru_RU.

ByTheNumbers.java (see ByTheNumbers.java: PropertyResourceBundle example ) displays the names of the numbers 0 through 10 in several different languages. On entry, the default locale is compared to those supported (English, French, German and Russian). If the default locale does not match one of these, English is selected as the default and the base ResourceBundle is used for resources; otherwise, the default locale ResourceBundle is used. Locale Display Names of the supported languages are obtained using the default locale and loaded into a JComboBox . The user can key-in the numbers for the appropriate names and press OK. The program validates the entries and displays either a congratulatory or retry message. We provide a button to display the number names in random order. The user can select any of the languages from the JComboBox , and the fields will initially appear in numerical order in the selected language. The program uses the Lucida Sans font and can therefore properly display all of the supported languages. Unfortunately, our translators have not yet returned our request for title translation, so the "title=Key in numbers to match the words:" key/value pair appears only in the basename file, giving us an opportunity to see that keys not located lower in the hierarchy will be found in ancestor files.

To run the program, use any of the following:

  • java ByTheNumbers // uses the default locale if supported, otherwise English.
  • java -Duser.language=de -Duser.region=DE ByTheNumbers // German
  • java -Duser.language=en -Duser.region=US ByTheNumbers // English
  • java -Duser.language=fr -Duser.region=FR ByTheNumbers // French
  • java -Duser.language=ru -Duser.region=RU ByTheNumbers // Russian

Two of the five .properties files are shown below:

ByTheNumbersrb.properties (same as 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

PropertyResourceBundle code example: I18N details

Let's take a look at the portions of the code related to I18N. First, the supported locales and the ResourceBundle base name are established:

Locale[]   alSupported = {Locale.US,Locale.FRANCE,Locale.GERMANY,new Locale( "ru", "RU" )};
...String sRBName = getClass().getName() + "rb";

Next, a Lucida Sans font is created using the same style and size as the OK button's font, and the Display Names for the supported languages in the default locale language are obtained. In addition, the default locale is compared to determine if it is supported. If not, English numbers will be the first set displayed.

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

Next, JLabel s and JTextField s are created in a loop and loaded into arrays. Each JLabel 's font and Name are set. Once the arrays are built, loadFromResourceBundle() is invoked to set each JLabel 's text value. The localized jbRandom button and title text are set next. Notice that the attributes for these two components are set only once, which is the normal case for all components in the typical program wherein the locale doesn't change during a given run.

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 );

Following is the loadFromResourceBundle() method, which accesses the appropriate ResourceBundle , using the selected locale. The JLabel 's text is set, using the JLabel.Name attribute as a key for getString(String key) . If a particular resource is not found, an error dialog is displayed. This method is also called when a language is selected from the JComboBox .

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

Again, for the complete program listing and contents of all .properties files, see ByTheNumbers.java: PropertyResourceBundle example .

Working with dates, numbers, and currencies

Dates, numbers, and currencies

Formatting and parsing dates, numbers, and currencies appears straightforward to anyone who has never been outside his own country or otherwise been exposed to "foreign" usage of these values. After all, everyone can understand lundi 1 avril 2002 or at least the month and day portions of 4.1.02, right? And, while few of us could actually purchase 32 1500,7 items at 150,75

, we can easily understand how many items at what price in euros.

或者可能不是。 These examples may not seem typical, but they do occur and demonstrate why non-natives often have problems understanding native date, number, and currency formats.

It turns out that there are a variety of orders and symbols used in dates around the world. It's the same for numbers and currencies. In addition, currency symbols may be more than one character and can appear at the beginning or end of the value, with or without spacing. In most programming languages, you are pretty much on your own to handle these situations. The Java API, however, can deal with all of the various formats for every supported locale. And, by using the DateFormatSymbols and DecimalFormatSymbols classes, you can obtain information like localized long and short month and day names, decimal and monetary separators, and currency and percent signs.

The API documentation encourages you to use the getInstance() and getXXXInstance() methods of the parent abstract classes DateFormat and NumberFormat for I18N applications. As of the 1.3 (and 1.4) reference implementation, instances of SimpleDateFormat and DecimalFormat , respectively, are returned. Both classes have default patterns and symbols for formatting and parsing, and also allow for customization.

The example programs in the following section all use default patterns to help you to understand how these work. You will see that, because of the API design, the code is very similar in all three examples. They are also similar from an end-user perspective: an input field is provided in the native locale. When the user presses the OK button, the value is displayed in separate fields for the user-selected locale and the standard parsed "raw" value. All three examples will handle every locale supported by the JDK API. The Lucida Sans font is used for all displays. The "Toggle Display Names" button toggles the display of locale names from the user's native language to the specific locale's native language. When the font does not have a glyph for the first character of the localized display name, " - font can't display." has been appended to the locale name in the drop-down box. The program will still work, but in that case you will probably see the familiar boxes or question marks for some portion of the output.

The programs are invoked using:

java AppName

Because all API locales are supported, you may also call them using

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

where lc is the ISO-639 language code, and cc is the ISO-3166 country code for API supported locales, to have the inputs formatted in that locale's style.

Note that these applications will take longer than normal to start up due to accessing both entire locale Display Names sets.

Date formatting example

This JIBDateGUI example uses German as the default locale -- de_DE.

JIBDateGUI (see JIBDateGUI.java: DateFormat example ) allows the user to input a date in his native format. The native locale is determined on input and displayed next to the OK button. When the user presses OK, the input date is parsed and displayed in the selected locale. This value is also parsed and displayed separately in ISO format. The program may be invoked with arguments of "full", "long", "medium" or "short". If no argument or values other than these are sent, "short" is used. These values correspond to DateFormat.FULL , DateFormat.LONG , DateFormat.MEDIUM , and DateFormat.SHORT , and are used to create DateFormats in the selected style.

The program begins by defining default and selected DateFormats and locales . A java.sql.Date is initialized to the current date to show standard ISO date values (note that the date is not normalized for the example), then a Lucida font, a default locale, arrays for supported locales, and native and localized Locale Display Names are defined.

DateFormat dfLocal,dfSelected;java.sql.Date jsqlDate = new java.sql.Date( System.currentTimeMillis() );Font fLucida;...Locale     lDefault = Locale.getDefault();Locale[]   alSupported;String[]   asDNames,asLDNames;

In the constructor, the Lucida Sans font is created and assigned to display fields. The requested style is captured and the default DateFormat is created. Next, all available Display Names are gathered in both default and localized formats. The first character of each localized Display Name is checked by Font.canDisplay() ; if false is returned, " - font can't display." is appended to the name. If the default locale is supported by the Java API, the corresponding Display Name is selected; otherwise, the selection is row zero. In addition, the input field is set and formatted using the value of the java.sql.Date . DateFormat.setLenient(false) is applied to the default DateFormat and the default Display Name is obtained for display.

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 );

All other I18N functionality is handled in the ActionListener ( actionPerformed() method) for the Display Names JComboBox , jcb : a new DateFormat is created, based on the selection, and the display fields are cleared. If any errors occur in the next section, a dialog displays the ParseException message. The code attempts to parse a java.util.Date from the input and reformats it using the default DateFormat for output. Next, the display for the selected DateFormat is formatted. Finally, this value is parsed and used to create a java.sql.Date , which is used to show the ISO value.

if( oSource == jcb ){dfSelected = DateFormat.getDateInstance( iFormat, alSupported[ jcb.getSelectedIndex() ] ); }  // end if jcb, continue onjtD.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.DatejsqlDate = new java.sql.Date( d.getTime() );jtP.setText( jsqlDate.toString() );}catch( ParseException pe ) { JOptionPane.showMessageDialog( this,pe.getMessage(), "", JOptionPane.ERROR_MESSAGE);}

Again, the complete program is listed at JIBDateGUI.java: DateFormat example .

Number formatting example

This JIBNumberGUI example uses French as the default locale -- fr_FR.

JIBNumberGUI (see JIBNumberGUI.java: NumberFormat example ) intentionally operates very much like the Date formatting example . The app allows the user to input a number or a percent (if selected) in his native format. The native locale is determined on input and displayed next to the OK button. When the user presses OK, the input number is parsed and displayed in the selected locale. This value is also parsed and displayed separately as a standard numeric value.

The code is also very similar. The program begins by defining a Lucida font, a default locale, and default and selected NumberFormat s. Arrays for supported locales, and native and localized Locale Display Names are defined. An array is also defined to display a Number or Percent drop-down box.

Font fLucida;...Locale     lDefault = Locale.getDefault();Locale[]   alSupported;NumberFormat nfLocal = NumberFormat.getNumberInstance(),nfSelected;String[]   asDNames,asLDNames,asDP = { "Number", "Percent"};

In the constructor, the code is nearly identical to that in JIBDateGUI.java, except that the input field is initialized to a number and an additional JComboBox , jcbDP is added for Number/Percent input.

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

Again, the other I18N functionality is handled in the ActionListener . If there was a change between Number and Percent input, the flag that tracks input type is set and the current input value is parsed with the existing local NumberFormat . Then a new NumberFormat is created as appropriate, using NumberFormat.getNumberInstance() or NumberFormat.getPercentInstance() . The input value is reformatted using the new local NumberFormat and the code continues on to do the same work for the selected 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 operationoSource = jcb;}

If the Display Names drop-down changed, an appropriate new NumberFormat is created for the selected locale. The code then continues on to apply the new NumberFormat (s) to the input and display values.

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

Whether continuing from actions caused by combo box changes or directly by the OK button, the display fields are cleared. The code attempts to parse a Number from the input and reformats it using the default, local NumberFormat for output. Next, the display for the selected NumberFormat is formatted. Last, this value is parsed and used to create a Number , which is then used to show the raw value.

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);}

Again, the complete program is listed at JIBNumberGUI.java: NumberFormat example .

Currency formatting example

This JIBCurrencyGUI example uses Russian for the default locale -- ru_RU.

JIBCurrencyGUI (see JIBCurrencyGUI.java: CurrencyFormat example ), again, operates very much like the previous examples. The app allows the user to input a currency value in the native format. The native locale is determined on input and displayed next to the "Require Symbol" checkbox. When the user presses OK, the input value is parsed and displayed using the selected locale's currency symbol. This is purely mechanical formatting; there is no automatic value conversion between the two currencies. In real life, you will have to handle exchange rates yourself. The selected format text is also parsed and displayed separately as a standard numeric value.

The examples so far have used standard formatting and parsing, warts and all. If you play with values in these programs, you will see that these can be inflexible or irritating at times. This example will address a major inconvenience: while we expect a NumberFormat CurrencyInstance to display a currency symbol, we usually don't want to force the end user to include it when keying. If the "Require Symbol" checkbox is checked, the user must include the currency symbol on input to avoid a ParseException . This is standard behavior. If the checkbox is not selected, just the numeric value can be input. In the course of providing this behavior, we will briefly delve into DecimalFormatSymbols .

First, the by-now-familiar I18N-related data types are defined. The primary changes are that default and selected NumberFormat s now contain CurrencyInstance s. We also define a standard NumberFormat to deal with entries that don't contain currency symbols, and a String to contain the current currency symbol. This is a String rather than a char because more than one character can be in the symbol, for example, "DM" - Deutschmarks.

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

In the constructor, other than adding the new checkbox, everything is the same as in JIBNumberGUI until the last code block. First, the code ensures that a DecimalFormat was returned by the NumberFormat.getCurrencyInstance() request. If not, we can't get the information needed and the checkbox is disabled, meaning that "Symbol Required" will be true for the duration of this program run. Otherwise, we obtain the DecimalFormat 's related DecimalFormatSymbols and get the default currency symbol. The code also checks whether the MonetaryDecimalSeparator and DecimalSeparator are the same. If not, the DecimalSeparator is set to the MonetaryDecimalSeparator . Then the DecimalFormatSymbols for the alternate NumberFormat is set to that of the 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 DecimalFormatelse{ jchkb.setEnabled( false ); }

In actionPerformed() , if the selected locale changed, an appropriate new CurrencyInstance is obtained, then the code for the OK button is applied. If the user presses OK, the display fields are cleared.

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

Next is the code that makes it possible to accept the input without keying currency symbols: the code checks whether the currency symbol is required. If so, the local CurrencyInstance is used to parse the input; otherwise, the code determines whether a currency symbol was included in the input. If so, the CurrencyInstance is used; otherwise, the local DecimalFormat is used for parsing. The remainder of the code is similar to the previous examples, except that CurrencyInstance s are used for formatting.

jtD.setText( "" );jtP.setText( "" );try{if( bRequireSymbol ) {n = cfLocal.parse( sText ); }else { // currency symbol may still be present, checkif( 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);}

See the complete listing at JIBCurrencyGUI.java: CurrencyFormat example .

Putting the pieces together

Sanitation Engineer Maintenance overview

This JIBSEM example uses German for the default locale -- de_DE.

This last example puts everything together.

In our faux scenario, we will deal with a profession where the practitioners really clean up -- Sanitation Engineering. Fortunately for us, there are engineers in enough countries that the boss wants to be able to talk trash in any language. There will actually be two final programs required: 1) An inquiry that will be limited to selecting engineers from the user's specific country, to be used by low-level managers; and 2) A maintenance program that displays all engineers and allows editing of salary, hire date, and responsible tonnage, using the selected engineer's locale-specific formats. Only upper-level management will have access to this program.

JIBSEM.java is a prototype designed to handle both aspects to ensure that we can provide the desired functionality. Initially, the application will specifically handle US, French, German, and Russian locales, with the default being the US On entry, the descriptive labels in the program are displayed in the native locale, if supported. The data should display and be parsed and formatted in the locale of the specific engineer. This is required because the data is normally input locally at specific sites in the local formats and kept that way in the database. The database includes locale information for each engineer to support this capability. For simplicity, our prototype just matches the four rows to the four supported locales. We will, of course, need to modify and optimize the program for production, but it serves our purposes for now.

JIBSEM presents a JTable with mock data for each supported locale. When a row is selected, the appropriate data is displayed at the bottom of the screen twice: once in an editable field and again in a non-editable field so that the user can track its current value. The user can change the values and apply them by pressing OK. If the values pass editing, the new values are displayed in both fields, and the backing store is updated; otherwise, an error dialog is displayed.

You'll notice quite a bit of Swing code in this example, but the I18N relevant sections should be familiar. The backing classes JIBSEMATM.java (an AbstractTableModel implementation) and JIBSEMRow.java (to contain row data) are provided, as are the following properties files:

  • JIBSEMrb.properties (default with US values)
  • JIBSEMrb_de_DE.properties (German)
  • JIBSEMrb_fr_FR.properties (French)
  • JIBSEMrb_ru_RU.properties (Russian)

Notice that there is no need for duplication of the default file in this case, although it would do no harm; we only query the ResourceBundle on startup, and the locale is never changed for ResourceBundle access.

Sanitation Engineer Maintenance: I18N details

The I18N-related data types defined are:

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";

In the constructor, arrays containing formatters for currencies, dates, and numbers are loaded for supported locales. The code also determines if the default locale is supported; if not, the US locale is used for the default. Next, labels are loaded from the appropriate ResourceBundle via a call to loadFromResourceBundle() . Note that we also set the Lucida Sans font for the table display.

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 forif( !bLocaleMatched ) { lDefault = Locale.US; }...loadFromResourceBundle(); // get localized labels...jtbl.setFont( fLucidaNormal );jtbl.getTableHeader().setFont( fLucidaNormal );

Three formatting methods are defined to handle the three types of values: formatCurrency(double dSalary) , formatDate(java.util.Date d) , and formatNumber(double dTonnage) . Notice the similarity of the code.

public String formatCurrency( double dSalary ){cfSelected = aCF[iRowIndex];return cfSelected.format( dSalary );} // end formatCurrencypublic String formatDate( java.util.Date d ){dfSelected = aDF[iRowIndex];return dfSelected.format( d );} // end formatDatepublic String formatNumber( double dTonnage ){nfSelected = aNF[iRowIndex];return nfSelected.format( dTonnage );} // end formatDate

Here's the loadFromResourceBundle() method. As with most I18N programs, this method (and resource loading) is invoked only once, at startup:

public void loadFromResourceBundle(){try{ // get the PropertyResourceBundlerb = ResourceBundle.getBundle( sRBName, getLocale() );// get data associated with keysjlTitle.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 trycatch( MissingResourceException mre ){JOptionPane.showMessageDialog( this,"ResourceBundle problem;\n" +"Specific error: " + mre.getMessage(), "", JOptionPane.ERROR_MESSAGE);}} // end loadFromResourceBundle

Most of the action, as usual, is in actionPerformed() . When the user presses OK, the code attempts to parse the currency, date, and number values, using the selected formatters. If an exception is thrown, an error dialog is displayed and no further work is done before the method returns. Otherwise, the data is updated, both sets of fields show the new values, and we're ready for a new row. The iRowIndex field, used for indexing, is captured when a specific row is selected in valueChanged() . As mentioned previously, at this point the prototype's rows and array elements match.

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

See the listings for the complete application at JIBSEM.java: Sanitation Engineer Maintenance example .

Wrapping up

摘要

While this tutorial has only touched the tip of the iceberg of dealing with internationalization at the programming level, at this point you should have enough information and material to handle the majority of issues that I18N programmers typically face:

  • Java characters and the char datatype
  • Fonts, font properties, and the Lucida font
  • Creating locales
  • Using resource bundles
  • Dates, numbers, and currencies

We also briefly covered I18N in general in Internationalization and described Java API support at Internationalization and the Java programming language . Related topics are provided for your further exploration. Finally, we worked though several examples, including Sanitation Engineer Maintenance overview , which brought all the specific elements together in one application.

Appendix A: Complete code listings

charExample.java: character example

import javax.swing.*;public class charExample
{public static void main( String[] args ){boolean bFirst = true;    char aChar[] = {'A',    // character65,     // decimal0x41,   // hex0101,   // 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 intSystem.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 charSystem.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

ByTheNumbers.java: PropertyResourceBundle example

ByTheNumbersrb.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_de.properties

# Default properties in German
0=Null:
1=Eins:
2=Zwei:
3=Drei:
4=Vier:
5=Fünf:
6=Sechs:
7=Sieben:
8=Acht:
9=Neun:
10=Zehn:
random=aufs Geratewohl

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_fr.properties

# Default properties in French
0=Z豯:
1=Uun:
2=deux:
3=Trois:
4=Quatre:
5=cinq:
6=Six:
7=Sept:
8=Huit:
9=Neuf:
10=Dix:
random=au hasard

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

ByTheNumbers.java (Part 1)

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;public class ByTheNumbers extends    JFrameimplements ActionListener,WindowListener
{static final int sfiSIZE = 11;boolean bRandomize = false;Font fLucida;int iSelIndex = 0;int[] aiOrder = new int[sfiSIZE];JButton    jbOK  = new JButton( "OK" ),jbRandom  = new JButton();JComboBox  jcb = null;JLabel[] ajl = new JLabel[sfiSIZE];JTextField[] ajtf = new JTextField[sfiSIZE];JPanel     jpNorth  = new JPanel( new GridLayout(0,1) ),jpCenter = new JPanel(), jpCenterNest = new JPanel( new GridLayout(0,2) ),jpSouth  = new JPanel();Locale[]   alSupported = {Locale.US,Locale.FRANCE,Locale.GERMANY,new Locale( "ru", "RU" )};Random rNumber = new Random();ResourceBundle rb;String sRBName = getClass().getName() + "rb";String[]   asDNames;public ByTheNumbers(){int i = 0;JLabel jlTemp = null;JTextField jtfTemp = null;setTitle( "ByTheNumbers" );addWindowListener( this );Font fJB = jbOK.getFont();fLucida = new Font("Lucida Sans", fJB.getStyle(),fJB.getSize() );Container cp = getContentPane();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 forjcb = new JComboBox( asDNames );jcb.setFont( fLucida );jcb.setSelectedIndex( iSelIndex );jcb.addActionListener( this );for( i = 0; i < ajl.length; i++ ){jlTemp = new JLabel();jlTemp.setFont( fLucida );jlTemp.setName( i +  "" ); // set NamejtfTemp = new JTextField(3);jtfTemp.setHorizontalAlignment( JTextField.RIGHT );ajl[i] = jlTemp;ajtf[i] = jtfTemp;jpCenterNest.add( jlTemp );jpCenterNest.add( jtfTemp );}loadFromResourceBundle(); // get localized labelsjbOK.addActionListener( this );jbRandom.setFont( fLucida );jbRandom.setText( rb.getString( "random" ) );jbRandom.addActionListener( this );jpNorth.add( jcb );jlTemp = new JLabel( rb.getString( "title" ) );jlTemp.setFont( fLucida );jpNorth.add( jlTemp );jpCenter.add( jpCenterNest );jpSouth.add(jbOK);jpSouth.add(jbRandom);cp.add( jpNorth, BorderLayout.NORTH );cp.add( jpCenter, BorderLayout.CENTER );cp.add( jpSouth, BorderLayout.SOUTH );pack();setResizable( false );show();}  // end constructor

ByTheNumbers.java (Part 2)

public void checkAnswers(){boolean b = true;JTextField jtf = null;String s = null;for( int i = 0; i < sfiSIZE; i++ ){jtf = ajtf[i];s = jtf.getText().trim();if( !s.equals( ajl[i].getName() ) ){jtf.requestFocus();jtf.selectAll();b = false;break;}} // end forJOptionPane.showMessageDialog( this,b ? "Congratulations!" : "Keep Trying!", "", JOptionPane.ERROR_MESSAGE);}  // end checkAnswerspublic void loadFromResourceBundle(){try{ // get the PropertyResourceBundlerb = ResourceBundle.getBundle( sRBName, alSupported[iSelIndex] );// get data associated with keysfor( int i = 0; i < sfiSIZE; i++ ){aiOrder[i] = i;ajl[i].setText( rb.getString( ajl[i].getName() ) );}bRandomize = false;} // end trycatch( MissingResourceException mre ){JOptionPane.showMessageDialog( this,"ResourceBundle problem;\n" +"Specific error: " + mre.getMessage(), "", JOptionPane.ERROR_MESSAGE);}} // end loadFromResourceBundlepublic void loadGUI(){boolean bFirst = true;int i = 0,j = 0;if( bRandomize ){for( i = 0; i < sfiSIZE; i++ ){if( bFirst ) { // init array to known valueaiOrder[i] = 99;if( i == ( sfiSIZE - 1 )) { bFirst = !bFirst; i = -1;}continue;} // end if bFirstwhile( true ){j = rNumber.nextInt( sfiSIZE );if( aiOrder[j] == 99  ){aiOrder[j] = i;break;}} // end while} // end for} // end if bRandomizejpCenterNest.removeAll();for( i = 0; i < sfiSIZE; i++ ){j = aiOrder[i];ajtf[j].setText("");jpCenterNest.add( ajl[j] );jpCenterNest.add( ajtf[j] );}jpCenterNest.revalidate();}  // loadGUI// ActionListener Implementationpublic void actionPerformed(ActionEvent ae){Object oSource = ae.getSource();if( oSource == jbRandom ){bRandomize = true;loadGUI();return;}if( oSource == jbOK ){checkAnswers();return;}if( oSource == jcb ){iSelIndex = jcb.getSelectedIndex();loadFromResourceBundle();loadGUI();return;}}  // End actionPerformed// Window Listener Implementationpublic void windowOpened(WindowEvent we) {}public void windowClosing(WindowEvent we){dispose();System.exit(0);}public void windowClosed(WindowEvent we) {}public void windowIconified(WindowEvent we) {}public void windowDeiconified(WindowEvent we) {}public void windowActivated(WindowEvent we) {}public void windowDeactivated(WindowEvent we) {}
// End Window Listener Implementationpublic static void main(String[] args){new ByTheNumbers();}  // end main}  // end class ByTheNumbers

JIBDateGUI.java: DateFormat example

JIBDateGUI.java (Part 1)

import java.sql.*;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.text.*;
import java.util.*;public class JIBDateGUI extends    JFrameimplements ActionListener,WindowListener
{boolean bToggleFlag = true;DateFormat dfLocal,dfSelected;java.sql.Date jsqlDate = new java.sql.Date( System.currentTimeMillis() );Font fLucida;int iFormat, iSelIndex = 0;JButton    jbOK  = new JButton( "OK" ),jbToggle = new JButton( "Toggle Display Names" );JComboBox  jcb = null;JPanel     jpNorth  = new JPanel(),jpCenter = new JPanel(),jpSouth  = new JPanel();JTextField jtI = new JTextField( 10 ),jtD = new JTextField( 10 ),jtP = new JTextField( 10 );Locale     lDefault = Locale.getDefault();Locale[]   alSupported;String[]   asDNames,asLDNames;public JIBDateGUI(){this( DateFormat.SHORT );} // end default constructor

JIBDateGUI.java (Part 2)

public JIBDateGUI( int argiFormat ){String s1 = null;setTitle( "JIBDateGUI" );addWindowListener( this );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 fortoggleDisplayNames();jtI.setText( dfLocal.format( jsqlDate ) );// cause ActionPerformed eventjcb.setSelectedIndex( iSelIndex );jbOK.addActionListener( this );jbToggle.addActionListener( this );jtD.setEditable( false );jtD.setFont( fLucida );jtP.setEditable( false );jtP.setFont( fLucida );dfLocal.setLenient( false );jpNorth.add(new JLabel("Input a Date:"));jpNorth.add(jtI);jpNorth.add(jbOK);JLabel jlTemp = new JLabel("Default = " + lDefault.getDisplayName() );jlTemp.setFont( fLucida );jpNorth.add( jlTemp );jpCenter.add(new JLabel("Display:"));jpCenter.add(jtD);jpCenter.add(new JLabel("Parsed ISO:"));jpCenter.add(jtP);Container cp = getContentPane();cp.add( jpNorth, BorderLayout.NORTH );cp.add( jpCenter, BorderLayout.CENTER );cp.add( jpSouth, BorderLayout.SOUTH );pack();show();}  // end constructorpublic void toggleDisplayNames(){boolean bjcbExist = false;if( jcb != null ){jpSouth.remove( jcb );jpSouth.remove( jbToggle );iSelIndex = jcb.getSelectedIndex();bjcbExist = true;}if( bToggleFlag ){ jcb = new JComboBox( asDNames ); }else{ jcb = new JComboBox( asLDNames ); }bToggleFlag = !bToggleFlag;if( bjcbExist ){ jcb.setSelectedIndex( iSelIndex ); }jcb.setFont( fLucida );jcb.addActionListener( this );jpSouth.add( jcb );jpSouth.add( jbToggle );}  // end toggleDisplayNames// ActionListener Implementationpublic void actionPerformed(ActionEvent ae){Object oSource = ae.getSource();if( oSource == jbToggle ){toggleDisplayNames();pack();return;}if( oSource == jcb ){dfSelected = DateFormat.getDateInstance( iFormat, alSupported[ jcb.getSelectedIndex() ] ); }  // end if jcb, continue onjtD.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.DatejsqlDate = new java.sql.Date( d.getTime() );jtP.setText( jsqlDate.toString() );}catch( ParseException pe ) { JOptionPane.showMessageDialog( this,pe.getMessage(), "", JOptionPane.ERROR_MESSAGE);}}  // End actionPerformed// Window Listener Implementationpublic void windowOpened(WindowEvent we) {}public void windowClosing(WindowEvent we){dispose();System.exit(0);}public void windowClosed(WindowEvent we) {}public void windowIconified(WindowEvent we) {}public void windowDeiconified(WindowEvent we) {}public void windowActivated(WindowEvent we) {}public void windowDeactivated(WindowEvent we) {}
// End Window Listener Implementationpublic static void main(String[] args){int i = DateFormat.SHORT;String s = null;if( args.length == 1 ){if( args[0].equals( "full" ) ){ i = DateFormat.FULL; }elseif( args[0].equals( "long" ) ){ i = DateFormat.LONG; }elseif( args[0].equals( "medium" ) ){ i = DateFormat.MEDIUM; }}new JIBDateGUI( i );}  // end main}  // end class JIBDateGUI

JIBNumberGUI.java: NumberFormat example

JIBNumberGUI.java (Part 1)

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.text.*;
import java.util.*;public class JIBNumberGUI extends  JFrameimplements ActionListener,WindowListener
{boolean bToggleFlag   = true,bNumberFormat = true;Font fLucida;int iSelIndex = 0;JButton    jbOK  = new JButton( "OK" ),jbToggle = new JButton( "Toggle Display Names" );JComboBox  jcb = null,jcbDP = null;JPanel     jpNorth  = new JPanel(),jpCenter = new JPanel(),jpSouth  = new JPanel();JTextField jtI = new JTextField( 10 ),jtD = new JTextField( 10 ),jtP = new JTextField( 10 );Locale     lDefault = Locale.getDefault();Locale[]   alSupported;NumberFormat nfLocal = NumberFormat.getNumberInstance(),nfSelected;String[]   asDNames,asLDNames,asDP = { "Number", "Percent"};public JIBNumberGUI(){String s1 = null;setTitle( "JIBNumberGUI" );addWindowListener( this );Font fJCB = jbToggle.getFont();fLucida = new Font("Lucida Sans", fJCB.getStyle(),fJCB.getSize() );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 fortoggleDisplayNames();jtI.setText( nfLocal.format( 123456.7 ) );// cause ActionPerformed eventjcb.setSelectedIndex( iSelIndex );jcbDP = new JComboBox( asDP );jcbDP.addActionListener( this );jbOK.addActionListener( this );jbToggle.addActionListener( this );jtD.setEditable( false );jtD.setFont( fLucida );jtP.setEditable( false );jtP.setFont( fLucida );jpNorth.add(new JLabel("Input:"));jpNorth.add(jcbDP);jpNorth.add(jtI);jpNorth.add(jbOK);JLabel jlTemp = new JLabel("Default = " + lDefault.getDisplayName() );jlTemp.setFont( fLucida );jpNorth.add( jlTemp );jpCenter.add(new JLabel("Display:"));jpCenter.add(jtD);jpCenter.add(new JLabel("Parsed:"));jpCenter.add(jtP);Container cp = getContentPane();cp.add( jpNorth, BorderLayout.NORTH );cp.add( jpCenter, BorderLayout.CENTER );cp.add( jpSouth, BorderLayout.SOUTH );pack();show();}  // end constructorpublic void toggleDisplayNames(){boolean bjcbExisted = false;if( jcb != null ){jpSouth.remove( jcb );jpSouth.remove( jbToggle );iSelIndex = jcb.getSelectedIndex();bjcbExisted = true;}if( bToggleFlag ){ jcb = new JComboBox( asDNames ); }else{ jcb = new JComboBox( asLDNames ); }bToggleFlag = !bToggleFlag;if( bjcbExisted ){ jcb.setSelectedIndex( iSelIndex ); }jcb.setFont( fLucida );jcb.addActionListener( this );jpSouth.add( jcb );jpSouth.add( jbToggle );}  // end toggleDisplayNames

JIBNumberGUI.java (Part 2)

// ActionListener Implementationpublic void actionPerformed(ActionEvent ae){Number n = null;Object oSource = ae.getSource();if( oSource == jbToggle ){toggleDisplayNames();pack();return;}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 operationoSource = jcb;}if( oSource == jcb ){if( bNumberFormat ){nfSelected = NumberFormat.getNumberInstance( alSupported[ jcb.getSelectedIndex() ] ); }else{nfSelected = NumberFormat.getPercentInstance( alSupported[ jcb.getSelectedIndex() ] ); }}  // end if jcb, continue onjtD.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);}}  // End actionPerformed// Window Listener Implementationpublic void windowOpened(WindowEvent we) {}public void windowClosing(WindowEvent we){dispose();System.exit(0);}public void windowClosed(WindowEvent we) {}public void windowIconified(WindowEvent we) {}public void windowDeiconified(WindowEvent we) {}public void windowActivated(WindowEvent we) {}public void windowDeactivated(WindowEvent we) {}
// End Window Listener Implementationpublic static void main(String[] args){new JIBNumberGUI();}  // end main}  // end class JIBNumberGUI

JIBCurrencyGUI.java: CurrencyFormat example

JIBCurrencyGUI.java (Part 1)

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.text.*;
import java.util.*;public class JIBCurrencyGUI extends    JFrameimplements ActionListener,ItemListener,WindowListener
{boolean bRequireSymbol = true,bToggleFlag = true;Font fLucida;int iSelIndex = 0;JButton    jbOK  = new JButton( "OK" ),jbToggle = new JButton( "Toggle Display Names" );JCheckBox  jchkb = new JCheckBox("Require Symbol");JComboBox  jcb = null;JPanel     jpNorth  = new JPanel(),jpCenter = new JPanel(),jpSouth  = new JPanel();JTextField jtI = new JTextField( 10 ),jtD = new JTextField( 10 ),jtP = new JTextField( 10 );Locale     lDefault = Locale.getDefault();Locale[]   alSupported;NumberFormat cfLocal = NumberFormat.getCurrencyInstance(),cfSelected,nfLocal = NumberFormat.getInstance();String[]   asDNames,asLDNames;String     sCurSymbol = "";public JIBCurrencyGUI(){int i;String s1 = null;setTitle( "JIBCurrencyGUI");addWindowListener( this );Font fJCB = jbToggle.getFont();fLucida = new Font("Lucida Sans", fJCB.getStyle(),fJCB.getSize() );alSupported = Locale.getAvailableLocales();asDNames = new String[ alSupported.length ];asLDNames = new String[ alSupported.length ];for( 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 fortoggleDisplayNames();jtI.setText( cfLocal.format( 150.75 ) );// cause ActionPerformed eventjcb.setSelectedIndex( iSelIndex );jbOK.addActionListener( this );jbToggle.addActionListener( this );jchkb.setSelected(true);    jchkb.addItemListener( this );jtD.setEditable( false );jtD.setFont( fLucida );jtP.setEditable( false );jtP.setFont( fLucida );jpNorth.add(new JLabel("Input:"));jpNorth.add(jtI);jpNorth.add(jbOK);jpNorth.add(jchkb);JLabel jlTemp = new JLabel("Default = " + lDefault.getDisplayName() );jlTemp.setFont( fLucida );jpNorth.add( jlTemp );jpCenter.add(new JLabel("Display:"));jpCenter.add(jtD);jpCenter.add(new JLabel("Parsed:"));jpCenter.add(jtP);Container cp = getContentPane();cp.add( jpNorth, BorderLayout.NORTH );cp.add( jpCenter, BorderLayout.CENTER );cp.add( jpSouth, BorderLayout.SOUTH );pack();show();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 DecimalFormatelse{ jchkb.setEnabled( false ); }}  // end constructor

JIBCurrencyGUI.java (Part 2)

public void toggleDisplayNames(){boolean bjcbExist = false;if( jcb != null ){jpSouth.remove( jcb );jpSouth.remove( jbToggle );iSelIndex = jcb.getSelectedIndex();bjcbExist = true;}if( bToggleFlag ){ jcb = new JComboBox( asDNames ); }else{ jcb = new JComboBox( asLDNames ); }bToggleFlag = !bToggleFlag;if( bjcbExist ){ jcb.setSelectedIndex( iSelIndex ); }jcb.setFont( fLucida );jcb.addActionListener( this );jpSouth.add( jcb );jpSouth.add( jbToggle );}  // end toggleDisplayNames// ActionListener Implementationpublic void actionPerformed(ActionEvent ae){Object oSource = ae.getSource();Number n = null;String sText = jtI.getText();if( oSource == jbToggle ){toggleDisplayNames();pack();return;}if( oSource == jcb ){cfSelected = NumberFormat.getCurrencyInstance(alSupported[ jcb.getSelectedIndex() ] );}  // end if jcb, continue onjtD.setText( "" );jtP.setText( "" );try{if( bRequireSymbol ) {n = cfLocal.parse( sText ); }else { // currency symbol may still be present, checkif( 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);}}  // End actionPerformed// ItemListener Implementationpublic void itemStateChanged(ItemEvent ie) {bRequireSymbol = !bRequireSymbol;}  // End itemStateChanged// Window Listener Implementationpublic void windowOpened(WindowEvent we) {}public void windowClosing(WindowEvent we){dispose();System.exit(0);}public void windowClosed(WindowEvent we) {}public void windowIconified(WindowEvent we) {}public void windowDeiconified(WindowEvent we) {}public void windowActivated(WindowEvent we) {}public void windowDeactivated(WindowEvent we) {}
// End Window Listener Implementationpublic static void main(String[] args){new JIBCurrencyGUI();}  // end main}  // end class JIBCurrencyGUI

JIBSEM.java: Sanitation Engineer Maintenance example

JIBSEMrb.properties

# Default properties in English
# Label for Title
title=Sanitation Engineer Maintenance
# Engineer Table Column 1 Title and Employee label
Engineer=Engineer
# Name Table Column 2 Title
Name=Name
# Label for Edit
Edit=Edit
# Label for Current
Current=Current
# Label for Salary input field
Salary=Salary:
# Label for Hire Date input field
Date=Date:
# Label for Tons input field
Tons=Tons:

JIBSEMrb_de.properties

# Default properties in German
# Engineer Table Column 1 Title and Employee label
Engineer=Ingenieur
# Name Table Column 2 Title
Name=Name
# Label for Edit
Edit=Bearbeiten
# Label for Current
Current=Aktuell
# Label for Salary input field
Salary=Löhne:
# Label for Hire Date input field
Date=Date:
# Label for Tons input field
Tons=Tonne:

JIBSEMrb_fr.properties

# Default properties in French
# Engineer Table Column 1 Title and Employee label
Engineer=Ing譩eur
# Name Table Column 2 Title
Name=Nom
# Label for Edit
Edit=Editer
# Label for Current
Current=Actuel
# Label for Salary input field
Salary=Salaire:
# Label for Hire Date input field
Date=Date:
# Label for Tons input field
Tons=Tonne:

JIBSEMrb_ru.properties

# Default properties in Russian
# Engineer Table Column 1 Title and Employee label
Engineer=\u0418\u043D\u0436\u0435\u043D\u0435\u0440
# Name Table Column 2 Title
Name=\u0418\u043C\u044F
# Label for Edit
Edit=\u0420\u0435\u0434\u0430\u043A\u0442\u043E\u0440
# Label for Current
Current=\u0422\u0435\u043A\u0443\u0449\u0438\u0439
# Label for Salary input field
Salary=\u041E\u043A\u043B\u0430\u0434:
# Label for Hire Date input field
Date=\u0414\u0430\u0442\u0430:
# Label for Tons input field
Tons=\u0422\u043E\u043D\u043D\u044b:

JIBSEM.java (Part 1)

import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;
import java.awt.*;
import java.awt.event.*;
import java.text.*;
import java.util.*;public class JIBSEM extends    JFrameimplements ActionListener,FocusListener,ListSelectionListener,WindowListener
{DateFormat[] aDF;DateFormat dfSelected;Font fLucida,fLucidaNormal,fLucidaTitle;int iRowIndex = 0;JIBSEMATM ATM;JIBSEMRow[] aRows = new JIBSEMRow[4];JButton    jbOK  = new JButton( "OK" );JLabel     jlCI = new JLabel(),jlCurrent = new JLabel("", SwingConstants.CENTER),jlDI = new JLabel(),jlE = new JLabel(),jlEID = new JLabel(),jlEName = new JLabel(),jlEdit = new JLabel("", SwingConstants.CENTER),jlLName = new JLabel(),jlNI = new JLabel(),jlNow,jlTitle = new JLabel();JPanel     jpNorth  = new JPanel( new BorderLayout() ),jpNorthFlow = new JPanel(),jpCenter = new JPanel( new BorderLayout() ),jpCenterNorth = new JPanel(),jpCenterSouth = new JPanel(),jpCenterSouthNest = new JPanel( new GridLayout(0,3) ),jpSouth  = new JPanel();JScrollPane jsp;JTable     jtbl;JTextField jtCD = new JTextField( 9 ),jtCI = new JTextField( 9 ),jtDD = new JTextField( 9 ),jtDI = new JTextField( 9 ),jtND = new JTextField( 9 ),jtNI = new JTextField( 9 );Locale     lDefault = Locale.getDefault();Locale[]   alSupported = {Locale.US,Locale.FRANCE,Locale.GERMANY,new Locale( "ru", "RU" )};NumberFormat[] aCF,aNF;NumberFormat   cfSelected,nfSelected;Object[][] aoTableData = new Object[4][2];ResourceBundle rb;String[]   asHeaders = new String[2];String sRBName = getClass().getName() + "rb";public JIBSEM(){int i = 0;setTitle( "JIBSEM" );addWindowListener( this );Font fJB = jbOK.getFont();fLucida = new Font("Lucida Sans", fJB.getStyle(),fJB.getSize() );fLucidaNormal = new Font("Lucida Sans", Font.PLAIN,fJB.getSize() );fLucidaTitle = new Font("Lucida Sans", fJB.getStyle(),fJB.getSize() + 4 );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 forif( !bLocaleMatched ) { lDefault = Locale.US; }jlNow = new JLabel( DateFormat.getDateInstance( DateFormat.FULL, lDefault ).format( new Date( System.currentTimeMillis() )) + "   Default = " + lDefault.getDisplayName(),SwingConstants.CENTER );loadFromResourceBundle(); // get localized labelsjbOK.addActionListener( this );jlTitle.setFont( fLucidaTitle );jlNow.setFont( fLucidaNormal );jlTitle.setHorizontalAlignment( SwingConstants.CENTER );jlCI.setFont( fLucida );jlDI.setFont( fLucida );jlCurrent.setFont( fLucida );jlE.setFont( fLucida );jlE.setText( asHeaders[0] + ":" );jlEdit.setFont( fLucida );jlEID.setFont( fLucida );jlEID.setForeground( Color.black );jlEName.setFont( fLucida );jlLName.setFont( fLucidaNormal );jlEName.setForeground( Color.black );jlNI.setFont( fLucida );jtCD.addFocusListener(this);jtCD.setForeground( Color.green.darker().darker() );jtCD.setHorizontalAlignment( JTextField.RIGHT );jtCI.setHorizontalAlignment( JTextField.RIGHT );jtDD.addFocusListener(this);jtDD.setForeground( Color.green.darker().darker() );jtDD.setHorizontalAlignment( JTextField.RIGHT );jtDI.setHorizontalAlignment( JTextField.RIGHT );jtND.addFocusListener(this);jtND.setForeground( Color.green.darker().darker() );jtND.setHorizontalAlignment( JTextField.RIGHT );jtNI.setHorizontalAlignment( JTextField.RIGHT );loadData();ATM = new JIBSEMATM( aoTableData, asHeaders );jtbl = new JTable( ATM );jtbl.setFont( fLucidaNormal );jtbl.getTableHeader().setFont( fLucidaNormal );Dimension dim = jtCI.getPreferredSize();TableColumnModel tcm = jtbl.getColumnModel();TableColumn tc = tcm.getColumn(0);dim.width = tc.getPreferredWidth();tc = tcm.getColumn(1);i = tc.getPreferredWidth() * 3;tc.setPreferredWidth(i);dim.width += i;dim.height = jtbl.getRowHeight() * 4;jtbl.setPreferredScrollableViewportSize( dim );jtbl.setSelectionMode(ListSelectionModel.SINGLE_SELECTION );jtbl.getSelectionModel().addListSelectionListener( this );jtbl.setRowSelectionInterval(0, 0);jsp = new JScrollPane(jtbl);jpNorth.add( jlTitle, BorderLayout.NORTH );jpNorth.add(jlNow, BorderLayout.CENTER );jpNorthFlow.add( jsp );jpNorth.add( jpNorthFlow, BorderLayout.SOUTH );jpCenterNorth.add( jlE );jpCenterNorth.add( jlEID );jpCenterNorth.add(new JLabel(" "));jpCenterNorth.add( jlEName );jpCenterNorth.add( jlLName );jpCenterSouthNest.add(new JLabel(" "));jpCenterSouthNest.add(jlEdit);jpCenterSouthNest.add(jlCurrent);jpCenterSouthNest.add(jlCI);jpCenterSouthNest.add(jtCI);jpCenterSouthNest.add(jtCD);jpCenterSouthNest.add(jlDI);jpCenterSouthNest.add(jtDI);jpCenterSouthNest.add(jtDD);jpCenterSouthNest.add(jlNI);jpCenterSouthNest.add(jtNI);jpCenterSouthNest.add(jtND);jpCenterSouth.add(jpCenterSouthNest);jpCenter.add(jpCenterNorth, BorderLayout.NORTH);jpCenter.add(jpCenterSouth, BorderLayout.SOUTH);jpSouth.add(jbOK);Container cp = getContentPane();cp.add( jpNorth, BorderLayout.NORTH );cp.add( jpCenter, BorderLayout.CENTER );cp.add( jpSouth, BorderLayout.SOUTH );pack();show();}  // end constructor

JIBSEM.java (Part 2)

public String formatCurrency( double dSalary ){cfSelected = aCF[iRowIndex];return cfSelected.format( dSalary );} // end formatCurrencypublic String formatDate( java.util.Date d ){dfSelected = aDF[iRowIndex];return dfSelected.format( d );} // end formatDatepublic String formatNumber( double dTonnage ){nfSelected = aNF[iRowIndex];return nfSelected.format( dTonnage );} // end formatDatepublic void loadData(){aRows[0] = new JIBSEMRow( 12345,  "Annie Oakley", 50000.00f, java.sql.Date.valueOf("1998-05-19"), 25000.5, Locale.US );aRows[1] = new JIBSEMRow( 22345,  "Jeanne d'Arc", 379077.5f, java.sql.Date.valueOf("1999-06-15"), 25000.5, Locale.FRANCE );aRows[2] = new JIBSEMRow( 32345,  "Ludi Beethoven", 113027.5f, java.sql.Date.valueOf("2000-12-01"), 25000.5, Locale.GERMANY );aRows[3] = new JIBSEMRow( 42345,  "\u0414\u044f\u0434\u044f " + "\u0412\u0430\u043D\u044F", 1551500f, java.sql.Date.valueOf("2001-03-15"), 25000.5, new Locale( "ru", "RU") );for( int i = 0; i < aRows.length; i++ ){aoTableData[i][0] = new Integer(aRows[i].getID());aoTableData[i][1] = aRows[i].getName();}}  // end loadDatapublic void loadFromResourceBundle(){try{ // get the PropertyResourceBundlerb = ResourceBundle.getBundle( sRBName, getLocale() );// get data associated with keysjlTitle.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 trycatch( MissingResourceException mre ){JOptionPane.showMessageDialog( this,"ResourceBundle problem;\n" +"Specific error: " + mre.getMessage(), "", JOptionPane.ERROR_MESSAGE);}} // end loadFromResourceBundle// ActionListener Implementationpublic 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// FocusListener Implementationpublic void focusGained(FocusEvent fe){((Component)fe.getSource()).transferFocus();}  // End focusGainedpublic void focusLost(FocusEvent fe) {}  // ListSelectionListener Implementationpublic void valueChanged(ListSelectionEvent lse){if( lse.getValueIsAdjusting() ) { return; }iRowIndex = jtbl.getSelectedRow();    JIBSEMRow row = aRows[iRowIndex];jlEID.setText( "" + row.getID() );jlEName.setText(row.getName());jlLName.setText(row.getLocale().getDisplayName());cfSelected = aCF[iRowIndex];nfSelected = aNF[iRowIndex];jtCI.setText( formatCurrency( row.getSalary() ));jtCD.setText( jtCI.getText());jtDI.setText( formatDate( row.getHireDate() ));jtDD.setText( jtDI.getText());jtNI.setText( formatNumber(row.getTonnage() ));jtND.setText( jtNI.getText());} // end valueChanged// Window Listener Implementationpublic void windowOpened(WindowEvent we) {}public void windowClosing(WindowEvent we){dispose();System.exit(0);}public void windowClosed(WindowEvent we) {}public void windowIconified(WindowEvent we) {}public void windowDeiconified(WindowEvent we) {}public void windowActivated(WindowEvent we) {}public void windowDeactivated(WindowEvent we) {}
// End Window Listener Implementationpublic static void main(String[] args){new JIBSEM();}  // end main}  // end class JIBSEM

JIBSEMATM.java

import javax.swing.*;
import javax.swing.table.*;
import java.awt.*;public class JIBSEMATM extends AbstractTableModel
{Object rowData[][];Object columnNames[];public JIBSEMATM( Object[][] oData, Object[]   oCloumns ) {rowData = oData;columnNames = oCloumns;} // end constructorpublic String getColumnName(int column) {return columnNames[column].toString();}public int getRowCount() { return rowData.length; }public int getColumnCount() {return columnNames.length;}public Object getValueAt(int row, int col) {return rowData[row][col];}} // end class JIBSEMATM

JIBSEMRow.java

import java.util.*;public class JIBSEMRow
{// Employee IDprivate int    ID;// Employee nameprivate String Name;// Salaryprivate float  Salary;// Hire Dateprivate Date   HireDate;// Responsible Tonnageprivate double Tonnage;// Localeprivate Locale Locale;public JIBSEMRow( int iID,         String sName, float fSalary,   Date dHireDate, double dTonnage, Locale lLocale ){ID = iID;Name = sName;Salary = fSalary;HireDate = dHireDate;Tonnage = dTonnage;this.Locale = lLocale;} // end constructorpublic int getID(){return ID;}public String getName(){return Name;}public float getSalary(){return Salary;}public Date getHireDate(){return HireDate;}public double getTonnage(){return Tonnage;}public java.util.Locale getLocale(){return this.Locale;}public void setID( int iID ){ID = iID;}public void setName( String sName ){Name = sName;}public void setSalary( float fSalary ){Salary = fSalary;}public void setHireDate( Date dHireDate ){HireDate = dHireDate;}public void setTonnage( double dTonnage ){Tonnage = dTonnage;}public void setLocale( java.util.Locale lLocale ){this.Locale = lLocale;}public void report(){System.out.println( "JIBSEMRow reporting: " );System.out.println( toString() );}public String toString(){return ( "ID is: "        + ID +", Name is: "      + Name +", Salary is: "    + Salary +", HireDate is : " + HireDate +", Tonnage is : "  + Tonnage + ", Locale is: "    + this.Locale );}} // end class JIBSEMRow

翻译自: https://www.ibm.com/developerworks/java/tutorials/j-i18n/j-i18n.html

java 国际化_Java国际化基础相关推荐

  1. Java多版本国际化_Java -- 国际化 多语化

    1. 以中英两种语言做示例,显示 "hello" 2. 建立英文语言文件 "mess_en_US.properties ", 输入内容 "hello= ...

  2. java厨房_Java多线程基础

    目录: 进程和线程 为什么使用多线程? 多线程的创建方式 Runnable与Thread两种方式比较 start()与run()方法 线程的生命周期/状态转换 常用方法使用与解读 线程的优先级 守护线 ...

  3. 0基础java语法_Java零基础教程(二)基础语法

    Java 基础语法 一个 Java 程序可以认为是一系列对象的集合,而这些对象通过调用彼此的方法来协同工作.下面简要介绍下类.对象.方法和实例变量的概念. 对象:对象是类的一个实例,有状态和行为.例如 ...

  4. java 考试题_JAVA语言基础内部测试题(50道选择题)

    JAVA语言基础内部测试题 选择题(针对以下题目,请选择最符合题目要求的答案,针对每一道题目,所有答案都选对,则该题得分,所选答案错误或不能选出所有答案,则该题不得分.)(每题2分) 没有注明选择几项 ...

  5. java逆向_Java逆向基础之异常

    异常 由之前月份处理修改的例子 //清单1IncorrectMonthException.javapublic class IncorrectMonthException extends Except ...

  6. java 二进制运算_java二进制运算基础知识点详解|chu

    一.二进制位运算 1. 按位与(&) 位运算实质是将参与运算的数字转换为二进制,而后逐位对应进行运算. 按位与运算为:两位全为1,结果为1,即1&1=1,1&0=0,0& ...

  7. 实例化Java对象_Java面向对象基础之对象实例化

    1.实例化对象的过程可以分为两部分,例如下面代码: Person per = new Person(); 该代码分为两部分: 第一,声明对象:Person per,这部分是在栈内存中声明的,与数组一样 ...

  8. 递归java程序_JAVA编程基础之递归结构

    递归结构 递归是一种常见的解决问题的方法,即把问题逐渐简单化. 递归的基本思想就是 自己调用自己 ",一个使用递归技术的方法将会直接或者间接的调用自己.利用递归可以用简单的程序来解决一些复杂 ...

  9. java国际化程序_Java 国际化标准程序实现

    国际化程序实现 所谓的国家化应用指的就是根据当前的语言环境读取指定的语言资源文件. 如果要想实现国际化的操作,那么首先要解决的问题就是如何读取资源文件的问题:所谓的资源文件 就是指文件后缀名称为:&q ...

最新文章

  1. Linux - Red Hat 7.3 介绍安装
  2. mongodb聚合查询优化_MongoDB聚合查询详解
  3. 力扣【接雨水问题】 leetcode-42:暴力-备忘录-双指针三种方法
  4. java开发架构设计_跪了!阿里技术官出品:Java架构设计之完美,看完秒进大厂。...
  5. Linux下挂载ISO文件
  6. 【SVN】版本冲突处理之设置needs-lock:true属性
  7. FindBugs Maven插件教程
  8. 关于python的if条件语句,whilefor循环等的简单说。
  9. M0最高优先级的中断设计
  10. lamp搭建wordpress后升级安装主题,提示输入ftp账号密码
  11. Android Toast 总结
  12. 经典C语言编程100例——题目+答案代码(完结)
  13. java面试说话技巧,Java面试题及解答技巧解析介绍
  14. PPT幻灯片放映不显示备注,只让备注显示在自己屏幕上!
  15. 【USB】STM32模拟USB鼠标
  16. 上海python数据分析_python数据分析实例(四) 上海餐饮店数据
  17. C语言试题七十九之请编写函数实现自然底数 e=2.718281828
  18. android存储pdf文件怎么打开,android打开pdf文件
  19. oracle 主键、唯一键值、唯一索引关系
  20. 【Linux命令】modprobe命令

热门文章

  1. PDF文件不能编辑怎么办?
  2. 如何查看OpenAI的api-key?
  3. arduino leonardo 入门
  4. 神经网络的三种训练方法,神经网络常用训练方法
  5. 阿里云服务器开放80端口
  6. 运行时服务(二)、warnings模块
  7. android 多个style,Android style详解
  8. Android Interpolator属性 设置动画速度
  9. NetVLAD场景识别模型解读
  10. NitroSense无法打开(ACER)