Chapter 5. Internationalization

<feature><title></title> <objective>

LOCALES

</objective>
<objective>

NUMBER FORMATS

</objective>
<objective>

DATE AND TIME

</objective>
<objective>

COLLATION

</objective>
<objective>

MESSAGE FORMATTING

</objective>
<objective>

TEXT FILES AND CHARACTER SETS

</objective>
<objective>

RESOURCE BUNDLES

</objective>
<objective>

A COMPLETE EXAMPLE

</objective>
</feature>

There’s a big world out there; we hope that lots of its inhabitants will be interested in your software. The Internet, after all, effortlessly spans the barriers between countries. On the other hand, when you pay no attention to an international audience, you are putting up a barrier.

The Java programming language was the first language designed from the ground up to support internationalization. From the beginning, it had the one essential feature needed for effective internationalization: It used Unicode for all strings. Unicode support makes it easy to write programs in the Java programming language that manipulate strings in any one of multiple languages.

Many programmers believe that all they need to do to internationalize their application is to support Unicode and to translate the messages in the user interface. However, as this chapter demonstrates, there is a lot more to internationalizing programs than just Unicode support. Dates, times, currencies—even numbers—are formatted differently in different parts of the world. You need an easy way to configure menu and button names, message strings, and keyboard shortcuts for different languages.

In this chapter, we show you how to write internationalized Java applications and applets and how to localize date, time, numbers, text, and GUIs. We show you tools that Java offers for writing internationalized programs. We close this chapter with a complete example, a retirement calculator applet that can change how it displays its results depending on the location of the machine that is downloading it.

Note

Note

For additional information on internationalization, check out the informative web site http://www.joconner.com/javai18n, as well as the official Sun site http://java.sun.com/javase/technologies/core/basic/intl/.

Locales

When you look at an application that is adapted to an international market, the most obvious difference you notice is the language. This observation is actually a bit too limiting for true internationalization: Countries can share a common language, but you still might need to do some work to make computer users of both countries happy.[1]

In all cases, menus, button labels, and program messages will need to be translated to the local language; they might also need to be rendered in a different script. There are many more subtle differences; for example, numbers are formatted quite differently in English and in German. The number

123,456.78

should be displayed as

123.456,78

for a German user. That is, the role of the decimal point and the decimal comma separator are reversed. There are similar variations in the display of dates. In the United States, dates are somewhat irrationally displayed as month/day/year. Germany uses the more sensible order of day/month/year, whereas in China, the usage is year/month/day. Thus, the date

3/22/61

should be presented as

22.03.1961

to a German user. Of course, if the month names are written out explicitly, then the difference in languages becomes apparent. The English

March 22, 1961

should be presented as

22. März 1961

in German, or

1961Locales3Locales22Locales

in Chinese.

There are several formatter classes that take these differences into account. To control the formatting, you use the Locale class. A locale describes

  • A language.

  • Optionally, a location.

  • Optionally, a variant.

For example, in the United States, you use a locale with

language=English, location=United States.

In Germany, you use a locale with

language=German, location=Germany.

Switzerland has four official languages (German, French, Italian, and Rhaeto-Romance). A German speaker in Switzerland would want to use a locale with

language=German, location=Switzerland

This locale would make formatting work similarly to how it would work for the German locale; however, currency values would be expressed in Swiss francs, not German marks.

If you only specify the language, say,

language=German

then the locale cannot be used for country-specific issues such as currencies.

Variants are, fortunately, rare and are needed only for exceptional or system-dependent situations. For example, the Norwegians are having a hard time agreeing on the spelling of their language (a derivative of Danish). They use two spelling rule sets: a traditional one called Bokmål and a new one called Nynorsk. The traditional spelling would be expressed as a variant

language=Norwegian, location=Norway, variant=Bokmål

To express the language and location in a concise and standardized manner, the Java programming language uses codes that were defined by the International Organization for Standardization (ISO). The local language is expressed as a lowercase two-letter code, following ISO 639-1, and the country code is expressed as an uppercase two-letter code, following ISO 3166-1. Tables 5-1 and 5-2 show some of the most common codes.

Table 5-1. Common ISO 639-1 Language Codes

Language

Code

Chinese

zh

Danish

da

Dutch

nl

English

en

French

fr

Finnish

fi

German

de

Greek

el

Italian

it

Japanese

ja

Korean

ko

Norwegian

no

Portuguese

pt

Spanish

sp

Swedish

sv

Turkish

tr

Table 5-2. Common ISO 3166-1 Country Codes

Country

Code

Austria

AT

Belgium

BE

Canada

CA

China

CN

Denmark

DK

Finland

FI

Germany

DE

Great Britain

GB

Greece

GR

Ireland

IE

Italy

IT

Japan

JP

Korea

KR

The Netherlands

NL

Norway

NO

Portugal

PT

Spain

ES

Sweden

SE

Switzerland

CH

Taiwan

TW

Turkey

TR

United States

US

Note

Note

For a full list of ISO 639-1 codes, see, for example, http://www.loc.gov/standards/iso639-2/php/code_list.php. You can find a full list of the ISO 3166-1 codes at http://www.iso.org/iso/en/prods-services/iso3166ma/02iso-3166-code-lists/index.html.

These codes do seem a bit random, especially because some of them are derived from local languages (German = Deutsch = de, Chinese = zhongwen = zh), but at least they are standardized.

To describe a locale, you concatenate the language, country code, and variant (if any) and pass this string to the constructor of the Locale class.

Locale german = new Locale("de");
Locale germanGermany = new Locale("de", "DE");
Locale germanSwitzerland = new Locale("de", "CH");
Locale norwegianNorwayBokmål = new Locale("no", "NO", "B");

For your convenience, Java SE predefines a number of locale objects:

Locale.CANADA
Locale.CANADA_FRENCH
Locale.CHINA
Locale.FRANCE
Locale.GERMANY
Locale.ITALY
Locale.JAPAN
Locale.KOREA
Locale.PRC
Locale.TAIWAN
Locale.UK
Locale.US

Java SE also predefines a number of language locales that specify just a language without a location:

Locale.CHINESE
Locale.ENGLISH
Locale.FRENCH
Locale.GERMAN
Locale.ITALIAN
Locale.JAPANESE
Locale.KOREAN
Locale.SIMPLIFIED_CHINESE
Locale.TRADITIONAL_CHINESE

Besides constructing a locale or using a predefined one, you have two other methods for obtaining a locale object.

The static getDefault method of the Locale class initially gets the default locale as stored by the local operating system. You can change the default Java locale by calling setDefault; however, that change only affects your program, not the operating system. Similarly, in an applet, the getLocale method returns the locale of the user viewing the applet.

Finally, all locale-dependent utility classes can return an array of the locales they support. For example,

Locale[] supportedLocales = DateFormat.getAvailableLocales();

returns all locales that the DateFormat class can handle.

Tip

Tip

For testing, you might want to switch the default locale of your program. Supply language and region properties when you launch your program. For example, here we set the default locale to German (Switzerland):

java -Duser.language=de -Duser.region=CH Program

Once you have a locale, what can you do with it? Not much, as it turns out. The only useful methods in the Locale class are the ones for identifying the language and country codes. The most important one is getDisplayName. It returns a string describing the locale. This string does not contain the cryptic two-letter codes, but it is in a form that can be presented to a user, such as

German (Switzerland)

Actually, there is a problem here. The display name is issued in the default locale. That might not be appropriate. If your user already selected German as the preferred language, you probably want to present the string in German. You can do just that by giving the German locale as a parameter: The code

Locale loc = new Locale("de", "CH");
System.out.println(loc.getDisplayName(Locale.GERMAN));

prints

Deutsch (Schweiz)

This example shows why you need Locale objects. You feed it to locale-aware methods that produce text that is presented to users in different locations. You can see many examples in the following sections.

Number Formats

We already mentioned how number and currency formatting is highly locale dependent. The Java library supplies a collection of formatter objects that can format and parse numeric values in the java.text package. You go through the following steps to format a number for a particular locale:

  1. Get the locale object, as described in the preceding section.

  2. Use a “factory method” to obtain a formatter object.

  3. Use the formatter object for formatting and parsing.

The factory methods are static methods of the NumberFormat class that take a Locale argument. There are three factory methods: getNumberInstance, getCurrencyInstance, and getPercentInstance. These methods return objects that can format and parse numbers, currency amounts, and percentages, respectively. For example, here is how you can format a currency value in German:

Locale loc = new Locale("de", "DE");
NumberFormat currFmt = NumberFormat.getCurrencyInstance(loc);
double amt = 123456.78;
String result = currFmt.format(amt);

The result is

123.456,78€

Note that the currency symbol is € and that it is placed at the end of the string. Also, note the reversal of decimal points and decimal commas.

Conversely, to read in a number that was entered or stored with the conventions of a certain locale, use the parse method. For example, the following code parses the value that the user typed into a text field. The parse method can deal with decimal points and commas, as well as digits in other languages.

TextField inputField;
. . .
NumberFormat fmt = NumberFormat.getNumberInstance();
// get number formatter for default locale
Number input = fmt.parse(inputField.getText().trim());
double x = input.doubleValue();

The return type of parse is the abstract type Number. The returned object is either a Double or a Long wrapper object, depending on whether the parsed number was a floating-point number. If you don’t care about the distinction, you can simply use the doubleValue method of the Number class to retrieve the wrapped number.

Caution

Caution

Objects of type Number are not automatically unboxed—you cannot simply assign a Number object to a primitive type. Instead, use the doubleValue or intValue method.

If the text for the number is not in the correct form, the method throws a ParseException. For example, leading whitespace in the string is not allowed. (Call trim to remove it.) However, any characters that follow the number in the string are simply ignored, so no exception is thrown.

