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:
- By finding a properties
file
- 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:
- 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.
- 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.