Internationalization in JSF with UTF-8 encoded properties files

29 October 2010, by: Development

Introduction

To internationalize your (web)application, the normal approach is to use the ResourceBundle API in combination with properties files which contains externalized text. The ResourceBundle API will load the proper text based on the current Locale and the default (fallback) locale. This sounds nicer than it actually is. Deep under the covers, the ResourceBundle API uses Properties#load(InputStream) method to load the properties files. This method uses by default the ISO-8859-1 encoding. This is explicitly mentioned in its javadoc as well. Here’s an extract of relevance:

The load(InputStream) / store(OutputStream, String) methods work the same way as the load(Reader)/store(Writer, String) pair, except the input/output stream is encoded in ISO 8859-1 character encoding. Characters that cannot be directly represented in this encoding can be written using Unicode escapes ; only a single ‘u’ character is allowed in an escape sequence. The native2ascii tool can be used to convert property files to and from other character encodings.

In a nutshell, you can’t have something like the following as UTF-8 in your properties files:

some.dutch.text = Één van de wijken van Curaçao heet Saliña.

When incorrectly decoded using ISO-8859-1, it would end up as mojibake:

�én van de wijken van Curaçao heet Saliña.

For example the character ç ‘LATIN SMALL LETTER C WITH CEDILLA’ (U+00E7) exist in UTF-8, which is a 16-bit multi-byte encoding, of two bytes 0xC3 and 0xA7. In ISO-8859-1, which is an 8-bit single-byte encoding, those two bytes are separately represented as à and §, see also this ISO-8859-1 codepage. Note that the second byte of É ‘LATIN CAPITAL LETTER E WITH ACUTE’ (U+00C9), which is 0×89, doesn’t represent a valid character in ISO-8859-1. In many environments such a character will be replaced by a question mark ?.

The old solution

The common approach is (was) to escape the characters outside the ASCII range into unicode escape sequences like uXXXX where XXXX is the unicode codepoint of the character in hexadecimal. So, for example the ç has to be escaped as u00E7. Doing this manually is a pita, you can use the JDK-supplied native2ascii tool to convert an UTF-8 encoded properties file to an ISO-8859-1 encoded properties file.

/path/to/jdk/bin/native2ascii -encoding UTF-8 text_nl.utf8.properties text_nl.properties

This way the above example will end up like

some.dutch.text = u00c9u00e9n van de wijken van Curau00e7ao heet Saliu00f1a.

The better solution

Although using the native2ascii tool is to a certain degree automatable (ant, maven, etc), this is still a maintainability pain and prone to human errors. Fortunately, since the new i18n enhancements in Java 1.6, the Properties API got a new method Properties#load(Reader) which you could feed with an InputStreamReader wherein you can specify the character encoding. To use this in a ResourceBundle, you could make use of -also new since Java 1.6- ResourceBundle.Control class wherein you can control the loading of the propertiesfiles yourself. Therein you can use the -also new- PropertyResourceBundle constructor taking a Reader instead of InputStream.

Here’s a JSF-targeted example of such a ResourceBundle class:

package com.example.faces.i18n;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.util.Enumeration;
import java.util.Locale;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;

import javax.faces.context.FacesContext;

public class Text extends ResourceBundle {

    protected static final String BUNDLE_NAME = "com.example.faces.i18n.text";
    protected static final String BUNDLE_EXTENSION = "properties";
    protected static final Control UTF8_CONTROL = new UTF8Control();

    public Text() {
        setParent(ResourceBundle.getBundle(BUNDLE_NAME, 
            FacesContext.getCurrentInstance().getViewRoot().getLocale(), UTF8_CONTROL));
    }

    @Override
    protected Object handleGetObject(String key) {
        return parent.getObject(key);
    }

    @Override
    public Enumeration getKeys() {
        return parent.getKeys();
    }

    protected static class UTF8Control extends Control {
        public ResourceBundle newBundle
            (String baseName, Locale locale, String format, ClassLoader loader, boolean reload)
                throws IllegalAccessException, InstantiationException, IOException
        {
            // The below code is copied from default Control#newBundle() implementation.
            // Only the PropertyResourceBundle line is changed to read the file as UTF-8.
            String bundleName = toBundleName(baseName, locale);
            String resourceName = toResourceName(bundleName, BUNDLE_EXTENSION);
            ResourceBundle bundle = null;
            InputStream stream = null;
            if (reload) {
                URL url = loader.getResource(resourceName);
                if (url != null) {
                    URLConnection connection = url.openConnection();
                    if (connection != null) {
                        connection.setUseCaches(false);
                        stream = connection.getInputStream();
                    }
                }
            } else {
                stream = loader.getResourceAsStream(resourceName);
            }
            if (stream != null) {
                try {
                    bundle = new PropertyResourceBundle(new InputStreamReader(stream, "UTF-8"));
                } finally {
                    stream.close();
                }
            }
            return bundle;
        }
    }

}

