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}"/>

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:

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: where the default (English) messages are set, and 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 (not found) then in, and you’ll end up, as expected, with an English message.
However, if you call, in your Java source code,


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 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.