Note that the classes returned by the getXxxInstance factory methods are not actually of type NumberFormat. The NumberFormat type is an abstract class, and the actual formatters belong to one of its subclasses. The factory methods merely know how to locate the object that belongs to a particular locale.

You can get a list of the currently supported locales with the static getAvailableLocales method. That method returns an array of the locales for which number formatter objects can be obtained.

The sample program for this section lets you experiment with number formatters (see Figure 5-1). The combo box at the top of the figure contains all locales with number formatters. You can choose between number, currency, and percentage formatters. Each time you make another choice, the number in the text field is reformatted. If you go through a few locales, then you get a good impression of how many ways a number or currency value can be formatted. You can also type a different number and click the Parse button to call the parse method, which tries to parse what you entered. If your input is successfully parsed, then it is passed to format and the result is displayed. If parsing fails, then a “Parse error” message is displayed in the text field.

The NumberFormatTest program

Figure 5-1. The NumberFormatTest program

The code, shown in Listing 5-1, is fairly straightforward. In the constructor, we call NumberFormat.getAvailableLocales. For each locale, we call getDisplayName, and we fill a combo box with the strings that the getDisplayName method returns. (The strings are not sorted; we tackle this issue in the “Collation” section beginning on page 318.) Whenever the user selects another locale or clicks one of the radio buttons, we create a new formatter object and update the text field. When the user clicks the Parse button, we call the parse method to do the actual parsing, based on the locale selected.

Example 5-1. NumberFormatTest.java

  1. import java.awt.*;
  2. import java.awt.event.*;
  3. import java.text.*;
  4. import java.util.*;
  5.
  6. import javax.swing.*;
  7.
  8. /**
  9.  * This program demonstrates formatting numbers under various locales.
 10.  * @version 1.13 2007-07-25
 11.  * @author Cay Horstmann
 12.  */
 13. public class NumberFormatTest
 14. {
 15.    public static void main(String[] args)
 16.    {
 17.       EventQueue.invokeLater(new Runnable()
 18.          {
 19.             public void run()
 20.             {
 21.                JFrame frame = new NumberFormatFrame();
 22.                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 23.                frame.setVisible(true);
 24.             }
 25.          });
 26.    }
 27. }
 28.
 29. /**
 30.  * This frame contains radio buttons to select a number format, a combo box to pick a locale,
 31.  * a text field to display a formatted number, and a button to parse the text field contents.
 32.  */
 33. class NumberFormatFrame extends JFrame
 34. {
 35.    public NumberFormatFrame()
 36.    {
 37.       setLayout(new GridBagLayout());
 38.
 39.       ActionListener listener = new ActionListener()
 40.          {
 41.       setTitle("NumberFormatTest");
 42.             public void actionPerformed(ActionEvent event)
 43.             {
 44.                updateDisplay();
 45.             }
 46.          };
 47.
 48.       JPanel p = new JPanel();
 49.       addRadioButton(p, numberRadioButton, rbGroup, listener);
 50.       addRadioButton(p, currencyRadioButton, rbGroup, listener);
 51.       addRadioButton(p, percentRadioButton, rbGroup, listener);
 52.
 53.       add(new JLabel("Locale:"), new GBC(0, 0).setAnchor(GBC.EAST));
 54.       add(p, new GBC(1, 1));
 55.       add(parseButton, new GBC(0, 2).setInsets(2));
 56.       add(localeCombo, new GBC(1, 0).setAnchor(GBC.WEST));
 57.       add(numberText, new GBC(1, 2).setFill(GBC.HORIZONTAL));
 58.       locales = (Locale[]) NumberFormat.getAvailableLocales().clone();
 59.       Arrays.sort(locales, new Comparator<Locale>()
 60.          {
 61.             public int compare(Locale l1, Locale l2)
 62.             {
 63.                return l1.getDisplayName().compareTo(l2.getDisplayName());
 64.             }
 65.          });
 66.       for (Locale loc : locales)
 67.          localeCombo.addItem(loc.getDisplayName());
 68.       localeCombo.setSelectedItem(Locale.getDefault().getDisplayName());
 69.       currentNumber = 123456.78;
 70.       updateDisplay();
 71.
 72.       localeCombo.addActionListener(listener);
 73.
 74.       parseButton.addActionListener(new ActionListener()
 75.          {
 76.             public void actionPerformed(ActionEvent event)
 77.             {
 78.                String s = numberText.getText().trim();
 79.                try
 80.                {
 81.                   Number n = currentNumberFormat.parse(s);
 82.                   if (n != null)
 83.                   {
 84.                      currentNumber = n.doubleValue();
 85.                      updateDisplay();
 86.                   }
 87.                   else
 88.                   {
 89.                      numberText.setText("Parse error: " + s);
 90.                   }
 91.                }
 92.                catch (ParseException e)
 93.                {
 94.                   numberText.setText("Parse error: " + s);
 95.                }
 96.             }
 97.          });
 98.       pack();
 99.    }
100.
101.    /**
102.     * Adds a radio button to a container.
103.     * @param p the container into which to place the button
104.     * @param b the button
105.     * @param g the button group
106.     * @param listener the button listener
107.     */
108.    public void addRadioButton(Container p, JRadioButton b, ButtonGroup g,
109.                               ActionListener listener)
110.    {
111.       b.setSelected(g.getButtonCount() == 0);
112.       b.addActionListener(listener);
113.       g.add(b);
114.       p.add(b);
115.    }
116.
117.    /**
118.     * Updates the display and formats the number according to the user settings.
119.     */
120.    public void updateDisplay()
121.    {
122.       Locale currentLocale = locales[localeCombo.getSelectedIndex()];
123.       currentNumberFormat = null;
124.       if (numberRadioButton.isSelected()) currentNumberFormat = NumberFormat
125.             .getNumberInstance(currentLocale);
126.       else if (currencyRadioButton.isSelected()) currentNumberFormat = NumberFormat
127.             .getCurrencyInstance(currentLocale);
128.       else if (percentRadioButton.isSelected()) currentNumberFormat = NumberFormat
129.             .getPercentInstance(currentLocale);
130.       String n = currentNumberFormat.format(currentNumber);
131.       numberText.setText(n);
132.    }
133.
134.    private Locale[] locales;
135.    private double currentNumber;
136.    private JComboBox localeCombo = new JComboBox();
137.    private JButton parseButton = new JButton("Parse");
138.    private JTextField numberText = new JTextField(30);
139.    private JRadioButton numberRadioButton = new JRadioButton("Number");
140.    private JRadioButton currencyRadioButton = new JRadioButton("Currency");
141.    private JRadioButton percentRadioButton = new JRadioButton("Percent");
142.    private ButtonGroup rbGroup = new ButtonGroup();
143.    private NumberFormat currentNumberFormat;
144. }

 

Currencies

To format a currency value, you can use the NumberFormat.getCurrencyInstance method. However, that method is not very flexible—it returns a formatter for a single currency. Suppose you prepare an invoice for an American customer in which some amounts are in dollars and others are in Euros. You can’t just use two formatters

NumberFormat dollarFormatter = NumberFormat.getCurrencyInstance(Locale.US);
NumberFormat euroFormatter = NumberFormat.getCurrencyInstance(Locale.GERMANY);

Your invoice would look very strange, with some values formatted like $100,000 and others like 100.000 €. (Note that the Euro value uses a decimal point, not a comma.)

Instead, use the Currency class to control the currency that is used by the formatters. You get a Currency object by passing a currency identifier to the static Currency.getInstance method. Then call the setCurrency method for each formatter. Here is how you would set up the Euro formatter for your American customer:

NumberFormat euroFormatter = NumberFormat.getCurrencyInstance(Locale.US);
euroFormatter.setCurrency(Currency.getInstance("EUR"));

The currency identifiers are defined by ISO 4217—see http://www.iso.org/iso/en/prods-services/popstds/currencycodeslist.html. Table 5-3 provides a partial list.

Table 5-3. Currency Identifiers

Currency Value

Identifier

U.S. Dollar

USD

Euro

EUR

British Pound

GBP

Japanese Yen

JPY

Chinese Renminbi (Yuan)

CNY

Indian Rupee

INR

Russian Ruble

RUB

Date and Time

When you are formatting date and time, you should be concerned with four locale-dependent issues:

  • The names of months and weekdays should be presented in the local language.

  • There will be local preferences for the order of year, month, and day.

  • The Gregorian calendar might not be the local preference for expressing dates.

  • The time zone of the location must be taken into account.

The Java DateFormat class handles these issues. It is easy to use and quite similar to the NumberFormat class. First, you get a locale. You can use the default locale or call the static getAvailableLocales method to obtain an array of locales that support date formatting. Then, you call one of the three factory methods:

fmt = DateFormat.getDateInstance(dateStyle, loc);
fmt = DateFormat.getTimeInstance(timeStyle, loc);
fmt = DateFormat.getDateTimeInstance(dateStyle, timeStyle, loc);

To specify the desired style, these factory methods have a parameter that is one of the following constants:

  • DateFormat.DEFAULT

  • DateFormat.FULL (e.g., Wednesday, September 12, 2007 8:51:03 PM PDT for the U.S. locale)

  • DateFormat.LONG (e.g., September 12, 2007 8:51:03 PM PDT for the U.S. locale)

  • DateFormat.MEDIUM (e.g., Sep 12, 2007 8:51:03 PM for the U.S. locale)

  • DateFormat.SHORT (e.g., 9/12/07 8:51 PM for the U.S. locale)

The factory method returns a formatting object that you can then use to format dates.

Date now = new Date();
String s = fmt.format(now);

Just as with the NumberFormat class, you can use the parse method to parse a date that the user typed. For example, the following code parses the value that the user typed into a text field, using the default locale.

TextField inputField;
. . .
DateFormat fmt = DateFormat.getDateInstance(DateFormat.MEDIUM);
Date input = fmt.parse(inputField.getText().trim());

