Editor's note: Having your Java apps run correctly both down the street and across the globe presents some hefty challenges. Part one of this two-part excerpt from Java Examples in a Nutshell, 3rd Edition covered the first two steps to internationalization in Java: using Unicode character encoding and handling local customs. This week deals with the third step in the process: localizing user-visible messages.
The third task of internationalization involves ensuring that there are no user-visible strings that are hardcoded in an application; instead, strings should be looked up based on the locale. In Example 8-3, for example, the strings "Portfolio value", "Symbol", "Shares", and others are hardcoded in the application and appear in English, even when the program is run in the French locale. The only way to prevent this is to fetch all user-visible messages at runtime and to translate every message into each language your application must support.
|
Related Reading
|
Java helps you handle this task with the
ResourceBundle class of the
java.util package. This class represents a bundle
of resources that can be looked up by name. You define a localized
resource bundle for each locale you want to support, and Java loads
the correct bundle for the default (or specified) locale. With the
correct bundle loaded, you can look up the resources (typically
strings) your program needs at runtime.
To define a bundle of localized
resources, you create a subclass of ResourceBundle
and provide definitions for the handleGetObject( )
and getKeys( ) methods. handleGetObject(
) is passed the name of a resource; it should return an
appropriate localized version of that resource. getKeys(
) should return an Enumeration object
that gives the user a list of all resource names defined in the
ResourceBundle. Instead of subclassing
ResourceBundle directly, however, it is often
easier to subclass ListResourceBundle. You can
also simply provide a property file (see the
java.util.Properties class) that
ResourceBundle.getBundle( ) uses to create an
instance of PropertyResourceBundle.
To use
localized resources from a ResourceBundle in a
program, you should first call the static getBundle(
) method, which dynamically loads and instantiates a
ResourceBundle, as described shortly. The returned
ResourceBundle has the name you specify and is
appropriate for the specified locale (or for the default locale if no
locale is explicitly specified). Once you have obtained a
ResourceBundle object with getBundle(
), use the getObject( ) method to look
up resources by name. Note that there is a convenience method,
getString( ), that simply casts the value returned
by getObject( ) to be a String object.
When you call getBundle( ), you specify the base
name of the desired ResourceBundle and a desired
locale (if you don't want to rely on the default
locale). Recall that a Locale is specified with a
two-letter language code, an optional two-letter country code, and an
optional variant string. getBundle( ) looks for an
appropriate ResourceBundle class for the locale by
appending this locale information to the base name for the bundle.
The method looks for an appropriate class with the following
algorithm:
Search for a class with the following name:
basename_language_country_variant
If no such class is found or no variant string is specified for the locale, it goes to the next step.
Search for a class with the following name:
basename_language_country
If no such class is found or no country code is specified for the locale, it goes to the next step.
Search for a class with the following name:
basename_language
If no such class is found, it goes to the final step.
Search for a class that has the same name as the basename, or, in other words, search for a class with the following name:
basename
This represents a default resource bundle used by any locale that is not explicitly supported.
At each step in this process,
getBundle( ) checks first for a class file with
the given name. If no class file is found, it uses the
getResourceAsStream( ) method of
ClassLoader to look for a
Properties file with the same name as the class
and a .properties extension. If such a
properties file is found, its contents are used to create a
Properties object, and getBundle(
) instantiates and returns a
PropertyResourceBundle that exports the properties
in the Properties file through the
ResourceBundle API.
If
getBundle( ) cannot find a class or properties
file for the specified locale in any of the four previous search
steps, it repeats the search using the default locale instead of the
specified locale. If no appropriate ResourceBundle
is found in this search either, getBundle( )
throws a MissingResourceException.
Any
ResourceBundle object can have a parent
ResourceBundle specified for it. When you look up
a named resource in a ResourceBundle,
getObject( ) first looks in the specified bundle,
but if the named resource is not defined in that bundle, it
recursively looks in the parent bundle. Thus, every
ResourceBundle inherits the resources of its
parent and may choose to override some, or all, of these resources.
(Note that we are using the terms
"inherit" and
"override" in a different sense
than we do when talking about classes that inherit and override
methods in their superclass.) What this means is that every
ResourceBundle you define does not have to define
every resource required by your application. For example, you might
define a ResourceBundle of messages to display to
French-speaking users. Then you might define a smaller and more
specialized ResourceBundle that overrides a few of
these messages so that they are appropriate for French-speaking users
who live in Canada.
Your application is not required to find and set up the parent
objects for the ResourceBundle objects it uses.
The getBundle( ) method actually does this for
you. When getBundle( ) finds an appropriate class
or properties file as described previously, it does not immediately
return the ResourceBundle it has found. Instead,
it continues through the remaining steps in the previous search
process, looking for less-specific class or properties files from
which the ResourceBundle may inherit resources. If
and when getBundle( ) finds these less-specific
resource bundles, it sets them up as the appropriate ancestors of the
original bundle. Only once it has checked all possibilities does it
return the original ResourceBundle object that it
created.
To continue the example begun earlier, when a program runs in Quebec,
getBundle( ) might first find a small specialized
ResourceBundle class that has only a few specific
Quebecois resources. Next, it looks for a more general
ResourceBundle that contains French messages, and
it sets this bundle as the parent of the original Quebecois bundle.
Finally, getBundle( ) looks for (and probably
finds) a class that defines a default set of resources, probably
written in English (assuming that English is the native tongue of the
original programmer). This default bundle is set as the parent of the
French bundle (which makes it the grandparent of the Quebecois
bundle). When the application looks up a named resource, the
Quebecois bundle is searched first. If the resource
isn't defined there, the French bundle is searched,
and any named resource not found in the French bundle is looked up in
the default bundle.
Examining some code makes this discussion of resource bundles much clearer. Example 8-4 is a convenience routine for creating Swing menu panes. Given a list of menu item names, it looks up labels and menu shortcuts for those named menu items in a resource bundle and creates a localized menu pane. The example has a simple test program attached.
Figure 8-3 shows the menus it creates in the American, British, and French locales. This program cannot run, of course, without localized resource bundles from which the localized menu labels are looked up.

