How to localise your Servlets
Jan 3rd, 2008 by dave
Like most popular web sites, the one I own at my place of work has grown into a fair size. Now it needs to be localised. I’ll define Localisation as “The steps needed to translate to another language”, rather than worry right now about presenting date, currency and general formatting to the international user.
So, where do we start?
A quick look at Sun’s implementations tells us that we can go one of two routes, we can pop the translated strings into a properties file (called a resource bundle) or a coded class file called a ResourceBundle. heh.
Ok, why any developer would store translated strings inside a java class file, albeit that locale’s own translated class file, is beyond me. The last thing a developer wants to do is cut and copy from another source - and then maintain a bunch of class files is just crazy. Then factor in that java files can’t really contain non-escaped Unicode characters (my version of Eclipse certainly won’t let me try that one) and that translators don’t want to receive a java class file to translate, I think the best option is to go for a properties file.
The next problem is that Sun expect all resource bundles stored in property files to be in Latin-1. sigh. Why bother at all? Well, you get some nice stuff for free based around the Locale class - making it easy to add a fall back down to the most appropriate property file for the user if you do go for ResourceBundles but you have to fight the Latin-1 issue.
Sun’s solution is to use the <i>native2ascii</i> program. Bah. Running dos apps to convert stuff to escaped unicode isn’t that much fun and it doesn’t help the readability of your files once ready, so the answer for me is to go for UTF-8 property files and hack the ResourceBundleStore class.
I started doing this and then came across someone who’d already done the same thing. Here’s his page on the topic.
Make sure you use your ResourceBundleStore object as a Singleton, otherwise you’ll be creating hashmaps for every user on the site, per page!
I use a link or two on the home page, simply linking to the site’s first page with a request variable setting the locale. That in turns sets a session variable which is read subsequently to look up the correct string through the <i>getString</i> method.
I modified the ResourceBundleStore to go look in the English hashmap. This means that your localisation effort can lag behind your English pages and not give your reader a set of NULL strings to read.
Here’s the ResourceBundleStore:
package com.iasb.utils;import java.util.Enumeration;import java.util.HashMap;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import javax.servlet.ServletException;
public class ResourceBundleStore {
private String strBaseName;
private HashMap localeDataHash; // The top level hash containing
// the second-level
// hashes for each Locale. The top-level key is
// the Locale object.
public ResourceBundleStore() {
localeDataHash = new HashMap();
}
public ResourceBundleStore(String baseName) {
localeDataHash = new HashMap();
this.strBaseName=baseName;
}
public void setBaseName(String strBaseName) {
this.strBaseName = strBaseName;
}
// If the key is not found in the bundle, this method will return null if it
// can’t also find the corresponding english result.
public String getString(String strServletName, String strKey, Locale locale) throws ServletException {
strServletName=strServletName.substring(strServletName.lastIndexOf(”.”)+1);
// Check to see if the PropertyResourceBundle for this Locale has
// been loaded.
if (!localeDataHash.containsKey(locale)) {
try {
loadLocale(locale);
} catch (final MissingResourceException mre) {
System.out.println(”WARNING: The attempt to locate a resource file failed. “+locale.getCountry()+”-”+locale.getLanguage());
}
}
final String result = (String) ((HashMap) localeDataHash.get(locale)).get(strServletName + “.” + strKey);
if (result==null && (!locale.equals(new Locale(”en”))))
{
System.out.println(”WARNING: Couldn’t find: “+strServletName+”.”+strKey+” in “+locale.toString()+” resource.”);
return getString(strServletName,strKey,new Locale(”en”));
}
else
return result;
}
// This will load the resource bundle and add the data it contains
// to our top-level hash.
private void loadLocale(Locale locale) throws MissingResourceException {
System.out.println(”Loading: “+locale.getCountry()+”/”+locale.getLanguage()+” resource”);
final ResourceBundle bundle = Utf8ResourceBundle.getBundle(this.strBaseName, locale);
final HashMap secondaryHash = new HashMap();
final Enumeration en = bundle.getKeys();
while (en.hasMoreElements()) {
final String strKey = (String) en.nextElement();
secondaryHash.put(strKey, bundle.getString(strKey));
}
// Now add the newly-created hash to our top-level hash.
localeDataHash.put(locale, secondaryHash);
}
}
And UF8ResourceBundle is simply:
import java.io.UnsupportedEncodingException;
import java.util.Enumeration;
import java.util.Locale;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
public abstract class Utf8ResourceBundle {
public static final ResourceBundle getBundle(String baseName) {
ResourceBundle bundle = ResourceBundle.getBundle(baseName);
return createUtf8PropertyResourceBundle(bundle);
}
public static final ResourceBundle getBundle(String baseName, Locale locale) {
ResourceBundle bundle = ResourceBundle.getBundle(baseName, locale);
return createUtf8PropertyResourceBundle(bundle);
}
public static ResourceBundle getBundle(String baseName, Locale locale, ClassLoader loader) {
ResourceBundle bundle = ResourceBundle.getBundle(baseName, locale, loader);
return createUtf8PropertyResourceBundle(bundle);
}
private static ResourceBundle createUtf8PropertyResourceBundle(ResourceBundle bundle) {
if (!(bundle instanceof PropertyResourceBundle))
return bundle;
return new Utf8PropertyResourceBundle((PropertyResourceBundle) bundle);
}
private static class Utf8PropertyResourceBundle extends ResourceBundle {
PropertyResourceBundle bundle;
private Utf8PropertyResourceBundle(PropertyResourceBundle bundle) {
this.bundle = bundle;
}
/*
* (non-Javadoc)
*
* @see java.util.ResourceBundle#getKeys()
*/
public Enumeration getKeys() {
return bundle.getKeys();
}
/*
* (non-Javadoc)
*
* @see java.util.ResourceBundle#handleGetObject(java.lang.String)
*/
protected Object handleGetObject(String key) {
String value = (String)bundle.getString(key);
if (value==null) return null;
try {
return new String (value.getBytes(”ISO-8859-1″),”UTF-8″) ;
} catch (UnsupportedEncodingException e) {
// Shouldn’t fail - but should we still add logging message?
return null;
}
}
}
}
Property files are simply <servletname>.<key> = <value>, they live in the WEB-INF/classes directory and are simply named <app_name>.en_us, <app_name>.fr, etc.
You should note that the first line in the property file will not be read correctly, thanks to the ResourceBundle.getKeys method not expecting the UTF-8 byte order mark flag. Rather than fight the fight there, I simply add an empty line to the top of each property file.
Popularity: 33% [?]