Unfortunately, the user must type the date exactly in the expected format. For example, if the format is set to MEDIUM in the U.S. locale, then dates are expected to look like

Sep 12, 2007

If the user types

Sep 12 2007

(without the comma) or the short format

9/12/07

then a ParseException results.

A lenient flag interprets dates leniently. For example, February 30, 2007 will be automatically converted to March 2, 2007. This seems dangerous, but, unfortunately, it is the default. You should probably turn off this feature. The calendar object that interprets the parsed date will throw IllegalArgumentException when the user enters an invalid day/month/year combination.

Listing 5-2 shows the DateFormat class in action. You can select a locale and see how the date and time are formatted in different places around the world. If you see question-mark characters in the output, then you don’t have the fonts installed for displaying characters in the local language. For example, if you pick a Chinese locale, the date might be expressed as

2007Date and Time9Date and Time12Date and Time

Figure 5-2 shows the program (after Chinese fonts were installed). As you can see, it correctly displays the output.

The DateFormatTest program

Figure 5-2. The DateFormatTest program

You can also experiment with parsing. Enter a date or time, click the Parse lenient checkbox if desired, and click the Parse date or Parse time button.

We use a helper class EnumCombo to solve a technical problem (see Listing 5-3). We wanted to fill a combo with values such as Short, Medium, and Long and then automatically convert the user’s selection to integer values DateFormat.SHORT, DateFormat.MEDIUM, and DateFormat.LONG. Rather than writing repetitive code, we use reflection: We convert the user’s choice to upper case, replace all spaces with underscores, and then find the value of the static field with that name. (See Volume I, Chapter 5 for more details about reflection.)

Tip

Tip

To compute times in different time zones, use the TimeZone class. See http://java.sun.com/developer/JDCTechTips/2003/tt1104.html#2 for a brief tutorial.

Example 5-2. DateFormatTest.java

  1. import java.awt.*;
  2. import java.awt.event.*;
  3. import java.text.*;
  4. import java.util.*;
  5.
  6. import javax.swing.*;
  7.
  8. /**
  9.  * This program demonstrates formatting dates under various locales.
 10.  * @version 1.13 2007-07-25
 11.  * @author Cay Horstmann
 12.  */
 13. public class DateFormatTest
 14. {
 15.    public static void main(String[] args)
 16.    {
 17.       EventQueue.invokeLater(new Runnable()
 18.          {
 19.             public void run()
 20.             {
 21.                JFrame frame = new DateFormatFrame();
 22.                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 23.                frame.setVisible(true);
 24.             }
 25.          });
 26.    }
 27. }
 28.
 29. /**
 30.  * This frame contains combo boxes to pick a locale, date and time formats, text fields
 31.  * to display formatted date and time, buttons to parse the text field contents, and a
 32.  * "lenient" checkbox.
 33.  */
 34. class DateFormatFrame extends JFrame
 35. {
 36.    public DateFormatFrame()
 37.    {
 38.       setTitle("DateFormatTest");
 39.
 40.       setLayout(new GridBagLayout());
 41.       add(new JLabel("Locale"), new GBC(0, 0).setAnchor(GBC.EAST));
 42.       add(new JLabel("Date style"), new GBC(0, 1).setAnchor(GBC.EAST));
 43.       add(new JLabel("Time style"), new GBC(2, 1).setAnchor(GBC.EAST));
 44.       add(new JLabel("Date"), new GBC(0, 2).setAnchor(GBC.EAST));
 45.       add(new JLabel("Time"), new GBC(0, 3).setAnchor(GBC.EAST));
 46.       add(localeCombo, new GBC(1, 0, 2, 1).setAnchor(GBC.WEST));
 47.       add(dateStyleCombo, new GBC(1, 1).setAnchor(GBC.WEST));
 48.       add(timeStyleCombo, new GBC(3, 1).setAnchor(GBC.WEST));
 49.       add(dateParseButton, new GBC(3, 2).setAnchor(GBC.WEST));
 50.       add(timeParseButton, new GBC(3, 3).setAnchor(GBC.WEST));
 51.       add(lenientCheckbox, new GBC(0, 4, 2, 1).setAnchor(GBC.WEST));
 52.       add(dateText, new GBC(1, 2, 2, 1).setFill(GBC.HORIZONTAL));
 53.       add(timeText, new GBC(1, 3, 2, 1).setFill(GBC.HORIZONTAL));
 54.
 55.       locales = (Locale[]) DateFormat.getAvailableLocales().clone();
 56.       Arrays.sort(locales, new Comparator<Locale>()
 57.          {
 58.             public int compare(Locale l1, Locale l2)
 59.             {
 60.                return l1.getDisplayName().compareTo(l2.getDisplayName());
 61.             }
 62.          });
 63.       for (Locale loc : locales)
 64.          localeCombo.addItem(loc.getDisplayName());
 65.       localeCombo.setSelectedItem(Locale.getDefault().getDisplayName());
 66.       currentDate = new Date();
 67.       currentTime = new Date();
 68.       updateDisplay();
 69.
 70.       ActionListener listener = new ActionListener()
 71.          {
 72.             public void actionPerformed(ActionEvent event)
 73.             {
 74.                updateDisplay();
 75.             }
 76.          };
 77.
 78.       localeCombo.addActionListener(listener);
 79.       dateStyleCombo.addActionListener(listener);
 80.       timeStyleCombo.addActionListener(listener);
 81.
 82.       dateParseButton.addActionListener(new ActionListener()
 83.          {
 84.             public void actionPerformed(ActionEvent event)
 85.             {
 86.                String d = dateText.getText().trim();
 87.                try
 88.                {
 89.                   currentDateFormat.setLenient(lenientCheckbox.isSelected());
 90.                   Date date = currentDateFormat.parse(d);
 91.                   currentDate = date;
 92.                   updateDisplay();
 93.                }
 94.                catch (ParseException e)
 95.                {
 96.                   dateText.setText("Parse error: " + d);
 97.                }
 98.                catch (IllegalArgumentException e)
 99.                {
100.                   dateText.setText("Argument error: " + d);
101.                }
102.             }
103.          });
104.
105.       timeParseButton.addActionListener(new ActionListener()
106.          {
107.             public void actionPerformed(ActionEvent event)
108.             {
109.                String t = timeText.getText().trim();
110.                try
111.                {
112.                   currentDateFormat.setLenient(lenientCheckbox.isSelected());
113.                   Date date = currentTimeFormat.parse(t);
114.                   currentTime = date;
115.                   updateDisplay();
116.                }
117.                catch (ParseException e)
118.                {
119.                   timeText.setText("Parse error: " + t);
120.                }
121.                catch (IllegalArgumentException e)
122.                {
123.                   timeText.setText("Argument error: " + t);
124.                }
125.             }
126.          });
127.       pack();
128.    }
129.
130.    /**
131.     * Updates the display and formats the date according to the user settings.
132.     */
133.    public void updateDisplay()
134.    {
135.       Locale currentLocale = locales[localeCombo.getSelectedIndex()];
136.       int dateStyle = dateStyleCombo.getValue();
137.       currentDateFormat = DateFormat.getDateInstance(dateStyle, currentLocale);
138.       String d = currentDateFormat.format(currentDate);
139.       dateText.setText(d);
140.       int timeStyle = timeStyleCombo.getValue();
141.       currentTimeFormat = DateFormat.getTimeInstance(timeStyle, currentLocale);
142.       String t = currentTimeFormat.format(currentTime);
143.       timeText.setText(t);
144.    }
145.
146.    private Locale[] locales;
147.    private Date currentDate;
148.    private Date currentTime;
149.    private DateFormat currentDateFormat;
150.    private DateFormat currentTimeFormat;
151.    private JComboBox localeCombo = new JComboBox();
152.    private EnumCombo dateStyleCombo = new EnumCombo(DateFormat.class, new String[] { "Default",
153.          "Full", "Long", "Medium", "Short" });
154.    private EnumCombo timeStyleCombo = new EnumCombo(DateFormat.class, new String[] { "Default",
155.          "Full", "Long", "Medium", "Short" });
156.    private JButton dateParseButton = new JButton("Parse date");
157.    private JButton timeParseButton = new JButton("Parse time");
158.    private JTextField dateText = new JTextField(30);
159.    private JTextField timeText = new JTextField(30);
160.    private JCheckBox lenientCheckbox = new JCheckBox("Parse lenient", true);
161. }

 

Example 5-3. EnumCombo.java

 1. import java.util.*;
 2. import javax.swing.*;
 3.
 4. /**
 5.    A combo box that lets users choose from among static field
 6.    values whose names are given in the constructor.
 7.    @version 1.13 2007-07-25
 8.    @author Cay Horstmann
 9. */
10. public class EnumCombo extends JComboBox
11. {
12.    /**
13.       Constructs an EnumCombo.
14.       @param cl a class
15.       @param labels an array of static field names of cl
16.    */
17.    public EnumCombo(Class<?> cl, String[] labels)
18.    {
19.       for (String label : labels)
20.       {
21.          String name = label.toUpperCase().replace(' ', '_'),
22.          int value = 0;
23.          try
24.          {
25.             java.lang.reflect.Field f = cl.getField(name);
26.             value = f.getInt(cl);
27.          }
28.          catch (Exception e)
29.          {
30.             label = "(" + label + ")";
31.          }
32.          table.put(label, value);
33.          addItem(label);
34.       }
35.       setSelectedItem(labels[0]);
36.    }
37.
38.    /**
39.       Returns the value of the field that the user selected.
40.       @return the static field value
41.    */
42.    public int getValue()
43.    {
44.       return table.get(getSelectedItem());
45.    }
46.
47.    private Map<String, Integer> table = new TreeMap<String, Integer>();
48. }

 

Collation

