Let's talk about Java and internationalization. Java makes a pretty clear point about being platform independent and so it's only logical to also try to keep user interfaces language neutral. When you read the Java I18N trail however, it more or less only tells you to use ResourceBundleS, but keeps rather quiet about best practice. What you are essentially left alone with are the four most important questions:
- What's a good naming scheme for keys?
- How do you organize the property files?
- How do you access translation strings with minimal code overhead?
- What tools are available for outsourcing the actual translation job to non developers?
Organizing translations
The funny thing about organizing stuff is that the organizing scheme always seems so obvious and plausible only right after it has been established. However, coming up with a good scheme in the first place can often prove to be quite a challenge.
To illustrate my point, let me first demonstrate how you can go wrong when approaching the issue naively. After raising awareness for the problems you can stumble into, I'll introduce a method for doing it properly.
So, how should you organize your property files? The first idea might be to simply put everything into a single file and group keys by a prefix, like so:
mainwindow.title = My first program mainwindow.menubar.filemenu = File mainwindow.menubar.filemenu.new = New mainwindow.menubar.filemenu.open = Open mainwindow.menubar.filemenu.save = Save mainwindow.menubar.helpmenu = Help mainwindow.menubar.helpmenu.contents = Contents mainwindow.menubar.filemenu.quit = Quit quitdialog.title = Sure you want to quit? quitdialog.message = Unsaved work! Quit anyway?
This would certainly be very efficient from a performance point of view, as only a single ResourceBundle object is required and onyl a single IO call to load it is needed. Efficiency, however, comes at a price:
- Finding an entry you want to edit becomes increasingly tedious as the property file grows in size.
- Finding a place for inserting new entries is even worse (assuming you want to keep order).
- Keys become unwieldy long.
- It's easy to (accidentally) break your own naming convention, resulting in an inhomogeneous structure.
- It's easy to forget to remove obsolete entries, resulting in clutter.
- Central files are a nightmare when several developers work on the same project.
To put it short: the single file approach does not scale at all. Using multiple files, however, immediately raises the question of what should go where. Considering the example above, splitting by prefix seems like a viable approach. That is, you'd end up with mainwindow.properties and quitdialog.properties. Your keys would become shorter and more manageable, but you are still left with one big, unsolved problem: Your keys do not relate to source code position. Without using tools like grep, you'll have a hard time figuring out where a key is defined and where it is used.
So, with all this in mind, it becomes quite clear that some kind of robust addressing scheme is required that builds a strong, bidirectional relationship between properties and their respective source code position. Such an addressing scheme is defined by the following five rules:
- All property files should be located below a "resources" directory (e.g.
resources/i18n). - The directory structure of the sourcetree should be cloned below the "resources" directory. For example, when you have a package
gui.dialogs, there should also be a directoryresources/i18n/gui/dialogs - For every compile unit (.java file) that contains translatable strings, there should be a corresponding .properties file. For example, if you have
gui/dialogs/QuitDialog.javayou should also haveresources/i18n/gui/dialogs/quitdialog.properties(by convention, use all lowercase for filenames). - Within a properties file use methodname.tag as a naming scheme for keys, where "methodname" is the name of the method, the key is used in. A key that is used in a constructor or in an initializer for global field should have "< init>" as the methodname. Any non-empty string may be used as the tag value. Tags are needed when methods require more than one string.
- Within a compile unit (.java file), omit the methodname and only use the tag value when referencing a key.
Here is some example code and a corresponding properties file to illustrate these rules (note: Tr.t() is used to fetch translations and will be explained further below):
package de.onyxbits.example; import javax.swing.*; import java.awt.event.*; public class ExampleClass { private JLabel helloButton = new JLabel(Tr.t("helloButton")); private int count = 0; public Example() { super(Tr.t("title")); getContentPane().add(helloButton); helloButton.addActionListener(this); } public void actionPerformed(ActionEvent e) { System.err.println(Tr.t("message",count)); count++; if (count>=3) { System.err.println(Tr.t("enough")); System.exit(0); } } }
The corresponding resources/i18n/de/onyxbits/example/exampleclass.properties file should then have the following contents:
<init>.helloButton = Hello World <init>.title = This is the window title actionPerformed.message = The "Hello World" button was pressed {0} times! actionPerformed.enough = That's enough! I'm quitting!
You may have noticed that with this organizing schemes, you'll easily add redundancy as different keys may produce the same display message. This is entirely intended. Never ever try to save a few kilobytes of RAM by reusing properties in different parts of the application. You'll just learn the hard way that if two display strings look the same in one language, they do not nescessarily have to in another!
The English language for example has a couple of nouns that are only available in either singular or plural form (e.g. "sheep", "trousers", "information"). It is a mistake to assume that this would also be the case in German or French. Define a property like:
m.information = Information
and use it in singular as well as in plural contexts, then your German translator will strangle you! So, golden rule #1 in I18N: each and every display message gets it's own key. If that leads to 50 keys all producing the same message, be it so!
Accessing translations
Accessing translations should only add a minimal code overhead. The example code above already introduced the t() method of the Tr class and that's in fact also all the application developer should have to deal with: A simple call to a static method of a utility class that only takes the tag value and automatically, by using reflections, determines the right property key.
So without further ado, here's the implementation of the Tr class:
import java.util.*; import java.text.*; /** * Utility class for looking up I18N string translations. */ public final class Tr extends SecurityManager { /** * Base path for looking up property files - Adjust as needed. */ public static final String PREFIX="resources.i18n"; // For accessing SecurityManager.getClassContext() private static Tr tr = new Tr(); private Tr() {} // Don't allow instantiation! /** * Translate a (potentially) parameterized string. * @param key property to look up (relative to the name of the * calling method - in other words: the tag value in the methodname.tag * addressing scheme). * @param args string replacement values (varargs) * @return Either the translated string or the fully qualified * name of the missing property. */ public static String t(String key, Object... args) { Locale loc = Locale.getDefault(); ClassLoader cl = ClassLoader.getSystemClassLoader(); // Overly complicated but portable way to figure the classloader // of the calling method's class. Class[] stack = tr.getClassContext(); if (stack[1].getClassLoader()!=null) { cl = stack[1].getClassLoader(); } // Qualify the key by prepending the name of the calling method. StackTraceElement elem[] = Thread.currentThread().getStackTrace(); String tmp = elem[2].getMethodName()+"."+key; // Two is the magic number! try { // Decide on the right propertyfile to load from. String bname=PREFIX+"."+elem[2].getClassName(); tmp = ResourceBundle.getBundle(bname,loc, cl).getString(tmp); } catch (Exception e) { //e.printStackTrace(); } return new MessageFormat(tmp).format(args); } }
One thing noteworthy about the Tr class is that it extends SecurityManager which might seem a bit odd at first. The reason for doing this is to get access to the SecurityManager.getClassContext() method which is required to determine the classloader to use for looking up property files (important in applications that use a plugin framework where plugins are loaded by custom classloaders). Of course, using this little hack has the implication of making sure that nobody can neither instantiate nor subclass Tr as it is not really a security manager.
Managing translations
Translating an application should not be the developers job. After all, how many languages can a single person speak? Unfortunately, not everyone who could provide a translation will also be comfortable with editing property files in a text editor (especially not when translating to a language that makes heavy use Unicode characters). For this task, specialized editors are required. Here's a short list of free to use editors:
