1. Overview

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’.

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

Within Java, we have a fantastic feature at our disposal called the Locale class.

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.

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.

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

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.

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.

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.

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 = new Locale.Builder()
  .setLanguage("fr")
  .setRegion("CA")
  .setVariant("POSIX")
  .setScript("Latn")
  .build();

The String representation of the above Locale is 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.

Otherwise, it will throw 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.

2.3. Constructors

Locale has three constructors:

  • new Locale(String language)
  • new Locale(String language, String country)
  • new Locale(String language, String country, String variant)

A 3-parameter constructor:

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.

However, there’s one drawback of using constructors to create Locale objects – we can’t set extensions and script fields.

2.4. Constants

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 japan = Locale.JAPAN;
Locale japanese = Locale.JAPANESE;

2.5. Language Tags

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.

This is how we can create the UK Locale:

Locale uk = Locale.forLanguageTag("en-UK");

2.6. Available Locales

Even though we can create multiple combinations of Locale objects, we may not be able to use them.

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.

As we use Locales for formatting, the different formatters may have an even smaller set of Locales available that are installed in the 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.

We should remember that the set of available locales is different for various implementations of the Java Platform and various areas of functionality.

The complete list of supported locales is available on the Oracle’s Java SE Development Kit webpage.

2.7. Default Locale

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:

Locale defaultLocale = Locale.getDefault();

Also, we can specify a default Locale by calling a similar setter method:

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.

3. Numbers and Currencies

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.

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.

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

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.

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:

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.

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.

To finish on DateTimeFormatter, let’s see how we can format 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.

For a more in-depth look at Java 8 Date/Time API, we have an existing article here.

4.2. DateFormat and 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.

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:

  • 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:

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

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.

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.

It’s also possible to add a currency symbol. We can see below that the same result can be achieved by using DateFormatSymbol class:

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.

To customize SimpleDataFormat, we can use 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

Finally, the crucial part of internationalization in the JVM is the Resource Bundle mechanism.

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.

7. Conclusion

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.

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.

The source code of this tutorial, with full examples, can be found over on GitHub.

Course – LS (cat=Java)

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.