Formatting

«« Previous
Next »»

This lesson explains how to format numbers, currencies, dates, times, and text messages. Because end users can see these data elements, their format must conform to various cultural conventions. Following the examples in this lesson will teach you how to:

◈ Format data elements in a locale-sensitive manner
◈ Keep your code locale-independent
◈ Avoid the need to write formatting routines for specific locales

1. Numbers and Currencies


Programs store and operate on numbers in a locale-independent way. Before displaying or printing a number, a program must convert it to a String that is in a locale-sensitive format. For example, in France the number 123456.78 should be formatted as 123 456,78, and in Germany it should appear as 123.456,78. In this section, you will learn how to make your programs independent of the locale conventions for decimal points, thousands-separators, and other formatting properties.

1.1 Using Predefined Formats

By invoking the methods provided by the NumberFormat class, you can format numbers, currencies, and percentages according to Locale. The material that follows demonstrates formatting techniques with a sample program called NumberFormatDemo.java.

Numbers

You can use the NumberFormat methods to format primitive-type numbers, such as double, and their corresponding wrapper objects, such as Double.

The following code example formats a Double according to Locale. Invoking the getNumberInstance method returns a locale-specific instance of NumberFormat. The format method accepts the Double as an argument and returns the formatted number in a String.

static public void displayNumber(Locale currentLocale) {

    Integer quantity = new Integer(123456);
    Double amount = new Double(345987.246);
    NumberFormat numberFormatter;
    String quantityOut;
    String amountOut;

    numberFormatter = NumberFormat.getNumberInstance(currentLocale);
    quantityOut = numberFormatter.format(quantity);
    amountOut = numberFormatter.format(amount);
    System.out.println(quantityOut + "   " + currentLocale.toString());
    System.out.println(amountOut + "   " + currentLocale.toString());
}

This example prints the following; it shows how the format of the same number varies with Locale:

123 456   fr_FR
345 987,246   fr_FR
123.456   de_DE
345.987,246   de_DE
123,456   en_US
345,987.246   en_US

Using Digit Shapes Other Than Arabic Numerals

By default, when text contains numeric values, those values are displayed using Arabic digits. When other Unicode digit shapes are preferred, use the java.awt.font.NumericShaper class. The NumericShaper API enables you to display a numeric value represented internally as an ASCII value in any Unicode digit shape. See Converting Latin Digits to Other Unicode Digits for more information.

In addition, some locales have variant codes that specify that Unicode digit shapes be used in place of Arabic digits, such as the locale for the Thai language. See the section Variant Code in Creating a Locale for more information.

Currencies

If you are writing business applications, you will probably need to format and display currencies. You format currencies in the same manner as numbers, except that you call getCurrencyInstance to create a formatter. When you invoke the format method, it returns a String that includes the formatted number and the appropriate currency sign.

This code example shows how to format currency in a locale-specific manner:

static public void displayCurrency( Locale currentLocale) {

    Double currencyAmount = new Double(9876543.21);
    Currency currentCurrency = Currency.getInstance(currentLocale);
    NumberFormat currencyFormatter =
        NumberFormat.getCurrencyInstance(currentLocale);

    System.out.println(
        currentLocale.getDisplayName() + ", " +
        currentCurrency.getDisplayName() + ": " +
        currencyFormatter.format(currencyAmount));
}

The output generated by the preceding lines of code is as follows:

French (France), Euro: 9 876 543,21 €
German (Germany), Euro: 9.876.543,21 €
English (United States), US Dollar: $9,876,543.21

At first glance, this output may look wrong to you because the numeric values are all the same. Of course, 9 876 543,21 € is not equivalent to $9,876,543.21. However, bear in mind that the NumberFormat class is unaware of exchange rates. The methods belonging to the NumberFormat class format currencies but do not convert them.

Note that the Currency class is designed so that there is never more than one Currency instance for any given currency. Therefore, there is no public constructor. As demonstrated in the previous code example, you obtain a Currency instance using the getInstance methods.

The sample InternationalizedMortgageCalculator.java also demonstrates how to use the Currency class. (Note that this sample does not convert currency values.) The following uses the en-US locale:


The following uses the en-UK locale:


The sample InternationalizedMortgageCalculator.java requires the following resource files:

◈ resources/Resources.properties
◈ resources/Resources_ar.properties
◈ resources/Resources_fr.properties

