Java Internationalization: Localization with ResourceBundles

   
   

Articles Index


If you've developed global software, you know that one of the great commandments of internationalization is to separate text, labels, messages, and other locale-sensitive objects from the core source code. This helps to maintain a single source code base for all language versions of your product. It also facilitates translations because all localizable resources are identified and isolated.

Depending on the operating system (OS) and programming language environments, developers can use a variety of mechanisms to identify and separate culturally sensitive or locale-dependent objects from their source code. Windows programmers have their resource (RC) files and resource-only dynamic link libraries (DLLs). Macintosh developers have their resource forks and similar concepts. Of course, Java developers have a method too--the resource bundle. 

This article explores the types, structure, creation, and usage of Java resource bundles. You'll learn how to use these files to create a localizable program. 

Resource Bundles

The most obvious type of localizable object is a text string, but resource bundles can store any Java object. Whatever you choose to store, you'll put it into some form of java.util.ResourceBundle

To create a concrete class you must subclass ResourceBundle and implement two methods: handleGetObjects and getKeys. Directly subclassing ResourceBundle like this is a distraction from the main purpose of a bundle, which is to store localizable objects, not to write a lot of code logic. Fortunately, the Java Development Kit (JDK) provides two subclasses, ListResourceBundle and PropertyResourceBundle, that allow you to concentrate on providing the data for the file instead of writing code. The differences between bundle types are explained later in this article, but first take a look at the common ground. 

What's in a Name?

For each language translation of  your product, you create a new version of your initial resource bundle. For example, if you create MyResource to store all English text, you will create a similarly named file to store the French text. Resource bundles use a naming convention that distinguishes the potentially numerous versions of essentially the same bundle. Each bundle name consists of a base name and an optional locale identifier. Together, these two pieces uniquely identify a bundle in a package. 

Using the above example, the French version of the resources should be named MyResource_fr_FR. The Canadian French version should be MyResource_fr_CA

Appending the locale name to the base name of the bundle effectively links the bundle's usage to a specific locale. When you ask for the bundle using ResourcBundle.getBundle("MyResource"), the getBundle method appends the default locale identifier to the base name and loads the appropriate bundle. If the locale is fr_CA, then a call to ResourceBundle.getBundle("MyResource") will load the MyResource_fr_CA bundle. 

Actually, getBundle is overloaded. So, not only can you just pass it a bundle name and expect it to return the bundle for the default locale, but you can also pass a specific locale. For example, if you want to explicitly load a Japanese bundle, you can call ResourceBundle.getBundle("MyResource", new Locale("ja", "JP"))

The getBundle method provides a graceful degradation algorithm that attempts to find the nearest matched bundle in cases where the specified bundle can't be found or doesn't exist. In the following search, each element of the search name is separated with an '_' (UNDERSCORE) character. The algorithm searches for bundle names in the following order: 

<baseclass>+<specific language>+
  <specific country>+<specific variant>
<baseclass>+<specific language>+
  <specific country>
<baseclass>+<specific language>
<baseclass>+<default language>+
  <default country>+<default variant>
<baseclass>+<default language>+
  <default country>
<baseclass>+<default language>
<baseclass>

So, use a concrete example this time. Suppose the default locale determined from the operating system is en_US (U.S. English). However, you may want to load MyResource for the fr_CA (Canadian French) locale instead. The call to ResourceBundle.getBundle("MyResource", new Locale("fr","CA")) would produce the following search order:

MyResource_fr_CA
MyResource_fr
MyResource_en_US
MyResource_en
MyResource

The search would end, of course, as soon as any bundle is found along the way. If no bundle is found, getBundle throws a MissingResourceException

Accessing Objects

All ResourceBundle subclasses provide access methods to retrieve the locale-sensitive objects stored within themselves. The most basic access method is the following: 

public final Object getObject(String key)

The getObject method returns the object associated with the key. Because the return value is simply an Object, you may have to cast it to a more specific object type. For example, if you know the returned Object is a String, you can do the following: 

ResourceBundle res = 
  ResourceBundle.getBundle("MyResource");
String labelOK = 
  (String) res.getObject("OK_TEXT");

Casting the returned object is easy, but ResourceBundle provides a few convenience methods for returning common object types. Some examples follow: 

public final String getString(String key)
public final String[] getStringArray(String key)

These methods are convenient, but they don't do anything complex. If they save you some of the time or trouble necessary to cast the returned value, try them. The same code above becomes:

ResourceBundle res = 
   ResourceBundle.getBundle("MyResource");
String labelOK = res.getString("OK_TEXT");

Inheritance

