1. Overview
1.概述
Internationalization is a process of preparing an application to support various linguistic, regional, cultural or political-specific data. It is an essential aspect of any modern multi-language application.
国际化是一个准备应用程序以支持各种语言、区域、文化或政治特定数据的过程。它是任何现代多语言应用程序的一个重要方面。
For further reading, we should know that there’s a very popular abbreviation (probably more popular than the actual name) for internationalization – i18n due to the 18 letters between ‘i’ and ‘n’.
为了进一步阅读,我们应该知道,对于国际化,有一个非常流行的缩写(可能比实际名称更流行)–i18n,由于’i’和’n’之间有18个字母。
It’s crucial for present-day enterprise programs to serve people from different parts of the world or multiple cultural areas. Distinct cultural or language regions don’t only determine language-specific descriptions but also currency, number representation and even divergent date and time composition.
对于现今的企业项目来说,为来自世界不同地区或多个文化区域的人服务是至关重要的。不同的文化或语言区域不仅决定了语言的具体描述,也决定了货币、数字表示,甚至是不同的日期和时间组成。
For instance, let’s focus on country-specific numbers. They have various decimal and thousand separators:
例如,让我们专注于特定国家的数字。它们有各种小数和千位分隔符。
- 102,300.45 (United States)
- 102 300,45 (Poland)
- 102.300,45 (Germany)
There are different date formats as well:
也有不同的日期格式。
- Monday, January 1, 2018 3:20:34 PM CET (United States)
- lundi 1 janvier 2018 15 h 20 CET (France).
- 2018年1月1日 星期一 下午03时20分34秒 CET (China)
What’s more, different countries have unique currency symbols:
更重要的是,不同国家有独特的货币符号。
- £1,200.60 (United Kingdom)
- € 1.200,60 (Italy)
- 1 200,60 € (France)
- $1,200.60 (United States)
An important fact to know is that even if countries have the same currency and currency symbol – like France and Italy – the position of their currency symbol could be different.
要知道的一个重要事实是,即使各国拥有相同的货币和货币符号–如法国和意大利–其货币符号的位置也可能不同。
2. Localization
2.定位
Within Java, we have a fantastic feature at our disposal called the Locale class.
在Java中,我们有一个神奇的功能,叫做Locale类,供我们使用。
It allows us to quickly differentiate between cultural locales and format our content appropriately. It’s essential for within the internationalization process. The same as i18n, Localization also has its abbreviation – l10n.
它使我们能够迅速区分不同的文化区域,并适当地安排我们的内容。在国际化进程中,它是必不可少的。与i18n一样,本地化也有其缩写–l10n.。
The main reason for using Locale is that all required locale-specific formatting can be accessed without recompilation. An application can handle multiple locales at the same time so supporting new language is straightforward.
使用Locale的主要原因是,所有需要的特定于本地的格式都可以被访问,而无需重新编译。一个应用程序可以同时处理多个地区性语言,因此支持新的语言是很直接的。
Locales are usually represented by language, country, and variant abbreviation separated by an underscore:
地域通常由语言、国家和变体缩写表示,用下划线隔开。
- de (German)
- it_CH (Italian, Switzerland)
- en_US_UNIX (United State, UNIX platform)
2.1. Fields
2.1.领域
We have already learned that Locale consists of language code, country code, and variant. There are two more possible fields to set: script and extensions.
我们已经知道,Locale由语言代码、国家代码和变体组成。还有两个可能的字段需要设置:脚本和扩展。
Let’s have a look through a list of fields and see what the rules are:
让我们看一下字段的清单,看看有什么规则。
- Language can be an ISO 639 alpha-2 or alpha-3 code or registered language subtag.
- Region (Country) is ISO 3166 alpha-2 country code or UN numeric-3 area code.
- Variant is a case-sensitive value or set of values specifying a variation of a Locale.
- Script must be a valid ISO 15924 alpha-4 code.
- Extensions is a map which consists of single character keys and String values.
The IANA Language Subtag Registry contains possible values for language, region, variant and script.
IANA Language Subtag Registry包含language、region、variant和script的可能值。
There is no list of possible extension values, but the values must be well-formed BCP-47 subtags. The keys and values are always converted to lower case.
没有可能的扩展值的列表,但这些值必须是格式良好的BCP-47子标签。键和值总是被转换为小写。
2.2. Locale.Builder
2.2.Locale.Builder
There are several ways of creating Locale objects. One possible way utilizes Locale.Builder. Locale.Builder has five setter methods which we can use to build the object and at the same time validate those values:
有几种创建Locale对象的方法。一种可能的方法是利用Locale.Builder。Locale.Builder有五个setter方法,我们可以用它来建立对象,同时验证这些值。
Locale locale = new Locale.Builder()
.setLanguage("fr")
.setRegion("CA")
.setVariant("POSIX")
.setScript("Latn")
.build();
The String representation of the above Locale is fr_CA_POSIX_#Latn.
上述Locale的String表示为fr_CA_POSIX_#Latn。
It’s good to know that setting ‘variant’ may be a little bit tricky as there’s no official restriction on variant values, although the setter method requires it to be BCP-47 compliant.
很高兴知道设置’variant’可能有点棘手,因为对variant值没有官方限制,尽管setter方法要求它符合BCP-47。
Otherwise, it will throw IllformedLocaleException.
否则,它将抛出IllformedLocaleException。
In the case where we need to use a value that doesn’t pass validation, we can use Locale constructors as they don’t validate values.
在我们需要使用一个不通过验证的值的情况下,我们可以使用Locale构造函数,因为它们不验证值。
2.3. Constructors
2.3.构造函数
Locale has three constructors:
Locale有三个构造函数。
- new Locale(String language)
- new Locale(String language, String country)
- new Locale(String language, String country, String variant)
A 3-parameter constructor:
一个3参数的构造函数。
Locale locale = new Locale("pl", "PL", "UNIX");
A valid variant must be a String of 5 to 8 alphanumerics or single numeric followed by 3 alphanumerics. We can only apply “UNIX” to the variant field only via constructor as it doesn’t meet those requirements.
一个有效的variant必须是由5到8个字母数字组成的String或由3个字母数字组成的单一数字。我们只能通过构造函数将 “UNIX “应用于variant字段,因为它不符合这些要求。
However, there’s one drawback of using constructors to create Locale objects – we can’t set extensions and script fields.
然而,使用构造函数来创建Locale对象有一个缺点 – 我们不能设置扩展和脚本字段。
2.4. Constants
2.4.常数
This is probably the simplest and the most limited way of getting Locales. The Locale class has several static constants which represent the most popular country or language:
这可能是获取Locale的最简单和最有限的方法。Locale类有几个静态常数,代表最流行的国家或语言。
Locale japan = Locale.JAPAN;
Locale japanese = Locale.JAPANESE;
2.5. Language Tags
2.5.语言标签
Another way of creating Locale is calling the static factory method forLanguageTag(String languageTag). This method requires a String that meets the IETF BCP 47 standard.
创建Locale的另一种方法是调用静态工厂方法forLanguageTag(String languageTag)。这个方法需要一个符合IETF BCP 47标准的String。
This is how we can create the UK Locale:
这就是我们如何创建英国Locale。
Locale uk = Locale.forLanguageTag("en-UK");
2.6. Available Locales
2.6.可用的地域
Even though we can create multiple combinations of Locale objects, we may not be able to use them.
即使我们可以创建多种组合的Locale对象,我们也可能无法使用它们。
An important note to be aware of is that the Locales on a platform are dependent on those that have been installed within the Java Runtime.
需要注意的是,一个平台上的Locales取决于那些已经安装在Java Runtime中的Locales。
As we use Locales for formatting, the different formatters may have an even smaller set of Locales available that are installed in the Runtime.
由于我们使用Locales进行格式化,不同的格式化器可能有一套更小的Locales可用,它们被安装在Runtime中。
Let’s check how to retrieve arrays of available locales:
让我们看看如何检索可用的语言数组。
Locale[] numberFormatLocales = NumberFormat.getAvailableLocales();
Locale[] dateFormatLocales = DateFormat.getAvailableLocales();
Locale[] locales = Locale.getAvailableLocales();
After that, we can check whether our Locale resides among available Locales.
之后,我们可以检查我们的Locale是否存在于可用的Locale中。
We should remember that the set of available locales is different for various implementations of the Java Platform and various areas of functionality.
我们应该记住,可用的语言集对于Java平台的各种实现和各种功能领域是不同的。
The complete list of supported locales is available on the Oracle’s Java SE Development Kit webpage.
支持的地区性的完整列表可在Oracle的Java SE开发工具包网页上找到。
2.7. Default Locale
2.7.默认的地区设置
While working with localization, we might need to know what the default Locale on our JVM instance is. Fortunately, there’s a simple way to do that:
在进行本地化工作时,我们可能需要知道我们JVM实例上的默认Locale是什么。幸运的是,有一个简单的方法可以做到这一点。
Locale defaultLocale = Locale.getDefault();
Also, we can specify a default Locale by calling a similar setter method:
另外,我们可以通过调用类似的setter方法来指定一个默认的Locale。
Locale.setDefault(Locale.CANADA_FRENCH);
It’s especially relevant when we’d like to create JUnit tests that don’t depend on a JVM instance.
当我们想创建不依赖JVM实例的JUnit测试时,这一点尤其重要。
3. Numbers and Currencies
3.数字和货币
This section refers to numbers and currencies formatters that should conform to different locale-specific conventions.
本节提到的数字和货币格式化应符合不同地方的特定惯例。
To format primitive number types (int, double) as well as their object equivalents (Integer, Double), we should use NumberFormat class and its static factory methods.
为了格式化原始数字类型(int, double)以及它们的对象等价物(Integer, Double),我们应该使用NumberFormat类和其静态工厂方法。
Two methods are interesting for us:
有两种方法对我们来说很有意思。
- NumberFormat.getInstance(Locale locale)
- NumberFormat.getCurrencyInstance(Locale locale)
Let’s examine a sample code:
让我们来研究一个示例代码。
Locale usLocale = Locale.US;
double number = 102300.456d;
NumberFormat usNumberFormat = NumberFormat.getInstance(usLocale);
assertEquals(usNumberFormat.format(number), "102,300.456");
As we can see it’s as simple as creating Locale and using it to retrieve NumberFormat instance and formatting a sample number. We can notice that the output includes locale-specific decimal and thousand separators.
我们可以看到,这就像创建Locale并使用它来检索NumberFormat实例和格式化一个样本数字一样简单。我们可以注意到,输出包括当地特定的小数和千位分隔符。
Here’s another example:
下面是另一个例子。
Locale usLocale = Locale.US;
BigDecimal number = new BigDecimal(102_300.456d);
NumberFormat usNumberFormat = NumberFormat.getCurrencyInstance(usLocale);
assertEquals(usNumberFormat.format(number), "$102,300.46");
Formatting a currency involves the same steps as formatting a number. The only difference is that the formatter appends currency symbol and round decimal part to two digits.
格式化货币的步骤与格式化数字的步骤相同。唯一的区别是,格式化器附加了货币符号,并将小数部分四舍五入到两位数。
4. Date and Time
4.日期和时间
Now, we’re going to learn about dates and times formatting which’s probably more complex than formatting numbers.
现在,我们要学习日期和时间的格式化,这可能比数字的格式化更复杂。
First of all, we should know that date and time formatting significantly changed in Java 8 as it contains completely new Date/Time API. Therefore, we’re going to look through different formatter classes.
首先,我们应该知道,日期和时间的格式化在Java 8中发生了重大变化,因为它包含了全新的Date/Time API。因此,我们将通过不同的格式化类来看看。
4.1. DateTimeFormatter
4.1.DateTimeFormatter
Since Java 8 was introduced, the main class for localizing of dates and times is the DateTimeFormatter class. It operates on classes that implement TemporalAccessor interface, for example, LocalDateTime, LocalDate, LocalTime or ZonedDateTime. To create a DateTimeFormatter we must provide at least a pattern, and then Locale. Let’s see an example code:
自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));
We can see that after retrieving DateTimeFormatter all we have to do is to call the format() method.
我们可以看到,在检索到DateTimeFormatter之后,我们要做的就是调用format()方法。
For a better understanding, we should familiarize with possible pattern letters.
为了更好地理解,我们应该熟悉可能的模式字母。
Let’s look at letters for example:
让我们来看看字母的例子。
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
All possible pattern letters with explanation can be found in the Java documentation of DateTimeFormatter. It’s worth to know that final value depends on the number of symbols. There is ‘MMMM’ in the example which prints the full month name whereas a single ‘M’ letter would give the month number without a leading 0.
所有可能的模式字母和解释可以在DateTimeFormatter的Java文档中找到。 值得一提的是,最终值取决于符号的数量。例子中有’MMMM’,它打印的是完整的月份名称,而一个单一的’M’字母将给出没有前导0的月份编号。
To finish on DateTimeFormatter, let’s see how we can format LocalizedDateTime:
为了完成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);
In order to format LocalizedDateTime, we can use the ofLocalizedDateTime(FormatStyle dateTimeStyle) method and provide a predefined FormatStyle.
为了格式化LocalizedDateTime,我们可以使用ofLocalizedDateTime(FormatStyle dateTimeStyle)方法并提供一个预定义的FormatStyle。
For a more in-depth look at Java 8 Date/Time API, we have an existing article here.
要更深入地了解 Java 8 Date/Time API,我们有一篇现有的文章这里。
4.2. DateFormat and SimpleDateFormatter
4.2.DateFormat和SimpleDateFormatter
As it’s still common to work on projects that make use of Dates and Calendars, we’ll briefly introduce capabilities of formatting dates and times with DateFormat and SimpleDateFormat classes.
由于使用Dates和Calendars的项目仍然很常见,我们将简要介绍使用DateFormat和SimpleDateFormat类来格式化日期和时间的能力。
Let’s analyze abilities of the first one:
让我们分析一下第一个人的能力。
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 works with Dates and has three useful methods:
DateFormat与Dates一起工作,有三个有用的方法。
- getDateTimeInstance
- getDateInstance
- getTimeInstance
All of them take predefined values of DateFormat as a parameter. Each method is overloaded, so passing Locale is possible as well. If we want to use a custom pattern, as it’s done in DateTimeFormatter, we can use SimpleDateFormat. Let’s see a short code snippet:
所有这些方法都接受预定义的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. Customization
5.定制化
Due to some good design decisions, we’re not tied to a locale-specific formatting pattern, and we can configure almost every detail to be fully satisfied with an output.
由于一些很好的设计决定,我们没有被一个特定于本地的格式化模式所束缚,而且我们可以配置几乎每一个细节,以便对一个输出结果完全满意。
To customize number formatting, we can use DecimalFormat and DecimalFormatSymbols.
为了定制数字格式,我们可以使用DecimalFormat和DecimalFormatSymbols。
Let’s consider a short example:
让我们考虑一个简短的例子。
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");
The DecimalFormat documentation shows all possible pattern characters. All we need to know now is that “000000000.000” determines leading or trailing zeros, ‘,’ is a thousand separator, and ‘.’ is decimal one.
DecimalFormat文档显示了所有可能的模式字符。我们现在需要知道的是,”00000000000.000 “决定了前导零或尾随零,’,’是千位分隔符,’.’是小数点后一位。
It’s also possible to add a currency symbol. We can see below that the same result can be achieved by using DateFormatSymbol class:
也可以添加一个货币符号。我们可以在下面看到,通过使用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^2300@46");
As we can see, DecimalFormatSymbols class enables us to specify any number formatting we can imagine.
正如我们所看到的,DecimalFormatSymbols类使我们能够指定任何我们能够想象的数字格式。
To customize SimpleDataFormat, we can use DateFormatSymbols.
为了定制SimpleDataFormat,我们可以使用DateFormatSymbols。
Let’s see how simple is a change of day names:
让我们看看改变日名是多么简单。
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. Resource Bundles
6.资源捆绑
Finally, the crucial part of internationalization in the JVM is the Resource Bundle mechanism.
最后,JVM中国际化的关键部分是Resource Bundle机制。
The purpose of a ResourceBundle is to provide an application with localized messages/descriptions which can be externalized to the separate files. We cover usage and configuration of the Resource Bundle in one of our previous articles – guide to the Resource Bundle.
ResourceBundle的目的是为应用程序提供本地化的消息/描述,这些消息/描述可以被外化为独立的文件。我们在之前的一篇文章中介绍了资源包的用法和配置–资源包指南。
7. Conclusion
7.结语
Locales and the formatters that utilize them are tools that help us create an internationalized application. These tools allow us to create an application which can dynamically adapt to the user’s linguistic or cultural settings without multiple builds or even needing to worry about whether Java supports the Locale.
Locales和利用它们的格式化器是帮助我们创建国际化应用程序的工具。这些工具使我们能够创建一个能够动态地适应用户语言或文化设置的应用程序,而不需要多次构建,甚至不需要担心Java是否支持Locale。
In a world where a user can be anywhere and speak any language, the ability to apply these changes means our applications can be more intuitive and understandable by more users globally.
在一个用户可以在任何地方,讲任何语言的世界里,应用这些变化的能力意味着我们的应用程序可以更加直观,让全球更多的用户能够理解。
When working with Spring Boot applications, we also have a convenient article for Spring Boot Internationalization.
在处理Spring Boot应用程序时,我们也有一篇关于Spring Boot国际化的便利文章。
The source code of this tutorial, with full examples, can be found over on GitHub.
本教程的源代码,包括完整的示例,可以在GitHub上找到over。