Internationalization of components is nothing new, but switching locales at run time without changing existing code is new and very cool.
Although Java and its GUI library Swing provide software developers with a highly customizable framework for creating truly international applications, the Swing library is not sensitive to locale switches: it cannot automatically change an application's appearance to conform to the conventions of a specific locale at run time. Several types of applications benefit from the ability to easily switch the language at run time. Training applications and other programs that run on computers in public spaces (such as libraries, airports, or government offices) may need to support multiple languages. Other applications (like travel dictionaries or translation programs) are inherently multilingual and are specifically designed to support users of dissimilar tongues. Such applications would greatly benefit if the user-interface language could be customized at run time.
This article shows you how to customize Swing to support locale switching at run time. I have created a new look-and-feel called the MLMetalLookandFeel (where ML stands for multilingual). This new look-and-feel extends the standard Metal look-and-feel but is locale-sensitive at run time.
As an example, Ill modify the Notepad application from the J2SDK distribution in the demo/jfc/Notepad/ directory. The original version is localized for French, Swedish, and Chinese, as can be seen from the different resource files located in the resources/ subdirectory. Depending on the host locale that JVM is running on, the application will get all the text resources visible in the GUI from the corresponding resource file. You can load the resource file with the following code (from Notepad.java):
try { resources = ResourceBundle .getBundle("resources.Notepad", Locale.getDefault()); } catch (MissingResourceException e) { System.err.println("resources/" + "Notepad.properties not found"); System.exit(1); }The ResourceBundle class (see sidebar) tries to load the file resources/Notepad_XX_YY.properties where XX is the two letter ISO-639 language code [1] of the current default locale and YY the two letter ISO-3166 country code [2]. For more detailed information about locales, see the JavaDoc documentation of java.util.Locale. The sidebar describes the exact resolution mechanism for resource bundles if there is no exact match for the requested locales. In any case, the file resources/Notepad.properties is the last resort if no better match is found.
You can try out the available resources by setting the default locale at program startup with the two properties user.language and user.country [3]. For example, to run the Notepad application with a Swedish user interface, type:
java -Duser.language=sv NotepadA user interface internationalized in this way is only customizable once, at program startup. After the resources for the default locale are loaded, there is no way to switch the locale until the next program startup. I call this type of internationalization static internationalization. Throughout this article, I modify Notepad.java to make it dynamically internationalized (i.e., locale-sensitive at run time). I call this new application IntNotepad.
The Java Swing Architecture
A GUI application is composed of many UI components such as labels, buttons, menus, tool tips, etc. Each of these components must display some text (usually specified in the constructor) in order to be useful. In more complex components, like file choosers, the text can be accessed with set and get methods.
Internationalized applications like Notepad do not hard code these text strings into the program, but read the displayed text from resource files. Instead of:
JFrame frame = new JFrame(); frame.setTitle("Notepad");they use the following code:
JFrame frame = new JFrame(); frame.setTitle( resources.getString("Title"));where resources denotes the resource bundle opened in the previous code snippet. (Its unfortunate that the String class doesnt have a constructor that does the resource lookup automatically, but thats life.)
You could reset all these strings at run time every time the user chooses a different locale, but a manual switch is impractical in an application that uses hundreds of different components. Even worse, some components like JFileChooser do not offer accessory methods for all the strings they display. You must come up with another solution, which requires a closer look at the Swing architecture.
Swings design is based on a simplified MVC (Model-View-Controller) [4] architecture, called Separable Model (or Model/UI-Delegate) [5]. The Model-Delegate pattern combines the View and the Controller into a single object called the UI Delegate (see Figure 1). In Swing, these delegates are look-and-feel specific. They derive from the abstract class ComponentUI. By convention, they have the name of the corresponding Swing component, with the J in the component class name replaced by the name of the specific look-and-feel. For example, the UI delegate for JLabel in the Metal look-and-feel is named MetalLabelUI.
The UI delegate must paint the component to which it is tied. In contrast to the AWT library, the paint method of a Swing component just calls the paint method of its delegate along with a reference to itself to render itself.
The Solution: Idea and Implementation
Knowing the internals of the Swing architecture, you are ready to make the Swing components aware of locale switches at run time. Instead of setting the text field of a component to the string that should be displayed, you set the field to contain a key that identifies the displayed string. Then you override the UI delegate so that it uses the key to look up the local-specific string (instead of painting the key string obtained from its associated component) and prints that locale-specific string.
Listing 1 shows how a JLabel is usually created and initialized, followed by a code snippet taken from the BasicLabelUI.paint method, which is responsible for rendering the labels text.
I will now create a new UI delegate for Jlabel, called MLBasicLabelUI, which overrides the paint method to interpret the string received from its associated JLabel as a key into a resource, parameterized by the current Locale. The string passed to the Jlabel is rendered only if the UI delegate cant find an entry in the resource file. The changes in the UI are fully transparent to the component itself.
Getting the Localized Resource Strings
Since the procedure for fetching the localized text of a component is the same in all UI delegates, I put the code into a static method called getResourceString, defined in the class MLUtils.java (Listing 2).
This method builds the name of the resource file. It first queries the system properties for an entry called MainClassName. If it succeeds, the resource file will be a file with the same name in the resources/ subdirectory. If not, it will assume ML as the default resource filename. This filename along with the original key argument is passed to the second, two-parameter version of getResourceString (shown in Listing 3).
This method finally translates the key text into the appropriate localized value. If it cannot find the corresponding value for a certain key, it returns the key itself, so a component that is unaware of the multilingual UI is rendered normally.
For performance reasons, getResourceString stores resource files in a static Map after using them for the first time. Subsequent accesses use this cached version, without reloading the file again.
Overloading the UI Delegates paint Method
Having seen how localized strings can be replaced by their localized equivalent, the overloaded version of the paint method in MLBasicLabelUI (Listing 4) should be no surprise. The label is now initialized to "MyApplication.HelloString", which is a key into the localized resource file resources/MainClassName_XX_YY.properties.
A string not found in the resource file is displayed as is in the label, so my example works fine with the usual component UI; it would not respond to locale changes at run time, however.
If you want to make an entire applications GUI locale-sensitive at run time, you must create new UI classes for each Swing component used in the GUI. This sounds like a lot of work, but all it requires is redefining the methods that request text data from the methods associated component.
One potential problem is that in Swing the actual look-and-feels (like the Metal or Windows look-and-feel) use their own UI classes that are not directly derived from ComponentUI (see Figure 2). Instead, all the different UI classes for a single component inherit from a class called BasicXXXUI where XXX stands for an arbitrary component name. This base class factors out the functionality common to all the different look-and-feels.
This derivation hierarchy makes your job more difficult, because you would like to override the UIs of a distinct look-and-feel, but often the task of requesting and painting the actual text is done only, or at least in part, in the BasicXXXUI base classes. Therefore, you must specialize two classes. First you specialize the BasicXXXUI class for your component and redefine the methods that query the components text fields. Call this class MLBasicXXXUI. Then you copy and rename the actual component UI belonging to your desired look-and-feel from MetalXXXUI to MLMetalXXXUI. Next, change the base class from which it inherits from BasicXXXUI to MLBasicXXXUI, which is the name of your overloaded version of BasicXXXUI. Again, Metal is just an example here. It could be Windows, Motif, or any other look-and-feel. Additionally, if necessary, you may need to redefine the methods in MLMetalXXXUI that display text attributes from your associated component.
After implementing all the needed UI delegates, you tell your application to use the new delegates instead of the old ones. This can be done in two ways. You can register your delegates along with the component names at program startup (shown in Listing 5), or you can define a new look-and-feel that uses your custom UI delegates (shown in Listing 6).
After each locale switch, you trigger a repaint of the dynamically internationalized components. This is done by the little helper function in Listing 7, which takes a root window as argument and simply invalidates all the necessary child components so they will be repainted. It uses the helper method, recursiveFindMLJComponents, which recursively finds all the child components of a given container. As shown in Listing 8, the method returns all components that are instances of JComponent, but a more sophisticated version could return only dynamically internationalized components.
Notice that my version of repaintMLJComponents works only in applications with a single root window. If an application uses more than one root window, or if it uses non-modal dialogs, these additional windows also must be repainted. This can be done by defining a static method for registering the additional windows and dialogs and by extending repaintMLJComponents to invalidate these registered components as well. The version of repaintMLJComponents included in the online source code (<www.cuj.com/java/code.htm>) contains these extensions.
The Locale Chooser
The last step in the custom-localization process it to build a widget that displays all the available locales and allows users to choose a new default locale from this list.
Figure 3 and 4 show the new IntNotepad application with the built-in locale chooser. I added a permanent status bar to demonstrate locale switches for labels. Figure 3 shows the application with the English default locale while the user is switching it to Russian.
Figure 4 shows the application after the switch to Russian. Menus, labels, buttons, and even tool tips are now displayed with Cyrillic letters in Russian. Notice that the size of the menus has been resized automatically in order to hold the longer Russian menu names.
The class LocaleChooser is a small extension of a JComboBox with a custom renderer that displays each available Locale with a picture of the countrys flag and the name of the corresponding language. The language name is displayed in its own language if available and in English otherwise. Please notice that there is no one-to-one mapping between languages and country flags, as many languages are spoken in more than one country and there are countries in which more then one language is spoken. Therefore one must be careful when choosing a flag to not hurt the feelings of people who speak that language in a different country. After all, the flags should be just visual hints to simplify the selection of a particular language.
The LocaleChooser constructor is passed a String that denotes the resource directory of the application and the Container that is the root component passed to the repaintMLJComponentes method shown in Listing 7.
For every language or language/country combination, the resource directory passed to the LocaleChooser constructor should contain a subdirectory named by the two-letter language code or the two-letter language code plus an underscore plus the two-letter country code. Each of these subdirectories should contain a file flag.gif, which will be the image icon displayed by the LocaleChooser for the corresponding language.
Add more locales to the list of locales simply by adding the corresponding directories and files to the resource directory. You do not need to recompile LocaleChooser. Remember, however, for a locale switch to show any effects, a resource file with the localized component strings must be available as well.
Putting It All Together
After discussing how to make Swing components aware of locale switches at run time, I will now summarize the important steps and show how they fit into a real application.
First of all, the new component UI delegates must be created for all components that should be dynamically internationalizable. These UI delegates should be packed together into a new look-and-feel that is derived from an existing look-and-feel. This way, you dont have to create UI delegates for the full set of Swing components at the very beginning, but you have the possibility to stepwise extend your new look-and-feel for new components.
Once your new look-and-feel is available, you can start to modify your application to make it locale-sensitive at run time. The first step is to set the system property MainClassName to the name of your application. This information will be needed by the getResourceString method shown in Listing 2. Then you set your new look-and-feel as the standard look-and-feel for your application. You can achieve these two steps with the following code:
System.setProperty("MainClassName", "IntNotepad"); UIManager.setLookAndFeel( new MLMetalLookAndFeel());As a third step, you install an instance of the LocaleChooser presented in the last section somewhere in your application. Usually this will be the tool bar, but it can also be installed in a menu or a special options window along with other configuration options. The LocaleChooser must be instantiated with a reference to the main application window in order for the repaint method shown in Listing 7 to work properly.
Thats all. From now on, whenever you create a new Swing component, you have the choice of setting its string attributes to either a concrete string or to a key value. If the string attribute will be available in the applications resource file as a key, the keys value is displayed instead, according to the current default locale. Otherwise, the string attribute itself will be displayed.
Conclusion
This article presents a technique to make Swing components locale-sensitive at run time. It works by simply creating a new look-and-feel without changing any code in the components themselves. As an example, I derived the IntNotepad application from the Notepad example application available in every JDK distribution. IntNotepad is aware of locale changes and rebuilds the whole user interface every time such a change occurs at run time. It is available for download from the CUJ website <www.cuj.com/java/code.htm>.
By using the techniques presented here, it would be possible to lift the entire Swing library and make it locale-sensitive for run-time locale switches without any compatibility problems with older library versions.
Finally, I want to thank Roland Weiss and Dieter Bühler for their assistance and for reviewing this paper.
Notes and References
[1] ISO. The ISO-639 Two Letter Language Codes, <www.unicode.org/unicode/onlinedata/languages.html>.
[2] ISO. The ISO-3166 Two Letter Country Codes, <www.unicode.org/unicode/onlinedata/countries.html>.
[3] Be aware that setting the default locale on the command line with help of the mentioned properties does not work with all JDK versions on all platforms. Refer to the bugs 4152725, 4179660, and 4127375 in the Java Bug Database [9].
[4] E. Gamma, R. Helm, R. Johnson, and J. Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1995).
[5] John Zukowski and Scott Stanchfield. Fundamentals of JFC/Swing, Part II, MageLang Institute, <http://developer.java.sun.com/developer/onlineTraining/GUI/Swing>.
[6] Andrew Deitsch and David Czarnecki. Java internationalization (OReilly & Associates, 2001).
[7] Notice that property files are restricted to the ISO 8859-1 encoding. Any characters not available in this encoding must be expressed as Unicode escape sequences. The Java utility native2ascii may be used to convert files from an arbitrary encoding into an ASCII-encoded file with Unicode escape sequences.
[8] Sun Microsystems, Inc. The Java Tutorial, <http://java.sun.com/docs/books/tutorial/i18n/resbundle/index.html>.
[9] Sun Microsystems, Inc. The Java Bug Database, <http://developer.java.sun.com/developer/bugParade>.
[10] Sun Microsystems, Inc. Java Internationalization and Localization Toolkit 2.0, <http://java.sun.com/products/jilkit>.
Volker H. Simonis received a Masters Degree in Computer Science from the University of Tübingen (Germany). He is an expert in the field of generic programming, C++, and Java, with about five years of working experience in these areas. Currently he is writing his PhD thesis at the Computer-Algebra department of the University of Tübingen. He can be reached at simonis@informatik.uni-tuebingen.de.