Convenient internationalization of JSP pages

2011-04-19

I’m going to explain an elegant way of doing i18n in JSP (JSPX) pages if you use Spring MVC. But first let’s recap the usual setup: you need to declare a “messageSource” bean of type MessageSource that reads localized messages from resource bundles:

<bean id="messageSource"
    class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
    <property name="fallbackToSystemLocale" value="false"/>
    <property name="cacheSeconds" value="2"/>
    <property name="defaultEncoding" value="UTF-8"/>
    <property name="basenames">
        <array>
            <value>WEB-INF/i18n/global_messages</value>
            <value>WEB-INF/i18n/spring_security_messages</value>
            <value>WEB-INF/i18n/validation_messages</value>
        </array>
    </property>
</bean>

Then, each time you need to localize something in your JSP, you use some tag library like this:

<spring:message code="button_text" var="buttonTextMessage" htmlEscape="true"/>
<input type="submit" value="${buttonTextMessage}"/>

Here we have a Spring tag that finds a message in a MessageSource by the given code, escapes it (in case it may contain markup characters like quotes, ampersands, etc.), assigns the result to a variable and prints that variable as the “value” attribute of HTML input element.

Now repeat this for every single piece of text that you want to internationalize, and your JSP pages will quickly become littered with i18n tags, making them barely readable. If you think that’s fine, well … good for you! But if you’re wondering why something as trivial as i18n should be so inconvenient, keep on reading.

What I’d like to do is replace previous code fragment with this:

<input type="submit" value="${msg.button_text}"/>

You should agree that it cannot get any simpler than that. And I cannot stress enough the relief I felt after discovering that internationalization can be as simple and pleasant as this. So let’s move on to implementation.

The key part of my solution is “msg” – an application scope attribute that implements java.util.Map interface and holds all MessageSource messages keyed by their code. To be more precise, it delegates to MessageSource for message lookup. Here’s the implementation:

public class MessageSourceMapAdapter implements Map<String, String> {

    private final MessageSource messageSource;

    public MessageSourceMapAdapter(MessageSource messageSource) {
        this.messageSource = messageSource;
    }

    public String get(Object key) {
        Locale locale = LocaleContextHolder.getLocale();
        String message = messageSource.getMessage(String.valueOf(key), null, locale);
        return StringEscapeUtils.escapeXml(message);
    }

    public boolean containsKey(Object key) {
        return true;
    }

    public boolean isEmpty() {
        return false;
    }

    public int size() {
        throw new UnsupportedOperationException();
    }

    public Set<String> keySet() {
        throw new UnsupportedOperationException();
    }

    public Collection<String> values() {
        throw new UnsupportedOperationException();
    }

    public Set<Entry<String, String>> entrySet() {
        throw new UnsupportedOperationException();
    }

    public boolean containsValue(Object value) {
        throw new UnsupportedOperationException();
    }

    public String put(String key, String value) {
        throw new UnsupportedOperationException();
    }

    public void putAll(Map<? extends String, ? extends String> m) {
        throw new UnsupportedOperationException();
    }

    public String remove(Object key) {
        throw new UnsupportedOperationException();
    }

    public void clear() {
        throw new UnsupportedOperationException();
    }
}

As you can see, it’s a simple adapter that exposes MessageSource through Map interface. Most of it is stub implementation of Map methods, except the get(key) method, which will be called by JSP Expression Language whenever we use . or [] operators on “msg” attribute. This method uses the Locale of current thread to lookup messages (LocaleContext is initialized by DispatcherServlet on each request). It also uses StringEscapeUtils of Apache Commons Lang to automatically escape markup characters in message text, if any.

The last step is to set up our MessageSourceMapAdapter as an application-scope attribute “msg”, so that it can be used in JSP pages. This can be done at application startup in a Spring application event handler:

public class ServletContextAttributeInitializer
    implements ApplicationListener<ContextRefreshedEvent> {

    @Autowired
    private ServletContext servletContext;
    @Autowired
    private MessageSource messageSource;

    public void onApplicationEvent(ContextRefreshedEvent event) {
        servletContext.setAttribute("msg", new MessageSourceMapAdapter(messageSource));
        servletContext.setAttribute("cp", servletContext.getContextPath());
    }
}

Spring will automatically inject ServletContext and MessageSource and call onApplicationEvent() method after application context is loaded. We only need to register our event handler as a bean:

<bean class="my.project.ServletContextAttributeInitializer"/>

You may have noticed that I also save servlet context path to “cp” attribute. That’s another convenience that allows me to use short notation of relative URL paths in JSP pages, e.g. <a href="${cp}/login"> instead of <a href="${pageContext.request.contextPath}/login">.

You can use this approach to set up arbitrary servlet context attributes and then access them directly in JSP.