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.
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/.
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
1961322
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.
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.
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.
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:
Get the locale object, as described in the preceding section.
Use a “factory method” to obtain a formatter object.
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.
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 get
XxxInstance
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 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. }
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.
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
2007912
Figure 5-2 shows the program (after Chinese fonts were installed). As you can see, it correctly displays the output.
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.)
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. }
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);
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.
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.
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.
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 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. }
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.
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 });
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}
.
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.
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.
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.
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.)
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
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”.
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.
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.
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.
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.
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");
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);
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}, . . . } }
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 } } } }
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.
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.
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 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.
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.
13.58.51.36