Resource Bundles in Depth

Resource bundles are implemented in the JDK class java.util.ResourceBundle. The Javadoc for ResourceBundle provides a wealth of information regarding the usage of resource bundles. This document will repeat some of the information in the Javadoc, then explain the text2gui library's own system for creating resource bundles which provides additional capabilities.

Resource Bundles as Implemented by java.util.ResourceBundle

What Resource Bundles Do

Basically, resource bundles are used to provide a map from string keys to values that depend on the desired locale. Resource bundles are usually read in from properties files that look like this:

key1=value1
key2=value2
# .... '#' starts a comment -- more key / value pairs follow

Assuming the above file backs the resource bundle bundle, a call to bundle.getObject("key1") would return the string "value1".

Where to Put Resource Bundles

Let's say that the bundle is to be named com.acme.junk.MyResourceBundle. Commonly, the name of the resource bundle has the same name qualifier (com.acme.junk. in this example) as the code that uses it. So presumably, the bundle is going to be used by some class in the package com.acme.junk. Resource bundles are searched by changing all the dots to slashes, then appending the changed name to each path in the class path. Since the path containing the com.acme.junk package is going to be in the classpath when a class in the package is executed, the resource bundle should be saved in the same directory as the class that uses it. Because the file above is a properties file, it should be given the name "MyResourceBundle.properties".

Resource Bundles Implemented By Classes

Another way that resource bundles can be created is by compiling a Java class that extends java.util.ResourceBundle. The fully qualified class name must be exactly the same as the name of the resource bundle. For example, we could compile a class named OtherResourceBundle in the package foo.bar. Then we could retrieve the bundle by the name "foo.bar.OtherResourceBundle". The advantage of resource bundles implemented by classes is that they can vend non-string objects for values. The disadvantage is that they must be recompiled whenever changed. For this reason, code that uses the text2gui library will almost always backs resource bundles with properties files.

Retrieving Resource Bundles

How exactly do we retrieve a bundle by name? Using the java.util package, we can call ResourceBundle.getBundle(String name, Locale locale), or simply ResourceBundle.getBundle(String name) if we want to use the default locale. In our above example, the line

bundle = ResourceBundle.getBundle("com.acme.junk.MyResourceBundle");

will do the trick.

Value Inheritance / Overriding Mechanism

How does a resource bundle vend different values depending on the locale? It's a bit complicated, but basically, the locale passed to ResouceBundle.getBundle(String name, Locale locale) determines what other bundles to look for. If say, the locale was for Japan, the additional bundles com.acme.junk.MyResourceBundle_ja and com.acme.junk.MyResourceBundle_ja_JP would be also be loaded if present. ("ja" is the language code for Japanese, and "JP" is the country code for Japan). Now a inheritance hierarchy is created:

com.acme.junk.MyResourceBundle_ja_JP
com.acme.junk.MyResourceBundle_ja
com.acme.junk.MyResourceBundle

(Actually this is only a subset of the inheritance hierarchy -- see java.util.ResourceBundle for the real details).

When a key is looked up with a call to bundle.getObject(), the most specific bundle is checked first. In this case we would check com.acme.junk.MyResourceBundle_ja_JP. If the key is defined, it is returned. Otherwise, it checks the next most specific bundle, com.acme.junk.MyResourceBundle_ja. This procedure is repeated until all the bundles are searched. The general idea is that keys defined in more specific bundles override those defined in less specific bundles. This is a powerful mechanism because it allows locale-specific keys to be overridden while locale-independent keys are inherited.

An Example

As an example, we put 3 files in the same directory, which is in our classpath.

HelloResourceBundle_ja.properties:

language=Nihongo
hello=Konnichi wa!

HelloResourceBundle.properties:

language=English
hello=Hello!
email=loser@msn.com

Hello.java:

import java.util.ResourceBundle;

public class Hello {
  public Hello() {
      bundle = ResourceBundle.getBundle(getClass().getName() + "ResourceBundle");
  }

  public void sayIt() {
     System.out.println(bundle.getString("language"));
     System.out.println(bundle.getString("hello"));
     System.out.println("from " + bundle.getString("email"));
  }

  public static void main(String[] args) {
     Hello h = new Hello();
     h.sayIt();
  }

  ResourceBundle bundle;
}

Running "java Hello" on a computer in America produces:

ENGLISH
Hello!
from loser@msn.com

This is because no resource bundles are available for "Hello_en" or "Hello_en_US". ("en" is the language code for English). Only the base resource bundle is available, so all values come from there.

Running "java Hello" on a computer in Japan produces:

Nihongo
Konnichi wa!
from loser@msn.com

since "Hello_ja.properties" is searched before "Hello.properties". The last line is the same because the email key was not overridden.

To get the same effect on any computer in the world, we need to specify that we want the locale for Japan instead of the default locale:

bundle = ResourceBundle.getBundle(getClass().getName() + "ResourceBundle",
  Locale.JAPAN);


Also, notice how the name of the bundle was constructed. We use the fully qualified class name, followed by "ResourceBundle". In this example, this would resolve to "HelloResourceBundle", since the class is in the anonymous package. If we changed the package to com.acme.junk, and assuming we move the resource bundles to the corresponding directory, the above line would use the name "com.acme.junk.HelloResourceBundle", as desired. So getting resource bundles like this protects the code from requiring additional changes if you decide to change packages.

properties syntax

We already created and used resource bundles based on properties files, but actually the syntax is a bit more complicated than just plain text. So that properties files can support text in multiple languages, the properties file is encoded in a special format which allows for escape sequences that describe Unicode characters. In this format, the characters '\', ':', '=', '!'  and '#' must be escaped with a backslash. Also, properties files can contain comments -- lines beginning with an unescaped '#' or '!' are ignored. Here is a snippet that demonstrates the syntax:

# This is a comment. Everything on this line will be thrown away!
truism=2 + 3 \= 5\!

If the above file is used to back a resource bundle, the key "truism" will map to the string "2 + 3 = 5!".

See the Javadoc for java.util.Properties for more information on the properties syntax -- but be aware that the text2gui library uses a slightly modified syntax called multi-line properties, which is described later.

Here's a hint for i18n developers on Windows systems. It's easiest to enter in foreign text with an Input Method Editor (IME) installed in Windows. A properties file that provides text for a foreign language can initially be edited with Notepad. When saving the file, save using the Unicode (NOT Unicode big endian) encoding. Let's assume you give the file the name "infile.props". Then use the native2ascii utility supplied with the Java Development Kit like this:

native2ascii -encoding "UnicodeLittle" infile.props MyResourceBundle.properties

This will output the file "MyResourceBundle.properties" that has the characters in "infile.props", but properly encoded for use by the Java library classes.

text2gui Extensions to Resource Bundles

Resource bundles are a great idea, but their implementation as is leaves some features to be desired. These features, described below, have been implemented in the text2gui library.

Extension of the Inheritance Hierarchy

First of all, the inheritance hierarchy as implemented by ResourceBundle.getBundle() only includes resource bundles with the same base name (HelloResourceBundle in the above example). However, we might want to inherit key / values from a resource bundle with a different name. Such a bundle might contain strings used in a variety of applications, such as "OK", "Cancel", "Yes", "No". Of course, these strings would change depending on the locale passed to the bundle creation method. Let's call this bundle foo.bar.CommonResourceBundle and define it as:

ok=OK
cancel=Cancel
yes=Yes
no=No

We also have a Spanish version with simple name "CommonResourceBundle_es":

ok=Acepta

cancel=Cancele
yes=Sí

We need a way for a bundle such as AudioPlayerResourceBundle to inherit the keys of foo.bar.CommonResourceBundle. To do this we set the parentBundle property in AudioPlayerResourceBundle:

parentBundle=foo.bar.CommonResourceBundle

play.text=Play
# ... more properties here

There is also a Spanish version in "AudioPlayerResourceBundle_es":

play.text=Toca
# ... más properties aquí

Now we just need a way to get a resource bundle object that recognizes the parentBundle property and constructs an inheritance hierarchy like this, assuming the desired locale is for Argentina:

AudioPlayerResourceBundle_es_AR
AudioPlayerResourceBundle_es
AudioPlayerResourceBundle
foo.bar.CommonResourceBundle_es_AR
foo.bar.CommonResourceBundle_es
foo.bar.CommonResourceBundle

com.taco.util.ChainedResourceBundleFactory does just this, so a bundle with this hierarchy can be retrieved by calling

bundle = ChainedResourceBundleFactory.DEFAULT_INSTANCE.getBundle("AudioPlayerResourceBundle",
  new Locale("es", "AR"));

Now bundle.getObject("Play") returns "Toca", while bundle.getObject("yes") returns "Sí".

com.taco.util.ChainedResourceBundleFactory also has the ability to create a bundle with a hierarchy specified by string.  The string is composed of bundle names, separated by semicolons (';'). Bundles names occuring earlier in the string have their corresponding bundles checked before bundles specified later in the string. For example,

bundle = ChainedResourceBundleFactory.DEFAULT_INSTANCE.getBundle(
  "AudioPlayerResourceBundle;OtherResourceBundle;com.acme.junk.WastedResourceBundle",

  Locale.TAIWAN);

creates a hierarchy like this:

AudioPlayerResourceBundle_zh_TW
AudioPlayerResourceBundle_zh
AudioPlayerResourceBundle
foo.bar.CommonResourceBundle_zh_TW
foo.bar.CommonResourceBundle_zh
foo.bar.CommonResourceBundle
OtherResourceBundle_zh_TW
OtherResourceBundle_zh
OtherResourceBundle
com.acme.junk.WastedResourceBundle_zh_TW
com.acme.junk.WastedResourceBundle_zh
com.acme.junk.WastedResourceBundle