Bundles can share the same <key, value> pairs if the bundles share the same base name. For example, imagine you have created MyResource_en, which will serve as your default bundle for all English speaking locales. If your largest customer base speaks U.S. English, you might choose to create this bundle using U.S. English. To create a British English version of this bundle, you create MyResource_en_GB, which should contain only the objects that are different from the contents in MyResource_en. Imagine that you have stored a <key, value> pair for "Hello, world!" in MyResource_en. You then decide that the British English version of the string is the same, so there's no need to reproduce the string in MyResource_en_GB. When you then ask for the "Hello, world!" string in MyResource_en_GB, the ResourceBundle class is smart enough to look in MyResource_en when it doesn't find the string in MyResource_en_GB.

The ResourceBundle class associates a parent to any bundle. If an object value cannot be found in the specified class, ResourceBundle searches the parent class in the relationship. You establish this relationship among bundles by giving them the same base name.

Figure 1 ResourceBundle Inheritance

ListResourceBundle

The java.util.ListResourceBundle is a subclass of java.util.ResourceBundle. It implements both handleGetObjects and getKeys for you. The purpose of this bundle is to allow you to define localizable elements as a two-dimensional array of <key, value> pairs. This bundle is easy to use and requires only minimal code, which allows you to focus on providing data in the bundle instead. 

You can create your own ListResourceBundle by subclassing java.util.ListResourceBundle. You must implement a single method, getContents(), which should return an array of <key, value> pairs. The following code shows the structure of a simple bundle:

import java.util.ListResourceBundle;

import java.awt.Button;