Most programmers know how to compare strings with the compareTo method of the String class. The value of a.compareTo(b) is a negative number if a is lexicographically less than b, zero if they are identical, and positive otherwise.

Unfortunately, unless all your words are in uppercase English ASCII characters, this method is useless. The problem is that the compareTo method in the Java programming language uses the values of the Unicode character to determine the ordering. For example, lowercase characters have a higher Unicode value than do uppercase characters, and accented characters have even higher values. This leads to absurd results; for example, the following five strings are ordered according to the compareTo method:

America
Zulu
able
zebra
Ångström

For dictionary ordering, you want to consider upper case and lower case to be equivalent. To an English speaker, the sample list of words would be ordered as

able
America
Ångström
zebra
Zulu

However, that order would not be acceptable to a Swedish user. In Swedish, the letter Å is different from the letter A, and it is collated after the letter Z! That is, a Swedish user would want the words to be sorted as

able
America
zebra
Zulu
Ångström

Fortunately, once you are aware of the problem, collation is quite easy. As always, you start by obtaining a Locale object. Then, you call the getInstance factory method to obtain a Collator object. Finally, you use the compare method of the collator, not the compareTo method of the String class, whenever you want to sort strings.

Locale loc = . . .;
Collator coll = Collator.getInstance(loc);
if (coll.compare(a, b) < 0) // a comes before b . . .;

Most important, the Collator class implements the Comparator interface. Therefore, you can pass a collator object to the Collections.sort method to sort a list of strings:

Collections.sort(strings, coll);

Collation Strength

You can set a collator’s strength to select how selective it should be. Character differences are classified as primary, secondary, tertiary, and identical. For example, in English, the difference between “A” and “Z” is considered primary, the difference between “A” and “Å” is secondary, and between “A” and “a” is tertiary.

By setting the strength of the collator to Collator.PRIMARY, you tell it to pay attention only to primary differences. By setting the strength to Collator.SECONDARY, you instruct the collator to take secondary differences into account. That is, two strings will be more likely to be considered different when the strength is set to “secondary” or “tertiary,” as shown in Table 5-4.

Table 5-4. Collations with Different Strengths (English Locale)

Primary

Secondary

Tertiary

Angstrom = Ångström

Angstrom ≠ Ångström

Angstrom ≠ Ångström

Able = able

Able = able

Able ≠ able

When the strength has been set to Collator.IDENTICAL, no differences are allowed. This setting is mainly useful in conjunction with the second, rather technical, collator setting, the decomposition mode, which we discuss in the next section.

Decomposition

Occasionally, a character or sequence of characters can be described in more than one way in Unicode. For example, an “Å” can be Unicode character U+00C5, or it can be expressed as a plain A (U+0065) followed by a ° (“combining ring above”; U+030A). Perhaps more surprisingly, the letter sequence “ffi” can be described with a single character “Latin small ligature ffi” with code U+FB03. (One could argue that this is a presentation issue and it should not have resulted in different Unicode characters, but we don’t make the rules.)

The Unicode standard defines four normalization forms (D, KD, C, and KC) for strings. See http://www.unicode.org/unicode/reports/tr15/tr15-23.html for the details. Two of them are used for collation. In normalization form D, accented characters are decomposed into their base letters and combining accents. For example, Å is turned into a sequence of an A and a combining ring above °. Normalization form KD goes further and decomposes compatibility characters such as the ffi ligature or the trademark symbol ™.

You choose the degree of normalization that you want the collator to use. The value Collator.NO_DECOMPOSITION does not normalize strings at all. This option is faster, but it might not be appropriate for text that expresses characters in multiple forms. The default, Collator.CANONICAL_DECOMPOSITION, uses normalization form D. This is the most useful form for text that contains accents but not ligatures. Finally, “full decomposition” uses normalization form KD. See Table 5-5 for examples.

Table 5-5. Differences Between Decomposition Modes

No Decomposition

Canonical Decomposition

Full Decomposition

Å ≠ A°

Å = A°

Å = A°

™ ≠ TM

™ ≠ TM

™ = TM

It is wasteful to have the collator decompose a string many times. If one string is compared many times against other strings, then you can save the decomposition in a collation key object. The getCollationKey method returns a CollationKey object that you can use for further, faster comparisons. Here is an example:

String a = . . .;
CollationKey aKey = coll.getCollationKey(a);
if(aKey.compareTo(coll.getCollationKey(b)) == 0) // fast comparison
   . . .

Finally, you might want to convert strings into their normalized forms even when you don’t do collation; for example, when storing strings in a database or communicating with another program. As of Java SE 6, the java.text.Normalizer class carries out the normalization process. For example,

String name = "Ångström";
String normalized = Normalizer.normalize(name, Normalizer.Form.NFD); // uses normalization form D

The normalized string contains ten characters. The “Å” and “ö” are replaced by “A°” and “o¨” sequences.

However, that is not usually the best form for storage and transmission. Normalization form C first applies decomposition and then combines the accents back in a standardized order. According to the W3C, this is the recommended mode for transferring data over the Internet.

The program in Listing 5-4 lets you experiment with collation order. Type a word into the text field and click the Add button to add it to the list of words. Each time you add another word, or change the locale, strength, or decomposition mode, the list of words is sorted again. An = sign indicates words that are considered identical (see Figure 5-3).

The CollationTest program

Figure 5-3. The CollationTest program

The locale names in the combo box are displayed in sorted order, using the collator of the default locale. If you run this program with the US English locale, note that “Norwegian (Norway,Nynorsk)” comes before “Norwegian (Norway)”, even though the Unicode value of the comma character is greater than the Unicode value of the closing parenthesis.