Using it in JSF is extraordinary simple: just define the fully qualified classname instead of the fully qualified bundle name as resource-bundle base-name in faces-config.xml:

    <application>
        <locale-config>
            <default-locale>en</default-locale>
            <supported-locale>nl</supported-locale>
            <supported-locale>es</supported-locale>
        </locale-config>
        <resource-bundle>
            <base-name>com.example.faces.i18n.Text</base-name>
            <var>text</var>
        </resource-bundle>
    </application>

Note the var element. This enables you to reference the bundle as #{text} without the need for f:loadBundle tag in every view.

This way you can keep your properties files UTF-8 all the way. In the above code example, their location is definied by the BUNDLE_NAME constant in the Text class. It expects them to be inside the com.example.faces.i18n package with the filenames text.properties (for generic content, this file is mandatory anyway), text_en.properties (for English), text_nl.properties (for Dutch) and text_es.properties (for Spanish).

Changing the locale in JSF/Facelets

As an extra bonus, here’s some code which shows at its simplest way how you could change the locale (actually, the language) from in the view side. It’s targeted on JSF 2.x / Facelets, but can be done as good on JSF 1.x and/or JSP with minor changes.

The view:

<!DOCTYPE html>
<html lang="#{localeBean.language}"
    xmlns:f="http://java.sun.com/jsf/core" 
    xmlns:h="http://java.sun.com/jsf/html">
<f:view locale="#{localeBean.locale}">
    <h:head>
        <title>JSF/Facelets i18n example</title>
    </h:head>
    <h:body>
        <h:form>
            <h:selectOneMenu value="#{localeBean.language}" onchange="submit()">
                <f:selectItem itemValue="en" itemLabel="English" />
                <f:selectItem itemValue="nl" itemLabel="Nederlands" />
                <f:selectItem itemValue="es" itemLabel="Español" />
            </h:selectOneMenu>
        </h:form>
        <p><h:outputText value="#{text['some.text']}" /></p>
    </h:body>
</f:view>
</html>

The bean:

package com.example.faces;

import java.util.Locale;

import javax.faces.bean.ManagedBean;
import javax.faces.bean.SessionScoped;
import javax.faces.context.FacesContext;

@ManagedBean
@SessionScoped
public class LocaleBean {

    private Locale locale = FacesContext.getCurrentInstance().getViewRoot().getLocale();

    public Locale getLocale() {
        return locale;
    }

    public String getLanguage() {
        return locale.getLanguage();
    }

    public void setLanguage(String language) {
        this.locale = new Locale(language);
    }

}

That’s it!

More background information about the world of characters and bytes as it is seen by computers and humans can be found in this article: Unicode – How to get the characters right?.

Bauke Scholtz

