Know your APIs – Lessons learned from ResourceBundle
Last week I spent some time hunting down an internationalization-issue that came along while developing for a recent project. Let me explain what happened:
Message-Lookup – of course – always stands together with Locales (
java.util.Locale) of the client the message is resolved for. The problem was, that messages for the English users were not resolved to the English translation, but to the German one.
Within the project I am working on, there were the following message-files at that time:
messages.properties(containing the english translation)
messages_de.properties(containing the German translation)
messages_it.properties(containing Italian translation)
Usually you provide a property-file per language containing all the translations.
ResourceBundle uses a fallback-mechanism from the full locale down to more general ones (e.g. it first checks for
messages_de.properties down to
messages.properties in the end, being the overall default).
This actually makes much sense because in this way you can provide values for stuff relevant for all languages in
messages.properties, English language specific values in
messages_en.properties and stuff that is different for different english speaking Countries in files like
messages_en_UK.properties and so on. The fallback works perfect, because if you for example don’t specify US/UK specific files message-lookup for both en_US and en_UK result in resolving keys from
messages_en.properties. Additionally, if
messages_en_US.properties would exist but the key-lookup fails (the file does not provide a translation for the key), the key gets also looked up from
messages_en.properties (which may also provide the key).
In my concrete case, resolving of German values (coming from
messages_de.properties) worked well, but if you change the users locale to English, the keys also resolved to German values.
I did not create a
messages_en.properties because English should also be the overall fallback if no i18n for the requested language was available. I thought if the user had the locale fr the system would check for
messages_fr.properties and then in
messages.properties, which would display English messages for the user because no French translation is available. I debugged a while, basically within our own framework and later down to Springs i18n related classes and I could not find the mistake.
Then, when I excluded all possible mistakes on our side and inside the Spring Framework where our application is based on, my way lead me down to
java.util.ResourceBundle. Since this class makes extensive use of caching (for good reasons of course) and static factory methods (which are almost impossible to debug) my way lead me to the API-Doc of ResourceBundle. Here I found the mistake that I made:
getBundleuses the base name, the specified locale, and the default locale (obtained from
Locale.getDefault) to generate a sequence of candidate bundle names.
I forgot that
ResourceBundle also looks up files for the JVMs Default-Locale before falling back to the base-file (
messages.properties). The Default-Locale of my JVM is de_DE, which lead to the following path:
messages_de_DE.properties(from Default-Locale, not found)
messages_de.properties(from Default-Locale, FOUND)
messages.propertieswas not checked because the key was found)
So the fix was easy. There are even several ways to fix it:
messages_en.properties(which leads to the French dude having to learn German).
message_en.properties(this is copy paste but this could be solved within the build-process using mvn)
- set the default-locale of the JVM to an English one by calling
Locale.setDefault(englishLocale)early in application-boot
- set the default-locale as commandline-argument of your JVM (e.g.
And, last but not least: What do we (or better I) learn from this?
- My usual approach “first think about whats happening, if you cannot figure out what leads to the problem immediately attach debugger” was obviously not the best approach in this case (although starting immediately to debug has turned out to be a very efficient way to hunt down bugs for me in general).
- Code is not the only place where bugs can be found. A big problem is also understanding the basic APIs you use.
- Even if you use high-level APIs (i18n in this case with about 5 delegates before the ResourceBundle was actually reached) you still need to know the very basics.
Reading APIs earlier (ok, the whole hunting only took about 1-2 hours, but still) or even (in a perfect world) know all your APIs you use directly or indirectly would have saved some time.
Ah, good ol' Ukrainian English. A project of mine made the same slip - should be en_GB :-)