Isolating Locale-Specific Data

«« Previous
Next »»

Locale-specific data must be tailored according to the conventions of the end user's language and region. The text displayed by a user interface is the most obvious example of locale-specific data. For example, an application with a Cancel button in the U.S. will have an Abbrechen button in Germany. In other countries this button will have other labels. Obviously you don't want to hardcode this button label. Wouldn't it be nice if you could automatically get the correct label for a given Locale? Fortunately you can, provided that you isolate the locale-specific objects in a ResourceBundle.

In this lesson you'll learn how to create and access ResourceBundle objects. If you're in a hurry to examine some coding examples, go ahead and check out the last two sections in this lesson. Then you can come back to the first two sections to get some conceptual information about ResourceBundle objects.

01. About the ResourceBundle Class


How a ResourceBundle is Related to a Locale

Conceptually each ResourceBundle is a set of related subclasses that share the same base name. The list that follows shows a set of related subclasses. ButtonLabel is the base name. The characters following the base name indicate the language code, country code, and variant of a Locale. ButtonLabel_en_GB, for example, matches the Locale specified by the language code for English (en) and the country code for Great Britain (GB).

ButtonLabel
ButtonLabel_de
ButtonLabel_en_GB
ButtonLabel_fr_CA_UNIX

To select the appropriate ResourceBundle, invoke the ResourceBundle.getBundle method. The following example selects the ButtonLabel ResourceBundle for the Locale that matches the French language, the country of Canada, and the UNIX platform.

Locale currentLocale = new Locale("fr", "CA", "UNIX");
ResourceBundle introLabels = ResourceBundle.getBundle(
                                 "ButtonLabel", currentLocale);

If a ResourceBundle class for the specified Locale does not exist, getBundle tries to find the closest match. For example, if ButtonLabel_fr_CA_UNIX is the desired class and the default Locale is en_US, getBundle will look for classes in the following order:

ButtonLabel_fr_CA_UNIX
ButtonLabel_fr_CA
ButtonLabel_fr
ButtonLabel_en_US
ButtonLabel_en
ButtonLabel

Note that getBundle looks for classes based on the default Locale before it selects the base class (ButtonLabel). If getBundle fails to find a match in the preceding list of classes, it throws a MissingResourceException. To avoid throwing this exception, you should always provide a base class with no suffixes.

The ListResourceBundle and PropertyResourceBundle Subclasses

The abstract class ResourceBundle has two subclasses: PropertyResourceBundle and ListResourceBundle.

A PropertyResourceBundle is backed by a properties file. A properties file is a plain-text file that contains translatable text. Properties files are not part of the Java source code, and they can contain values for String objects only. If you need to store other types of objects, use a ListResourceBundle instead. The section Backing a ResourceBundle with Properties Files shows you how to use a PropertyResourceBundle.

The ListResourceBundle class manages resources with a convenient list. Each ListResourceBundle is backed by a class file. You can store any locale-specific object in a ListResourceBundle. To add support for an additional Locale, you create another source file and compile it into a class file. The section Using a ListResource Bundle has a coding example you may find helpful.

The ResourceBundle class is flexible. If you first put your locale-specific String objects in a PropertyResourceBundle and then later decided to use ListResourceBundle instead, there is no impact on your code. For example, the following call to getBundle will retrieve a ResourceBundle for the appropriate Locale, whether ButtonLabel is backed up by a class or by a properties file:

ResourceBundle introLabels = ResourceBundle.getBundle(
                                 "ButtonLabel", currentLocale);
Key-Value Pairs

ResourceBundle objects contain an array of key-value pairs. You specify the key, which must be a String, when you want to retrieve the value from the ResourceBundle. The value is the locale-specific object. The keys in the following example are the OkKey and CancelKey strings:

class ButtonLabel_en extends ListResourceBundle {
    // English version
    public Object[][] getContents() {
        return contents;
    }
    static final Object[][] contents = {
        {"OkKey", "OK"},
        {"CancelKey", "Cancel"},
    };
}

To retrieve the OK String from the ResourceBundle, you would specify the appropriate key when invoking getString:

String okLabel = ButtonLabel.getString("OkKey");

A properties file contains key-value pairs. The key is on the left side of the equal sign, and the value is on the right. Each pair is on a separate line. The values may represent String objects only. The following example shows the contents of a properties file named ButtonLabel.properties:

OkKey = OK
CancelKey = Cancel

02. Preparing to Use a ResourceBundle


Identifying the Locale-Specific Objects

If your application has a user interface, it contains many locale-specific objects. To get started, you should go through your source code and look for objects that vary with Locale. Your list might include objects instantiated from the following classes:

◈ String
◈ Image
◈ Color
◈ AudioClip

You'll notice that this list doesn't contain objects representing numbers, dates, times, or currencies. The display format of these objects varies with Locale, but the objects themselves do not. For example, you format a Date according to Locale, but you use the same Date object regardless of Locale. Instead of isolating these objects in a ResourceBundle, you format them with special locale-sensitive formatting classes. You'll learn how to do this in the Dates and Times section of the Formatting lesson.

In general, the objects stored in a ResourceBundle are predefined and ship with the product. These objects are not modified while the program is running. For instance, you should store a Menu label in a ResourceBundle because it is locale-specific and will not change during the program session. However, you should not isolate in a ResourceBundle a String object the end user enters in a TextField. Data such as this String may vary from day to day. It is specific to the program session, not to the Locale in which the program runs.

Usually most of the objects you need to isolate in a ResourceBundle are String objects. However, not all String objects are locale-specific. For example, if a String is a protocol element used by interprocess communication, it doesn't need to be localized, because the end users never see it.

The decision whether to localize some String objects is not always clear. Log files are a good example. If a log file is written by one program and read by another, both programs are using the log file as a buffer for communication. Suppose that end users occasionally check the contents of this log file. Shouldn't the log file be localized? On the other hand, if end users rarely check the log file, the cost of translation may not be worthwhile. Your decision to localize this log file depends on a number of factors: program design, ease of use, cost of translation, and supportability.

Organizing ResourceBundle Objects

You can organize your ResourceBundle objects according to the category of objects they contain. For example, you might want to load all of the GUI labels for an order entry window into a ResourceBundle called OrderLabelsBundle. Using multiple ResourceBundle objects offers several advantages:

◈ Your code is easier to read and to maintain.
◈ You'll avoid huge ResourceBundle objects, which may take too long to load into memory.
◈ You can reduce memory usage by loading each ResourceBundle only when needed.

03. Backing a ResourceBundle with Properties Files


This section steps through a sample program named PropertiesDemo.

1. Create the Default Properties File

A properties file is a simple text file. You can create and maintain a properties file with just about any text editor.

You should always create a default properties file. The name of this file begins with the base name of your ResourceBundle and ends with the .properties suffix. In the PropertiesDemo program the base name is LabelsBundle. Therefore the default properties file is called LabelsBundle.properties. This file contains the following lines:

# This is the default LabelsBundle.properties file
s1 = computer
s2 = disk
s3 = monitor
s4 = keyboard