The Currency class contains other methods to retrieve currency related information:

◈ getAvailableCurrencies : Returns all available currencies in the JDK

◈ getCurrencyCode : Returns the ISO 4217 numeric code for a Currency instance

◈ getSymbol : Returns the symbol for a Currency instance. You can optionally specify as an argument a Locale object. Consider the following excerpt:

Locale enGBLocale = 
    new Locale.Builder().setLanguage("en").setRegion("GB").build();

Locale enUSLocale =
    new Locale.Builder().setLanguage("en").setRegion("US").build();

Currency currencyInstance = Currency.getInstance(enUSLocale);

System.out.println(
    "Symbol for US Dollar, en-US locale: " +
    currencyInstance.getSymbol(enUSLocale));

System.out.println(
    "Symbol for US Dollar, en-UK locale: " +
    currencyInstance.getSymbol(enGBLocale));
The excerpt prints the following:

Symbol for US Dollar, en-US locale: $
Symbol for US Dollar, en-UK locale: USD

This excerpt demonstrates that the symbol of a currency can vary depending on the locale.

◈ getDisplayName : Returns the display name for a Currency instance. Like the getSymbol method, you can optionally specify a Locale object.

Extensible Support for ISO 4217 Currency Codes

ISO 4217 is a standard published by the International Standards Organization. It specifies three-letter codes (and equivalent three-digit numeric codes) to represent currencies and funds. This standard is maintained by an external agency and is released independent of the Java SE platform.