Example 5-4. CollationTest.java

  1. import java.awt.*;
  2. import java.awt.event.*;
  3. import java.text.*;
  4. import java.util.*;
  5. import java.util.List;
  6.
  7. import javax.swing.*;
  8.
  9. /**
 10.  * This program demonstrates collating strings under various locales.
 11.  * @version 1.13 2007-07-25
 12.  * @author Cay Horstmann
 13.  */
 14. public class CollationTest
 15. {
 16.    public static void main(String[] args)
 17.    {
 18.       EventQueue.invokeLater(new Runnable()
 19.          {
 20.             public void run()
 21.             {
 22.
 23.                JFrame frame = new CollationFrame();
 24.                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 25.                frame.setVisible(true);
 26.             }
 27.          });
 28.    }
 29. }
 30.
 31. /**
 32.  * This frame contains combo boxes to pick a locale, collation strength and decomposition
 33.  * rules, a text field and button to add new strings, and a text area to list the
 34.  * collated strings.
 35.  */
 36. class CollationFrame extends JFrame
 37. {
 38.    public CollationFrame()
 39.    {
 40.       setTitle("CollationTest");
 41.
 42.       setLayout(new GridBagLayout());
 43.       add(new JLabel("Locale"), new GBC(0, 0).setAnchor(GBC.EAST));
 44.       add(new JLabel("Strength"), new GBC(0, 1).setAnchor(GBC.EAST));
 45.       add(new JLabel("Decomposition"), new GBC(0, 2).setAnchor(GBC.EAST));
 46.       add(addButton, new GBC(0, 3).setAnchor(GBC.EAST));
 47.       add(localeCombo, new GBC(1, 0).setAnchor(GBC.WEST));
 48.       add(strengthCombo, new GBC(1, 1).setAnchor(GBC.WEST));
 49.       add(decompositionCombo, new GBC(1, 2).setAnchor(GBC.WEST));
 50.       add(newWord, new GBC(1, 3).setFill(GBC.HORIZONTAL));
 51.       add(new JScrollPane(sortedWords), new GBC(0, 4, 2, 1).setFill(GBC.BOTH));
 52.
 53.       locales = (Locale[]) Collator.getAvailableLocales().clone();
 54.       Arrays.sort(locales, new Comparator<Locale>()
 55.          {
 56.             private Collator collator = Collator.getInstance(Locale.getDefault());
 57.
 58.             public int compare(Locale l1, Locale l2)
 59.             {
 60.                return collator.compare(l1.getDisplayName(), l2.getDisplayName());
 61.             }
 62.          });
 63.       for (Locale loc : locales)
 64.          localeCombo.addItem(loc.getDisplayName());
 65.       localeCombo.setSelectedItem(Locale.getDefault().getDisplayName());
 66.
 67.       strings.add("America");
 68.       strings.add("able");
 69.       strings.add("Zulu");
 70.       strings.add("zebra");
 71.       strings.add("u00C5ngstru00F6m");
 72.       strings.add("Au030angstrou0308m");
 73.       strings.add("Angstrom");
 74.       strings.add("Able");
 75.       strings.add("office");
 76.       strings.add("ouFB03ce");
 77.       strings.add("Javau2122");
 78.       strings.add("JavaTM");
 79.       updateDisplay();
 80.
 81.       addButton.addActionListener(new ActionListener()
 82.          {
 83.             public void actionPerformed(ActionEvent event)
 84.             {
 85.                strings.add(newWord.getText());
 86.                updateDisplay();
 87.             }
 88.          });
 89.
 90.       ActionListener listener = new ActionListener()
 91.          {
 92.             public void actionPerformed(ActionEvent event)
 93.             {
 94.                updateDisplay();
 95.             }
 96.          };
 97.
 98.       localeCombo.addActionListener(listener);
 99.       strengthCombo.addActionListener(listener);
100.       decompositionCombo.addActionListener(listener);
101.       pack();
102.    }
103.
104.    /**
105.     * Updates the display and collates the strings according to the user settings.
106.     */
107.    public void updateDisplay()
108.    {
109.       Locale currentLocale = locales[localeCombo.getSelectedIndex()];
110.       localeCombo.setLocale(currentLocale);
111.
112.       currentCollator = Collator.getInstance(currentLocale);
113.       currentCollator.setStrength(strengthCombo.getValue());
114.       currentCollator.setDecomposition(decompositionCombo.getValue());
115.
116.       Collections.sort(strings, currentCollator);
117.
118.       sortedWords.setText("");
119.       for (int i = 0; i < strings.size(); i++)
120.       {
121.          String s = strings.get(i);
122.          if (i > 0 && currentCollator.compare(s, strings.get(i - 1)) == 0) sortedWords
123.                .append("= ");
124.          sortedWords.append(s + "
");
125.       }
126.       pack();
127.    }
128.
129.    private List<String> strings = new ArrayList<String>();
130.    private Collator currentCollator;
131.    private Locale[] locales;
132.    private JComboBox localeCombo = new JComboBox();
133.
134.    private EnumCombo strengthCombo = new EnumCombo(Collator.class, new String[] { "Primary",
135.          "Secondary", "Tertiary", "Identical" });
136.    private EnumCombo decompositionCombo = new EnumCombo(Collator.class, new String[] {
137.          "Canonical Decomposition", "Full Decomposition", "No Decomposition" });
138.    private JTextField newWord = new JTextField(20);
139.    private JTextArea sortedWords = new JTextArea(20, 20);
140.    private JButton addButton = new JButton("Add");
141. }

 

Message Formatting

The Java library has a MessageFormat class that formats text with variable parts, like this:

"On {2}, a {0} destroyed {1} houses and caused {3} of damage."

The numbers in braces are placeholders for actual names and values. The static method MessageFormat.format lets you substitute values for the variables. As of JDK 5.0, it is a “varargs” method, so you can simply supply the parameters as follows:

String msg = MessageFormat.format("On {2}, a {0} destroyed {1} houses and caused {3} of damage.",
   "hurricane", 99, new GregorianCalendar(1999, 0, 1).getTime(), 10.0E8);

In this example, the placeholder {0} is replaced with "hurricane", {1} is replaced with 99, and so on.

The result of our example is the string

On 1/1/99 12:00 AM, a hurricane destroyed 99 houses and caused 100,000,000 of damage.

That is a start, but it is not perfect. We don’t want to display the time “12:00 AM,” and we want the damage amount printed as a currency value. The way we do this is by supplying an optional format for some of the placeholders:

"On {2,date,long}, a {0} destroyed {1} houses and caused {3,number,currency} of damage."

This example code prints:

On January 1, 1999, a hurricane destroyed 99 houses and caused $100,000,000 of damage.

In general, the placeholder index can be followed by a type and a style. Separate the index, type, and style by commas. The type can be any of

number
time
date
choice

If the type is number, then the style can be

integer
currency
percent

or it can be a number format pattern such as $,##0. (See the documentation of the DecimalFormat class for more information about the possible formats.)

If the type is either time or date, then the style can be

short
medium
long
full

or a date format pattern such as yyyy-MM-dd. (See the documentation of the SimpleDateFormat class for more information about the possible formats.)

Choice formats are more complex, and we take them up in the next section.

Caution

Caution

The static MessageFormat.format method uses the current locale to format the values. To format with an arbitrary locale, you have to work a bit harder because there is no “varargs” method that you can use. You need to place the values to be formatted into an Object[] array, like this:

MessageFormat mf = new MessageFormat(pattern, loc);
String msg = mf.format(new Object[] { values });

Choice Formats

Let’s look closer at the pattern of the preceding section:

"On {2}, a {0} destroyed {1} houses and caused {3} of damage."

If we replace the disaster placeholder {0} with "earthquake", then the sentence is not grammatically correct in English.

On January 1, 1999, a earthquake destroyed . . .

That means what we really want to do is integrate the article “a” into the placeholder:

"On {2}, {0} destroyed {1} houses and caused {3} of damage."

The {0} would then be replaced with "a hurricane" or "an earthquake". That is especially appropriate if this message needs to be translated into a language where the gender of a word affects the article. For example, in German, the pattern would be

"{0} zerstörte am {2} {1} Häuser und richtete einen Schaden von {3} an."

The placeholder would then be replaced with the grammatically correct combination of article and noun, such as "Ein Wirbelsturm", "Eine Naturkatastrophe".

Now let us turn to the {1} parameter. If the disaster isn’t all that catastrophic, then {1} might be replaced with the number 1, and the message would read:

On January 1, 1999, a mudslide destroyed 1 houses and . . .

We would ideally like the message to vary according to the placeholder value, so that it can read

no houses
one house
2 houses
. . .

depending on the placeholder value. The choice formatting option was designed for this purpose.

A choice format is a sequence of pairs, each of which contains

  • A lower limit

  • A format string

The lower limit and format string are separated by a # character, and the pairs are separated by | characters.

For example,

{1,choice,0#no houses|1#one house|2#{1} houses}

Table 5-6 shows the effect of this format string for various values of {1}.

Table 5-6. String Formatted by Choice Format

{1}

Result

0

"no houses"

1

one house

3

3 houses”

-1

no houses

Why do we use {1} twice in the format string? When the message format applies the choice format on the {1} placeholder and the value is $2, the choice format returns "{1} houses". That string is then formatted again by the message format, and the answer is spliced into the result.

Note

Note

This example shows that the designer of the choice format was a bit muddleheaded. If you have three format strings, you need two limits to separate them. In general, you need one fewer limit than you have format strings. As you saw in Table 5-4, the MessageFormat class ignores the first limit.

The syntax would have been a lot clearer if the designer of this class realized that the limits belong between the choices, such as

no houses|1|one house|2|{1} houses // not the actual format

You can use the < symbol to denote that a choice should be selected if the lower bound is strictly less than the value.

You can also use the ≤ symbol (expressed as the Unicode character code u2264) as a synonym for #. If you like, you can even specify a lower bound of -∞ as -u221E for the first value.

For example,

-∞<no houses|0<one house|2≤{1} houses

or, using Unicode escapes,

-u221E<no houses|0<one house|2u2264{1} houses

Let’s finish our natural disaster scenario. If we put the choice string inside the original message string, we get the following format instruction:

String pattern = "On {2,date,long}, {0} destroyed {1,choice,0#no houses|1#one house|2#{1}
   houses}" + "and caused {3,number,currency} of damage.";

Or, in German,

String pattern = "{0} zerstörte am {2,date,long} {1,choice,0#kein Haus|1#ein Haus|2#{1} Häuser}"
   + "und richtete einen Schaden von {3,number,currency} an.";

Note that the ordering of the words is different in German, but the array of objects you pass to the format method is the same. The order of the placeholders in the format string takes care of the changes in the word ordering.

Text Files and Character Sets

As you know, the Java programming language itself is fully Unicode based. However, operating systems typically have their own character encoding, such as ISO-8859 -1 (an 8-bit code sometimes called the “ANSI” code) in the United States, or Big5 in Taiwan.

When you save data to a text file, you should respect the local character encoding so that the users of your program can open the text file with their other applications. Specify the character encoding in the FileWriter constructor:

out = new FileWriter(filename, "ISO-8859-1");

You can find a complete list of the supported encodings in Volume I, Chapter 12.

Unfortunately, there is currently no connection between locales and character encodings. For example, if your user has selected the Taiwanese locale zh_TW, no method in the Java programming language tells you that the Big5 character encoding would be the most appropriate.

Character Encoding of Source Files

It is worth keeping in mind that you, the programmer, will need to communicate with the Java compiler. And you do that with tools on your local system. For example, you can use the Chinese version of Notepad to write your Java source code files. The resulting source code files are not portable because they use the local character encoding (GB or Big5, depending on which Chinese operating system you use). Only the compiled class files are portable—they will automatically use the “modified UTF-8” encoding for identifiers and strings. That means that even when a program is compiling and running, three character encodings are involved:

  • Source files: local encoding

  • Class files: modified UTF-8

  • Virtual machine: UTF-16

(See Volume I, Chapter 12 for a definition of the modified UTF-8 and UTF-16 formats.)

Tip

Tip

You can specify the character encoding of your source files with the -encoding flag, for example,

javac -encoding Big5 Myfile.java

To make your source files portable, restrict yourself to using the plain ASCII encoding. That is, you should change all non-ASCII characters to their equivalent Unicode encodings. For example, rather than using the string "Häuser", use "Hu0084user". The JDK contains a utility, native2ascii, that you can use to convert the native character encoding to plain ASCII. This utility simply replaces every non-ASCII character in the input with a u followed by the four hex digits of the Unicode value. To use the native2ascii program, provide the input and output file names.

native2ascii Myfile.java Myfile.temp

You can convert the other way with the -reverse option:

native2ascii -reverse Myfile.temp Myfile.java

You can specify another encoding with the -encoding option. The encoding name must be one of those listed in the encodings table in Volume I, Chapter 12.

native2ascii -encoding Big5 Myfile.java Myfile.temp

Tip

Tip

It is a good idea to restrict yourself to plain ASCII class names. Because the name of the class also turns into the name of the class file, you are at the mercy of the local file system to handle any non-ASCII coded names. Here is a depressing example. Windows 95 used the so-called Code Page 437 or original PC encoding, for its file names. If you compiled a class Bär and tried to run it in Windows 95, you got an error message “cannot find class BΣr”.

Resource Bundles

When localizing an application, you’ll probably have a dauntingly large number of message strings, button labels, and so on, that all need to be translated. To make this task feasible, you’ll want to define the message strings in an external location, usually called a resource. The person carrying out the translation can then simply edit the resource files without having to touch the source code of the program.

In Java, you use property files to specify string resources, and you implement classes for resources of other types.

Note

Note

Java technology resources are not the same as Windows or Macintosh resources. A Macintosh or Windows executable program stores resources such as menus, dialog boxes, icons, and messages in a section separate from the program code. A resource editor can inspect and update these resources without affecting the program code.

Note

Note

Volume I, Chapter 10 describes a concept of JAR file resources, whereby data files, sounds, and images can be placed in a JAR file. The getResource method of the class Class finds the file, opens it, and returns a URL to the resource. By placing the files into the JAR file, you leave the job of finding the files to the class loader, which already knows how to locate items in a JAR file. However, that mechanism has no locale support.

Locating Resource Bundles

When localizing an application, you produce a set of resource bundles. Each bundle is a property file or a class that describes locale-specific items (such as messages, labels, and so on). For each bundle, you provide versions for all locales that you want to support.

You need to use a specific naming convention for these bundles. For example, resources specific for Germany go to a file bundleName_de_DE, whereas those that are shared by all German-speaking countries go into bundleName_de. In general, use

bundleName_language_country

for all country-specific resources, and use

bundleName_language

for all language-specific resources. Finally, as a fallback, you can put defaults into a file without any suffix.

You load a bundle with the command

ResourceBundle currentResources = ResourceBundle.getBundle(bundleName, currentLocale);

The getBundle method attempts to load the bundle that matches the current locale by language, country, and variant. If it is not successful, then the variant, country, and language are dropped in turn. Then the same search is applied to the default locale, and finally, the default bundle file is consulted. If even that attempt fails, the method throws a MissingResourceException.

That is, the getBundle method tries to load the following bundles:

bundleName_currentLocaleLanguage_currentLocaleCountry_currentLocaleVariant
bundleName_currentLocaleLanguage_currentLocaleCountry
bundleName_currentLocaleLanguage
bundleName_defaultLocaleLanguage_defaultLocaleCountry_defaultLocaleVariant
bundleName_defaultLocaleLanguage_defaultLocaleCountry
bundleName_defaultLocaleLanguage
bundleName

Once the getBundle method has located a bundle, say, bundleName_de_DE, it will still keep looking for bundleName_de and bundleName. If these bundles exist, they become the parents of the bundleName_de_DE bundle in a resource hierarchy. Later, when looking up a resource, the parents are searched if a lookup was not successful in the current bundle. That is, if a particular resource was not found in bundleName_de_DE, then the bundleName_de and bundleName will be queried as well.

This is clearly a very useful service and one that would be tedious to program by hand. The resource bundle mechanism of the Java programming language automatically locates the items that are the best match for a given locale. It is easy to add more and more localizations to an existing program: All you have to do is add additional resource bundles.

Tip

Tip

You need not place all resources for your application into a single bundle. You could have one bundle for button labels, one for error messages, and so on.

Property Files

Internationalizing strings is quite straightforward. You place all your strings into a property file such as MyProgramStrings.properties. This is simply a text file with one key/value pair per line. A typical file would look like this:

computeButton=Rechnen
colorName=black
defaultPaperSize=210×297

Then you name your property files as described in the preceding section, for example:

MyProgramStrings.properties
MyProgramStrings_en.properties
MyProgramStrings_de_DE.properties

You can load the bundle simply as

ResourceBundle bundle = ResourceBundle.getBundle("MyProgramStrings", locale);

To look up a specific string, call

String computeButtonLabel = bundle.getString("computeButton");

Caution

Caution

Files for storing properties are always ASCII files. If you need to place Unicode characters into a properties file, encode them by using the uxxxx encoding. For example, to specify "colorName=Grün", use

colorName=Gru00FCn

You can use the native2ascii tool to generate these files.

Bundle Classes

To provide resources that are not strings, you define classes that extend the ResourceBundle class. You use the standard naming convention to name your classes, for example

MyProgramResources.java
MyProgramResources_en.java
MyProgramResources_de_DE.java

You load the class with the same getBundle method that you use to load a property file:

ResourceBundle bundle = ResourceBundle.getBundle("MyProgramResources", locale);

Caution

Caution

When searching for bundles, a bundle in a class is given prefererence over a property file when the two bundles have the same base names.

Each resource bundle class implements a lookup table. You provide a key string for each setting you want to localize, and you use that key string to retrieve the setting. For example,

Color backgroundColor = (Color) bundle.getObject("backgroundColor");
double[] paperSize = (double[]) bundle.getObject("defaultPaperSize");

The simplest way of implementing resource bundle classes is to extend the ListResourceBundle class. The ListResourceBundle lets you place all your resources into an object array and then does the lookup for you. Follow this code outline:

public class bundleName_language_country extends ListResourceBundle
{
   public Object[][] getContents() { return contents; }
   private static final Object[][] contents =
   {
      { key1, value2},
      { key2, value2},
      . . .
   }
}

For example,

public class ProgramResources_de extends ListResourceBundle
{
   public Object[][] getContents() { return contents; }
   private static final Object[][] contents =
   {
      { "backgroundColor", Color.black },
      { "defaultPaperSize", new double[] { 210, 297 } }
   }
}

public class ProgramResources_en_US extends ListResourceBundle
{
   public Object[][] getContents() { return contents; }
   private static final Object[][] contents =
   {
      { "backgroundColor", Color.blue },
      { "defaultPaperSize", new double[] { 216, 279 } }
   }
}

Note

Note

The paper sizes are given in millimeters. Everyone on the planet, with the exception of the United States and Canada, uses ISO 216 paper sizes. For more information, see http://www.cl.cam.ac.uk/~mgk25/iso-paper.html. According to the U.S. Metric Association (http://lamar.colostate.edu/~hillger), only three countries in the world have not yet officially adopted the metric system: Liberia, Myanmar (Burma), and the United States of America.

Alternatively, your resource bundle classes can extend the ResourceBundle class. Then you need to implement two methods, to enumerate all keys and to look up the value for a given key:

Enumeration<String> getKeys()
Object handleGetObject(String key)

The getObject method of the ResourceBundle class calls the handleGetObject method that you supply.

Note

Note

As of Java SE 6, you can choose alternate mechanisms for storing your resources. For example, you can customize the resource loading mechanism to fetch resources from XML files or databases. See http://java.sun.com/developer/technicalArticles/javase/i18n_enhance for more information.

A Complete Example

In this section, we apply the material from this chapter to localize a retirement calculator applet. The applet calculates whether or not you are saving enough money for your retirement. You enter your age, how much money you save every month, and so on (see Figure 5-4).

The retirement calculator in English

Figure 5-4. The retirement calculator in English

The text area and the graph show the balance of the retirement account for every year. If the numbers turn negative toward the later part of your life and the bars in the graph appear below the x-axis, you need to do something; for example, save more money, postpone your retirement, die earlier, or be younger.

The retirement calculator works in three locales (English, German, and Chinese). Here are some of the highlights of the internationalization:

  • The labels, buttons, and messages are translated into German and Chinese. You can find them in the classes RetireResources_de, RetireResources_zh. English is used as the fallback—see the RetireResources file. To generate the Chinese messages, we first typed the file, using Notepad running in Chinese Windows, and then we used the native2ascii utility to convert the characters to Unicode.

  • Whenever the locale changes, we reset the labels and reformat the contents of the text fields.

  • The text fields handle numbers, currency amounts, and percentages in the local format.

  • The computation field uses a MessageFormat. The format string is stored in the resource bundle of each language.

  • Just to show that it can be done, we use different colors for the bar graph, depending on the language chosen by the user.

Listings 5-5 through 5-8 show the code. Listings 5-9 through 5-11 are the property files for the localized strings. Figures 5-5 and 5-6 show the outputs in German and Chinese, respectively. To see Chinese characters, be sure you have Chinese fonts installed and configured with your Java runtime. Otherwise, all Chinese characters show up as “missing character” icons.

The retirement calculator in German

Figure 5-5. The retirement calculator in German

The retirement calculator in Chinese

Figure 5-6. The retirement calculator in Chinese

Example 5-5. Retire.java

  1. import java.awt.*;
  2. import java.awt.event.*;
  3. import java.awt.geom.*;
  4. import java.util.*;
  5. import java.text.*;
  6. import javax.swing.*;
  7.
  8. /**
  9.  * This applet shows a retirement calculator. The UI is displayed in English, German,
 10.  * and Chinese.
 11.  * @version 1.22 2007-07-25
 12.  * @author Cay Horstmann
 13.  */
 14. public class Retire extends JApplet
 15. {
 16.    public void init()
 17.    {
 18.       EventQueue.invokeLater(new Runnable()
 19.          {
 20.             public void run()
 21.             {
 22.                initUI();
 23.             }
 24.          });
 25.    }
 26.
 27.    public void initUI()
 28.    {
 29.       setLayout(new GridBagLayout());
 30.       add(languageLabel, new GBC(0, 0).setAnchor(GBC.EAST));
 31.       add(savingsLabel, new GBC(0, 1).setAnchor(GBC.EAST));
 32.       add(contribLabel, new GBC(2, 1).setAnchor(GBC.EAST));
 33.       add(incomeLabel, new GBC(4, 1).setAnchor(GBC.EAST));
 34.       add(currentAgeLabel, new GBC(0, 2).setAnchor(GBC.EAST));
 35.       add(retireAgeLabel, new GBC(2, 2).setAnchor(GBC.EAST));
 36.       add(deathAgeLabel, new GBC(4, 2).setAnchor(GBC.EAST));
 37.       add(inflationPercentLabel, new GBC(0, 3).setAnchor(GBC.EAST));
 38.       add(investPercentLabel, new GBC(2, 3).setAnchor(GBC.EAST));
 39.       add(localeCombo, new GBC(1, 0, 3, 1));
 40.       add(savingsField, new GBC(1, 1).setWeight(100, 0).setFill(GBC.HORIZONTAL));
 41.       add(contribField, new GBC(3, 1).setWeight(100, 0).setFill(GBC.HORIZONTAL));
 42.       add(incomeField, new GBC(5, 1).setWeight(100, 0).setFill(GBC.HORIZONTAL));
 43.       add(currentAgeField, new GBC(1, 2).setWeight(100, 0).setFill(GBC.HORIZONTAL));
 44.       add(retireAgeField, new GBC(3, 2).setWeight(100, 0).setFill(GBC.HORIZONTAL));
 45.       add(deathAgeField, new GBC(5, 2).setWeight(100, 0).setFill(GBC.HORIZONTAL));
 46.       add(inflationPercentField, new GBC(1, 3).setWeight(100, 0).setFill(GBC.HORIZONTAL));
 47.       add(investPercentField, new GBC(3, 3).setWeight(100, 0).setFill(GBC.HORIZONTAL));
 48.       add(retireCanvas, new GBC(0, 4, 4, 1).setWeight(100, 100).setFill(GBC.BOTH));
 49.       add(new JScrollPane(retireText), new GBC(4, 4, 2, 1).setWeight(0, 100).setFill(GBC.BOTH));
 50.
 51.       computeButton.setName("computeButton");
 52.       computeButton.addActionListener(new ActionListener()
 53.          {
 54.             public void actionPerformed(ActionEvent event)
 55.             {
 56.                getInfo();
 57.                updateData();
 58.                updateGraph();
 59.             }
 60.          });
 61.       add(computeButton, new GBC(5, 3));
 62.
 63.       retireText.setEditable(false);
 64.       retireText.setFont(new Font("Monospaced", Font.PLAIN, 10));
 65.
 66.       info.setSavings(0);
 67.       info.setContrib(9000);
 68.       info.setIncome(60000);
 69.       info.setCurrentAge(35);
 70.       info.setRetireAge(65);
 71.       info.setDeathAge(85);
 72.       info.setInvestPercent(0.1);
 73.       info.setInflationPercent(0.05);
 74.
 75.       int localeIndex = 0; // US locale is default selection
 76.       for (int i = 0; i < locales.length; i++)
 77.          // if current locale one of the choices, select it
 78.          if (getLocale().equals(locales[i])) localeIndex = i;
 79.       setCurrentLocale(locales[localeIndex]);
 80.
 81.       localeCombo.addActionListener(new ActionListener()
 82.          {
 83.             public void actionPerformed(ActionEvent event)
 84.             {
 85.                setCurrentLocale((Locale) localeCombo.getSelectedItem());
 86.                validate();
 87.             }
 88.          });
 89.    }
 90.
 91.    /**
 92.     * Sets the current locale.
 93.     * @param locale the desired locale
 94.     */
 95.    public void setCurrentLocale(Locale locale)
 96.       currentLocale = locale;
 97.       localeCombo.setSelectedItem(currentLocale);
 98.       localeCombo.setLocale(currentLocale);
 99.    {
100.
101.       res = ResourceBundle.getBundle("RetireResources", currentLocale);
102.       resStrings = ResourceBundle.getBundle("RetireStrings", currentLocale);
103.       currencyFmt = NumberFormat.getCurrencyInstance(currentLocale);
104.       numberFmt = NumberFormat.getNumberInstance(currentLocale);
105.       percentFmt = NumberFormat.getPercentInstance(currentLocale);
106.
107.       updateDisplay();
108.       updateInfo();
109.       updateData();
110.       updateGraph();
111.    }
112.
113.    /**
114.     * Updates all labels in the display.
115.     */
116.    public void updateDisplay()
117.    {
118.       languageLabel.setText(resStrings.getString("language"));
119.       savingsLabel.setText(resStrings.getString("savings"));
120.       contribLabel.setText(resStrings.getString("contrib"));
121.       incomeLabel.setText(resStrings.getString("income"));
122.       currentAgeLabel.setText(resStrings.getString("currentAge"));
123.       retireAgeLabel.setText(resStrings.getString("retireAge"));
124.       deathAgeLabel.setText(resStrings.getString("deathAge"));
125.       inflationPercentLabel.setText(resStrings.getString("inflationPercent"));
126.       investPercentLabel.setText(resStrings.getString("investPercent"));
127.       computeButton.setText(resStrings.getString("computeButton"));
128.    }
129.
130.    /**
131.     * Updates the information in the text fields.
132.     */
133.    public void updateInfo()
134.    {
135.       savingsField.setText(currencyFmt.format(info.getSavings()));
136.       contribField.setText(currencyFmt.format(info.getContrib()));
137.       incomeField.setText(currencyFmt.format(info.getIncome()));
138.       currentAgeField.setText(numberFmt.format(info.getCurrentAge()));
139.       retireAgeField.setText(numberFmt.format(info.getRetireAge()));
140.       deathAgeField.setText(numberFmt.format(info.getDeathAge()));
141.       investPercentField.setText(percentFmt.format(info.getInvestPercent()));
142.       inflationPercentField.setText(percentFmt.format(info.getInflationPercent()));
143.    }
144.
145.    /**
146.     * Updates the data displayed in the text area.
147.     */
148.    public void updateData()
149.    {
150.       retireText.setText("");
151.       MessageFormat retireMsg = new MessageFormat("");
152.       retireMsg.setLocale(currentLocale);
153.       retireMsg.applyPattern(resStrings.getString("retire"));
154.
155.       for (int i = info.getCurrentAge(); i <= info.getDeathAge(); i++)
156.       {
157.          Object[] args = { i, info.getBalance(i) };
158.          retireText.append(retireMsg.format(args) + "
");
159.       }
160.    }
161.
162.    /**
163.     * Updates the graph.
164.     */
165.    public void updateGraph()
166.    {
167.       retireCanvas.setColorPre((Color) res.getObject("colorPre"));
168.       retireCanvas.setColorGain((Color) res.getObject("colorGain"));
169.       retireCanvas.setColorLoss((Color) res.getObject("colorLoss"));
170.       retireCanvas.setInfo(info);
171.       repaint();
172.    }
173.
174.    /**
175.     * Reads the user input from the text fields.
176.     */
177.    public void getInfo()
178.    {
179.       try
180.       {
181.          info.setSavings(currencyFmt.parse(savingsField.getText()).doubleValue());
182.          info.setContrib(currencyFmt.parse(contribField.getText()).doubleValue());
183.          info.setIncome(currencyFmt.parse(incomeField.getText()).doubleValue());
184.          info.setCurrentAge(numberFmt.parse(currentAgeField.getText()).intValue());
185.          info.setRetireAge(numberFmt.parse(retireAgeField.getText()).intValue());
186.          info.setDeathAge(numberFmt.parse(deathAgeField.getText()).intValue());
187.          info.setInvestPercent(percentFmt.parse(investPercentField.getText()).doubleValue());
188.          info.setInflationPercent(percentFmt.parse(
189.                inflationPercentField.getText()).doubleValue());
190.       }
191.       catch (ParseException e)
192.       {
193.       }
194.    }
195.
196.    private JTextField savingsField = new JTextField(10);
197.    private JTextField contribField = new JTextField(10);
198.    private JTextField incomeField = new JTextField(10);
199.    private JTextField currentAgeField = new JTextField(4);
200.    private JTextField retireAgeField = new JTextField(4);
201.    private JTextField deathAgeField = new JTextField(4);
202.    private JTextField inflationPercentField = new JTextField(6);
203.    private JTextField investPercentField = new JTextField(6);
204.    private JTextArea retireText = new JTextArea(10, 25);
205.    private RetireCanvas retireCanvas = new RetireCanvas();
206.    private JButton computeButton = new JButton();
207.    private JLabel languageLabel = new JLabel();
208.    private JLabel savingsLabel = new JLabel();
209.    private JLabel contribLabel = new JLabel();
210.    private JLabel incomeLabel = new JLabel();
211.    private JLabel currentAgeLabel = new JLabel();
212.    private JLabel retireAgeLabel = new JLabel();
213.    private JLabel deathAgeLabel = new JLabel();
214.    private JLabel inflationPercentLabel = new JLabel();
215.    private JLabel investPercentLabel = new JLabel();
216.
217.    private RetireInfo info = new RetireInfo();
218.
219.    private Locale[] locales = { Locale.US, Locale.CHINA, Locale.GERMANY };
220.    private Locale currentLocale;
221.    private JComboBox localeCombo = new LocaleCombo(locales);
222.    private ResourceBundle res;
223.    private ResourceBundle resStrings;
224.    private NumberFormat currencyFmt;
225.    private NumberFormat numberFmt;
226.    private NumberFormat percentFmt;
227. }
228.
229. /**
230.  * The information required to compute retirement income data.
231.  */
232. class RetireInfo
233. {
234.    /**
235.     * Gets the available balance for a given year.
236.     * @param year the year for which to compute the balance
237.     * @return the amount of money available (or required) in that year
238.     */
239.    public double getBalance(int year)
240.    {
241.       if (year < currentAge) return 0;
242.       else if (year == currentAge)
243.       {
244.          age = year;
245.          balance = savings;
246.          return balance;
247.       }
248.       else if (year == age) return balance;
249.       if (year != age + 1) getBalance(year - 1);
250.       age = year;
251.       if (age < retireAge) balance += contrib;
252.       else balance -= income;
253.       balance = balance * (1 + (investPercent - inflationPercent));
254.       return balance;
255.    }
256.
257.    /**
258.     * Gets the amount of prior savings.
259.     * @return the savings amount
260.     */
261.    public double getSavings()
262.    {
263.       return savings;
264.    }
265.
266.    /**
267.     * Sets the amount of prior savings.
268.     * @param newValue the savings amount
269.     */
270.    public void setSavings(double newValue)
271.    {
272.       savings = newValue;
273.    }
274.
275.    /**
276.     * Gets the annual contribution to the retirement account.
277.     * @return the contribution amount
278.     */
279.    public double getContrib()
280.    {
281.       return contrib;
282.    }
283.
284.    /**
285.     * Sets the annual contribution to the retirement account.
286.     * @param newValue the contribution amount
287.     */
288.    public void setContrib(double newValue)
289.    {
290.       contrib = newValue;
291.    }
292.
293.    /**
294.     * Gets the annual income.
295.     * @return the income amount
296.     */
297.    public double getIncome()
298.    {
299.       return income;
300.    }
301.
302.    /**
303.     * Sets the annual income.
304.     * @param newValue the income amount
305.     */
306.    public void setIncome(double newValue)
307.    {
308.       income = newValue;
309.    }
310.
311.    /**
312.     * Gets the current age.
313.     * @return the age
314.     */
315.    public int getCurrentAge()
316.    {
317.       return currentAge;
318.    }
319.
320.    /**
321.     * Sets the current age.
322.     * @param newValue the age
323.     */
324.    public void setCurrentAge(int newValue)
325.    {
326.       currentAge = newValue;
327.    }
328.
329.    /**
330.     * Gets the desired retirement age.
331.     * @return the age
332.     */
333.    public int getRetireAge()
334.    {
335.       return retireAge;
336.    }
337.
338.    /**
339.     * Sets the desired retirement age.
340.     * @param newValue the age
341.     */
342.    public void setRetireAge(int newValue)
343.    {
344.       retireAge = newValue;
345.    }
346.
347.    /**
348.     * Gets the expected age of death.
349.     * @return the age
350.     */
351.    public int getDeathAge()
352.    {
353.       return deathAge;
354.    }
355.
356.    /**
357.     * Sets the expected age of death.
358.     * @param newValue the age
359.     */
360.    public void setDeathAge(int newValue)
361.    {
362.       deathAge = newValue;
363.    }
364.
365.    /**
366.     * Gets the estimated percentage of inflation.
367.     * @return the percentage
368.     */
369.    public double getInflationPercent()
370.    {
371.       return inflationPercent;
372.    }
373.
374.    /**
375.     * Sets the estimated percentage of inflation.
376.     * @param newValue the percentage
377.     */
378.    public void setInflationPercent(double newValue)
379.    {
380.       inflationPercent = newValue;
381.    }
382.
383.    /**
384.     * Gets the estimated yield of the investment.
385.     * @return the percentage
386.     */
387.    public double getInvestPercent()
388.    {
389.       return investPercent;
390.    }
391.
392.    /**
393.     * Sets the estimated yield of the investment.
394.     * @param newValue the percentage
395.     */
396.    public void setInvestPercent(double newValue)
397.    {
398.       investPercent = newValue;
399.    }
400.
401.    private double savings;
402.    private double contrib;
403.    private double income;
404.    private int currentAge;
405.    private int retireAge;
406.    private int deathAge;
407.    private double inflationPercent;
408.    private double investPercent;
409.
410.    private int age;
411.    private double balance;
412. }
413.
414. /**
415.  * This panel draws a graph of the investment result.
416.  */
417. class RetireCanvas extends JPanel
418. {
419.    public RetireCanvas()
420.    {
421.       setSize(PANEL_WIDTH, PANEL_HEIGHT);
422.    }
423.
424.    /**
425.     * Sets the retirement information to be plotted.
426.     * @param newInfo the new retirement info.
427.     */
428.    public void setInfo(RetireInfo newInfo)
429.    {
430.       info = newInfo;
431.       repaint();
432.    }
433.
434.    public void paintComponent(Graphics g)
435.    {
436.       Graphics2D g2 = (Graphics2D) g;
437.       if (info == null) return;
438.
439.       double minValue = 0;
440.       double maxValue = 0;
441.       int i;
442.       for (i = info.getCurrentAge(); i <= info.getDeathAge(); i++)
443.       {
444.          double v = info.getBalance(i);
445.          if (minValue > v) minValue = v;
446.          if (maxValue < v) maxValue = v;
447.       }
448.       if (maxValue == minValue) return;
449.
450.       int barWidth = getWidth() / (info.getDeathAge() - info.getCurrentAge() + 1);
451.       double scale = getHeight() / (maxValue - minValue);
452.
453.       for (i = info.getCurrentAge(); i <= info.getDeathAge(); i++)
454.       {
455.          int x1 = (i - info.getCurrentAge()) * barWidth + 1;
456.          int y1;
457.          double v = info.getBalance(i);
458.          int height;
459.          int yOrigin = (int) (maxValue * scale);
460.
461.          if (v >= 0)
462.          {
463.             y1 = (int) ((maxValue - v) * scale);
464.             height = yOrigin - y1;
465.          }
466.          else
467.          {
468.             y1 = yOrigin;
469.             height = (int) (-v * scale);
470.          }
471.
472.          if (i < info.getRetireAge()) g2.setPaint(colorPre);
473.          else if (v >= 0) g2.setPaint(colorGain);
474.          else g2.setPaint(colorLoss);
475.          Rectangle2D bar = new Rectangle2D.Double(x1, y1, barWidth - 2, height);
476.          g2.fill(bar);
477.          g2.setPaint(Color.black);
478.          g2.draw(bar);
479.       }
480.    }
481.
482.    /**
483.     * Sets the color to be used before retirement.
484.     * @param color the desired color
485.     */
486.    public void setColorPre(Color color)
487.    {
488.       colorPre = color;
489.       repaint();
490.    }
491.
492.    /**
493.     * Sets the color to be used after retirement while the account balance is positive.
494.     * @param color the desired color
495.     */
496.    public void setColorGain(Color color)
497.    {
498.       colorGain = color;
499.       repaint();
500.    }
501.
502.    /**
503.     * Sets the color to be used after retirement when the account balance is negative.
504.     * @param color the desired color
505.     */
506.    public void setColorLoss(Color color)
507.    {
508.       colorLoss = color;
509.       repaint();
510.    }
511.
512.    private RetireInfo info = null;
513.    private Color colorPre;
514.    private Color colorGain;
515.    private Color colorLoss;
516.    private static final int PANEL_WIDTH = 400;
517.    private static final int PANEL_HEIGHT = 200;
518. }

 

Example 5-6. RetireResources.java

 1. import java.awt.*;
 2.
 3. /**
 4.  * These are the English non-string resources for the retirement calculator.
 5.  * @version 1.21 2001-08-27
 6.  * @author Cay Horstmann
 7.  */
 8. public class RetireResources extends java.util.ListResourceBundle
 9. {
10.    public Object[][] getContents()
11.    {
12.       return contents;
13.    }
14.
15.    static final Object[][] contents = {
16.    // BEGIN LOCALIZE
17.          { "colorPre", Color.blue }, { "colorGain", Color.white }, { "colorLoss", Color.red }
18.    // END LOCALIZE
19.    };
20. }

Example 5-7. RetireResources_de.java

 1. import java.awt.*;
 2.
 3. /**
 4.  * These are the German non-string resources for the retirement calculator.
 5.  * @version 1.21 2001-08-27
 6.  * @author Cay Horstmann
 7.  */
 8. public class RetireResources_de extends java.util.ListResourceBundle
 9. {
10.    public Object[][] getContents()
11.    {
12.       return contents;
13.    }
14.
15.    static final Object[][] contents = {
16.    // BEGIN LOCALIZE
17.          { "colorPre", Color.yellow }, { "colorGain", Color.black }, { "colorLoss", Color.red }
18.    // END LOCALIZE
19.    };
20. }

Example 5-8. RetireResources_zh.java

 1. import java.awt.*;
 2.
 3. /**
 4.  * These are the Chinese non-string resources for the retirement calculator.
 5.  * @version 1.21 2001-08-27
 6.  * @author Cay Horstmann
 7.  */
 8. public class RetireResources_zh extends java.util.ListResourceBundle
 9. {
10.    public Object[][] getContents()
11.    {
12.       return contents;
13.    }
14.
15.    static final Object[][] contents = {
16.    // BEGIN LOCALIZE
17.          { "colorPre", Color.red }, { "colorGain", Color.blue }, { "colorLoss", Color.yellow }
18.    // END LOCALIZE
19.    };
20. }

Example 5-9. RetireStrings.properties

 1. language=Language
 2. computeButton=Compute
 3. savings=Prior Savings
 4. contrib=Annual Contribution
 5. income=Retirement Income
 6. currentAge=Current Age
 7. retireAge=Retirement Age
 8. deathAge=Life Expectancy
 9. inflationPercent=Inflation
10. investPercent=Investment Return
11. retire=Age: {0,number} Balance: {1,number,currency}

Example 5-10. RetireStrings_de.properties

 1. language=Sprache
 2. computeButton=Rechnen
 3. savings=Vorherige Ersparnisse
 4. contrib=Ju00e4hrliche Einzahlung
 5. income=Einkommen nach Ruhestand
 6. currentAge=Jetziges Alter
 7. retireAge=Ruhestandsalter
 8. deathAge=Lebenserwartung
 9. inflationPercent=Inflation
10. investPercent=Investitionsgewinn
11. retire=Alter: {0,number} Guthaben: {1,number,currency}

Example 5-11. RetireStrings_zh.properties

 1. language=u8bedu8a00
 2. computeButton=u8ba1u7b97
 3. savings=u65e2u5b58
 4. contrib=u6bcfu5e74u5b58u91d1
 5. income=u9000u4f11u6536u5165
 6. currentAge=u73b0u9f84
 7. retireAge=u9000u4f11u5e74u9f84
 8. deathAge=u9884u671fu5bffu547d
 9. inflationPercent=u901au8d27u81a8u6da8
10. investPercent=u6295u8d44u62a5u916c
11. retire=u5e74u9f84: {0,number} u603bu7ed3: {1,number,currency}

 

You have now seen how to use the internationalization features of the Java language. You use resource bundles to provide translations into multiple languages, and you use formatters and collators for locale-specific text processing.

In the next chapter, we delve into advanced Swing programming.



[1] “We have really everything in common with America nowadays, except, of course, language.” Oscar Wilde.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.174.0