Isolating Locale-Specific Data
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.
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 Filesshows 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
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 Timessection 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.
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 getBundle
method, 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
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
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.