15 comments to “Internationalization in JSF with UTF-8 encoded properties files”

  1. Merve Özbey says:

    Thank you Bauke for your solution, it worked fine in our case. We are grateful to you.

  2. Anonymous says:

    Thx a lot!

  3. JSF odkazy | BRAIN SNIPPETS says:

    [...] i18n & UTF8 property file [...]

  4. Andras Liter says:

    A great solution! Thanks for sharing.

  5. Hesam says:

    Thnx Man! it worked for me.

  6. Struts2: UTF .properties | SuperBlog says:

    [...] http://jdevelopment.nl/internationalization-jsf-utf8-encoded-properties-files/ [...]

  7. Mattias says:

    Great job!

    Worked unexpectively seamless!

  8. Anatoly says:

    Great article but is it locale change bean implemented right? It doesn’t work at me and I have had to do:

    FacesContext.getCurrentInstance().getViewRoot().setLocale(new Locale(language));

    actually I’m confusing and according to second answer from here: http://stackoverflow.com/questions/40480/is-java-pass-by-reference-or-pass-by-value
    private Locale locale isn’t same locale object which points to locale object which we get from viewRoot. Am I missing something, can you provide some explanation?
    Thank you.

  9. Luiggi Mendoza says:

    As Anatoly posted, there’s a missing LoC in the solution proposed here (very odd):

    
        public void setLanguage(String language) {
            this.locale = new Locale(language);
            FacesContext.getCurrentInstance().getViewRoot().setLocale(locale); //this is missing
        }
    

    Taken from your answer in stackoverflow: http://stackoverflow.com/a/4830669/1065197

  10. Bauke Scholtz says:

    @Luiggi: from the answer: “this part is not necessary when you perform a redirect afterwards”.

  11. Bauke Scholtz says:

    In other words, if it didn’t work, then you simply wasn’t redirecting (e.g. you just used an ajax submit). The locale is actually being set in . So if you don’t explicitly/implcitly rebuild the view, then the locale won’t change in the view and you’d need to do it programmatically.

  12. lokesh says:

    Thank you, it worked for me.

  13. erictan1974 says:

    @BalusC

    I got an error as follows:

    1100: /lang.xhtml @8,43 locale=”#{localeBean.locale}” Cant instantiate class: com.stmpi.dukevisorjsf.mb.LocaleBean.
    javax.faces.view.facelets.TagAttributeException: /lang.xhtml @8,43 locale=”#{localeBean.locale}” Cant instantiate class: com.stmpi.dukevisorjsf.mb.LocaleBean.
    at com.sun.faces.facelets.tag.TagAttributeImpl.getObject(TagAttributeImpl.java:358)
    at com.sun.faces.facelets.tag.TagAttributeImpl.getObject(TagAttributeImpl.java:276)
    at com.sun.faces.facelets.tag.jsf.core.ViewHandler.apply(ViewHandler.java:211)
    at com.sun.faces.facelets.compiler.NamespaceHandler.apply(NamespaceHandler.java:93)
    at com.sun.faces.facelets.compiler.EncodingHandler.apply(EncodingHandler.java:87)
    at com.sun.faces.facelets.impl.DefaultFacelet.apply(DefaultFacelet.java:161)
    at com.sun.faces.application.view.ViewMetadataImpl.createMetadataView(ViewMetadataImpl.java:116)
    at com.sun.faces.lifecycle.RestoreViewPhase.execute(RestoreViewPhase.java:241)
    at com.sun.faces.lifecycle.Phase.doPhase(Phase.java:101)
    at com.sun.faces.lifecycle.RestoreViewPhase.doPhase(RestoreViewPhase.java:121)
    at com.sun.faces.lifecycle.LifecycleImpl.execute(LifecycleImpl.java:198)
    at javax.faces.webapp.FacesServlet.service(FacesServlet.java:646)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:303)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:220)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:122)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:501)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:171)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:102)
    at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:950)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:116)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:408)
    at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1040)
    at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:607)
    at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:316)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:745)
    Caused by: com.sun.faces.mgbean.ManagedBeanCreationException: Cant instantiate class: com.stmpi.dukevisorjsf.mb.LocaleBean.
    at com.sun.faces.mgbean.BeanBuilder.newBeanInstance(BeanBuilder.java:191)
    at com.sun.faces.mgbean.BeanBuilder.build(BeanBuilder.java:100)
    at com.sun.faces.mgbean.BeanManager.createAndPush(BeanManager.java:409)
    at com.sun.faces.mgbean.BeanManager.create(BeanManager.java:269)
    at com.sun.faces.el.ManagedBeanELResolver.resolveBean(ManagedBeanELResolver.java:257)
    at com.sun.faces.el.ManagedBeanELResolver.getValue(ManagedBeanELResolver.java:117)
    at com.sun.faces.el.DemuxCompositeELResolver._getValue(DemuxCompositeELResolver.java:176)
    at com.sun.faces.el.DemuxCompositeELResolver.getValue(DemuxCompositeELResolver.java:203)
    at org.apache.el.parser.AstIdentifier.getValue(AstIdentifier.java:71)
    at org.apache.el.parser.AstValue.getValue(AstValue.java:160)
    at org.apache.el.ValueExpressionImpl.getValue(ValueExpressionImpl.java:184)
    at com.sun.faces.facelets.el.TagValueExpression.getValue(TagValueExpression.java:109)
    at com.sun.faces.facelets.tag.TagAttributeImpl.getObject(TagAttributeImpl.java:356)
    … 31 more
    Caused by: java.lang.NullPointerException
    at com.stmpi.dukevisorjsf.mb.LocaleBean.(LocaleBean.java:20)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:57)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:526)
    at java.lang.Class.newInstance(Class.java:374)
    at com.sun.faces.mgbean.BeanBuilder.newBeanInstance(BeanBuilder.java:186)
    … 43 more

  14. Vivek says:

    It’s targeted on JSF 2.x / Facelets, but can be done as good on JSF 1.x and/or JSP with minor changes.

    I am trying to use it for JSF 1.2, what are the changes we have to do in order to make it working. Right now its just displaying random characters for Thai characters.

    Regards
    Vivek

  15. Paul says:

    Thanks for the solution! but now, I’m trying with no success to inject that ResourceBundle in a ManagedBean.

    Should I tag with @Resource the “public class Text extends ResourceBundle”??
    How It could be injected on my ManagedBean?

    For now, this is as far as I’ve come:

    import java.util.ResourceBundle;
    import org.aspectj.org.eclipse.jdt.internal.compiler.batch.Main.ResourceBundleFactory;

    @Named(“myBean”)
    @ViewScoped
    public class MyManagedBean

    private ResourceBundle bundle;

    public ResourceBundle getBundle() {
    if (bundle == null) {
    String BUNDLE_NAME = “com.example.faces.i18n.text”;
    Locale locale = FacesContext.getCurrentInstance().getExternalContext().getRequestLocale();
    ResourceBundle factory = ResourceBundleFactory.getBundle(locale);
    bundle = factory.getBundle(BUNDLE_NAME, locale);
    }
    return bundle;
    }

    Some help? Thanks in anticipation

Type your comment below:


eight + 4 =

css.php best counter