It’s pretty important to be able to create applications that support multiple languages, and most libraries should provide some kind of support for this. The first step is making your code support internationalization, but then you need to localize the application for each language (or locale). We’ve included internationalization (or “i18n”) in JBoss DNA from the beginning, but we haven’t done much with localization (or “L10n”), and have only one (default) localization in English.
Java really does a crappy job at supporting internationalization. Sure, it has great Unicode support, and it does provide a standard mechanism for identifying locales and looking up bundles given a locale. But where is the standard approach for representing an internationalized message ready for localization into any locale? ResourceBundle.getString()
? Seriously?
What I want is something analogous to an internationalized String capable of holding onto the replacement parameters. Each internationalized string should be associated with the key used in the resource bundles. I want to localize an internationalized string into the default locale, or into whatever locale you supply, and even into multiple locales (after all, web applications don’t support just one locale). And I should be able to use my IDE to find where each internationalized string is used. I should be able to test that my localization files contain localized messages for each of the internationalized strings used in the code, and that there are no duplicate or obsolete entries in the files. I also don’t want that many resource files (one per package – like Eclipse used to do – sucks); one per Maven project is just about right.
I’m not asking for much.
Meet the players
There are quite a few existing internationalization (aka, “i18n”) open source libraries, including JI18n, J18n, Apache Commons I18n, just to name a few. Too many of these try to be too smart and do too much. (Like automatically localizing a message identified by a Java annotation into the current locale, or using aspects to do things automatically.) This stuff just tends to confuse IDE dependency, search, and/or debuggers. We found nothing we liked, and lots of things we didn’t like. Internationalization shouldn’t be this hard.
Sweet and to the point
So we did what we don’t like to do: we invented our own very simple framework. And by simple, I mean there’s only one I18n
class that represents an internationalized string with some static utility methods (and an abstract JUnit test class; see below). To use, simply create an “internationalization class” that contains a static I18n
instances for the messages in a bundle, and then create a resource bundle properties file for each of these classes. That’s it!
So, let’s assume that we have a Maven project and we want to create an internationalization class that represents the internationalized strings for that project. (We could create as many as we want, but one is the simplest.) Here’s the code:
public final class DnaSubprojectI18n {
// These are the internationalized strings ...
public static I18n propertyIsRequired;
public static I18n nodeDoesNotExistAtPath;
public static I18n errorRemovingNodeFromCache;
static {
// Initializes the I18n instances
try {
I18n.initialize(DnaSubprojectI18n.class);
} catch (final Exception err) {
System.err.println(err); // logging depends on I18n, so we can't log
}
}
}
Notice that we have a static I18n
instance for each of our internationalized strings. The name of each I18n
variable corresponds to the key in the corresponding property file. Pretty simple boilerplate code.
The actual localized messages are kept same package as this class, but since we’re using Maven the file goes in src/main/resources
):
propertyIsRequired = The {0} property is required but has no value
nodeDoesNotExistAtPath = No node exists at {0} (or below {1})
errorRemovingNodeFromCache = Error while removing {0} from cache
Again, pretty simple and nothing new.
Using in your code
At this point, all we’ve done is defined a bunch of internationalized strings. Now all we need to do to use an internationalized string is to reference the I18n
instance you want (e.g., DnaSubprojectI18n.propertyIsRequired
). Pass it (and any parameter values) around. And when you’re ready, localize the message by calling I18n.text(Object...params)
or I18n.text(Locale locale, Object...params)
. The beauty of this approach is that IDE’s love it. Want to know where an internationalized message is used? Go to the static I18n
member and find where it’s used.
The logging framework used in JBoss DNA has methods that take an I18n
instance and zero or more parameters. (Debug and trace methods just take String
, since in order to understand these messages you really have to have access to the code, so English messages are sufficient.) This static typing helps make sure that all the developers internationalize where they’re supposed to.
With exceptions, we’ve chosen to have our exceptions use Strings (just like JDK exceptions), so we simply call the I18n.text(Object...params)
method:
throw new RepositoryException(DnaSubprojectI18n.propertyIsRequired.text(path));
We’ll probably make this even easier by adding constructors that take the I18n
instance and the parameters, saving a little bit of typing and delaying localization until it’s actually needed.
Testing localizations
Testing your internationalization classes is equally simple. Create a JUnit test class and subclass the AbstractI18nTest
class, passing to its constructor your DnaSubprojectI18n
class reference:
public class DnaSubprojectI18nTest extends AbstractI18nTest {
public DnaSubprojectI18nTest() {
super(DnaSubprojectI18n.class);
}
}
That’s it. The test class inherits test methods that compare the messages in the properties file with the I18n
instances in the class, ensuring there aren’t any extra or missing messages in any of the localization files. That’s a huge benefit!
One more thing …
Remember when I said there was only one class to our framework? Okay, I stretched the truth a bit. We also abstracted how the framework loads the localized messages, so there’s an interface and an implementation class that loads from standard resource bundle property files. So if you want to use a different loading mechanism for your localized messages, feel free.
Props to John Verhaeg and Dan Florian for the design of this simple but really powerful framework.
Filed under: techniques, testing, tools