Skip to content

I18ning a Java web app

Internationalization (i18n in short) is a tedious task. You need to be meticulous, and to know enough about some foreign languages and cultures in order to avoid making bad assumptions.

A common mistake made by beginners is to use concatenation to format parameterized messages. It not only makes the process harder than necessary, but it also makes it impossible to translate some sentences.

For example, if you try to internationalize the sentence January 3 is the third day of the year, where January, 3 and third are runtime parameters of the message, the naive option is to concatenate :

${month} ${day} <fmt:message key="isThe"/> ${position} <fmt:message key="dayOfTheYear"/>

The problem is that this sentence, in French, should be translated this way: Le 3 janvier est le troisième jour de l’année. The order of the runtime parameters is not the same in French and in English. The solutions is obvious: use parameterized messages.

<fmt:message key="dayOfTheYearSentence">
    <fmt:param value="${month}"/>
    <fmt:param value="${day}"/>
    <fmt:param value="${position}"/>
</fmt:message>

In the English resource bundle, you’ll find the following entry:

dayOfTheYearSentence=${0} {1} is the {2} day of the year

and the French resource bundle will contain this entry:

dayOfTheYearSentence=Le {1} {0} est le {2} jour de l''année

If this example may seem obvious, more subtle mistakes can happen. For example, do you know that you’ll write $50 in English, but 50 $ in French? Do you know that a space must be placed before the colon symbol in French, but not in English? That’s why the complete form labels, including the colon, should be in the resource bundles:

# English resource bundle
nameLabel=Please enter your name:
# French resource bundle. A non-breaking space is used before the colon,
# to make sure it doesn't end up alone on a new line
nameLabel=Veuillez saisir votre nom&nbsp;:

In the previous example, you noticed that the quote before année had been doubled. This is necessary because the JSTL uses java.text.MessageFormat under the hood. And this class requires that quotes be doubled. This is really confusing to translators, because they don’t know why and when quotes must be doubled. It’s so confusing that the Javadoc for MessageFormat has this warning:

Warning:
The rules for using quotes within message format patterns unfortunately have shown to be somewhat confusing. In particular, it isn’t always obvious to localizers whether single quotes need to be doubled or not. Make sure to inform localizers about the rules, and tell them (for example, by using comments in resource bundle source files) which strings will be processed by MessageFormat. Note that localizers may need to use single quotes in translated strings where the original version doesn’t have them.

Another thing that many persons have a hard time to catch is that some things depend on the language of the user, and some depend on the country of the user. The confusion is reinforced by all these web pages that use country flags to let the user choose his preferred language. Hey! French is not spoken only in France. And even more important: some countries have several official languages. Belgium, for instance, has Dutch, French and German as official languages.

Another gotcha to be aware of when i18ning a web app is that the JSTL and the java.util.ResourceBundle class don’t obey the same rules regarding the choice of the resource bundle. Let’s say that my web app has two resource bundles: app.properties where the default (English) messages are set, and app_fr.properties where the French messages are set. Let’s say that my web app is deployed on a French server, where the system locale of the JVM is thus fr.
If the user’s locale is set to English (en), the JSTL will look for messages in app_en.properties (not found) then in app.properties, and you’ll end up, as expected, with an English message.
However, if you call, in your Java source code,

ResourceBundle.getBundle(user.getLocale()).getString("myKey");

you’ll end up with a French message. The reason is that ResourceBundle.getBundle() first looks up for properties files with the system locale (French) before falling down to the default properties file.

The trick is obvious to get the same behavior: add an empty app_en.properties file to the web app. This way, an English resource bundle will be found by ResourceBundle.getBundle(), but every lookup in this bundle will fall down the the parent, default, properties file.

Cobertura on Google App Engine

Code coverageA good web application should be tested, preferrably with automated integration tests. HtmlUnit is great to write these tests. Measuring the code coverage of these integration tests is as important in my opinion than measuring the code coverage of pure unit tests.

Cobertura is a great tool to measure it. Unfortunately, the usual and only documented ways of getting the code coverage data are

  • to stop the application server, in order for the cobertura shutdown hook to flush its recorded data to a file
  • to remotely call a URL, bound to an action that will flush the recorded data to a file

These two methods don’t work on Google App Engine, because all the applications run in a sandbox where access to the file system is forbidden (even when run locally, on the development server).

There is a solution, though: rewrite the flush action so that it flushes to the HTTP response rather than a file. At the end of the test suite, you just have to remotely invoke the action, through its bound URL, and save the response to a file.

Here’s an example of such an action, using the Stripes framework. Every call to cobertura is implemented using reflection, so that this action compiles even without the cobertura jar files in the classpath.

public Resolution flushCobertura() throws ClassNotFoundException,
                                          SecurityException,
                                          NoSuchMethodException,
                                          IllegalArgumentException,
                                          IllegalAccessException,
                                          InvocationTargetException,
                                          IOException {
    String className = "net.sourceforge.cobertura.coveragedata.ProjectData";
    String methodName = "getGlobalProjectData";
    Class projectDataClass = Class.forName(className);
    Method getGlobalProjectDataMethod =
        projectDataClass.getDeclaredMethod(methodName, new Class[0]);
    Object globalProjectData = getGlobalProjectDataMethod.invoke(null, new Object[0]);
    getContext().getResponse().setContentType("application/octet-stream");
    OutputStream out = getContext().getResponse().getOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(out);
    try {
        oos.writeObject(globalProjectData);
    }
    finally {
        oos.close();
    }
    return null;
}

Pourquoi « Jaybee » ?

En anglais, JB se prononce Jaybee, ce qui a tendance à faire rire quelques-uns de mes amis francophones (ils se reconnaîtront), surtout quand c’est moi qui le dit.

Comme ce blog contiendra des articles écrits tantôt en français, tantôt en anglais, j’ai trouvé que Jaybee était un nom adéquat pour ce blog.