public class MyResource extends
           java.util.ListResourceBundle {

  public Object getContents() {
    return contents;
  }

  static Object[][] contents = {
    { "HELLO_TEXT",
      "Hello, world!" },
    { "GOODBYE_TEXT",
      "Goodbye everyone!" },
    { "CANCEL_BUTTON, 
      new Button("Cancel") },
    { "ADDRESS_PANEL",
      new AddressPanel()}
  };


ListResourceBundle allows you to store any class of object. Notice the definition of CANCEL_BUTTON above. CANCEL_BUTTON is an AWT Button object that gets instantiated inside the resource bundle itself. Although value objects can be any type, keys are always a String object in both ListResourceBundle and PropertyResourceBundles.

ListResourceBundles are classes, so you store them in a .java file. Of course, you must compile them to byte-code to use them.

PropertyResourceBundle

A PropertyResourceBundle is definitely the easiest bundle to implement. It is nothing more than a text file. A PropertyResourceBundle can have multiple lines, and each line is either a comment, blank line, or a <key>=<value> entry. Property bundles should follow the same naming conventions as those used by a ListResourceBundle. However, the differences are that property bundles are not compiled and that they have a properties extension. So, the bundle below could be named MyResource.properties.

The following example illustrates the structure and contents of the above resource bundle implemented as a property file:

HELLO_TEXT=Hello, world!
GOODBYE_TEXT=Goodbye everyone!
CANCEL_BUTTON_TEXT=Cancel

// We'd like to say new AddressPanel() here but
// property bundles are restricted in that area
ADDRESS_PANEL=AddressPanel

// Other objects cannot be stored in a
// PropertyResourceBundle
// Only strings can live here.

Unfortunately, property files of this type have a limitation on the objects that can be stored. You are limited to storing String <value> entries. However, this limitation is tolerable, especially since there is a workaround. You can create an object instance if you know its class name. Imagine that you need to create an AddressPanel object as indicated in the above property file. The following code shows how to handle both the "Cancel" button and the panel.

ResourceBundle res =
  ResourceBundle.getBundle("MyResource");
// we get text in the same way
String strHello = res.getString(
                  "HELLO_TEXT");
String strGoodbye = res.getString(
                "GOODBYE_TEXT");
// but we need to create the button ourselves
Button button = new Button();
// and set the text too
button.setText(res.getString(
  "CANCEL_BUTTON_TEXT");
// creating a class from a text
// string is a little trickier
// but is still easy
Class addrPanelClass = 
  Class.forName(res.getString(
               "ADDRESS_PANEL");
AddressPanel addrPanel = 
             addrPanelClass.newInstance();


Other Resource Bundles

You can create additional resource bundle types yourself by subclassing the java.util.ResourceBundle class. There are plenty of reasons why you might want to do this. Maybe you'd like to read all text from a database. Or maybe you want to have a different type of key for locating text objects. Or perhaps you want to create bundle classes that specialize in types of data access for performance reasons. It's up to you, and if you find a reason why the available bundles don't work for you, you can modify and extend their behavior and performance.

Inprise, Inc. provides a good example of a new ResourceBundle type in their JBuilder product. JBuilder is an integrated development environment (IDE) for Java, and it includes a new class named borland.jbcl.util.ArrayResourceBundle. ArrayResourceBundle is interesting because it implements keys as integer offsets into arrays of objects. The motivation for creating this new bundle is performance, because array offsets can be used quickly and more efficiently than Strings.

So, just how much better is performance? The results are surprising. Inprise's international software development team for JBuilder helped evaluate the performance differences among these three classes:

  • java.util.PropertyResourceBundle
  • java.util.ListResourceBundle
  • borland.jbcl.util.ArrayResourceBundle

Table 1 ResourceBundle Performance

Class Name Size (bytes) Average  Run Time (seconds)
PropertyResourceBundle 13114 9.37
ListResourceBundle 19041 6.10
ArrayResourceBundle 13585 0.90

Compiling Non-ASCII Bundles

Java uses Unicode as its internal character set. However, most operating system, editors, and other tools still use regional code pages to represent text. In most cases, this means that you will create your *.java files in a regional encoding, not Unicode. Of course, the Java compiler must compensate for the difference by converting your native code page into Unicode. Several character converters are included in the JDK. You can see a partial list in Table 2.

The Java compiler automatically assumes that your resource files are written in the default code page of your operating system. So, if you're using a U.S. English version of Solaris, the compiler will probably assume your character set is ISO 8859-1. Although 8859-1 supports many languages, it doesn't represent everything. So, if you need to compile a Cyrillic language bundle, you would get incorrect results. Fortunately, the compiler allows a command line directive that specifies the character set of the source file. Using the appropriate converter, you can direct the compiler to accurately convert the *.java source into Unicode. The following example shows the command line you would use to compile a Japanese resource bundle that's written in Shift JIS, a popular Japanese encoding: 
 
javac -encoding SJIS MyResource_ja.java 

Table 2 Partial List of Supported Languages and Converters 

Language Converter
Arabic (ar) 8859_6
Byelorussian (be) 8859_5
Czech (cs) 8859_2
Dutch (nl) 8859_1, Cp1252
English (en) 8859_1, Cp1252
German (de) 8859_1, Cp1252
Greek (el) 8859_7
Hebrew (iw) 8859_8
Japanese (ja) SJIS, JIS, EUCJIS
Macedonian (mk) 8859_5
Maltese (mt) 8859_3
Norwegian (no) 8859_1, Cp1252
Polish (pl) 8859_2
Russian (ru) KOI8_R, 8859_5
Turkish (tr) 8859_9, Cp1254
Ukrainian (uk) 8859_5

Sun Microsystems continually updates its list of supported character sets. If you need one that's not already supported, chances are that it's in progress. Of course, it wouldn't hurt to submit a request at the JDC Bug Database either.

You'll need to understand another caveat regarding property bundles; you must create them in ASCII, or use Unicode escape codes to represent Unicode characters. Since you don't compile a property file to byte codes, there is no way to tell the Java 1 Virtual Machine (JVM) which character set to use. So you must explicitly stick with ASCII or use the escape sequences. You can represent a Unicode character in any Java file using \uXXXX, where XXXX is the hexidecimal representation of the character. If you don't know the hexidecimal value for a character, just use the native2ascii tool provided in the JDK. This tool will convert a file written in your native encoding to one that represents non-ASCII characters as Unicode escape sequences. A partial listing of the %JDK_HOME%\lib\font.properties.ja file provides an example of these escape sequences:

# AWT Font default Properties for Japanese Windows
#
dialog.0=Arial,ANSI_CHARSET
dialog.1=\uff2d\uff33 \u30b4\u30b7\u30c3\u30af,
  SHIFTJIS_CHARSET
dialog.2=WingDings,SYMBOL_CHARSET,NEED_CONVERTED
dialog.3=Symbol,SYMBOL_CHARSET,NEED_CONVERTED
dialoginput.0=Courier New,ANSI_CHARSET
dialoginput.1=\uff2d\uff33 \u30b4\u30b7\u30c3\u30af,
  SHIFTJIS_CHARSET
dialoginput.2=
           WingDings,SYMBOL_CHARSET,NEED_CONVERTED
dialoginput.3=Symbol,SYMBOL_CHARSET,NEED_CONVERTED

Graceful Degradation

Java searches for bundles by first looking for the fully qualified bundle name including a complete locale. If it can't find that file, it repeatedly chops off or replaces pieces of the locale identifier. The search algorithm is useful, since it provides graceful degradation when the requested bundle cannot be found.

Conclusion

Resource bundles are a great way to isolate translatable text or localizable objects from your core source code. They're both easy to create and easy to use. Moreover, many of the good integrated development environments are including resource-bundle creation and management tools. Make sure you use resource bundles in your next application. Better yet, explore ways to incorporate them into your existing applications. They will definitely make localizing your product easier.

_______
1 As used on this web site, the terms "Java virtual machine" or "JVM" mean a virtual machine for the Java platform.

John O'Conner teaches software internationalization topics and consults for global development projects. He also enjoys speaking Japanese, playing softball, and spending time with his family.