国际化与本地化开发指南
1. 国际化与本地化概述
在软件开发中,“一次编写,到处运行”的理念意味着代码要在不同语言和文化环境中正常工作。为了让程序能适应这些差异,需要进行国际化和本地化处理。
国际化(Internationalization)是使程序具有灵活性,能适应不同地区变化的过程。而本地化(Localization)则是使用国际化工具,将程序适配到特定地区,比如将消息翻译成当地语言。
国际化有一些工具可用。首先,语言本身就具备支持,字符串采用 Unicode 编码,几乎能表达地球上任何书面语言。不过,字符串仍需人工翻译,并且向用户显示翻译后的文本需要相应字符的字体。Unicode 对代码本地化有很大帮助。
国际化和本地化的核心概念是“区域设置”(Locale),它定义了一个“地方”,可以是语言、文化或国家等,只要有一套相关的习俗需要程序行为做出改变。每个运行的程序都有一个默认的区域设置,反映用户的偏好。程序应尽可能适应区域设置的习俗。区域设置由
java.util
包中的
Locale
类表示。
给定一个区域设置,有几个工具可帮助程序以当地可理解的方式运行。常见的模式是类定义执行区域敏感操作的方法。该类的通用“get instance”静态方法返回适合默认区域设置的对象(可能是子类的对象)。该类还会提供每个“get instance”方法的重载版本,接受一个区域设置参数并返回适合特定区域设置的对象。例如,通过调用类的
getInstance
方法,可以获得一个合适的
java.util.Calendar
对象,该对象能处理用户偏好的日期和时间。返回的
Calendar
对象会根据默认区域设置的习俗将系统时间转换为日期。如果用户是墨西哥人,可能会返回一个适应墨西哥习俗的
Calendar
对象;中国用户可能会得到一个按照中国历法习俗工作的不同子类的对象。
如果程序向用户显示信息,通常需要对输出进行本地化。对不懂英语的人说 “That’s not right” 可能毫无意义,因此需要将消息翻译成其他地区用户能理解的语言。资源束(Resource Bundle)机制通过将字符串键映射到任意资源,帮助实现这一点。使用资源束返回的值,程序就能用其他语言表达,而不是在代码中直接编写文字消息字符串,而是通过字符串键从资源束中查找字符串。当程序迁移到另一个区域设置时,有人可以翻译资源束中的消息,程序无需修改一行代码就能在新的区域设置下工作。
2. 区域设置(Locale)
java.util.Locale
对象描述了一个特定的地方,包括文化、政治或地理方面。使用区域设置,对象可以根据用户期望进行本地化行为。能做到这一点的对象称为区域敏感对象。例如,使用区域敏感的
DateFormat
类可以对日期格式进行本地化。在英国写成 26/11/72 的日期,在冰岛会写成 26.11.72,在美国是 11/26/72,在拉脱维亚则是 72.26.11。
一个单一的区域设置代表了语言、国家和其他传统方面的问题。对于美国英语、英国英语、澳大利亚英语、巴基斯坦英语等,可以有不同的区域设置。虽然这些区域设置的语言可能相同,但日期、货币和数字表示的习俗各不相同。
代码很少直接获取或创建
Locale
对象,而是使用反映用户偏好的默认区域设置。通常通过获取资源或资源束隐式地使用这个区域设置,就像其他区域敏感类那样。例如,获取默认日历对象的代码如下:
Calendar now = Calendar.getInstance();
Calendar
类的
getInstance
方法会查找默认区域设置来配置返回的日历对象。当编写自己的区域敏感类时,可以从
Locale
类的静态
getDefault
方法获取默认区域设置。
如果编写的代码允许用户选择区域设置,可能需要创建
Locale
对象。
Locale
类有三个构造函数:
-
public Locale(String language, String country, String variant)
:创建一个表示给定语言和国家的
Locale
对象。其中,
language
是语言的双字母 ISO 639 代码(如 “et” 表示爱沙尼亚语),
country
是国家的双字母 ISO 3166 代码(如 “KY” 表示开曼群岛)。变体可以指定任何内容,如操作环境(如 “POSIX” 或 “MAC”)、公司或时代。如果指定多个变体,用下划线分隔。若要不指定区域设置的某个部分,使用空字符串
""
,而不是
null
。
-
public Locale(String language, String country)
:相当于
Locale(language, country, "")
。
-
public Locale(String language)
:相当于
Locale(language, "", "")
。
语言和国家代码可以是任意大小写,但为了符合标准,语言代码会转换为小写,国家代码会转换为大写,变体转换为大写。
Locale
类为几个知名的区域设置定义了静态
Locale
对象,如
CANADA_FRENCH
和
KOREA
用于表示国家,
KOREAN
和
TRADITIONAL_CHINESE
用于表示语言。这些对象只是为了方便,与自己创建的任何
Locale
对象相比没有特殊权限。
静态方法
setDefault
可以更改默认区域设置。默认区域设置是共享状态,应始终反映用户的偏好。如果有代码必须在不同的区域设置下运行,可以在获取资源或进行特定操作时将该区域设置作为参数指定给区域敏感类。通常很少需要更改默认区域设置。
Locale
类提供了获取区域设置描述各部分的方法。
getCountry
、
getLanguage
和
getVariant
方法返回构造时定义的值,这些是大多数用户不熟悉的简洁代码。这些方法有 “display” 变体,如
getDisplayCountry
、
getDisplayLanguage
和
getDisplayVariant
,返回值的人类可读版本。
getDisplayName
方法返回整个区域设置描述的人类可读摘要,
toString
方法返回简洁的等效表示,用下划线分隔各部分。这些 “display” 方法返回的值会根据默认区域设置进行本地化。
可以选择为任何 “display” 方法提供一个
Locale
参数,以获取给定区域设置在提供的区域设置下的描述。例如:
System.out.println(Locale.ITALY.getDisplayCountry(Locale.FRANCE));
输出结果为:
Italie
即意大利在法语中的名称。
getISO3Country
和
getISO3Language
方法分别返回区域设置的国家和语言的三字符 ISO 代码。
3. 资源束(Resource Bundles)
在代码国际化过程中,通常有一些意义单元,如文本或声音,需要针对每个区域设置进行翻译或调整。如果直接将英文文本放入程序,对代码进行本地化会很困难,需要在程序中找到所有字符串,识别哪些是显示给用户的,然后在代码中进行翻译,从而为例如斯瓦希里语用户创建程序的第二个版本。当为大量区域设置重复这个过程时,任务会变得非常繁琐。
java.util
中的资源束类以更简洁、更灵活的方式帮助解决这个问题。抽象类
ResourceBundle
定义了通过字符串键在束中查找资源的方法,并提供了一个父束,如果束中没有某个键,会搜索父束。这种继承特性允许一个束与另一个束类似,只是修改或添加了一些资源值。例如,美国英语束可能使用英国英语束作为父束,为拼写不同的资源提供替换。
ResourceBundle
提供了以下公共方法:
| 方法 | 描述 |
| ---- | ---- |
|
public final String getString(String key) throws MissingResourceException
| 返回束中给定键对应的字符串。 |
|
public final String[] getStringArray(String key) throws MissingResourceException
| 返回束中给定键对应的字符串数组。 |
|
public final Object getObject(String key) throws MissingResourceException
| 返回束中给定键对应的对象。 |
|
public abstract Enumeration getKeys()
| 返回该束理解的键的枚举,包括父束的所有键。 |
每个资源束定义了一组字符串键,这些键映射到区域敏感资源。这些字符串可以是任意的,但最好具有记忆性。需要使用资源时,通过名称查找。如果找不到资源,会抛出
MissingResourceException
异常。资源本身可以是任何类型,但通常是字符串,因此提供了
getString
方法以方便使用。
下面是一个国际化的 “Hello, world” 示例:
import java.util.*;
public class GlobalHello {
public static void main(String[] args) {
ResourceBundle res = ResourceBundle.getBundle("GlobalRes");
String msg;
if (args.length > 0)
msg = res.getString(GlobalRes.GOODBYE);
else
msg = res.getString(GlobalRes.HELLO);
System.out.println(msg);
}
}
程序首先获取其资源束,然后检查命令行是否提供了参数。如果有参数,程序说再见;否则说你好。程序逻辑决定显示哪个消息,但实际要打印的字符串是通过键(
GlobalRes.HELLO
或
GlobalRes.GOODBYE
)查找的。
每个资源束是一组相关的类和属性文件。在这个示例中,
GlobalRes
是一个继承自
ResourceBundle
的类的名称,它实现了将消息键映射到该消息的本地化翻译所需的方法。为想要本地化消息的各个区域设置定义类,类名反映区域设置。例如,管理 Lingala 语言的
GlobalRes
消息的束类将是
GlobalRes_ln
,因为 “ln” 是 Lingala 语的双字母代码。法语将映射到
GlobalRes_fr
,加拿大法语将是
GlobalRes_fr_CA
,它可能以
GlobalRes_fr
作为父束。
在
GlobalRes
类中,将键字符串定义为常量。使用常量可以防止拼写错误。如果将像 “hello” 这样的文字字符串传递给
getString
方法,拼写错误只有在执行错误的
getString
时才会显示出来,而且在测试期间可能不会出现这种情况。如果使用常量,拼写错误会被编译器捕获(除非不小心拼写成了另一个常量的名称)。
通过调用
ResourceBundle
中的两个静态
getBundle
方法之一来查找资源:一个是上述示例中使用的方法,它在当前区域设置中搜索你指定名称的束的最佳可用版本;另一个方法允许你同时指定束名称和所需的区域设置。完全限定的束名称形式为
package.Bundle_la_CO_va
,其中
package.Bundle
是束类的通用完全限定名称(如
GlobalRes
),
la
是双字母语言代码(小写),
CO
是双字母国家代码(大写),
va
是用下划线分隔的变体列表。如果找不到具有完整名称的束,则去掉最后一个组件,用较短的名称再次搜索。这个过程会重复进行,直到只剩下最后一个区域设置修饰符。如果即使这样的搜索也失败了,并且你在调用
getBundle
时指定了区域设置,则使用默认区域设置的束的完整名称重新开始搜索。如果第二次搜索仍未找到束,或者你是在默认区域设置中搜索,
getBundle
会仅使用束名称进行检查。如果连这个束也不存在,
getBundle
会抛出
MissingBundleException
异常。
例如,假设你请求
GlobalRes
束,指定一个居住在基里巴斯的左撇子世界语使用者的区域设置,而用户的默认区域设置是在不丹为 Acme 公司工作的尼泊尔语使用者。最长可能的搜索顺序如下:
1.
GlobalRes_eo_KI_left
2.
GlobalRes_eo_KI
3.
GlobalRes_eo
4.
GlobalRes_ne_BT_Acme
5.
GlobalRes_ne_BT
6.
GlobalRes_ne
7.
GlobalRes
找到的第一个资源束将结束搜索,被认为是最佳匹配。
前面的示例使用资源束来获取字符串,但要记住可以使用
getObject
方法获取任何类型的对象。束用于存储图像、URL、音频源、图形组件以及任何其他可以用对象表示的区域敏感资源。
将字符串键映射到本地化资源对象通常很直接,只需使用
ResourceBundle
提供的子类之一来实现查找功能:
ListResourceBundle
和
PropertyResourceBundle
。
4. 资源束子类
4.1 ListResourceBundle
ListResourceBundle
将简单的键列表映射到它们的本地化对象。它是
ResourceBundle
的抽象子类,需要提供一个
getContents
方法,该方法返回一个键/资源对数组,作为
Object
类型的二维数组。键必须是字符串,但资源可以是任何类型的对象。
ListResourceBundle
会使用这个数组为各种 “get” 方法构建映射。
以下是使用
ListResourceBundle
为
GlobalRes
定义几个区域设置的类:
public class GlobalRes extends ListResourceBundle {
public static final String HELLO = "hello";
public static final String GOODBYE = "goodbye";
public Object[][] getContents() {
return contents;
}
private static final Object[][] contents = {
{ GlobalRes.HELLO, "Ciao" },
{ GlobalRes.GOODBYE, "Ciao" },
};
}
这是顶级束,当找不到其他束时将使用它。这里选择意大利语作为默认语言。在执行任何 “get” 方法之前,会调用
GlobalRes.getContents
方法,
contents
数组的值将为 “get” 方法使用的数据结构提供初始值。
ListResourceBundle
使用内部查找表进行高效访问,不会遍历键数组。
GlobalRes
类还定义了束中已知资源的常量名称。
以下是一个针对更具体区域设置的束:
public class GlobalRes_en extends ListResourceBundle {
public Object[][] getContents() {
return contents;
}
private static final Object[][] contents = {
{ GlobalRes.HELLO, "Hello" },
{ GlobalRes.GOODBYE, "Goodbye" },
};
}
这个束覆盖了英语区域设置 “en”,为每个可本地化的字符串提供了特定的值。
下一个束使用了继承特性:
public class GlobalRes_en_AU extends ListResourceBundle {
// mostly like basic English - our parent bundle
public Object[][] getContents() { return contents; }
private static final Object[][] contents = {
{ GlobalRes.HELLO, "G'day" },
};
}
这个束针对澳大利亚(,“AU”)的英语使用者。它为 “HELLO” 字符串提供了更口语化的版本,并从通用英语区域设置
GlobalRes_en
继承所有其他字符串。每当实例化一个资源束时,会建立其父链。这通过从基本束名称中依次去掉变体、国家和语言组件,并实例化这些束(如果它们存在)来实现。如果它们存在,则在前面的束上调用
setParent
方法,将新束作为父束传递。所以在这个示例中,当创建
GlobalRes_en_AU
时,系统将创建
GlobalRes_en
并将其设置为
GlobalRes_en_AU
的父束。反过来,
GlobalRes_en
的父束将是基本束
GlobalRes
。
给定这些类,具有英语区域设置(“en”)的人将获得
GlobalRes_en
返回的值,除非区域设置还指定了国家为澳大利亚(“AU”),在这种情况下将使用
GlobalRes_en_AU
的值。其他所有人将看到
GlobalRes
中的值。
4.2 PropertyResourceBundle
PropertyResourceBundle
是
ResourceBundle
的子类,它从文本属性描述中读取资源列表。文本中包含键/资源对,形式为
key=value
,键和值都必须是字符串。
PropertyResourceBundle
对象从传递给其构造函数的
InputStream
中读取文本,并使用读取的信息构建一个查找表,以便高效访问。
前面描述的束搜索过程实际上还有一个额外的步骤,即在查找类
ResName
之后,会查找文件
ResName.properties
。例如,如果搜索过程找不到类
GlobalRes_eo_KI_left
,它将在查找下一个资源类之前查找文件
GlobalRes_eo_KI_left.properties
。如果该文件存在,则为其创建一个输入流,并用于构造一个
PropertyResourceBundle
对象,该对象将从文件中读取属性。
使用属性文件比创建
ListResourceBundle
的子类更容易,但文件有两个限制。首先,它们只能定义字符串资源,而
ListResourceBundle
可以定义任意对象。其次,属性文件的唯一合法字符编码是 ISO 8859 - 1 的字节格式。这意味着其他 Unicode 字符必须使用
\uxxxx
转义序列进行编码。
4.3 自定义 ResourceBundle 子类
对于大多数束,
ListResourceBundle
、
PropertyResourceBundle
和
.properties
文件就足够了,但如果这些不够用,也可以创建自己的
ResourceBundle
子类。必须实现两个方法:
protected abstract Object handleGetObject(String key) throws MissingResourceException
返回与给定键关联的对象。如果该键在这个束中未定义,则返回
null
,这会导致
ResourceBundle
检查其父束(如果有)。除非自己检查父束而不是让束自动处理,否则不要抛出
MissingResourceException
异常。所有 “get” 方法都是基于这个方法编写的。
public abstract Enumeration getKeys()
返回该束理解的键的枚举,包括父束的所有键。
5. 练习
尝试让
GlobalHello
程序在示例区域设置下工作。使用
ListResourceBundle
、
.properties
文件和自己的
ResourceBundle
特定子类添加更多区域设置。
通过以上内容,我们了解了在软件开发中如何进行国际化和本地化处理,包括区域设置的使用、资源束的概念和实现方式,以及如何通过这些技术让程序在不同的语言和文化环境中正常工作。希望这些知识能帮助开发者更好地开发出具有全球适用性的软件。
国际化与本地化开发指南(续)
6. 实际应用中的考虑
在实际开发中,进行国际化和本地化处理时,还需要考虑以下几个方面:
6.1 性能优化
- 资源束加载 :资源束的加载可能会影响程序的性能,尤其是在频繁切换区域设置或加载大量资源时。可以考虑使用缓存机制,避免重复加载相同的资源束。例如,可以在程序启动时将常用的资源束加载到内存中,后续使用时直接从缓存中获取。
-
查找效率
:
ListResourceBundle和PropertyResourceBundle内部都使用了查找表来提高资源查找的效率。在自定义ResourceBundle子类时,也应该优化查找逻辑,避免不必要的遍历和比较操作。
6.2 多线程环境
-
线程安全
:在多线程环境中,需要确保资源束的访问是线程安全的。
ResourceBundle类本身是线程安全的,但如果在自定义子类中进行了复杂的操作,可能需要考虑使用同步机制来保证线程安全。 - 区域设置切换 :当多个线程同时进行区域设置切换时,可能会导致资源束加载和使用的混乱。可以通过使用锁机制或其他同步手段来确保区域设置的切换是有序的。
6.3 兼容性问题
- 字符编码 :由于不同的操作系统和环境可能支持不同的字符编码,在处理国际化字符串时,需要确保使用统一的字符编码,如 UTF - 8。特别是在使用属性文件时,要注意其字符编码的限制。
- 字体支持 :为了正确显示不同语言的字符,需要确保系统中安装了相应的字体。在开发过程中,可以进行字体测试,避免出现字符显示乱码的问题。
7. 总结与建议
7.1 总结
-
区域设置
:
Locale类是国际化和本地化的基础,它定义了不同地区的语言、文化和习俗。通过合理使用Locale对象,可以让程序根据用户的偏好进行本地化处理。 -
资源束
:资源束机制是实现国际化的关键,它通过将字符串键映射到本地化资源,使得程序可以方便地在不同区域设置下使用不同的语言和资源。
ListResourceBundle和PropertyResourceBundle是常用的资源束实现方式,同时也可以自定义子类来满足特定需求。
7.2 建议
- 提前规划 :在项目开发初期就应该考虑国际化和本地化的需求,设计合理的架构和数据结构,避免后期进行大规模的修改。
- 使用常量 :将资源束的键字符串定义为常量,可以减少拼写错误,提高代码的可维护性。
- 测试覆盖 :对不同的区域设置进行充分的测试,确保程序在各种语言和文化环境下都能正常工作。
8. 流程图示例
下面是一个资源束查找过程的 mermaid 流程图:
graph TD;
A[开始查找资源束] --> B[指定束名称和区域设置];
B --> C[查找完整名称的束];
C -- 找到 --> D[使用该束];
C -- 未找到 --> E[去掉最后一个组件,缩短名称];
E --> F[重复查找];
F -- 找到 --> D;
F -- 未找到 --> G[检查是否指定了区域设置];
G -- 是 --> H[使用默认区域设置的完整名称重新查找];
H -- 找到 --> D;
H -- 未找到 --> I[仅使用束名称查找];
I -- 找到 --> D;
I -- 未找到 --> J[抛出 MissingBundleException];
G -- 否 --> I;
9. 表格总结
| 技术点 | 描述 | 优点 | 缺点 |
|---|---|---|---|
Locale
| 描述特定的地方,用于本地化行为 | 提供了统一的区域设置表示,方便程序根据不同地区进行调整 | 需要了解 ISO 代码,使用不当可能导致区域设置不准确 |
ListResourceBundle
| 将键列表映射到本地化对象 | 可以定义任意类型的资源对象,继承特性方便复用和扩展 |
需要编写代码实现
getContents
方法,对于简单的字符串资源可能略显繁琐
|
PropertyResourceBundle
| 从文本属性描述中读取资源列表 | 使用简单,易于维护 | 只能定义字符串资源,字符编码有一定限制 |
自定义
ResourceBundle
子类
| 满足特定的资源查找和处理需求 | 灵活性高,可以根据具体业务逻辑进行定制 |
需要实现
handleGetObject
和
getKeys
方法,开发难度相对较大
|
通过以上的介绍和总结,我们对国际化和本地化开发有了更深入的了解。在实际开发中,需要根据具体的需求和场景选择合适的技术和方法,确保程序能够在全球范围内提供良好的用户体验。同时,不断实践和总结经验,提高自己在国际化和本地化开发方面的能力。
希望这些内容对开发者在进行国际化和本地化开发时有所帮助,让软件能够更好地适应不同的语言和文化环境,走向更广阔的市场。
超级会员免费看
895

被折叠的 条评论
为什么被折叠?