Suppose that a country adopts a different currency and the ISO 4217 maintenance agency releases a currency update. To implement this update and thereby supercede the default currency at runtime, create a properties file named <JAVA_HOME>/lib/currency.properties. This file contains the key/value pairs of the ISO 3166 country code, and the ISO 4217 currency data. The value part consists of three comma-separated ISO 4217 currency values: an alphabetic code, a numeric code, and a minor unit. Any lines beginning with the hash character (#), are treated as comment lines. For example:

# Sample currency property for Canada
CA=CAD,124,2

CAD stands for the Canadian dollar; 124 is the numeric code for the Canadian dollar; and 2 is the minor unit, which is the number of decimal places the currency requires to represent fractional currencies. For example, the following properties file will supercede the default Canadian currency to a Canadian dollar that does not have any units smaller than the dollar:

CA=CAD,124,0

Percentages

You can also use the methods of the NumberFormat class to format percentages. To get the locale-specific formatter, invoke the getPercentInstance method. With this formatter, a decimal fraction such as 0.75 is displayed as 75%.

The following code sample shows how to format a percentage.

static public void displayPercent(Locale currentLocale) {

    Double percent = new Double(0.75);
    NumberFormat percentFormatter;
    String percentOut;

    percentFormatter = NumberFormat.getPercentInstance(currentLocale);
    percentOut = percentFormatter.format(percent);
    System.out.println(percentOut + "   " + currentLocale.toString());
}

This sample prints the following:

75 %   fr_FR
75%   de_DE
75%   en_US

1.2 Customizing Formats

You can use the DecimalFormat class to format decimal numbers into locale-specific strings. This class allows you to control the display of leading and trailing zeros, prefixes and suffixes, grouping (thousands) separators, and the decimal separator. If you want to change formatting symbols, such as the decimal separator, you can use the DecimalFormatSymbols in conjunction with the DecimalFormat class. These classes offer a great deal of flexibility in the formatting of numbers, but they can make your code more complex.

The text that follows uses examples that demonstrate the DecimalFormat and DecimalFormatSymbols classes. The code examples in this material are from a sample program called DecimalFormatDemo.

Constructing Patterns

You specify the formatting properties of DecimalFormat with a pattern String. The pattern determines what the formatted number looks like. For a full description of the pattern syntax, see Number Format Pattern Syntax.

The example that follows creates a formatter by passing a pattern String to the DecimalFormat constructor. The format method accepts a double value as an argument and returns the formatted number in a String:

DecimalFormat myFormatter = new DecimalFormat(pattern);
String output = myFormatter.format(value);
System.out.println(value + " " + pattern + " " + output);

The output for the preceding lines of code is described in the following table. The value is the number, a double , that is to be formatted. The pattern is the String that specifies the formatting properties. The output, which is a String, represents the formatted number.

Output from DecimalFormatDemo Program

value pattern  output  Explanation 
123456.789 ###,###.###  123,456.789  The pound sign (#) denotes a digit, the comma is a placeholder for the grouping separator, and the period is a placeholder for the decimal separator. 
123456.789 ###.## 123456.79  The value has three digits to the right of the decimal point, but the pattern has only two. The format method handles this by rounding up. 
123.78  000000.000  000123.780 The pattern specifies leading and trailing zeros, because the 0 character is used instead of the pound sign (#). 
12345.67  $###,###.###  $12,345.67  The first character in the pattern is the dollar sign ($). Note that it immediately precedes the leftmost digit in the formatted output. 
12345.67  \u00A5###,###.###  ¥12,345.67 The pattern specifies the currency sign for Japanese yen (¥) with the Unicode value 00A5. 

Locale-Sensitive Formatting

The preceding example created a DecimalFormat object for the default Locale. If you want a DecimalFormat object for a nondefault Locale, you instantiate a NumberFormat and then cast it to DecimalFormat. Here's an example:

NumberFormat nf = NumberFormat.getNumberInstance(loc);
DecimalFormat df = (DecimalFormat)nf;
df.applyPattern(pattern);
String output = df.format(value);
System.out.println(pattern + " " + output + " " + loc.toString());

Running the previous code example results in the output that follows. The formatted number, which is in the second column, varies with Locale:

###,###.###      123,456.789     en_US
###,###.###      123.456,789     de_DE
###,###.###      123 456,789     fr_FR

So far the formatting patterns discussed here follow the conventions of U.S. English. For example, in the pattern ###,###.## the comma is the thousands-separator and the period represents the decimal point. This convention is fine, provided that your end users aren't exposed to it. However, some applications, such as spreadsheets and report generators, allow the end users to define their own formatting patterns. For these applications the formatting patterns specified by the end users should use localized notation. In these cases you'll want to invoke the applyLocalizedPattern method on the DecimalFormat object.

Altering the Formatting Symbols

You can use the DecimalFormatSymbols class to change the symbols that appear in the formatted numbers produced by the format method. These symbols include the decimal separator, the grouping separator, the minus sign, and the percent sign, among others.

The next example demonstrates the DecimalFormatSymbols class by applying a strange format to a number. The unusual format is the result of the calls to the setDecimalSeparator, setGroupingSeparator, and setGroupingSize methods.

DecimalFormatSymbols unusualSymbols = new DecimalFormatSymbols(currentLocale);
unusualSymbols.setDecimalSeparator('|');
unusualSymbols.setGroupingSeparator('^');

String strange = "#,##0.###";
DecimalFormat weirdFormatter = new DecimalFormat(strange, unusualSymbols);
weirdFormatter.setGroupingSize(4);

String bizarre = weirdFormatter.format(12345.678);
System.out.println(bizarre);

When run, this example prints the number in a bizarre format:

1^2345|678

Number Format Pattern Syntax

You can design your own format patterns for numbers by following the rules specified by the following BNF diagram:

pattern    := subpattern{;subpattern}
subpattern := {prefix}integer{.fraction}{suffix}
prefix     := '\\u0000'..'\\uFFFD' - specialCharacters
suffix     := '\\u0000'..'\\uFFFD' - specialCharacters
integer    := '#'* '0'* '0'
fraction   := '0'* '#'*

The notation used in the preceding diagram is explained in the following table:

Notation Description
X* 0 or more instances of X
(X | Y)  either X or Y 
X..Y  any character from X up to Y, inclusive 
S - T  characters in S, except those in T
S - T  X is optional 

In the preceding BNF diagram, the first subpattern specifies the format for positive numbers. The second subpattern, which is optional, specifies the format for negative numbers.

Although not noted in the BNF diagram, a comma may appear within the integer portion.

Within the subpatterns, you specify formatting with special symbols. These symbols are described in the following table:

Symbol Description
a digit 
a digit, zero shows as absent 
placeholder for decimal separator
placeholder for grouping separator
E separates mantissa and exponent for exponential formats 
separates formats 
default negative prefix 
multiply by 100 and show as percentage 
multiply by 1000 and show as per mille 
¤  currency sign; replaced by currency symbol; if doubled, replaced by international currency symbol; if present in a pattern, the monetary decimal separator is used instead of the decimal separator 
X any other characters can be used in the prefix or suffix 
used to quote special characters in a prefix or suffix 

2. Dates and Times


Date objects represent dates and times. You cannot display or print a Date object without first converting it to a String that is in the proper format. Just what is the "proper" format? First, the format should conform to the conventions of the end user's Locale. For example, Germans recognize 20.4.09 as a valid date, but Americans expect that same date to appear as 4/20/09. Second, the format should include the necessary information. For instance, a program that measures network performance may report on elapsed milliseconds. An online appointment calendar probably won't display milliseconds, but it will show the days of the week.

This section explains how to format dates and times in various ways and in a locale-sensitive manner. If you follow these techniques your programs will display dates and times in the appropriate Locale, but your source code will remain independent of any specific Locale.

2.1 Using Predefined Formats

The DateFormat class allows you to format dates and times with predefined styles in a locale-sensitive manner. The sections that follow demonstrate how to use the DateFormat class with a program called DateFormatDemo.java.

Dates

Formatting dates with the DateFormat class is a two-step process. First, you create a formatter with the getDateInstance method. Second, you invoke the format method, which returns a String containing the formatted date. The following example formats today's date by calling these two methods:

Date today;
String dateOut;
DateFormat dateFormatter;

dateFormatter = DateFormat.getDateInstance(DateFormat.DEFAULT, currentLocale);
today = new Date();
dateOut = dateFormatter.format(today);

System.out.println(dateOut + " " + currentLocale.toString());

The output generated by this code follows. Notice that the formats of the dates vary with Locale. Since DateFormat is locale-sensitive, it takes care of the formatting details for each Locale.

30 juin 2009     fr_FR
30.06.2009       de_DE
Jun 30, 2009     en_US

The preceding code example specified the DEFAULT formatting style. The DEFAULT style is just one of the predefined formatting styles that the DateFormat class provides, as follows:

◈ DEFAULT
◈ SHORT
◈ MEDIUM
◈ LONG
◈ FULL

The following table shows how dates are formatted for each style with the U.S. and French locales:

Sample Date Formats

Style U.S. Locale  French Locale 
DEFAULT Jun 30, 2009 30 juin 2009 
SHORT  6/30/09  30/06/09 
MEDIUM  Jun 30, 2009  30 juin 2009 
LONG  June 30, 2009  30 juin 2009
FULL Tuesday, June 30, 2009  mardi 30 juin 2009 

Times

Date objects represent both dates and times. Formatting times with the DateFormat class is similar to formatting dates, except that you create the formatter with the getTimeInstance method, as follows:

DateFormat timeFormatter =
    DateFormat.getTimeInstance(DateFormat.DEFAULT, currentLocale);

The table that follows shows the various predefined format styles for the U.S. and German locales:

Sample Time Formats

Style U.S. Locale  German Locale
DEFAULT 7:03:47 AM 7:03:47
SHORT  7:03 AM 07:03
MEDIUM  7:03:47 AM 07:03:07
LONG  7:03:47 AM PDT 07:03:45 PDT
FULL 7:03:47 AM PDT 7.03 Uhr PDT

Both Dates and Times

To display a date and time in the same String, create the formatter with the getDateTimeInstance method. The first parameter is the date style, and the second is the time style. The third parameter is the Locale . Here's a quick example:

DateFormat formatter = DateFormat.getDateTimeInstance(
                           DateFormat.LONG, 
                           DateFormat.LONG, 
                           currentLocale);

The following table shows the date and time formatting styles for the U.S. and French locales:

Sample Date and Time Formats

Style U.S. Locale  French Locale
DEFAULT Jun 30, 2009 7:03:47 AM 30 juin 2009 07:03:47
SHORT  6/30/09 7:03 AM 30/06/09 07:03
MEDIUM  Jun 30, 2009 7:03:47 AM 30 juin 2009 07:03:47
LONG  Jun 30, 2009 7:03:47 AM 30 juin 2009 07:03:47 PDT
FULL Tuesday, June 30, 2009 7:03:47 AM PDT mardi 30 juin 2009 07 h 03 PDT

2.2 Customizing Formats

Version note: This Date and Time section uses the date and time APIs in the java.util package. The java.time APIs, available in the JDK 8 release, provides a comprehensive date and time model that offers significant improvements over the java.util classes. The java.time APIs are described in the Date Time trail. The Legacy Date-Time Code page might be of particular interest.

The code examples that follow demonstrate the methods of the SimpleDateFormat class. You can find the full source code for the examples in the file named SimpleDateFormatDemo.

About Patterns

When you create a SimpleDateFormat object, you specify a pattern String. The contents of the pattern String determine the format of the date and time. For a full description of the pattern's syntax, see the tables in Date Format Pattern Syntax.

The following code formats a date and time according to the pattern String passed to the SimpleDateFormat constructor. The String returned by the format method contains the formatted date and time that are to be displayed.

Date today;
String output;
SimpleDateFormat formatter;

formatter = new SimpleDateFormat(pattern, currentLocale);
today = new Date();
output = formatter.format(today);
System.out.println(pattern + " " + output);

The following table shows the output generated by the previous code example when the U.S. Locale is specified:

Customized Date and Time Formats

Pattern Output 
dd.MM.yy 30.06.09
yyyy.MM.dd G 'at' hh:mm:ss z  2009.06.30 AD at 08:29:36 PDT 
EEE, MMM d, ''yy  Tue, Jun 30, '09 
h:mm a  8:29 PM 
H:mm  8:29 
H:mm:ss:SSS  8:28:36:249 
K:mm a,z  8:29 AM,PDT 
yyyy.MMMMM.dd GGG hh:mm aaa  2009.June.30 AD 08:29 AM 

Patterns and Locale

The SimpleDateFormat class is locale-sensitive. If you instantiate SimpleDateFormat without a Locale parameter, it will format the date and time according to the default Locale. Both the pattern and the Locale determine the format. For the same pattern, SimpleDateFormat may format a date and time differently if the Locale varies.

In the example code that follows, the pattern is hardcoded in the statement that creates the SimpleDateFormat object:

Date today;
String result;
SimpleDateFormat formatter;

formatter = new SimpleDateFormat("EEE d MMM yy", currentLocale);
today = new Date();
result = formatter.format(today);
System.out.println("Locale: " + currentLocale.toString());
System.out.println("Result: " + result);

When the currentLocale is set to different values, the preceding code example generates this output:

Locale: fr_FR
Result: mar. 30 juin 09
Locale: de_DE
Result: Di 30 Jun 09
Locale: en_US
Result: Tue 30 Jun 09

Date Format Pattern Syntax

You can design your own format patterns for dates and times from the list of symbols in the following table:

Symbol Meaning Presentation Example 
G era designator Text  AD
year Number  2009 
month in year Text & Number   July & 07 
day in month Number  10 
hour in am/pm (1-12) Number  12 
hour in day (0-23) Number 
minute in hour Number  30 
second in minute Number  55 
millisecond Number  978 
day in week Text  Tuesday
day in year Number  189 
day of week in month Number  2 (2nd Wed in July) 
week in year Number  27 
week in month Number 
am/pm marker Text  PM 
hour in day (1-24) Number  24 
hour in am/pm (0-11) Number 
z time zone Text   Pacific Standard Time 
escape for text Delimiter  (none)
single quote Literal

Characters that are not letters are treated as quoted text. That is, they will appear in the formatted text even if they are not enclosed within single quotes.

The number of symbol letters you specify also determines the format. For example, if the "zz" pattern results in "PDT," then the "zzzz" pattern generates "Pacific Daylight Time." The following table summarizes these rules:

Presentation Number of Symbols  Result 
Text 1 - 3 abbreviated form, if one exists
Text  >= 4  full form 
Number  minimum number of digits is required  shorter numbers are padded with zeros (for a year, if the count of 'y' is 2, then the year is truncated to 2 digits) 
Text & Number   1 - 2  number form 
Text & Number   text form 

2.3 Changing Date Format Symbols

Version note: This Date and Time section uses the date and time APIs in the java.util package. The java.time APIs, available in the JDK 8 release, provides a comprehensive date and time model that offers significant improvements over the java.util classes. The java.time APIs are described in the Date Time trail. The Legacy Date-Time Code page might be of particular interest.

The format method of the SimpleDateFormat class returns a String composed of digits and symbols. For example, in the String "Friday, April 10, 2009," the symbols are "Friday" and "April." If the symbols encapsulated in SimpleDateFormat don't meet your needs, you can change them with the DateFormatSymbols. You can change symbols that represent names for months, days of the week, and time zones, among others. The following table lists the DateFormatSymbols methods that allow you to modify the symbols:

DateFormatSymbol Methods

Setter Method Example of a Symbol the Method Modifies 
setAmPmStrings PM
setEras  AD 
setMonths  December 
setShortMonths  Dec 
setShortWeekdays  Tue 
setWeekdays  Tuesday 
setZoneStrings  PST 

The following example invokes setShortWeekdays to change the short names of the days of the week from lowercase to uppercase characters. The full source code for this example is in DateFormatSymbolsDemo. The first element in the array argument of setShortWeekdays is a null String. Therefore the array is one-based rather than zero-based. The SimpleDateFormat constructor accepts the modified DateFormatSymbols object as an argument. Here is the source code:

Date today;
String result;
SimpleDateFormat formatter;
DateFormatSymbols symbols;
String[] defaultDays;
String[] modifiedDays;

symbols = new DateFormatSymbols( new Locale("en", "US"));
defaultDays = symbols.getShortWeekdays();

for (int i = 0; i < defaultDays.length; i++) {
    System.out.print(defaultDays[i] + " ");
}
System.out.println();

String[] capitalDays = {
    "", "SUN", "MON",
    "TUE", "WED", "THU",
    "FRI", "SAT"
};
symbols.setShortWeekdays(capitalDays);

modifiedDays = symbols.getShortWeekdays();
for (int i = 0; i < modifiedDays.length; i++) {
    System.out.print(modifiedDays[i] + " ");
}
System.out.println();
System.out.println();

formatter = new SimpleDateFormat("E", symbols);
today = new Date();
result = formatter.format(today);
System.out.println("Today's day of the week: " + result);
The preceding code generates this output:

 Sun Mon Tue Wed Thu Fri Sat 
 SUN MON TUE WED THU FRI SAT 

Today's day of the week: MON

3. Messages


We all like to use programs that let us know what's going on. Programs that keep us informed often do so by displaying status and error messages. Of course, these messages need to be translated so they can be understood by end users around the world. The section Isolating Locale-Specific Data discusses translatable text messages. Usually, you're done after you move a message String into a ResourceBundle. However, if you've embedded variable data in a message, you'll have to take some extra steps to prepare it for translation.

A compound message contains variable data. In the following list of compound messages, the variable data is underlined:

The disk named MyDisk contains 300 files.
The current balance of account #34-09-222 is $2,745.72.
405,390 people have visited your website since January 1, 2009.
Delete all files older than 120 days.

You might be tempted to construct the last message in the preceding list by concatenating phrases and variables as follows:

double numDays;
ResourceBundle msgBundle;
// ...
String message = msgBundle.getString(
                     "deleteolder" +
                     numDays.toString() +
                     msgBundle.getString("days"));

This approach works fine in English, but it won't work for languages in which the verb appears at the end of the sentence. Because the word order of this message is hardcoded, your localizers won't be able to create grammatically correct translations for all languages.

How can you make your program localizable if you need to use compound messages? You can do so by using the MessageFormat class, which is the topic of this section.

3.1 Dealing with Compound Messages

A compound message may contain several kinds of variables: dates, times, strings, numbers, currencies, and percentages. To format a compound message in a locale-independent manner, you construct a pattern that you apply to a MessageFormat object, and store this pattern in a ResourceBundle.

By stepping through a sample program, this section demonstrates how to internationalize a compound message. The sample program makes use of the MessageFormat class. The full source code for this program is in the file called MessageFormatDemo.java. The German locale properties are in the file called MessageBundle_de_DE.properties.

1. Identify the Variables in the Message

Suppose that you want to internationalize the following message:

Formatting

Notice that we've underlined the variable data and have identified what kind of objects will represent this data.

2. Isolate the Message Pattern in a ResourceBundle

Store the message in a ResourceBundle named MessageBundle, as follows:

ResourceBundle messages =
   ResourceBundle.getBundle("MessageBundle", currentLocale);

This ResourceBundle is backed by a properties file for each Locale. Since the ResourceBundle is called MessageBundle, the properties file for U.S. English is named MessageBundle_en_US.properties. The contents of this file is as follows:

template = At {2,time,short} on {2,date,long}, \
    we detected {1,number,integer} spaceships on \
    the planet {0}.
planet = Mars

The first line of the properties file contains the message pattern. If you compare this pattern with the message text shown in step 1, you'll see that an argument enclosed in braces replaces each variable in the message text. Each argument starts with a digit called the argument number, which matches the index of an element in an Object array that holds the argument values. Note that in the pattern the argument numbers are not in any particular order. You can place the arguments anywhere in the pattern. The only requirement is that the argument number have a matching element in the array of argument values.

The next step discusses the argument value array, but first let's look at each of the arguments in the pattern. The following table provides some details about the arguments:

Arguments for template in MessageBundle_en_US.properties

Argument Description
{2,time,short} The time portion of a Date object. The short style specifies the DateFormat.SHORT formatting style. 
{2,date,long} The date portion of a Date object. The same Date object is used for both the date and time variables. In the Object array of arguments the index of the element holding the Date object is 2. (This is described in the next step.) 
{1,number,integer} A Number object, further qualified with the integer number style. 
{0} The String in the ResourceBundle that corresponds to the planet key. 

For a full description of the argument syntax, see the API documentation for the MessageFormat class.

3. Set the Message Arguments

The following lines of code assign values to each argument in the pattern. The indexes of the elements in the messageArguments array match the argument numbers in the pattern. For example, the Integer element at index 1 corresponds to the {1,number,integer} argument in the pattern. Because it must be translated, the String object at element 0 will be fetched from the ResourceBundle with the getString method. Here is the code that defines the array of message arguments:

Object[] messageArguments = {
    messages.getString("planet"),
    new Integer(7),
    new Date()
};

4. Create the Formatter

Next, create a MessageFormat object. You set the Locale because the message contains Date and Number objects, which should be formatted in a locale-sensitive manner.

MessageFormat formatter = new MessageFormat("");
formatter.setLocale(currentLocale);

5. Format the Message Using the Pattern and the Arguments

This step shows how the pattern, message arguments, and formatter all work together. First, fetch the pattern String from the ResourceBundle with the getString method. The key to the pattern is template. Pass the pattern String to the formatter with the applyPattern method. Then format the message using the array of message arguments, by invoking the format method. The String returned by the format method is ready to be displayed. All of this is accomplished with just two lines of code:

formatter.applyPattern(messages.getString("template"));
String output = formatter.format(messageArguments);

6. Run the Demo Program

The demo program prints the translated messages for the English and German locales and properly formats the date and time variables. Note that the English and German verbs ("detected" and "entdeckt") are in different locations relative to the variables:

currentLocale = en_US
At 10:16 AM on July 31, 2009, we detected 7
spaceships on the planet Mars.
currentLocale = de_DE
Um 10:16 am 31. Juli 2009 haben wir 7 Raumschiffe
auf dem Planeten Mars entdeckt.

3.2 Handling Plurals

The words in a message may vary if both plural and singular word forms are possible. With the ChoiceFormat class, you can map a number to a word or a phrase, allowing you to construct grammatically correct messages.

In English the plural and singular forms of a word are usually different. This can present a problem when you are constructing messages that refer to quantities. For example, if your message reports the number of files on a disk, the following variations are possible:

There are no files on XDisk.
There is one file on XDisk.
There are 2 files on XDisk.

The fastest way to solve this problem is to create a MessageFormat pattern like this:

There are {0,number} file(s) on {1}.

Unfortunately the preceding pattern results in incorrect grammar:

There are 1 file(s) on XDisk.

You can do better than that, provided that you use the ChoiceFormat class. In this section you'll learn how to deal with plurals in a message by stepping through a sample program called ChoiceFormatDemo. This program also uses the MessageFormat class, which is discussed in the previous section, Dealing with Compound Messages.

1. Define the Message Pattern

First, identify the variables in the message:

Formatting

Next, replace the variables in the message with arguments, creating a pattern that can be applied to a MessageFormat object:

There {0} on {1}.

The argument for the disk name, which is represented by{1}, is easy enough to deal with. You just treat it like any other String variable in a MessageFormat pattern. This argument matches the element at index 1 in the array of argument values.

Dealing with argument{0} is more complex, for a couple of reasons:

The phrase that this argument replaces varies with the number of files. To construct this phrase at run time, you need to map the number of files to a particular String. For example, the number 1 will map to the String containing the phrase is one file. The ChoiceFormat class allows you to perform the necessary mapping.

If the disk contains multiple files, the phrase includes an integer. The MessageFormat class lets you insert a number into a phrase.

2. Create a ResourceBundle

Because the message text must be translated, isolate it in a ResourceBundle:

ResourceBundle bundle = ResourceBundle.getBundle(
    "ChoiceBundle", currentLocale);

The sample program backs the ResourceBundle with properties files. The ChoiceBundle_en_US.propertiescontains the following lines:

pattern = There {0} on {1}.
noFiles = are no files
oneFile = is one file
multipleFiles = are {2} files

The contents of this properties file show how the message will be constructed and formatted. The first line contains the pattern for MessageFormat . (See step 1.) The other lines contain phrases that will replace argument {0} in the pattern. The phrase for the multipleFiles key contains the argument {2}, which will be replaced by a number.

Here is the French version of the properties file, ChoiceBundle_fr_FR.properties

pattern = Il {0} sur {1}.
noFiles = n'y a pas de fichiers
oneFile = y a un fichier
multipleFiles = y a {2} fichiers

3. Create a Message Formatter

In this step you instantiate MessageFormat and set its Locale:

MessageFormat messageForm = new MessageFormat("");
messageForm.setLocale(currentLocale);

4. Create a Choice Formatter

The ChoiceFormat object allows you to choose, based on a double number, a particular String. The range of double numbers, and the String objects to which they map, are specified in arrays:

double[] fileLimits = {0,1,2};
String [] fileStrings = {
    bundle.getString("noFiles"),
    bundle.getString("oneFile"),
    bundle.getString("multipleFiles")
};

ChoiceFormat maps each element in the double array to the element in the String array that has the same index. In the sample code the 0 maps to the String returned by calling bundle.getString("noFiles"). By coincidence the index is the same as the value in the fileLimits array. If the code had set fileLimits[0] to seven, ChoiceFormat would map the number 7 to fileStrings[0].

You specify the double and String arrays when instantiating ChoiceFormat:

ChoiceFormat choiceForm = new ChoiceFormat(fileLimits, fileStrings);

5. Apply the Pattern

Remember the pattern you constructed in step 1? It's time to retrieve the pattern from the ResourceBundle and apply it to the MessageFormat object:

String pattern = bundle.getString("pattern");
messageForm.applyPattern(pattern);

6. Assign the Formats

In this step you assign to the MessageFormat object the ChoiceFormat object created in step 4:

Format[] formats = {choiceForm, null, NumberFormat.getInstance()};
messageForm.setFormats(formats);

The setFormats method assigns Format objects to the arguments in the message pattern. You must invoke the applyPattern method before you call the setFormats method. The following table shows how the elements of the Format array correspond to the arguments in the message pattern:

The Format Array of the ChoiceFormatDemo Program

Array Element Pattern Argument 
choiceForm {0}
null  {1} 
NumberFormat.getInstance()  {2} 

7. Set the Arguments and Format the Message

At run time the program assigns the variables to the array of arguments it passes to the MessageFormat object. The elements in the array correspond to the arguments in the pattern. For example, messageArgument[1] maps to pattern argument {1}, which is a String containing the name of the disk. In the previous step the program assigned a ChoiceFormat object to argument {0} of the pattern. Therefore the number assigned to messageArgument[0] determines which String the ChoiceFormat object selects. If messageArgument[0] is greater than or equal to 2, the String containing the phrase are {2} files replaces argument {0} in the pattern. The number assigned to messageArgument[2] will be substituted in place of pattern argument {2}. Here's the code that tries this out:

Object[] messageArguments = {null, "XDisk", null};

for (int numFiles = 0; numFiles < 4; numFiles++) {
    messageArguments[0] = new Integer(numFiles);
    messageArguments[2] = new Integer(numFiles);
    String result = messageForm.format(messageArguments);
    System.out.println(result);
}

8. Run the Demo Program

Compare the messages displayed by the program with the phrases in the ResourceBundle of step 2. Notice that the ChoiceFormat object selects the correct phrase, which the MessageFormat object uses to construct the proper message. The output of the ChoiceFormatDemo program is as follows:

currentLocale = en_US
There are no files on XDisk.
There is one file on XDisk.
There are 2 files on XDisk.
There are 3 files on XDisk.

currentLocale = fr_FR
Il n'y a pas des fichiers sur XDisk.
Il y a un fichier sur XDisk.
Il y a 2 fichiers sur XDisk.
Il y a 3 fichiers sur XDisk.

«« Previous
Next »»