A Centralized Bean for Translation

Mon Sep 01 16:54:59 EDT 2014

Tags: java

The normal method for doing translation in XPages is by using the built-in Designer tooling, which creates properties files for each language for each XPage in your app. This is okay, though it requires using Designer to update the properties (since apparently the translation happens as part of the build process). For the refresh of my blog I'm working on, I'm taking a different tack, inspired by the way a client does it and similar to how I do it in the Framework.

Specifically, I have a centralized bean named "translation" that accepts strings and returns a best-match translation for the current session's locale. This "best match" is the routine that the XSP framework uses for getting resource bundles. So, taking my browser as an example, requesting the bundle named "translation" will cause it to search the classpath for these files until it finds one:

  1. translation_en_US.properties
  2. translation_en.properties
  3. translation.properties
  4. translation_fr.properties

(I'm not sure why it uses translation.properties before translation_fr.properties - maybe it assumes that it's the English strings when there's no locale code on the file).

So I take advantage of this by creating a DataObject bean to do my translation:

package config;

import java.io.IOException;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;

import javax.faces.context.FacesContext;

import com.ibm.xsp.application.ApplicationEx;
import com.ibm.xsp.designer.context.XSPContext;
import com.ibm.xsp.model.DataObject;

import frostillicus.xsp.bean.SessionScoped;
import frostillicus.xsp.bean.ManagedBean;
import frostillicus.xsp.util.FrameworkUtils;

@ManagedBean(name="translation")
@SessionScoped
public class Translation implements Serializable, DataObject {
	private static final long serialVersionUID = 1L;

	public static Translation get() {
		Translation existing = (Translation)FrameworkUtils.resolveVariable(Translation.class.getAnnotation(ManagedBean.class).name());
		return existing == null ? new Translation() : existing;
	}

	private transient ResourceBundle bundle_;
	private Map<Object, String> cache_ = new HashMap<Object, String>();

	public Class<String> getType(final Object key) {
		return String.class;
	}

	public String getValue(final Object key) {
		if(!cache_.containsKey(key)) {
			try {
				ResourceBundle bundle = getTranslationBundle();
				cache_.put(key, bundle.getString(String.valueOf(key)));
			} catch(IOException ioe) {
				throw new RuntimeException(ioe);
			} catch(MissingResourceException mre) {
				cache_.put(key, "[Untranslated " + key + "]");
			}
		}
		return cache_.get(key);
	}

	public boolean isReadOnly(final Object key) {
		return true;
	}

	public void setValue(final Object key, final Object value) {
		throw new UnsupportedOperationException();
	}


	private ResourceBundle getTranslationBundle() throws IOException {
		if(bundle_ == null) {
			FacesContext facesContext = FacesContext.getCurrentInstance();
			ApplicationEx app = (ApplicationEx)facesContext.getApplication();
			bundle_ = app.getResourceBundle("translation", XSPContext.getXSPContext(facesContext).getLocale());
		}
		return bundle_;
	}
}

This uses the Framework's annotation-based managed-bean declaration, but it'd work just fine in faces-config. This allows you to use EL to request a translation, such as #{translation.home}, #{translation['home']}, or #{translation[someVar.prop]}, or by using translation.getValue(...) in Java or JavaScript.

I've found this approach to be much easier to work with. There's only one central file to manage (you could split it up into multiple beans), you don't have to re-generate files for new languages, and the keys can be natural and human-friendly, instead of XPaths.

In addition, you could easily change this bean to get its translation information elsewhere without modifying the XPages that use it - it could look up, for example, against Domino documents to allow non-developers to change the translation values without any special rights (though you'd likely want to tweak the cache in that case).

Commenter Photo

Txemanu - Tue Sep 02 10:51:28 EDT 2014

Hi Jesse,

Thanks for the article.

I use a similar multi-lang solution but loading the translations from documents (from an administration section for key users)

One question, would it be valid use the applicationScope instead of sessionScope?

Many thanks again!

Commenter Photo

Jesse Gallagher - Thu Sep 04 14:30:19 EDT 2014

You could certainly put it in applicationScope, but you'd want to make sure that your bean also keys internally by the active locale. The advantage of sessionScope is that it's a lazy way to handle that, but the applicationScope+locale lookup is better.

New Comment