Java 中文官方教程 2022 版(二十八)(3)https://developer.aliyun.com/article/1486870
在国际化之前
假设你已经编写了一个显示三条消息的程序,如下所示:
public class NotI18N { static public void main(String[] args) { System.out.println("Hello."); System.out.println("How are you?"); System.out.println("Goodbye."); } }
你已经决定这个程序需要为居住在法国和德国的人显示相同的消息。不幸的是,你的编程人员不懂多种语言,所以你需要帮助将这些消息翻译成法语和德语。由于翻译人员不是程序员,你需要将消息从源代码中移出,放入文本文件供翻译人员编辑。此外,程序必须足够灵活,以便能够显示其他语言的消息,但目前没有人知道那些语言会是什么。
看起来这个程序需要国际化。
国际化后
国际化程序的源代码如下。请注意,消息的文本未包含在软件代码中。
import java.util.*; public class I18NSample { static public void main(String[] args) { String language; String country; if (args.length != 2) { language = new String("en"); country = new String("US"); } else { language = new String(args[0]); country = new String(args[1]); } Locale currentLocale; ResourceBundle messages; currentLocale = new Locale(language, country); messages = ResourceBundle.getBundle("MessagesBundle", currentLocale); System.out.println(messages.getString("greetings")); System.out.println(messages.getString("inquiry")); System.out.println(messages.getString("farewell")); } }
要编译和运行此程序,您需要这些源文件:
I18NSample.java
MessagesBundle.properties
MessagesBundle_de_DE.properties
MessagesBundle_en_US.properties
MessagesBundle_fr_FR.properties
运行示例程序
国际化程序是灵活的;它允许最终用户在命令行上指定语言和国家。在下面的示例中,语言代码是fr
(法语),国家代码是FR
(法国),因此程序会以法语显示消息:
% java I18NSample fr FR Bonjour. Comment allez-vous? Au revoir.
在下一个示例中,语言代码是en
(英语),国家代码是US
(美国),因此程序会以英语显示消息:
% java I18NSample en US Hello. How are you? Goodbye.
国际化示例程序
如果你查看国际化的源代码,你会注意到硬编码的英文消息已经被移除。因为消息不再是硬编码的,而且语言代码在运行时指定,同一个可执行文件可以在全球范围内分发。本地化不需要重新编译。该程序已经国际化。
你可能想知道消息的文本发生了什么变化,或者语言和国家代码的含义是什么。别担心。当你逐步学习国际化示例程序的过程中,你会了解这些概念。
1. 创建属性文件
一个属性文件存储了关于程序或环境特性的信息。属性文件是纯文本格式。你可以用几乎任何文本编辑器创建这个文件。
在示例中,属性文件存储了要显示的可翻译消息的文本。在程序国际化之前,这些文本的英文版本是硬编码在System.out.println
语句中的。默认的属性文件名为MessagesBundle.properties
,包含以下内容:
greetings = Hello farewell = Goodbye inquiry = How are you?
现在消息已经在一个属性文件中,它们可以被翻译成各种语言。不需要对源代码进行任何更改。法语翻译者创建了一个名为MessagesBundle_fr_FR.properties
的属性文件,其中包含以下内容:
greetings = Bonjour. farewell = Au revoir. inquiry = Comment allez-vous?
注意等号右侧的值已经被翻译,但左侧的键没有改变。这些键不能改变,因为当你的程序获取翻译后的文本时会引用这些键。
属性文件的名称很重要。例如,MessagesBundle_fr_FR.properties
文件的名称包含了fr
语言代码和FR
国家代码。在创建Locale
对象时也会使用这些代码。
2. 定义 Locale
Locale
对象标识特定的语言和国家。以下语句定义了一个Locale
,其中语言为英语,国家为美国:
aLocale = new Locale("en","US");
下一个示例创建了法语语言在加拿大和法国的Locale
对象:
caLocale = new Locale("fr","CA"); frLocale = new Locale("fr","FR");
该程序是灵活的。程序不再使用硬编码的语言和国家代码,而是在运行时从命令行获取它们:
String language = new String(args[0]); String country = new String(args[1]); currentLocale = new Locale(language, country);
Locale
对象只是标识符。在定义了一个Locale
之后,你可以将它传递给执行有用任务的其他对象,比如格式化日期和数字。这些对象是区域敏感的,因为它们的行为根据Locale
的不同而变化。ResourceBundle
就是一个区域敏感的对象的例子。
3. 创建 ResourceBundle
ResourceBundle
对象包含特定于区域设置的对象。你可以使用ResourceBundle
对象来隔离与区域设置相关的数据,比如可翻译的文本。在示例程序中,ResourceBundle
由包含我们想要显示的消息文本的属性文件支持。
ResourceBundle
的创建方式如下:
messages = ResourceBundle.getBundle("MessagesBundle", currentLocale);
传递给getBundle
方法的参数标识将要访问的属性文件。第一个参数MessagesBundle
指的是这一系列属性文件:
MessagesBundle_en_US.properties MessagesBundle_fr_FR.properties MessagesBundle_de_DE.properties
Locale
是getBundle
的第二个参数,指定了选择哪个MessagesBundle
文件。创建Locale
时,语言代码和国家代码被传递给其构造函数。请注意,语言和国家代码在属性文件的名称中跟随MessagesBundle
。
现在你只需要从ResourceBundle
中获取翻译后的消息即可。
4. 从 ResourceBundle 中获取文本
属性文件包含键值对。值包含了程序将显示的翻译文本。在从ResourceBundle
中获取翻译后的消息时,你需要使用getString
方法指定键。例如,要检索由greetings
键标识的消息,你可以这样调用getString
:
String msg1 = messages.getString("greetings");
示例程序使用了键greetings
,因为它反映了消息的内容,但也可以使用另一个String
,比如s1
或msg1
。只需记住,键是硬编码在程序中的,必须存在于属性文件中。如果你的翻译人员意外修改了属性文件中的键,getString
将无法找到消息。
结论
这就是全部。正如你所看到的,国际化一个程序并不太困难。它需要一些规划和一点额外的编码,但好处是巨大的。为了让你了解国际化过程的概况,本课程中的示例程序被故意保持简单。在接下来的课程中,你将了解 Java 编程语言更高级的国际化特性。
检查表
原文:
docs.oracle.com/javase/tutorial/i18n/intro/checklist.html
许多程序在最初编写时并未国际化。这些程序可能最初是原型,或者可能并非用于国际分发。如果你必须国际化现有程序,请执行以下步骤:
识别文化相关数据
文本消息是随文化变化最明显的数据形式。然而,其他类型的数据可能会随地区或语言而变化。以下列表包含了文化相关数据的例子:
- 消息
- GUI 组件上的标签
- 在线帮助
- 声音
- 颜色
- 图形
- 图标
- 日期
- 时间
- 数字
- 货币
- 测量
- 电话号码
- 敬语和个人头衔
- 邮政地址
- 页面布局
在资源包中隔离可翻译文本
翻译是昂贵的。你可以通过将必须翻译的文本隔离在ResourceBundle
对象中来帮助降低成本。可翻译的文本包括状态消息、错误消息、日志文件条目和 GUI 组件标签。这些文本包含在尚未国际化的程序中。你需要找到所有显示给最终用户的包含文本的出现。例如,你应该清理这样的代码:
String buttonLabel = "OK"; // ... JButton okButton = new JButton(buttonLabel);
详细信息请参见隔离特定区域数据部分。
处理复合消息
复合消息包含可变数据。在消息"The disk contains 1100 files."中,整数 1100 可能会变化。这个消息很难翻译,因为整数在句子中的位置在所有语言中都不相同。下面的消息是不可翻译的,因为句子元素的顺序是由串联固定的:
Integer fileCount; // ... String diskStatus = "The disk contains " + fileCount.toString() + " files";
尽可能避免构建复合消息,因为它们很难翻译。然而,如果你的应用程序需要复合消息,你可以使用消息部分中描述的技术来处理它们。
格式化数字和货币
如果你的应用程序显示数字和货币,你必须以与地区无关的方式格式化它们。以下代码尚未国际化,因为它在所有国家中都不会正确显示数字:
Double amount; TextField amountField; // ... String displayAmount = amount.toString(); amountField.setText(displayAmount);
你应该用一个能正确格式化数字的例程替换前面的代码。Java 编程语言提供了几个格式化数字和货币的类。这些类在数字和货币部分有讨论。
格式化日期和时间
日期和时间格式因地区和语言而异。如果你的代码包含如下语句,你需要进行更改:
Date currentDate = new Date(); TextField dateField; // ... String dateString = currentDate.toString(); dateField.setText(dateString);
如果你使用日期格式化类,你的应用程序可以在全球范围内正确显示日期和时间。有关示例和说明,请参见日期和时间部分。
使用 Unicode 字符属性
以下代码尝试验证一个字符是否为字母:
char ch; // This code is incorrect if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z'))
注意类似这样的代码,因为它在除英语以外的语言中无法正常工作。例如,if
语句在德语单词 Grün 中漏掉了字符ü。
Character
比较方法使用 Unicode 标准来识别字符属性。因此,您应该用以下代码替换先前的代码:
char ch; // ... if (Character.isLetter(ch))
有关Character
比较方法的更多信息,请参阅检查字符属性部分。
正确比较字符串
在对文本进行排序时,通常需要比较字符串。如果文本是显示的,您不应该使用String
类的比较方法。一个未国际化的程序可能会这样比较字符串:
String target; String candidate; // ... if (target.equals(candidate)) { // ... if (target.compareTo(candidate) < 0) { // ...
String.equals
和String.compareTo
方法执行二进制比较,在大多数语言中排序时效率低下。相反,您应该使用Collator
类,该类在比较字符串部分有描述。
转换非 Unicode 文本
Java 编程语言中的字符是以 Unicode 编码的。如果您的应用程序处理非 Unicode 文本,您可能需要将其转换为 Unicode。有关更多信息,请参阅转换非 Unicode 文本部分。
课程:设置区域设置
一个国际化的程序可以在世界各地以不同方式显示信息。例如,该程序将在巴黎、东京和纽约显示不同的消息。如果本地化过程已经经过精细调整,该程序将显示不同的消息在纽约和伦敦,以考虑美式英语和英式英语之间的差异。一个国际化的程序如何识别其最终用户的适当语言和地区?简单。它引用一个Locale
对象。
一个Locale
对象是特定语言和地区组合的标识符。如果一个类根据Locale
变化其行为,那么它被称为区域敏感。例如,NumberFormat
类是区域敏感的;它返回的数字格式取决于Locale
。因此,NumberFormat
可能会将数字返回为 902 300(法国)、902.300(德国)或 902,300(美国)。Locale
对象只是标识符。真正的工作,比如格式化和检测单词边界,是由区域敏感类的方法执行的。
以下各节解释了如何使用Locale
对象:
创建一个 Locale
在创建Locale
对象时,通常会指定语言代码和国家代码。第三个参数,变体,是可选的。
BCP 47 扩展
本节向您展示如何向Locale
添加 Unicode 区域扩展或私有使用扩展。
识别可用的 Locale
区域敏感类仅支持特定的Locale
定义。本节将向您展示如何确定支持哪些Locale
定义。
语言标签过滤和查找
本节描述了语言标签、语言标签过滤和语言标签查找的国际化支持。
Locale 的范围
在 Java 平台上,您不需要通过在运行应用程序之前设置环境变量来指定全局Locale
。相反,您要么依赖于默认 Locale,要么为每个区域敏感对象分配一个Locale
。
区域敏感服务 SPI
本节解释了如何启用依赖于区域设置的数据和服务的插件。这些 SPI(服务提供者接口)提供了对当前可用区域设置之外更多区域设置的支持。
创建 Locale
有几种方法可以创建 Locale
对象。无论使用哪种技术,创建可以简单地指定语言代码。但是,您还可以通过设置区域(也称为“国家”)和变体代码来进一步区分区域。如果您使用 JDK 7 发布或更高版本,还可以指定脚本代码和 Unicode 区域扩展。
创建 Locale
对象的四种方法是:
Locale.Builder
类Locale
构造函数Locale.forLanguageTag
工厂方法Locale
常量
版本说明: Locale.Builder
类和 forLanguageTag
方法是在 Java SE 7 发布中添加的。
LocaleBuilder
类
Locale.Builder
实用类可用于构造符合 IETF BCP 47 语法的 Locale
对象。例如,要指定法语和加拿大国家,您可以调用 Locale.Builder
构造函数,然后链接设置器方法如下:
Locale aLocale = new Locale.Builder().setLanguage("fr").setRegion("CA").build();
下一个示例创建了英语在美国和英国的 Locale
对象:
Locale bLocale = new Locale.Builder().setLanguage("en").setRegion("US").build(); Locale cLocale = new Locale.Builder().setLanguage("en").setRegion("GB").build();
最后一个示例创建了俄语的 Locale
对象:
Locale dLocale = new Locale.Builder().setLanguage("ru").setScript("Cyrl").build();
Locale
构造函数
Locale
类有三个可用的构造函数用于创建 Locale
对象:
Locale(String language)
Locale(String language, String country)
Locale(String language, String country, String variant)
以下示例创建了法语在加拿大,英语在美国和英国,以及俄语的 Locale
对象。
aLocale = new Locale("fr", "CA"); bLocale = new Locale("en", "US"); cLocale = new Locale("en", "GB"); dLocale = new Locale("ru");
在 JDK 7 之前的版本中,无法在 Locale
对象上设置脚本代码。
forLanguageTag
工厂方法
如果您有符合 IETF BCP 47 标准的语言标记字符串,可以使用在 Java SE 7 发布中引入的 forLanguageTag(String)
工厂方法。例如:
Locale aLocale = Locale.forLanguageTag("en-US"); Locale bLocale = Locale.forLanguageTag("ja-JP-u-ca-japanese");
Locale
常量
为了方便起见,Locale
类为一些语言和国家提供了常量。例如:
cLocale = Locale.JAPAN; dLocale = Locale.CANADA_FRENCH;
当您指定语言常量时,Locale
的区域部分是未定义的。下面的三个语句创建等效的 Locale
对象:
j1Locale = Locale.JAPANESE; j2Locale = new Locale.Builder().setLanguage("ja").build(); j3Locale = new Locale("ja");
由以下三个语句创建的Locale
对象也是等效的:
j4Locale = Locale.JAPAN; j5Locale = new Locale.Builder().setLanguage("ja").setRegion("JP").build(); j6Locale = new Locale("ja", "JP");
代码
以下部分讨论语言代码和可选的脚本、地区和变体代码。
语言代码
语言代码是符合 ISO 639 标准的两个或三个小写字母。您可以在www.loc.gov/standards/iso639-2/php/code_list.php
找到 ISO 639 代码的完整列表。
以下表格列出了一些语言代码。
示例语言代码
语言代码 | 描述 |
de |
德语 |
en |
英语 |
fr |
法语 |
ru |
俄语 |
ja |
日语 |
jv |
爪哇语 |
ko |
韩语 |
zh |
中文 |
脚本代码
脚本代码以大写字母开头,后跟三个小写字母,并符合 ISO 15924 标准。您可以在unicode.org/iso15924/iso15924-codes.html
找到 ISO 15924 代码的完整列表。
以下表格列出了一些脚本代码。
示例脚本代码
脚本代码 | 描述 |
Arab |
阿拉伯语 |
Cyrl |
西里尔字母 |
Kana |
片假名 |
Latn |
拉丁字母 |
有三种方法可以检索Locale
的脚本信息:
getScript()
– 返回Locale
对象的 4 字母脚本代码。如果未为区域设置定义脚本,则返回空字符串。getDisplayScript()
– 返回适合显示给用户的区域设置脚本的名称。如果可能,名称将针对默认区域设置进行本地化。因此,例如,如果脚本代码是"Latn",则返回的显示脚本名称将在英语语言区域设置下为"Latin"。getDisplayScript(Locale)
– 返回指定Locale
的显示名称,如果可能的话进行本地化。
地区代码
地区(国家)代码由符合 ISO 3166 标准的两个或三个大写字母组成,或者符合 UN M.49 标准的三个数字。代码的副本可以在www.chemie.fu-berlin.de/diverse/doc/ISO_3166.html
找到。
以下表格包含几个示例国家和地区代码。
示例地区代码
A-2 代码 | A-3 代码 | 数字代码 | 描述 |
AU |
AUS |
036 |
澳大利亚 |
BR |
BRA |
076 |
巴西 |
CA |
CAN |
124 |
加拿大 |
CN |
CHN |
156 |
中国 |
DE |
DEU |
276 |
德国 |
FR |
FRA |
250 |
法国 |
IN |
IND |
356 |
印度 |
RU |
RUS |
643 |
俄罗斯联邦 |
US |
USA |
840 |
美国 |
变体代码
可选的variant
代码可用于进一步区分您的Locale
。例如,变体代码可用于指示区域代码未涵盖的方言差异。
版本说明: 在 Java SE 7 发布之前,变体代码有时用于标识不特定于语言或区域的差异。例如,它可能已用于标识计算平台之间的差异,如 Windows 或 UNIX。根据 IETF BCP 47 标准,不鼓励此用法。
要定义与您的环境相关的非语言特定变体,请使用扩展机制,如 BCP 47 扩展中所解释的那样。
自 Java SE 7 发布以来,符合 IETF BCP 47 标准的变体代码专门用于指示定义语言或其方言的附加变体。IETF BCP 47 标准对变体子标记施加了语法限制。您可以在www.iana.org/assignments/language-subtag-registry
上查看变体代码列表(搜索变体)。
例如,Java SE 使用变体代码支持泰语。按照惯例,th
和 th_TH
区域设置的NumberFormat
对象将使用常见的阿拉伯数字形状或阿拉伯数字来格式化泰国数字。然而,th_TH_TH
区域设置的NumberFormat
将使用泰国数字形状。ThaiDigits.java
中的摘录演示了这一点:
String outputString = new String(); Locale[] thaiLocale = { new Locale("th"), new Locale("th", "TH"), new Locale("th", "TH", "TH") }; for (Locale locale : thaiLocale) { NumberFormat nf = NumberFormat.getNumberInstance(locale); outputString = outputString + locale.toString() + ": "; outputString = outputString + nf.format(573.34) + "\n"; }
以下是此示例的屏幕截图: