Java 8中的国际化和本土化
1.概述
国际化是一个准备应用程序的过程,以支持各种语言、区域、文化或政治特定的数据。它是任何现代多语言应用程序的一个重要方面。
为了进一步阅读,我们应该知道国际化有一个非常流行的缩写(可能比实际名称更流行) – i18n,因为“i”和“n”之间有 18 个字母。
对于现今的企业项目来说,为来自世界不同地区或多个文化区域的人服务是至关重要的。不同的文化或语言区域不仅决定了语言的具体描述,还决定了货币、数字表示,甚至是不同的日期和时间组成。
例如,让我们专注于特定国家的数字。它们有各种小数和千位分隔符:
- 102,300.45 (美国)
- 102 300,45 (波兰)
- 102,300.45 (德国)
也有不同的日期格式:
- Monday, January 1, 2018 3:20:34 PM CET (美国)
- lundi 1 janvier 2018 15 h 20 CET (法国).
- 2018年1月1日 星期一 下午03时20分34秒 CET (中国)
更重要的是,不同的国家都有独特的货币符号:
- £1,200.60 (英国)
- € 1.200,60 (意大利)
- 1 200,60 € (法国)
- $1,200.60 (美国)
需要知道的一个重要事实是,即使各国拥有相同的货币和货币符号 – 如法国和意大利 – 其货币符号的位置也可能是不同的。
2.定位
在Java中,我们有一个神奇的功能,叫做Locale类,供我们使用。
它使我们能够迅速区分不同的文化区域,并适当地安排我们的内容。在国际化进程中,它是必不可少的。与i18n一样,本地化也有其缩写 – l10n。
使用Locale的主要原因是,所有需要的特定于本地的格式都可以被访问,而无需重新编译。一个应用程序可以同时处理多个地区性语言,因此支持新的语言是很直接的。
地域通常由语言、国家和用下划线隔开的变体缩写来表示:
- de (德语)
- it_CH (意大利语, 瑞士)
- en_US_UNIX (美国,UNIX平台)
2.1.字段
我们已经了解到,Locale包括语言代码、国家代码和变体组成。还有两个可能的字段需要设置:script 和 extension。
让我们看一下字段的清单,看看有什么规则:
- Language可以是ISO 639 alpha-2或alpha-3代码或已注册的语言子标签。
- Region(国家)是ISO 3166 alpha-2国家代码或UN numeric-3地区代码。
- Variant是一个区分大小写的值或一组值,用于指定Locale设置的变体。
- Script必须是一个有效的ISO 15924 alpha-4代码。
- Extensions是一个由单字符键和String值组成的映射。
IANA Language Subtag Registry包含了language、region、variant和script的可能值。
没有可能的extension值的列表,但这些值必须是格式良好的BCP-47子标签。键和值总是被转换为小写。
2.2. Locale.Builder
有几种创建Locale对象的方法。一种可能的方法是利用Locale.Builder。Locale.Builder有五个setter方法,我们可以利用这些方法来建立对象,同时验证这些值:
Locale locale = new Locale.Builder()
.setLanguage("fr")
.setRegion("CA")
.setVariant("POSIX")
.setScript("Latn")
.build();
上述String的Locale表示为fr_CA_POSIX_#Latn。
好在设置"variant"可能有点麻烦,因为官方对变体值没有限制,虽然setter方法要求它符合BCP-47的要求。
否则,它将抛出IllformedLocaleException。
在我们需要使用一个不通过验证的值的情况下,我们可以使用Locale构造函数,因为它们不验证值。
2.3.构建函数
Locale有三个构造函数:
- new Locale(String language)
- new Locale(String language, String country)
- new Locale(String language, String country, String variant)
一个3个参数的构造函数:
Locale locale = new Locale("pl", "PL", "UNIX");
一个有效的variant必须是5到8个字母数字的String,或者是单个数字后跟3个字母数字。我们只能通过构造函数将“UNIX”应用于variant字段,因为它不符合这些要求。
然而,使用构造函数来创建Locale对象有一个缺点–我们不能设置extension和script字段。
2.4.常量
这可能是获得Locale的最简单和最有限的方法。Locale类有几个静态常数,代表最流行的国家或语言:
Locale japan = Locale.JAPAN;
Locale japanese = Locale.JAPANESE;
2.5.语言标签
创建Locale的另一种方法是调用静态工厂方法forLanguageTag(String languageTag)。这个方法需要一个符合IETF BCP 47标准的String。
这就是我们如何创建英国Locale的方法:
Locale uk = Locale.forLanguageTag("en-UK");
2.6.可用的Locale
即使我们可以创建多种组合的Locale对象,我们也可能无法使用它们。
需要注意的是,一个平台上的Locale取决于那些已经安装在Java Runtime中的Locale。
由于我们使用Locale进行格式化,不同的格式化器可能有一套更小的Locale可用,这些都安装在Runtime中。
让我们来看看如何检索可用的地区性数组:
Locale[] numberFormatLocales = NumberFormat.getAvailableLocales();
Locale[] dateFormatLocales = DateFormat.getAvailableLocales();
Locale[] locales = Locale.getAvailableLocales();
之后,我们可以检查我们的Locale是否存在于可用的Locales中。
我们应该记住,对于Java平台的各种实现和各种功能各个领域,可用的语言环境集是不同的 。
支持的语言环境的完整列表可在Oracle的Java SE开发工具包的网页上找到。
2.7.默认Locale
在进行本地化工作时,我们可能需要知道我们JVM实例上的默认Locale是什么。幸运的是,有一个简单的方法可以做到这一点:
Locale defaultLocale = Locale.getDefault();
此外,我们还可以通过调用类似的setter方法来指定一个默认的Locale:
Locale.setDefault(Locale.CANADA_FRENCH);
当我们想创建不依赖于JVM实例的JUnit测试时,这一点尤其重要。
3.数字和货币
本节提到了数字和货币的格式化,它们应该符合不同地方的特定惯例。
为了格式化原始数字类型(int, double)以及它们的等效对象(Integer, Double),我们应该使用NumberFormat类和它的静态工厂方法。
有两种方法对我们来说是很有意思的:
- NumberFormat.getInstance(Locale locale)
- NumberFormat.getCurrencyInstance(Locale locale)
让我们来看看一个例子的代码:
Locale usLocale = Locale.US;
double number = 102300.456d;
NumberFormat usNumberFormat = NumberFormat.getInstance(usLocale);
assertEquals(usNumberFormat.format(number), "102,300.456");
我们可以看到,这就像创建Locale并使用它来检索NumberFormat实例和格式化一个样本数字一样简单。我们可以注意到,输出包括当地特定的小数和千位分隔符。
下面是另一个例子:
Locale usLocale = Locale.US;
BigDecimal number = new BigDecimal(102_300.456d);
NumberFormat usNumberFormat = NumberFormat.getCurrencyInstance(usLocale);
assertEquals(usNumberFormat.format(number), "$102,300.46");
格式化货币的步骤与格式化数字的步骤相同。唯一不同的是,格式化程序附加了货币符号,并将小数部分四舍五入到两位数。
4.日期和时间
现在,我们要学习日期和时间的格式化,这可能比数字的格式化更复杂。
首先,我们应该知道,日期和时间的格式化在Java 8中发生了重大变化,因为它包含了全新的Date/Time API。因此,我们将通过不同的格式化类来看看。
4.1. DateTimeFormatter
自Java 8推出以来,日期和时间本地化的主要类是DateTimeFormatter类。它对实现TemporalAccessor接口的类进行操作,例如,LocalDateTime、LocalDate、LocalTime或ZonedDateTime。要创建一个DateTimeFormatter我们必须至少提供一个模式,然后是Locale。让我们看看一个示例代码:
Locale.setDefault(Locale.US);
LocalDateTime localDateTime = LocalDateTime.of(2018, 1, 1, 10, 15, 50, 500);
String pattern = "dd-MMMM-yyyy HH:mm:ss.SSS";
DateTimeFormatter defaultTimeFormatter = DateTimeFormatter.ofPattern(pattern);
DateTimeFormatter deTimeFormatter = DateTimeFormatter.ofPattern(pattern, Locale.GERMANY);
assertEquals(
"01-January-2018 10:15:50.000",
defaultTimeFormatter.format(localDateTime));
assertEquals(
"01-Januar-2018 10:15:50.000",
deTimeFormatter.format(localDateTime));
我们可以看到,在检索了DateTimeFormatter之后,我们要做的就是调用format()方法。
为了更好地理解,我们应该熟悉可能的模式化字母。
让我们来看看字母的例子:
Symbol Meaning Presentation Examples
------ ------- ------------ -------
y year-of-era year 2004; 04
M/L month-of-year number/text 7; 07; Jul; July; J
d day-of-month number 10
H hour-of-day (0-23) number 0
m minute-of-hour number 30
s second-of-minute number 55
S fraction-of-second fraction 978
所有可能的带解释的模式字母都可以在 DateTimeFormatter 的 Java 文档。 值得一提的是,最终值取决于符号的数量。示例中有“MMMM”,它会打印完整的月份名称,而单个“M”字母会给出没有前导 0 的月份编号。
为了完成DateTimeFormatter的工作,让我们看看如何格式化LocalizedDateTime:
LocalDateTime localDateTime = LocalDateTime.of(2018, 1, 1, 10, 15, 50, 500);
ZoneId losAngelesTimeZone = TimeZone.getTimeZone("America/Los_Angeles").toZoneId();
DateTimeFormatter localizedTimeFormatter = DateTimeFormatter
.ofLocalizedDateTime(FormatStyle.FULL);
String formattedLocalizedTime = localizedTimeFormatter.format(
ZonedDateTime.of(localDateTime, losAngelesTimeZone));
assertEquals("Monday, January 1, 2018 10:15:50 AM PST", formattedLocalizedTime);
为了格式化LocalizedDateTime,我们可以使用ofLocalizedDateTime(FormatStyle dateTimeStyle)方法并提供一个预定义的FormatStyle。
要更深入地了解 Java 8 Date/Time API,我们有一篇现有的文章,在这里。
4.2.DateFormat和SimpleDateFormatter
由于使用Date和Calendar的项目仍然很常见,我们将简要介绍用DateFormat 和SimpleDateFormat类来格式化日期和时间的功能。
让我们来分析一下第一个的能力:
GregorianCalendar gregorianCalendar = new GregorianCalendar(2018, 1, 1, 10, 15, 20);
Date date = gregorianCalendar.getTime();
DateFormat ffInstance = DateFormat.getDateTimeInstance(
DateFormat.FULL, DateFormat.FULL, Locale.ITALY);
DateFormat smInstance = DateFormat.getDateTimeInstance(
DateFormat.SHORT, DateFormat.MEDIUM, Locale.ITALY);
assertEquals("giovedì 1 febbraio 2018 10.15.20 CET", ffInstance.format(date));
assertEquals("01/02/18 10.15.20", smInstance.format(date));
DateFormat与Date一起使用,并有三个有用的方法:
- getDateTimeInstance
- getDateInstance
- getTimeInstance
所有这些方法都接受DateFormat 的预定义值作为参数。每个方法都是重载的,所以传递Locale也是可能的。如果我们想使用一个自定义的模式,像DateTimeFormatter那样,我们可以使用SimpleDateFormat。让我们看看一个简短的代码片断:
GregorianCalendar gregorianCalendar = new GregorianCalendar(
2018, 1, 1, 10, 15, 20);
Date date = gregorianCalendar.getTime();
Locale.setDefault(new Locale("pl", "PL"));
SimpleDateFormat fullMonthDateFormat = new SimpleDateFormat(
"dd-MMMM-yyyy HH:mm:ss:SSS");
SimpleDateFormat shortMonthsimpleDateFormat = new SimpleDateFormat(
"dd-MM-yyyy HH:mm:ss:SSS");
assertEquals(
"01-lutego-2018 10:15:20:000", fullMonthDateFormat.format(date));
assertEquals(
"01-02-2018 10:15:20:000" , shortMonthsimpleDateFormat.format(date));
5.定制
由于一些很好的设计决定,我们没有被一个特定于本地的格式化模式所束缚,而且我们可以配置几乎每一个细节,以便对一个输出结果完全满意。
为了自定义数字格式,我们可以使用DecimalFormat和DecimalFormatSymbols。
让我们来考虑一个简短的例子:
Locale.setDefault(Locale.FRANCE);
BigDecimal number = new BigDecimal(102_300.456d);
DecimalFormat zeroDecimalFormat = new DecimalFormat("000000000.0000");
DecimalFormat dollarDecimalFormat = new DecimalFormat("$###,###.##");
assertEquals(zeroDecimalFormat.format(number), "000102300,4560");
assertEquals(dollarDecimalFormat.format(number), "$102 300,46");
DecimalFormat文档显示了所有可能的模式字符。我们现在需要知道的是,“0000000.000”决定了前导零或尾随零,‘,'是千位分隔符,‘.'是小数点一。
也可以添加一个货币符号。我们可以在下面看到,通过使用DateFormatSymbol类,可以达到同样的效果:
Locale.setDefault(Locale.FRANCE);
BigDecimal number = new BigDecimal(102_300.456d);
DecimalFormatSymbols decimalFormatSymbols = DecimalFormatSymbols.getInstance();
decimalFormatSymbols.setGroupingSeparator('^');
decimalFormatSymbols.setDecimalSeparator('@');
DecimalFormat separatorsDecimalFormat = new DecimalFormat("$###,###.##");
separatorsDecimalFormat.setGroupingSize(4);
separatorsDecimalFormat.setCurrency(Currency.getInstance(Locale.JAPAN));
separatorsDecimalFormat.setDecimalFormatSymbols(decimalFormatSymbols);
assertEquals(separatorsDecimalFormat.format(number), "$10^[email protected]");
正如我们所看到的,DecimalFormatSymbols类使我们能够指定我们所能想象的任何数字格式化。
为了定制SimpleDataFormat,我们可以使用DateFormatSymbols。
让我们来看看改变日名有多简单:
Date date = new GregorianCalendar(2018, 1, 1, 10, 15, 20).getTime();
Locale.setDefault(new Locale("pl", "PL"));
DateFormatSymbols dateFormatSymbols = new DateFormatSymbols();
dateFormatSymbols.setWeekdays(new String[]{"A", "B", "C", "D", "E", "F", "G", "H"});
SimpleDateFormat newDaysDateFormat = new SimpleDateFormat(
"EEEE-MMMM-yyyy HH:mm:ss:SSS", dateFormatSymbols);
assertEquals("F-lutego-2018 10:15:20:000", newDaysDateFormat.format(date));
6.资源包
最后,在JVM中,国际化的关键部分是Resource Bundle 机制。
资源包ResourceBundle的目的是为应用程序提供本地化的信息/描述,这些信息/描述可以被外化为独立的文件。我们在之前的一篇文章 – 资源包指南中介绍了资源包的使用和配置。
7.结语
Locale和利用它们的格式化程序是帮助我们创建一个国际化的应用程序的工具。这些工具使我们能够创建一个能够动态地适应用户的语言或文化设置的应用程序,而不需要多次构建,甚至不需要担心Java是否支持Locale。
在一个用户可以在任何地方说任何语言的世界里,应用这些变化的能力意味着我们的应用程序可以更加直观,并能被全球更多的用户所理解。
在处理Spring Boot应用程序时,我们还有一篇关于Spring Boot国际化的文章,非常方便。
本教程的源代码,包括完整的例子,可以在GitHub上找到。