ICU4J
ICU4J provides improvements over JDK internationalization capabilities and is highly recommended
for localizing a Java application.
The "ICU" message format pattern have been adopted by many other libraries in a variety of languages.
This guide covers the most common usages and showcases some differences between languages when it comes to formatting numbers and dates as well as handling plurals and gender in messages.
Check the installation guides to add the dependency and the ICU documentation for more information.
Basic formatting
ICU4J support both named and numbered arguments (see
differences with JDK).
L10nMessages always references placeholders by their name as a "string". A numbered argument {0}
is referenced by the key "0".
It is recommended to always use named arguments since it is self documenting and gives more context to translators, leading to better translations.
welcome_user=Welcome {username}!
welcome_user_numbered=Welcome {0}!
welcome_user_numbered=Bienvenue {username}!
class Example {
{
L10nMessages<Messages> m = L10nMessages.builder(Messages.class).build();
System.out.println(m.format(welcome_user, "username", "Mary"));
// Welcome Mary!
System.out.println(m.format(welcome_user_numbered, "0", "Mary"));
// Welcome Mary!
}
{
L10nMessages<Messages> m = L10nMessages.builder(Messages.class).locale(Locale.FRANCE).build();
System.out.println(m.format(welcome_user, "username", "Mary"));
// Bienvenue Mary!
}
}
Numbers
For number formatting, use the number argument. Optionally, add a pre-defined argStyle such as
integer, percent, currency to customize output.
numbers=Examples: {basic, number}, {basic, number, integer}, {percentage, number, percent}, \
{currency, number, currency}
numbers=Exemples: {basic, number}, {basic, number, integer}, {percentage, number, percent}, \
{currency, number, currency}
class Example {
{
L10nMessages<Messages> m = L10nMessages.builder(Messages.class).build();
System.out.println(m.format(numbers, "basic", 1000, "percentage", 0.5, "currency", 50));
// Examples: 1,000.1, 1,000, 50%, $50.00
}
{
L10nMessages<Messages> m = L10nMessages.builder(Messages.class).locale(Locale.FRANCE).build();
System.out.println(m.format(numbers, "basic", 1000, "percentage", 0.5, "currency", 50));
// Exemples: 1 000,1, 1 000, 50 %, 50,00 €
}
}
Dates and Times
For date formatting, use the date argument and for time formatting the time argument.
Optionally, add pre-defined argStyle such as short, medium, long, full to customize
output.
today_is=Today is: {todayDate, date}, it is: {todayDate, time}
today_is_short=Today is: {todayDate, date, short}, it is: {todayDate, time, short}
today_is=Aujourd'hui c'est le: {todayDate, date}, il est: {todayDate, time}
today_is_short=Aujourd'hui c'est le: {todayDate, date, short}, il est: {todayDate, time, short}
class Example {
{
L10nMessages<Messages> m = L10nMessages.builder(Messages.class).build();
System.out.println(m.format(today_is, "todayDate", new Date()));
// Today is: Jun 22, 2022, it is: 8:36:08 PM
System.out.println(m.format(today_is_short, "todayDate", new Date()));
// Today is: 6/22/22, it is: 8:36 PM
}
{
L10nMessages<Messages> m = L10nMessages.builder(Messages.class).locale(Locale.FRANCE).build();
System.out.println(m.format(today_is, "todayDate", new Date()));
// Aujourd'hui c'est le: 22 juin 2022, il est: 20:36:08
System.out.println(m.format(today_is_short, "todayDate", new Date()));
// Aujourd'hui c'est le: 22/06/2022, il est: 20:36
}
}
Skeletons
Further customization for number, date and time formatting can be done using skeletons. Here is just a basic example to show the syntax. For more information, see number skeletons and date skeletons.
skeletons={number, number, :: .00 currency/CAD} - {date, date, :: MMMMdjmm}
class Example {
{
L10nMessages<Messages> m = L10nMessages.builder(Messages.class).build();
System.out.println(m.format(skeletons, "number", 1000.1234, "date", new Date()));
// CA$1,000.12 - June 22, 9:29:42 PM
}
{
L10nMessages<Messages> m = L10nMessages.builder(Messages.class).locale(Locale.FRANCE).build();
System.out.println(m.format(skeletons, "number", 1000.1234, "date", new Date()));
// 1 000,12 $CA - 22 juin, 21:29:42
}
}
Pluralization - Message with quantity
Consider the following sentence: You have 5 messages where the quantity: 5 varies at runtime:
0, 1, 5, 100, etc.
The ending of the noun message will change based on the quantity:
You have 0 messagesYou have 1 messageYou have 5 messagesYou have 100 messages
From this, we can identify the 2 forms (singular and plural) needed to render all variations:
You have {numberMessage} messageYou have {numberMessage} messages
Sometimes, it is also preferable to customize the output for a specific quantity, typically 0. For
example, You have no messages reads better than You have 0 messages.
The plural argument is a special construct provided by ICU to list the different plural forms of
a message in a single string and to customize them based on the language. It also has a mechanism to
specify a message for a given quantity.
Many languages will need adaptation to the number of plural forms. Some require a single form only, while others may need up to six forms to cover all the linguistic requirements. The mapping from quantity to plural form is performed by the library at runtime based on the CLDR plural rules .
French uses 2 forms like English but the singular is used for 0
Vous avez 0 messageVous avez 1 messageVous avez 5 messages
Japanese uses a single form:
メッセージが 0 件メッセージが 1 件メッセージが 5 件
Russian needs 4 forms and is cyclic:
У вас есть 0 сообщенийУ вас есть 1 сообщениеУ вас есть 2 сообщенияУ вас есть 5 сообщенийУ вас есть 21 сообщениеУ вас есть 22 сообщенияУ вас есть 25 сообщений
A common pitfall is to skip pluralization when the quantity is know to be greater than one. While this work in English, it will prevent proper localization in some other languages like Russian.
Always use a plural argument when working with a variable quantity.
Mapping to CLDR forms
When writing the source string in English, the singular string You have {numberMessage} message
maps to CLDR's one form and the plural string You have {numberMessage} messages maps to CLDR's
other form.
Some languages like Japanese will use a single form (other) in the localized message, whereas
Russian will use 4 forms (one, few, many and other) and Arabic will use six forms (zero,
one, two, few, many and other).
See CLDR Plural Rules for more details.
you_have_messages={numberMessages, plural, \
one {You have {numberMessages} message} \
other {You have {numberMessages} messages}}
you_have_messages={numberMessages, plural, \
one {Vous avez {numberMessages} message} \
other {Vous avez {numberMessages} messages}}
you_have_messages={numberMessages, plural, \
other {メッセージが {numberMessages} 件}}
you_have_messages={numberMessages, plural, \
one {У вас есть {numberMessages} сообщение} \
few {У вас есть {numberMessages} сообщения} \
many {У вас есть {numberMessages} сообщений} \
other {У вас есть {numberMessages} сообщения}}
class Example {
{
L10nMessages<Messages> m = L10nMessages.builder(Messages.class).build();
IntStream.range(0, 3)
.forEach(
numberMessages ->
System.out.println(m.format(you_have_messages, "numberMessages", numberMessages)));
// You have 0 messages
// You have 1 message
// You have 2 messages
}
{
L10nMessages<Messages> m = L10nMessages.builder(Messages.class).locale(Locale.FRANCE).build();
IntStream.range(0, 3)
.forEach(
numberMessages ->
System.out.println(m.format(you_have_messages, "numberMessages",
numberMessages)));
// Vous avez 0 message
// Vous avez 1 message
// Vous avez 2 messages
}
{
L10nMessages<Messages> m = L10nMessages.builder(Messages.class).locale(Locale.JAPANESE).build();
IntStream.range(0, 3)
.forEach(
numberMessages ->
System.out.println(m.format(you_have_messages, "numberMessages",
numberMessages)));
// メッセージが 0 件
// メッセージが 1 件
// メッセージが 2 件
}
{
L10nMessages<Messages> m = L10nMessages.builder(Messages.class)
.locale(Locale.forLanguageTag("ru")).build();
IntStream.of(0, 1, 2, 5, 21, 22, 25)
.forEach(
numberMessages ->
System.out.println(m.format(you_have_messages, "numberMessages",
numberMessages)));
// У вас есть 0 сообщений
// У вас есть 1 сообщение
// У вас есть 2 сообщения
// У вас есть 5 сообщений
// У вас есть 21 сообщение
// У вас есть 22 сообщения
// У вас есть 25 сообщений
}
}
The other form must always be provided. By default, L10nMessages ensures it is the case as part
of the message format validation.
Customized message for a specific quantity
It is possible to provide a customized message for a specific quantity. For example, use =0 to
provide the specific message: You have no messages when the number of messages is 0.
you_have_messages={numberMessage, plural, \
=0 {You have no messages} \
one {You have {numberMessage} message} \
other {You have {numberMessage} messages}}
you_have_messages={numberMessage, plural, \
=0 {Vous n'avez aucun message} \
one {Vous avez {numberMessage} message} \
other {Vous avez {numberMessage} messages}}
class Example {
{
L10nMessages<Messages> m = L10nMessages.builder(Messages.class).build();
IntStream.range(0, 3)
.forEach(
numberMessage ->
System.out.println(m.format(you_have_messages, "numberMessage", numberMessage)));
// You have no messages
// You have 1 message
// You have 2 messages
}
{
L10nMessages<Messages> m = L10nMessages.builder(Messages.class).locale(Locale.FRANCE).build();
IntStream.range(0, 3)
.forEach(
numberMessage ->
System.out.println(m.format(you_have_messages, "numberMessage",
numberMessage)));
// Vous n'avez aucun message
// Vous avez 1 message
// Vous avez 2 messages
}
}
Complex messages - Genderization, list formatting and more
ICU provides many other features that are useful to localize messages. Combining them allows
building more complex messages, but the complexity might quickly explode. It may also introduce edge
cases that are not properly translatable in all languages so it is important to keep messages as
simple as possible.
This example shows how to genderize a sentence using the select argument and how to use the
ListFormatter
formatter that is not directly accessible in the message format. It is combined with the plural
argument to handle the empty list and the list with a single element.
It is recommended to write full sentences, hence to move the arguments to the outer part of the
messages. The plural arguments should be nested inside the select argument that is used for the
gender.
favorite_numbers={userGender, select, \
female {{numbersCount, plural, \
=0 {She has no favorite numbers} \
one {Her favorite number is {numbers}} \
other {Her favorite numbers are {numbers}}}} \
male {{numbersCount, plural, \
=0 {He has no favorite numbers} \
one {His favorite number is {numbers}} \
other {His favorite numbers are {numbers}}}} \
other {{numbersCount, plural, \
=0 {They have no favorite numbers} \
one {Their favorite number is {numbers}} \
other {Their favorite numbers are {numbers}}}} \
}
favorite_numbers={userGender, select, \
female {{numbersCount, plural, \
=0 {Elle n'a pas de nombre préferé} \
one {Son nombre préferé est {numbers}} \
other {Ses nombres préferés sont {numbers}}}} \
male {{numbersCount, plural, \
=0 {Il n'a pas de nombre préferé} \
one {Son nombre préferé est {numbers}} \
other {Ses nombres préferés sont {numbers}}}} \
other {{numbersCount, plural, \
=0 {Il/Elle n'a pas de nombre préferé} \
one {Son nombre préferé est {numbers}} \
other {Ses nombres préferés sont {numbers}}}} \
}
class Example {
{
L10nMessages<Messages> m = L10nMessages.builder(Messages.class).build();
ListFormatter lf = ListFormatter.getInstance();
Stream.of("female", "male", "other").forEach(userGender ->
Stream.of(Arrays.asList(), Arrays.asList("1"), Arrays.asList("3", "7"))
.forEach(numbers -> System.out.println(
m.format(favorite_numbers, "numbers", lf.format(numbers),
"numbersCount", numbers.size(), "userGender", userGender))));
// She has no favorite numbers
// Her favorite number is 1
// Her favorite numbers are 3 and 7
// He has no favorite numbers
// His favorite number is 1
// His favorite numbers are 3 and 7
// They have no favorite numbers
// Their favorite number is 1
// Their favorite numbers are 3 and 7
}
{
L10nMessages<Messages> m = L10nMessages.builder(Messages.class).locale(Locale.FRANCE).build();
ListFormatter lf = ListFormatter.getInstance(Locale.FRANCE);
Stream.of("female", "male", "other").forEach(gender ->
Stream.of(Arrays.asList(), Arrays.asList("1"), Arrays.asList("3", "7"))
.forEach(numbers -> System.out.println(
m.format(favorite_numbers, "numbers", lf.format(numbers),
"numbersCount", numbers.size(), "userGender", gender))));
// Elle n'a pas de nombre préferé
// Son nombre préferé est 1
// Ses nombres préferés sont 3 et 7
// Il n'a pas de nombre préferé
// Son nombre préferé est 1
// Ses nombres préferés sont 3 et 7
// Il/Elle n'a pas de nombre préferé
// Son nombre préferé est 1
// Ses nombres préferés sont 3 et 7
}
}