A Guide To Writing i18n Applications with text2gui

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:

Day:

Month:

Year:


In America, land of of the strange, the month comes first, so the layout might look like this:

Month:

Day:

Year


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:
  1. 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:
  1. Retrieve a resource bundle appropriate for the new locale
  2. Create a new argument map, putting key /values, and adding listeners as necessary
  3. Re-configure the this pointer
  4. Call adjustFontsForLocale() with the this pointer as the component
  5. 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.