Note that in the preceding file the comment lines begin with a pound sign (#). The other lines contain key-value pairs. The key is on the left side of the equal sign and the value is on the right. For instance, s2 is the key that corresponds to the value disk. The key is arbitrary. We could have called s2 something else, like msg5 or diskID. Once defined, however, the key should not change because it is referenced in the source code. The values may be changed. In fact, when your localizers create new properties files to accommodate additional languages, they will translate the values into various languages.

2. Create Additional Properties Files as Needed

To support an additional Locale, your localizers will create a new properties file that contains the translated values. No changes to your source code are required, because your program references the keys, not the values.

For example, to add support for the German language, your localizers would translate the values in LabelsBundle.properties and place them in a file named LabelsBundle_de.properties. Notice that the name of this file, like that of the default file, begins with the base name LabelsBundle and ends with the .properties suffix. However, since this file is intended for a specific Locale, the base name is followed by the language code (de). The contents of LabelsBundle_de.properties are as follows:

# This is the LabelsBundle_de.properties file
s1 = Computer
s2 = Platte
s3 = Monitor
s4 = Tastatur

The PropertiesDemo sample program ships with three properties files:

LabelsBundle.properties
LabelsBundle_de.properties
LabelsBundle_fr.properties

3. Specify the Locale

The PropertiesDemo program creates the Locale objects as follows:

Locale[] supportedLocales = {
    Locale.FRENCH,
    Locale.GERMAN,
    Locale.ENGLISH
};

These Locale objects should match the properties files created in the previous two steps. For example, the Locale.FRENCH object corresponds to the LabelsBundle_fr.properties file. The Locale.ENGLISH has no matching LabelsBundle_en.properties file, so the default file will be used.

4. Create the ResourceBundle

This step shows how the Locale, the properties files, and the ResourceBundle are related. To create the ResourceBundle, invoke the getBundlemethod, specifying the base name and Locale:

ResourceBundle labels = ResourceBundle.getBundle("LabelsBundle", currentLocale);

The getBundle method first looks for a class file that matches the base name and the Locale. If it can't find a class file, it then checks for properties files. In the PropertiesDemo program we're backing the ResourceBundle with properties files instead of class files. When the getBundle method locates the correct properties file, it returns a PropertyResourceBundle object containing the key-value pairs from the properties file.

5. Fetch the Localized Text

To retrieve the translated value from the ResourceBundle, invoke the getString method as follows:

String value = labels.getString(key);

The String returned by getString corresponds to the key specified. The String is in the proper language, provided that a properties file exists for the specified Locale.

6. Iterate through All the Keys

This step is optional. When debugging your program, you might want to fetch values for all of the keys in a ResourceBundle. The getKeys method returns an Enumeration of all the keys in a ResourceBundle. You can iterate through the Enumeration and fetch each value with the getString method. The following lines of code, which are from the PropertiesDemo program, show how this is done:

ResourceBundle labels = ResourceBundle.getBundle("LabelsBundle", currentLocale);
Enumeration bundleKeys = labels.getKeys();

while (bundleKeys.hasMoreElements()) {
    String key = (String)bundleKeys.nextElement();
    String value = labels.getString(key);
    System.out.println("key = " + key + ", " + "value = " + value);
}

7. Run the Demo Program

Running the PropertiesDemo program generates the following output. The first three lines show the values returned by getString for various Locale objects. The program displays the last four lines when iterating through the keys with the getKeys method.

Locale = fr, key = s2, value = Disque dur
Locale = de, key = s2, value = Platte
Locale = en, key = s2, value = disk

key = s4, value = Clavier
key = s3, value = Moniteur
key = s2, value = Disque dur
key = s1, value = Ordinateur

04. Using a ListResourceBundle


This section illustrates the use of a ListResourceBundle object with a sample program called ListDemo. The text that follows explains each step involved in creating the ListDemo program, along with the ListResourceBundle subclasses that support it.

1. Create the ListResourceBundle Subclasses

A ListResourceBundle is backed up by a class file. Therefore the first step is to create a class file for every supported Locale. In the ListDemo program the base name of the ListResourceBundle is StatsBundle. Since ListDemo supports three Locale objects, it requires the following three class files:

StatsBundle_en_CA.class
StatsBundle_fr_FR.class
StatsBundle_ja_JP.class

The StatsBundle class for Japan is defined in the source code that follows. Note that the class name is constructed by appending the language and country codes to the base name of the ListResourceBundle. Inside the class the two-dimensional contents array is initialized with the key-value pairs. The keys are the first element in each pair: GDP, Population, and Literacy. The keys must be String objects and they must be the same in every class in the StatsBundle set. The values can be any type of object. In this example the values are two Integer objects and a Double object.

import java.util.*;
public class StatsBundle_ja_JP extends ListResourceBundle {
    public Object[][] getContents() {
        return contents;
    }

    private Object[][] contents = {
        { "GDP", new Integer(21300) },
        { "Population", new Integer(125449703) },
        { "Literacy", new Double(0.99) },
    };
}

2. Specify the Locale

The ListDemo program defines the Locale objects as follows:

Locale[] supportedLocales = {
    new Locale("en", "CA"),
    new Locale("ja", "JP"),
    new Locale("fr", "FR")
};

Each Locale object corresponds to one of the StatsBundle classes. For example, the Japanese Locale, which was defined with the ja and JP codes, matches StatsBundle_ja_JP.class.

3. Create the ResourceBundle

To create the ListResourceBundle, invoke the getBundle method. The following line of code specifies the base name of the class (StatsBundle) and the Locale:

ResourceBundle stats = ResourceBundle.getBundle("StatsBundle", currentLocale);
The getBundle method searches for a class whose name begins with StatsBundle and is followed by the language and country codes of the specified Locale. If the currentLocale is created with the ja and JP codes, getBundle returns a ListResourceBundle corresponding to the class StatsBundle_ja_JP, for example.

4. Fetch the Localized Objects

Now that the program has a ListResourceBundle for the appropriate Locale, it can fetch the localized objects by their keys. The following line of code retrieves the literacy rate by invoking getObject with the Literacy key parameter. Since getObject returns an object, cast it to a Double:

Double lit = (Double)stats.getObject("Literacy");

5. Run the Demo Program

ListDemo program prints the data it fetched with the getBundle method:

Locale = en_CA
GDP = 24400
Population = 28802671
Literacy = 0.97

Locale = ja_JP
GDP = 21300
Population = 125449703
Literacy = 0.99

Locale = fr_FR
GDP = 20200
Population = 58317450
Literacy = 0.99

05. Customizing Resource Bundle Loading


Earlier in this lesson you have learned how to create and access objects of the ResourceBundle class. This section extents your knowledge and explains how to take an advantage from the ResourceBundle.Control class capabilities.

The ResourceBundle.Control was created to specify how to locate and instantiate resource bundles. It defines a set of callback methods that are invoked by the ResourceBundle.getBundle factory methods during the bundle loading process.

Unlike a ResourceBundle.getBundle method described earlier, this ResourceBundle.getBundle method defines a resource bundle using the specified base name, the default locale and the specified control.

public static final ResourceBundle getBundle(
    String baseName,
    ResourceBundle.Control cont
    // ...
The specified control provide information for the resource bundle loading process.

The following sample program called RBControl.java illustrates how to define your own search paths for Chinese locales.

1. Create the properties Files.

As it was described before you can load your resources either from classes or from properties files. These files contain descriptions for the following locales:

◈ RBControl.properties – Global
◈ RBControl_zh.properties – Language only: Simplified Chinese
◈ RBControl_zh_cn.properties – Region only: China
◈ RBControl_zh_hk.properties – Region only: Hong Kong
◈ RBControl_zh_tw.properties – Taiwan

In this example an application creates a new locale for the Hong Kong region.

2. Create a ResourceBundle instance.

As in the example in the previous section, this application creates a ResourceBundle instance by invoking the getBundle method:

private static void test(Locale locale) {
    ResourceBundle rb = ResourceBundle.getBundle(
                            "RBControl",
                            locale,
                            new ResourceBundle.Control() {
                                    // ...
                            }
                        );
The getBundle method searches for properties files with the RBControl prefix. However, this method contains a Control parameter, which drives the process of searching the Chineese locales.

3. Invoke the getCandidateLocales method

The getCandidateLocales method returns a list of the Locales objects as candidate locales for the base name and locale.

new ResourceBundle.Control() {
    @Override
    public List<Locale> getCandidateLocales(
                            String baseName,
                            Locale locale) {
                // ...                                     
    }
}

The default implementation returns a list of the Locale objects as follows: Locale(language, country).

However, this method is overriden to implement the following specific behavior:

if (baseName == null)
    throw new NullPointerException();

if (locale.equals(new Locale("zh", "HK"))) {
    return Arrays.asList(
               locale,
               Locale.TAIWAN,
               // no Locale.CHINESE here
               Locale.ROOT);
} else if (locale.equals(Locale.TAIWAN)) {
    return Arrays.asList(
               locale,
               // no Locale.CHINESE here
               Locale.ROOT);
}

Note, that the last element of the sequence of candidate locales must be a root locale.

4. Call the test class

Call the test class for the following four different locales:

public static void main(String[] args) {
    test(Locale.CHINA);
    test(new Locale("zh", "HK"));
    test(Locale.TAIWAN);
    test(Locale.CANADA);
}

5. Run the Sample Program

You will see the program output as follows:

locale: zh_CN
        region: China
        language: Simplified Chinese
locale: zh_HK
        region: Hong Kong
        language: Traditional Chinese
locale: zh_TW
        region: Taiwan
        language: Traditional Chinese
locale: en_CA
        region: global
        language: English

Note that the newly created was assigned the Hong Kong region, because it was specified in an appropriate properties file. Traditional Chinese was assigned as the language for the Taiwan locale.

Two other interesting methods of the ResourceBundle.Control class were not used in the RBControl example, but they deserved to be mentioned. The getTimeToLive method is used to determine how long the resource bundle can exist in the cache. If the time limit for a resource bundle in the cache has expired, the needsReload method is invoked to determine whether the resource bundle needs to be reloaded.

«« Previous
Next »»