Note the appearance of foo.bar.CommonResourceBundle even though it wasn't specified in the string. The parentBundle property is still respected. foo.bar.CommonResourceBundle and its locale-specific friends are considered part of AudioPlayerResourceBundle so it is checked before bundles later in the string. 

The parentBundle property also may be set to a ';' separated list of resource bundles, to get multiple inheritence. If multiple resource bundles inherit from a common resource bundle, each common resource bundle will only be searched once. Also there is no danger of infinite recursion if a bundle inherits from itself, directly or indirectly.

Support for Different Implementations of ResourceBundle

Another limitation of ResourceBundle.getBundle() is that there are only two ways that a resource bundle can be loaded:
  1. By finding a properties file
  2. By finding a Java class
Furthermore, the syntax for a properties file is not ideal for code segments. In the syntax, every property value that takes multiple lines needs to use a line continuation marker '\' as the very last character on each line before the last one. Because the text2gui library relies heavily on BeanShell scripts and many other long strings as property values, the ordinary properties file syntax would be overly burdensome to the programmer. A modification of the properties file syntax was created, called multi-line properties. The multi-line properties syntax is different from the properties syntax in two ways:
  1. If a property value ends the line within a Java braced context (inside an unclosed ", ', (, {, or [), the next line is automatically concatenated with the last line. This process continues until all Java punctuation has been closed. Of course, brace characters that occur in a quoted or commented context are not treated as changing the brace level. Also, a brace character can be escaped with a backslash ('\') to indicate that it does not start a braced context.
  2. If a non-escaped backslash ('\') is detected, and only whitespace follows it, it will be treated as a line continuation marker. This alleviates the frustration of ensuring the backslash is the very last character of a line that is followed by another one.
These two modifications make writing multi-line property values considerably easier. Now we can write:

okButton.actionListeners.0={
  return new ActionListener() {
    public void actionPerfomed(ActionEvent event) {
      // Assume the dialog that this button belongs to is in the global "dialog".
     
getGlobal("dialog", argMap).dispose();
    }
  };
}

which would use the entired contents of the braces, including the braces themselves, as the property value for the okButton.action key, because the string remains in a braced context until the last line.

By default, com.taco.util.ChainedResourceBundleFactory interprets properties files it finds using com.taco.util.MultiLineProperties, which supports the multi-line syntax described above.

If you have existing properties files, the vast majority of them will be interpreted in the same way using the multi-line syntax. The only problem to watch out for is lines containing unclosed opening brace characters, which will make the lines after them all be part of the same property value, until a closing brace character is found. To work around this, escape brace characters with a backslash ('\') if you don't want them to start a braced context.

The resource bundle factory classes in the text2gui also provide support for an additional implementation of ResourceBundle: one based on javax.swing.UIManager, which holds icons, borders, etc. for the current UI. This resource bundle contains all key / value pairs in UIManager, for which the key is a string. To retrieve this bundle, use "UIManager" as the bundle name.

Finally, through subclassing, it is possible to provide your own implementation of ResourceBundle based on a name. One application of this ability might be to provide an implementation of ResourceBundle based on key / values defined in an XML file, if the properties syntax does not suit you. See the Javadoc for com.taco.util.ResourceBundleFactory._loadOrphanBundle() for details on how to do this.

Bundle Cache Invalidation

A feature built into java.util.ResourceBundle.getBundle() is resource bundle caching. That is, if a bundle name is requested more than once, the same bundle is returned. This avoids the expensive process of reloading the bundle, which may involve parsing a file or loading a class. This is normally a good thing, but consider an application whose GUI is defined by a resource bundle. While it's running, once it loads the bundle, that bundle cannot change. If the application allowed the user to upgrade its interface by specifying overwriting the file backing the bundle, or if a programmer edited the file backing the bundle, those changes cannot take effect until the application was restarted.

The text2gui library's resource bundle factories also cache resource bundles, but they allow the bundle cache to be invalidated, so that the next load of a bundle re-reads the file backing the bundle. This can be done by calling ChainedResourceBundleFactory.invalidateBundles(). Now that bundles can replaced at run-time, applications can upgrade their interfaces without restarting.

Summary

Resource bundles are a powerful way of providing values at runtime. Values can easily be overridden depending on the locale, so resource bundles are an ideal solution to internationalization problems. With the extensions in the text2gui library, several bundles can be combined in a structured way and alternate syntaxes for describing bundles can be supported. One extremely useful alternate syntax, multi-line properties, is already built into the text2gui system for loading bundles. com.taco.util.ChainedResourceBundleFactory is the only class that most programmers will need to access these features.