Figure 8-3. Localized menu panes
package je3.i18n;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.Locale;
import java.util.ResourceBundle;
import java.util.MissingResourceException;
/** A convenience class to automatically create localized menu panes */
public class SimpleMenu {
/** The convenience method that creates menu panes */
public static JMenu create(ResourceBundle bundle,
String menuname, String[] itemnames,
ActionListener listener)
{
// Get the menu title from the bundle. Use name as default label.
String menulabel;
try { menulabel = bundle.getString(menuname + ".label"); }
catch(MissingResourceException e) { menulabel = menuname; }
// Create the menu pane.
JMenu menu = new JMenu(menulabel);
// For each named item in the menu.
for(int i = 0; i < itemnames.length; i++) {
// Look up the label for the item, using name as default.
String itemlabel;
try {
itemlabel =
bundle.getString(menuname+"."+itemnames[i]+".label");
}
catch (MissingResourceException e) { itemlabel = itemnames[i]; }
JMenuItem item = new JMenuItem(itemlabel);
// Look up an accelerator for the menu item
try {
String acceleratorText = bundle.getString(menuname+"."+itemnames[i]+".accelerator");
item.setAccelerator(KeyStroke.getKeyStroke(acceleratorText));
}
catch (MissingResourceException e) {}
// Register an action listener and command for the item.
if (listener != null) {
item.addActionListener(listener);
item.setActionCommand(itemnames[i]);
}
// Add the item to the menu.
menu.add(item);
}
// Return the automatically created localized menu.
return menu;
}
/** A simple test program for the above code */
public static void main(String[] args) {
// Get the locale: default, or specified on command-line
Locale locale;
if (args.length == 2) locale = new Locale(args[0], args[1]);
else locale = Locale.getDefault( );
// Get the resource bundle for that Locale. This will throw an
// (unchecked) MissingResourceException if no bundle is found.
ResourceBundle bundle =
ResourceBundle.getBundle("com.davidflanagan.examples.i18n.Menus",
locale);
// Create a simple GUI window to display the menu with
final JFrame f = new JFrame("SimpleMenu: " + // Window title
locale.getDisplayName(Locale.getDefault( )));
JMenuBar menubar = new JMenuBar( ); // Create a menubar.
f.setJMenuBar(menubar); // Add menubar to window
// Define an action listener that our menu will use.
ActionListener listener = new ActionListener( ) {
public void actionPerformed(ActionEvent e) {
String s = e.getActionCommand( );
Component c = f.getContentPane( );
if (s.equals("red")) c.setBackground(Color.red);
else if (s.equals("green")) c.setBackground(Color.green);
else if (s.equals("blue")) c.setBackground(Color.blue);
}
};
// Now create a menu using our convenience routine with the resource
// bundle and action listener we've created
JMenu menu = SimpleMenu.create(bundle, "colors",
new String[] {"red", "green", "blue"},
listener);
// Finally add the menu to the GUI, and pop it up
menubar.add(menu); // Add the menu to the menubar
f.setSize(300, 150); // Set the window size.
f.setVisible(true); // Pop the window up.
}
}
As I've already said, this example does not stand alone. It relies on resource bundles to localize the menu. The following listing shows three property files that serve as resource bundles for this example. Note that this single listing contains the bodies of three separate files:
# The file Menus.properties is the default "Menus" resource bundle.
# As an American programmer, I made my own locale the default.
colors.label=Colors
colors.red.label=Red
colors.red.accelerator=alt R
colors.green.label=Green
colors.green.accelerator=alt G
colors.blue.label=Blue
colors.blue.accelerator=alt B
# This is the file Menus_en_GB.properties. It is the resource bundle for
# British English. Note that it overrides only a single resource definition
# and simply inherits the rest from the default (American) bundle.
colors.label=Colours
# This is the file Menus_fr.properties. It is the resource bundle for all
# French-speaking locales. It overrides most, but not all, of the resources
# in the default bundle.
colors.label=Couleurs
colors.red.label=Rouge
colors.green.label=Vert
colors.green.accelerator=control shift V
colors.blue.label=Bleu
|
We've seen that in order to internationalize programs, you must place all user-visible messages into resource bundles. This is straightforward when the text to be localized consists of simple labels such as those on buttons and menu items. It is trickier, however, with messages that are composed partially of static text and partially of dynamic values. For example, a compiler might have to display a message such as "Error at line 5 of file "hello.java"", in which the line number and filename are dynamic and locale-independent, while the rest of the message is static and needs to be localized.
The MessageFormat
class of the java.text package helps tremendously
with these types of messages. To use it, you store only the static
parts of a message in the ResourceBundle and
include special characters that indicate where the dynamic parts of
the message are to be placed. For example, one resource bundle might
contain the message: "Error at line {0} of file
{1}". And another resource bundle might contain a
"translation" that looks like this:
"Erreur: {1}: {0}".
To use such a localized message,
you create a MessageFormat object from the static
part of the message and then call its format( )
method, passing in an array of the values to be substituted. In this
case, the array contains an Integer object that
specifies the line number and a String object that
specifies the filename. The MessageFormat class
knows about other Format classes defined in
java.text. It creates and uses
NumberFormat objects to format numbers and
DateFormat objects to format dates and times. In
addition, you can design messages that create
ChoiceFormat objects to convert from numbers to
strings. This is useful when working with enumerated types, such as
numbers that correspond to month names, or when you need to use the
singular or plural form of a word based on the value of some number.
Example 8-5 demonstrates this kind of
MessageFormat usage. It is a convenience class
with a single static method for the localized display of exception
and error messages. When invoked, the code attempts to load a
ResourceBundle with the basename
"Errors". If found, it looks up a
message resource using the class name of the exception object that
was passed. If such a resource is found, it displays the error
message. An array of five values is passed to the format(
) method. The localized error message can include any or
all of these arguments.
The
LocalizedError.display( ) method defined in this
example was used in Example 8-2 at the beginning of
this chapter. The default Errors.properties
resource bundle used in conjunction with this example is shown
following the code listing. Error message display for the program is
nicely internationalized. Porting the program's
error message to a new locale is simply a matter of translating
(localizing) the Errors.properties file.
package je3.i18n;
import java.text.*;
import java.io.*;
import java.util.*;
/**
* A convenience class that can display a localized exception message
* depending on the class of the exception. It uses a MessageFormat,
* and passes five arguments that the localized message may include:
* {0}: the message included in the exception or error.
* {1}: the full class name of the exception or error.
* {2}: the file the exception occurred in
* {3}: a line number in that file.
* {4}: the current date and time.
* Messages are looked up in a ResourceBundle with the basename
* "Errors", using a the full class name of the exception object as
* the resource name. If no resource is found for a given exception
* class, the superclasses are checked.
**/
public class LocalizedError {
public static void display(Throwable error) {
ResourceBundle bundle;
// Try to get the resource bundle.
// If none, print the error in a nonlocalized way.
try {
String bundleName = "com.davidflanagan.examples.i18n.Errors";
bundle = ResourceBundle.getBundle(bundleName);
}
catch (MissingResourceException e) {
error.printStackTrace(System.err);
return;
}
// Look up a localized message resource in that bundle, using the
// classname of the error (or its superclasses) as the resource name.
// If no resource was found, display the error without localization.
String message = null;
Class c = error.getClass( );
while((message == null) && (c != Object.class)) {
try { message = bundle.getString(c.getName( )); }
catch (MissingResourceException e) { c = c.getSuperclass( ); }
}
if (message == null) { error.printStackTrace(System.err); return; }
// Get the filename and linenumber for the exception
// In Java 1.4, this is easy, but in prior releases, we had to try
// parsing the output Throwable.printStackTrace( );
StackTraceElement frame = error.getStackTrace( )[0]; // Java 1.4
String filename = frame.getFileName( );
int linenum = frame.getLineNumber( );
// Set up an array of arguments to use with the message
String errmsg = error.getMessage( );
Object[ ] args = {
((errmsg!= null)?errmsg:""), error.getClass( ).getName( ),
filename, new Integer(linenum), new Date( )
};
// Finally, display the localized error message, using
// MessageFormat.format( ) to substitute the arguments into the message.
System.err.println(MessageFormat.format(message, args));
}
/**
* This is a simple test program that demonstrates the display( ) method.
* You can use it to generate and display a FileNotFoundException or an
* ArrayIndexOutOfBoundsException
**/
public static void main(String[ ] args) {
try { FileReader in = new FileReader(args[0]); }
catch(Exception e) { LocalizedError.display(e); }
}
}
The
following listing shows the resource bundle properties file used to
localize the set of possible error messages that can be thrown by the
ConvertEncoding class of Example 8-2:
#
# This is the file Errors.properties
# One property for each class of exceptions that our program might
# report. Note the use of backslashes to continue long lines onto the
# next. Also note the use of \n and \t for newlines and tabs
#
java.io.FileNotFoundException: \
Error: File "{0}" not found\n\t\
Error occurred at line {3} of file "{2}"\n\tat {4}
java.io.UnsupportedEncodingException: \
Error: Specified encoding not supported\n\t\
Error occurred at line {3} of file "{2}"\n\tat {4,time} on {4,date}
java.io.CharConversionException:\
Error: Character conversion failure. Input data is not in specified format.
# A generic resource. Display a message for any error or exception that
# is not handled by a more specific resource.
java.lang.Throwable:\
Error: {1}: {0}\n\t\
Error occurred at line {3} of file "{2}"\n\t{4,time,long} {4,date,long}
With a resource bundle like this, ConvertEncoding
produces error messages like the following:
Error: File "myfile (No such file or directory)" not found
Error occurred at line 64 of file "FileInputStream.java"
at 7/9/00 9:28 PM
Or, if the current locale is fr_FR:
Error: File "myfile (Aucun fichier ou repertoire de ce type)" not found
Error occurred at line 64 of file "FileInputStream.java"
at 09/07/00 21:28
Exercise 8-1.Several
internationalization-related classes, such as
NumberFormat and DateFormat,
have static methods named getAvailableLocales( )
that return an array of the Locale objects they
support. You can look up the name of the country of a given
Locale object with the getDisplayCountry(
) method. Note that this method has two variants. One takes
no arguments and displays the country name as appropriate in the
default locale. The other version of getDisplayCountry(
) expects a Locale argument and displays
the country name in the language of the specified locale.
Write a program that displays the country names for all locales
returned by NumberFormat.getAvailableLocales( ).
Using the static locale constants defined by the
Locale class, display each country name in
English, French, German, and Italian.
View catalog information for Java Examples in a Nutshell, 3rd Edition
Return to ONJava.com.
Copyright © 2007 O'Reilly Media, Inc.