This document explains how text2gui
significantly eases the development of internationalized (i18n) GUI
applications written in Java. It also offers some tips for using the
text2gui library when applied to i18n applications.
We assume the you are already familiar with the use of resource bundles
and message formats. If this is not the case, you can consult Sun's
i18n
tutorial and the Javadoc for
ResourceBundle
and
MessageFormat.
Also, we assume you have some knowledge on using the text2gui library
to create components. If not, you should read the
Basics
and
Core text2gui documents first.
Problems and Solutions
Of course, the JDK already has many i18n features
built-in: locales,
resource
bundles, formatters for messages, dates, numbers, etc. These
features are geared towards the presentation of text. However, making a
GUI application i18n-ready is still a pain, isn't it?
This
section describes problems with the implementation of i18n applications
using the standard Java libraries, and how the text2gui library solves
these problems.
Problem: Code Complexity
With the existing infrastructure provided by the JDK, for every
property of a component that might depend on the locale, a resource
bundle needs to be consulted manually. Consider the creation and
configuration of a button:
JButton button = new JButton();
button.setText(bundle.getString("button.text"));
button.setIcon(new ImageIcon(bundle.getString("button.icon")));
button.setToolTipText(bundle.getString("button.tooltip"));
button.setMnemonic(bundle.getString("button.mnemonic").charAt(0));
That's a lot of code for just one button! And what about the other
properties
iconTextGap,
horizontalAlignment, etc.?
Their property values are integers so we need to either get an integer
from the resource bundle directly, or get a string, then convert the
string to an integer.
If the resource bundle vends an integer directly, the integer must come
from a
compiled subclass of
ResourceBundle.
This is very inconvenient for development because whenever a value
needs to change, the class needs to be recompiled. So we want our
resource bundles to come from
properties
files which don't require recompilation, but can only vend strings.
Thus we need to convert the string to an integer like this:
try {
button.setIconTextGap(Integer.parseInt(
bundle.getString("button.iconTextGap")));
} catch (NumberFormatException nfe) {
; // do nothing
}
I think the technical term for this kind of code is "Yuck!"
Solution: Creating Fully Configured
Components from Subkeys
Fortunately, text2gui has the ability to create fully-configured
components from resource bundle keys. So as an alternative to the code
above which configured each property of a button manually, we could
simply write:
JButton button = (JButton)
DispatchingComponentConverter.DEFAULT_INSTANCE.toObject(bundle,
"button", null, null);
Of course, that's not all that text2gui can do. text2gui can create
entire component hierarchies with a single line of Java code:
JFrame frame = (JFrame)
DispatchingComponentConverter.DEFAULT_INSTANCE.toObject(bundle,
"frame", null, null);
can create a frame with contents defined by a resource bundle. This
saves a lot of manual coding.
Problem: No Resource Bundle
Inheritence
Let's say you have two or more applications that use a common subset of
strings like
"OK",
"Cancel",
"Error", etc, and their
locale-specific translations. Ideally, you would have a single resource
bundle
with just these strings, and there would be numerous locale-specific
bundles with translations of these strings. But application A needs a
few extra strings of its own. Must it create two instances of
ResourceBundle, one to get strings
from the common set, and one to get its own strings? With only the JDK,
this seems to be the only option. Then it becomes very likely that the
code will mixup the two bundles.
Another solution might be to copy the entire contents of the common
resource bundle into a bundle just for application A, but this
duplication of data results in consistency problems. If someone updates
a string in a common set, application A might need the updated
string too.
Solution: Resource Bundle Chaining
A better solution would be to create a resource bundle that inherits
the key / values of another resource bundle. (This is a different type
of inheritance differs from locale-specific inheritence -- the base
name of
the bundle can vary here). Thus corrections and translations to a
parent bundle will automatically propagate down to a child bundle.
As described in
Resource
Bundles in
Depth, resource bundles created by
the text2gui library can inherit key / value pairs from another bundle,
by setting their
parentBundle
key appropriately.
com.taco.util.ChainedResourceBundleFactory
creates these "chained" resource bundles:
bundle = ChainedResourceBundleFactory.DEFAULT_INSTANCE.getBundle(
"MyResourceBundle", locale);
If MyResourceBundle.properties has the following line:
parentBundle=foo.bar.CommonResourceBundle
it will inherit all the key/value pairs of
foo.bar.CommonResourceBundle.
Problem: No Locale Dependent Layouts
Now let's consider another i18n problem: locale-dependent layout of
components. Let's consider a user input form which asks the user for a
date. In Europe, the layout of the form might look like this:
In America, land of of the strange, the month comes first, so the
layout might look like
this:
In East Asia, there are special Chinese characters used to
represent the words "month", "day", and "year". These characters follow
the numbers which are they label. So the layout might look like this (I
hope you have Japanese fonts installed):
With the infrastructure provided by the JDK, there is little choice but
to check the locale during the creation of the form, like this:
if (Locale.JAPAN.equals(locale)) {
// japanese layout
panel.add(yearTextField);
panel.add(yearLabel);
...
} else if (Locale.US.equals(locale)) {
// us layout
panel.add(monthLabel);
panel.add(monthTextField);
...
} else {
// european layout
panel.add(dayLabel);
panel.add(dayTextField);
...
}
Again, this is not pretty. If dates are displayed differently in yet
another locale, we need to change the code.
This problem is only one specific case of a more general problem: it is
difficult to retrieve many types of objects from a resource bundle
defined by a
properties
file. In the case above, we were not able to create a list of
components to add as the contents of the panel.
Solution: Locale Dependent Everything
Because resource bundle keys can be converted to any object by the
infrastructure provided by the text2gui library, all data can come from
resource bundles. Therefore
all
data used to construct a GUI can be locale-dependent. In the example
above, we could define the base bundle,
DateFormResourceBundle.properties, containing the following key:
panel.contents=[%dayLabel,
%dayTextField, %monthLabel, %monthTextField, %yearLabel, %yearTextField]
where
%dayLabel,
%dayTextField,
%monthLabel, etc. are
references to other resource bundle keys that define a label for the
day, the text field containing the day, a label for the month, etc.
This would work for the European layout.
To get a layout suitable for the US, we would define a locale-specific
bundle,
DateFormResourceBundle_en_US.properties that overrides the
panel.contents key:
panel.contents=[%monthLabel, %monthTextField, %dayLabel, %dayTextField, %yearLabel, %yearTextField]
Notice the month components precede the day components.
Finally, to get the layouts for East Asian countries, we would create
the properties files DateFormResourceBundle_zh.properties (for Chinese
speaking countries), DateFormResourceBundle_ja.properties (for Japan),
and DateFormResourceBundle_ko.properties (for Korea) that all override
the
panel.contents key:
panel.contents=[%dayTextField, %dayLabel,
%monthTextField, %monthLabel, %yearTextField, %yearLabel]
Of course, this is only a very specific example in which a
resource bundle key is converted to a collection of components. Other
locale dependent objects include fonts, borders, dimensions, spacing,
keyboard shortcuts, and icons. The
text2gui library contains converters from resource bundle keys to
instances of most of the classes used as property values by components.
Converters to other types can be implemented by extending classes
defined by the library. See
Can I use
the text2gui library to create instances of a custom type? for
details.
In general, any key in the bundle can be overridden in a
locale-specific bundle, and since all data used in conversion can come
from the bundle, all data can be locale-dependent.
Basic Strategy
The basic strategy for using the text2gui library to create i18n
applications can be summarized in a single rule:
Specify each locale dependent property
with its own resource bundle key.
This way, the property can be easily overridden in a locale-specific
resource bundle. What this means is to avoid using a long string to
specify an object; instead use a collection of subkeys of a resource
bundle key. For example,
panel=jpanel border={empty top=5 bottom=2} font=Serif-BOLD-12
is not as easy to internationalize as
panel.contents=[%monthLabel,
%monthTextField]
panel.border.dispathType=empty
panel.border.top=5
panel.border.bottom=2
panel.font=Serif-BOLD-12
because in the first description, the
panel key needs to redefined,
forcing the locale-specific bundle to set all of the
contents,
border, and
font properties. (Recall the
first step in the process text2gui uses to convert a resource bundle
key:
- If baseKey is
assigned to a value in bundle,
set val to that value.
- If val is a
string, perform string to object conversion to convert val, and return the result.
- Otherwise, return val
immediately.
The important implication of this rule is that
if a
value is
directly mapped to a resource
bundle key, the value is used for conversion, and the subkeys are
ignored. A locale-specific resource bundle inherits the key /
values pairs of its parent so once a base key is defined, a
locale-specific bundle can never use subkeys to describe the same
value.)
In the second description, a selected subset of the properties of the
panel can be easily overridden or added. For example, a locale-specific
bundle for Spain might look like this:
panel.font=SansSerif-BOLD-15
panel.border.dispatchType=titled
panel.border.title=Fecha
panel.prefSize={width=300,
height=100}
Building Strings with Message Formats
As you probably know, using message formats is extremely useful for
creating locale-specific strings. The text2gui library makes creating a
formatted string even easier than it is with the JDK, but you'll have
to learn how below.
QuotedStringConverter
can create a string based on two resource bundle keys: one specifying a
message format and another specifying a list of arguments. This saves
the application from having to do string formatting itself.
Consider a label that displays status messages. It might be defined as
follows:
statusLabel.text=$statusText
openStatusMessageText.format="{0}
was opened"
openStatusMessageText.args.0=$fileName
saveStatusMessageText.format="{0}
was saved"
saveStatusMessageText.args.0=$fileName
Since the text of the label is updated whenever the
statusText argument map key is
updated, all we need to do to change the text is to associate the
statusText key with the desired
string. We can use
QuotedStringConverter
to create the string using formatting. When a file is opened, we would
perform the following code:
// Assume "file" is an instance of File for the file we just opened.
argMap.putNoReturn("fileName", file.getName());
argMap.putNoReturn("statusText", QuotedStringConverter.instance.toString(
bundle, "openStatusMessageText", argMap);
It doesn't take much imagination to figure out that when a file is
saved,
saveStatusMessageText
should be used as the resource bundle key instead of
openStatusMessageText. By
overridding the
openStatusMessageText.format
key, locale-specific resource bundles can specify how the string should
be formatted. The locale-specific resource bundle for Spanish might
contain the following line:
openStatusMessageText.format=Se
abrió {0}
Note that just updating one of the arguments of the message format in
the argument map doesn't update the string! (Strings are immutable in
Java). The property value that uses the string must be set again when
you want the component to show the new message. Thus it's a good idea
to
use an updatable argument map key reference for a property value which
is a string that might change. That's exactly what we did above with
the line
statusLabel.text=$statusText.
If a string property value of a component never needs to change after
the component is created, it's even easier to format that string with a
message format; in fact no code needs to be written at all. Consider a
dialog box that informs the user that he owes a debt. It might be
defined in a
properties
file as follows:
dialog.title=No Soup For You!
dialog.optionPane=%optionPane
optionPane.message.format="You
owe me {0, number, currency}!
Please pay me by {1, date,
short}, or no soup for you ever again!"
optionPane.message.args.0=$debt
optionPane.message.args.1=$dueDate
optionPane.messageType=error
optionPane.optionType=default
Then the following code would create and show the dialog:
argMap.putNoReturn("debt", new Double(586.21));
// due date is one week after today:
argMap.putNoReturn("dueDate", new Date(System.currentTimeMillis() +
7.0 * 24 * 60 * 60 * 1000));
dialog = (JDialog) DispatchingComponentConverter.DEFAULT_INSTANCE.toComponent(
bundle, "dialog", argMap);
dialog.pack();
dialog.show();
Assuming the dialog is created on March 6, 2004 and that your default
locale is the US, the dialog will have the message
"You owe me $586.21!
Please pay me by 3/13/04, or no soup for you ever again!". Of
course, locale-specific bundles would probably override the
optionPane.message.format key
as well as the
dialog.title
key.
Notice no
Java code was needed to explicitly create the message string, since the
initial formatting is performed by the text2gui library, and no
re-formatting is required later.
Adjusting
Fonts to Match the Locale
Now that we have the tools to create applications localized for
say, the Chinese language, we are ready to test the GUI on a US
computer, right? Nope. Unfortunately, testing a GUI as would be
displayed by a computer with a different locale is not as easy as
simply changing the properties of components. The problem is, the
default fonts do not have the ability to display characters from all
languages, particularly East Asian languages. Therefore, simply
changing the properties of components results in the display of garbage
text.
The text2gui library provides a solution to this problem: it has the
capability to adjust the fonts of a component, and recursively, all its
contents. com.taco.i18n.gui.FontUtilities
contains the following static method:
void adjustFontsForLocale(Component component, Locale locale)
throws FontFormatException
adjustFontsForLocale()
first sees if the default font can display characters of the given
locale. If so, it does nothing. Thus calling
adjustFontForLocale() has no
effect on a platform whose default locale is the target locale. For
example, calling
adjustFontsForLocale(frame,
Locale.JAPANESE) on a Japanese version of Linux has no effect --
this is good thing since the frame is already fine as is.
If the default font cannot display characters of the given locale,
adjustFontsForLocale() searches
the system for fonts that can. (On Windows, these fonts are installed
when an Input Method Editor (IME) is installed.) If no capable font is
found, a
FontFormatException
is thrown. Otherwise, the font of the component and its border are set
to an appropriately sized version of one of the capable fonts. If a
child component implements
com.taco.i18n.gui.IFontAdjustedOnLocaleChange,
its
setLocale() method is
called (this may cause it to adjust its own fonts; see
Multi-Locale Components below).
Otherwise, its font is changed, recursively.
For more precise control over which font is used, you may implement the
strategy interface
com.taco.i18n.gui.FontUtilities.IFontMapper,
and pass it to the following method:
void adjustFontsForLocale(Locale oldLocale,
Component component, Locale locale, boolean recurse,
FontUtilities.IFontMapper fontMapper)
throws java.awt.FontFormatException
Although adjustFontsForLocale()
chooses fonts that are able to display characters in the target locale,
the resulting component is not exactly the same as would be displayed
by a computer whose default locale is the target locale. This is
because there is no way to determine what fonts are actually used on
such a computer, and these fonts are not likely to be available to
computers with different default locales anyway.
Another limitation of adjustFontsForLocale()
is that the fonts of tooltips are not changed. This means tooltips will
still show up as garbage text if the default font does not support
characters of the target locale.
Still, adjustFontsForLocale()
does give you a good idea of what your GUI would look like in
environments with a different locale. Furthermore, it gives you the
ability to present a GUI for different locales on any computer on which
the appropriate fonts are installed. This is useful for language
education software or any other software catering to non-native users.
Multi-Locale
Components
The FAQ discusses how to use
configuration
to create custom subclasses of
Component.
We now describe the steps necessary for such a component to be able
change its appearance when its locale changes. The component is
notified of a locale change when its
setLocale() method is called.
Typically, the
setLocale()
method should perform the following steps in order:
- Retrieve a resource bundle appropriate for the new locale
- Create a new argument map, putting key /values, and adding
listeners as necessary
- Re-configure the this
pointer
- Call adjustFontsForLocale()
with the this pointer as
the component
- Call the superclass's setLocale()
method to notify listeners
If your component is intended to be embedded in component hierarchies,
it should implement
IFontAdjustedOnLocaleChange.
This ensures that when
adjustFontsForLocale()
is called on a parent component, it calls the
setLocale() of your component
instead of adjusting the fonts of your component manually.
The applet
TimeZoneApplet in
the
demo/applet directory
of the developer's kit provides an example of how to implement
setLocale().
Summary
The text2gui library has several features that make the development of
i18n applications easier. Resource bundles and message formats remain
an integral part of
the infrastructure, but the text2gui library adds several additional
capabilities. Using the text2gui library, you can create resource
bundles that inherit from other resource bundles, create entire
component hierarchies with a single line of Java code, format
messages containing the string representation of data, and adjust fonts
to display characters of a target locale. These
capabilities make the development of i18n GUI applications
significantly easier.