I18n Java and ZUL files with GNU gettext

From Documentation

Template:Smalltalk


DocumentationSmall Talks2009SeptemberI18n Java and ZUL files with GNU gettext
I18n Java and ZUL files with GNU gettext

Author
Diego Pino, Software engineer, [email protected]
Date
September 27, 2009
Version
ZK 3.6.1

Purpose

Since ZK 2.2.0 is possible to add i18n support by using i3-label*.properties files to store localized strings and use resource class Labels to retrieve them. This approach is the most wide spread in ZK for localizing ZUL files, and it was introduced by Minjie Zha in his smalltalk I18N Implementation in ZK.

However, this approach has its drawbacks. It requires developers to think first of a key for the text they are going to localize, store it in a i3-label*.properties file together with the localized text, an enable the mechanisms necessary to retrieve keys from a ZUL file. In addition, ZUL files are flooded with keys which sometimes have an obscure meaning. This approach provides also a limited support for customizing texts with parameters, being not possible to place parameters in the middle of a text, for example.

This small talk presents a new approach for internationalizing texts both in Java and ZUL files using the power of GNU gettext utilities.

Goals

After this small talk you will know how to:

  • I18n texts in Java and ZUL files
  • I18n texts without keeping track of arbitrary property labels
  • I18n texts with parameters
  • Support dynamic change of local language seamlessly

Introduction to GNU gettext

GNU gettext utilities are perhaps the most popular tools for i18n among free and open source projects. Basically, the idea is to mark text to be localized in the source code with some special tag. For instance:

Label lblName = new Label("First name");

Texts to be translated should be wrapped by gettext function which later will be called to retrieve a localized string.

Label lblName = new Label(gettext("First name"));

Generally, it could be convenient ro rename gettext as _ for shortening tags and make them easier to identify in the code.

public String _(String str) {
    return gettext(str);
}

GNU gettex utilities define a standard workflow we should normally follow:

  • Mark texts to be internationalized in source code with _ function.
  • Parse source files fetching msgids. The result of this step is a keys.pot file containing all msgids in our project.
  • Translate keys.pot into a locale.po (for example, es.po in case of Spanish language).
  • Convert locale.po files into binary resources that could be used later from a programming language.

Example of a GNU gettext workflow

First, we mark texts to be internationalized in source code with _ function. After that, run command xgettext to parse those marks and fetch msgids. The result will be a keys.pot file containing all msgids in our project.

find ./src -name "*.java" -exec xgettext --from-code=utf-8 -k_ -o keys.pot '{}' \;

A keys.pot file has the following structure:

white-space
#  translator-comments
#. extracted-comments
#: reference...
#, flag...
#| msgid previous-untranslated-string
msgid untranslated-string
msgstr translated-string

A simple entry can look like this:

#: src/main/java/com/igalia/UnexpectedError.java:101
msgid "Run-time error"
msgstr "Error en tiempo de ejecución"

Every entry consists of a msgid, a msgstr, and list of comments refering to filename and line for every msgid that occurred in the source code. Notice that every msgid in a keys.pot file is in fact unique.

Once you got a keys.pot file, it is time to localize it. Run the following command:

msginit -l es_ES -o es.po -i keys.pot

The example above generates a Spanish locale file (es.po) out of keys.pot. In case es.po file already existed in our project, we could easily update it by running the following command:

msgmerge -U es.po keys.pot

This will update all missing entries from keys.pot to locale.po. Now you are ready for translating new entries in locale.po file. Use poedit or your favorite text editor to do that.

Lastly, convert locale.po files to binary format. These binary files, or resource bundles, can be used later from a programming language to retrieve localized strings.

msgfmt --java2 -d . -r app.i18n.Messages -l es es.po

GNU gettext diagram workflow

Understanding GNU gettext utilities workflow is essential before continuing with this tutorial. The following diagram, which is part of [GNU gettext utilities documentation], summarizes in form of a diagram the steps explained in the previous section.

GNU gettext workflow.png

I18n Java files

Setting up Gettext Commons

Gettext Commons provides Java classes for internationalization through GNU gettext.

Gettext Commons let us mark texts to be localized in source code, as well as taking care of retrieving localized strings properly. We can think of Gettext Commons as a bridge between our project and resource bundles generated as result of a Gettext workflow.

Gettext Commons comes as .jar. There is a Maven repository for it. If you are running a Maven2 project, simply add the following lines to your POM file:

<repositories>
   <!– Add Gettext Commons repository –>
   <repository>
      <id>gettext-commons-site</id>
      <url>http://gettext-commons.googlecode.com/svn/maven-repository</url>
   </repository>
</repositories>
<dependencies>
   <!– Add Gettext Commons dependency –>
   <dependency>
      <groupId>org.xnap.commons</groupId>
      <artifactId>gettext-commons</artifactId>
      <version>0.9.6</version>
   </dependency>
</dependencies>

Localizing Java files

Consider we wish to add i18n support for the following snippet of code:

class MyClass {

   public void render() {
      Listheader listheader = new Listheader("Header");
   }
}

Gettext Commons provides a series of classes for internationalizing text. The most important is I18NFactory. I18NFactory is a factory class which let us instantiate a resource bundle from a specific locale, and use it from source code for localizing strings.

class MyClass {
   I18n i18n = I18NFactory.getI18n(this.getClass(),
         new Locale("Es", "es),
         org.xnap.commons.i18n.I18nFactory.FALLBACK);
 
   public void render() {
      Listheader listheader = new Listheader(i18n.tr("Header"));
   }
}

In the example above, I called I18NFactory.getI18n() for retrieving a resource bundle for es_ES locale (Spanish language, Spanish dialect from Spain). The third parameter, provides a default resource bundle in case es_ES resource bundle did not exist.

In render() method, I called i18n.tr to wrap “Header” text. i18n.tr is used to mark texts to be internationalized. When processing java source files with xgettext command we should parametrized it properly so it can recognize tr as a marker.

On the other hand, typing i18n.tr every time we need to mark a text, plus instantiating a i18n object for every .java file, can be tedious and repetitive. Fortunately, this can be improved:

public class I18nHelper {
   I18n i18n = I18nFactory.getI18n(I18nHelper.class,
         new Locale("Es", "es),
         org.xnap.commons.i18n.I18nFactory.FALLBACK);

   public static String _(String str) {
      return i18n.tr(str);
   }

In the example above, I created a I18nHelper class. This class provides a static _ method that can be used from any class for internationalizing strings.

import static org.navalplanner.web.I18nHelper._;

class MyClass {
   public void render() {
      Listheader listheader = new Listheader(_("Header"));
   }
}

Where to place Resource Bundles

Sometimes I18nFactory complains about it could not find a requested resource bundle. Resource bundles, are the result of a Gettex workflowo, being generated on the last step via msgfmt command. In GNU gettext terminology these binary files are called Message Objects (.mo files). In Java Message Objects are called resource bundles.

After executing msgfmt, if everything went OK, you will get a Messages_XX.class file, where XX stands for the name of the locale (ES, for example). Since resource bundles are binary files, you must put them under your target/ directory. By default, Gettext Commons expects to find resource bundles under target/classes directory.

Gettext Commons Maven plugin

Apart from Gettext Commons jar library, there is also a Maven2 plugin for Gettext.

Gettext Maven plugin is basically a wrapper for some GNU gettext commands: xgettext, msmerge and msgfmt. Run the following maven goals from command line to:

  • mvn gettext:gettext, parses .java files and generates keys.pot file.
  • mvn gettext:merge, executes previous command plus generates locale files (es.po, de.po, cn.po, etc).
  • mvn gettext:dist, executes previous command plus generates a resource bundle file, Messages_XX.class, for every locale file.

In case you want to know more about how to setup this plugin, please check the following link: Setting up Gettext Commons for i18n Java files.

I18n ZUL pages

Localizing ZUL files

First of all, we are going to modify I18nHelper.java class from previous section.

What basically I am going to do is to add a getI18n() method. This method instantiates a I18n object. ZK engine provides method Locales.getCurrent(), which returns the current locale. Instantiation queries for current locale, loading it in case it exists, or using a default one instead. In addition, this new implementation provides an extra memory cache for storing instantiated i18n objects (Click to download final version of I18nHelper.java).

public class I18nHelper {

   private static HashMap<Locale, I18n> localesCache = new HashMap<Locale, I18n>();

   public static I18n getI18n() {
      if (localesCache.containsKey(Locales.getCurrent())) {
         return localesCache.get(Locales.getCurrent());
      }

      I18n i18n = I18nFactory.getI18n(I18nHelper.class, Locales
            .getCurrent(),
            org.xnap.commons.i18n.I18nFactory.FALLBACK);
      localesCache.put(Locales.getCurrent(), i18n);

      return i18n;
   }

   public static String _(String str) {
      return getI18n().tr(str);
   }
}

After that we are going to create a tag-lib which is simply a facade for I18nHelper.

<?xml version=”1.0″ encoding=”ISO-8859-1″ ?>
<taglib>
    <uri>http://com.igalia/i18n</uri>
    <description></description>
    <function>
        <name>_</name>
        <function-class>com.igalia.I18nHelper</function-class>
        <function-signature>
            java.lang.String _(java.lang.String name)
        </function-signature>
        <description></description>
    </function>
</taglib>

Name it i18n.tld and save it under your /WEB-INF/tld/ directory (Click to download i18n.tld).

Now you are ready to include i18n.tld tag-lib from any ZUL file. Include it and use the i18n prefix accordingly. For instance,

<zk>
    <?tag-lib uri="/WEB-INF/tld/i18n.tld" prefix="i18n"?>
    <window>
        <button label="${i18n:_('Add')}" />
    </window>
</zk>

Load this ZUL page on your favorite browser and see what happens.

Basically what we are doing here is to rely on I18nHelper. I18nHelper exposes _ function for localizing strings passed as a parameter. I18nHelper.getI18n() instantiates a i18n object querying a specific locale resource bundle. A resource bundle contains localized strings and knows how to convert msgids to locale strings.

How to compile msgids from ZUL files

The first obstacle that we need to surpass to generate a resource bundle for a ZUL page successfully is parsing that page searching for texts wrapped by ${i18n:_(’%s’)} string. GNU gettext utilities support a myriad of programming languages but unfortunately gettext does not support XML files. However, this is not a problem as parsing a XML file is much easier than parsing source code of a programming language. I wrote a small Perl script: gettext_zul.pl, which does exactly that. Run it like this:

gettext_zul.pl --dir path_to_zul_files --keys existing_keys.pot_file

You can ommit the --keys parameter, so a new keys.pot file will be created in your current directory. This option can be useful in case you have an existing keys.pot file, generated prior as the result of processing a set of Java files for example.

Once your keys.pot file is up-to-date, create a localized version out of it (locale.po). Lastly, run msgfmt to generate a locale resource bundle out of locale.po.

Supporting arguments

Generally text messages need extra parameters. For example, message “Confirm deleting element?” should be localized as ‘”Confirm deleting {0}?”, item.name’. To sort out this problem, we can extend I18nHelper and overload _ method with extra arguments.

public static String _(String str) {
return getI18n().tr(str);
}

public static String _(String text, Object o1) {
return getI18n().tr(text, o1);
}

public static String _(String text, Object o1, Object o2) {
return getI18n().tr(text, o1, o2);
}

public static String _(String text, Object o1, Object o2, Object o3) {
return getI18n().tr(text, o1, o2, o3);
}

public static String _(String text, Object o1, Object o2, Object o3,
Object o4) {
return getI18n().tr(text, o1, o2, o3, o4);
}

To make use of these new functions, we need to expose them in i18n.tld tag-lib. Tag libs do not support function overloading, so we need to provide different names for each function. For instance, we may add a new function, __, that receives one extra parameter.

<function>
   <name>__</name>
   <function-class>com.igalia.I18nHelper</function-class>
   <function-signature>
      java.lang.String _(java.lang.String name, java.lang.Object arg0)
   </function-signature>
   </description>
</function>

Now you are able to localize strings in ZUL pages that require one extra parameter.

<label value="Hello user"/>

Can be localized as:

<zscript>
   String user = "John";
</zscript>
<label value="${i18n:__('Hello', ${user})}"/>

I18n as a Macrocomponent

Another way of supporting arguments in ZUL files is to encapsulate I18nHelper funcionality into a HTMLMacroComponent.

First of all, create a HTMLMacroComponent, save it as i18n.zul at webapp/common/components/ (Click to download i18n.zul).

<zk>
    <label value="@{self.i18n}" />
</zk>

Create its corresponding Java file (Click to download I18n.java) and save it as I18n.java at webapp/common/components/. I18n tag is able to receive up to 4 arguments, named arg0, arg1, etc.

Finally, add this new HTMLMacroComponent to your lang-addon.xml file.

<component>
    <component-name>i18n</component-name>
    <component-class>com.igalia.common.components.I18n</component-class>
    <macro-uri>/common/components/i18n.zul</macro-uri>
</component>

Now you are ready to use i18n tag in ZUL pages for i18n texts.

<i18n value="Confirm deleting {0} ?" arg0="@{item.name}"/>


HTMLMacroComponent vs Taglib

Most part of the time we may just need to localize single values inside attributes, in this case using a tag-lib is the right way to go, for example:

<button label="${i18n:_('Accept')}"/>

Tag-libs are OK for static texts. Static texts are evaluated only once when the ZUL page is rendered for the first time. When showing texts with data bindings, we must use i18n tag. In most cases, strings with arguments have their arguments bound to dynamic data. As rule of thumb, whenever there are arguments, i18n tag is the right choice.

<i18n value="Confirm deleting {0} ?" arg0="@{item.name}"/>

However, enabling i18n tag-lib to support a different number of arguments can be convenient if we need to substitute an argument inside a literal value. Consider the following example:

<window title="Error ${requestScope['javax.servlet.error.status_code']}"/>

This is a very particular case but it still can happen in your code. Adding an extra __ function in your tag-lib can solve this problem.

<window title="${i18n:__('Error', requestScope['javax.servlet.error.status_code'])}"/>

NOTE: There is no need to interpolate requestScope variable as it is already inside another interpolation (${i18n:…})

Conclusions

This small talk proposes a new approach for internationalizing textx inside ZUL pages as well as Java files for your ZK applications.

This approach is based on GNU gettext, probably the most wide-spread way of i18n in the FOSS world. This new approach provides clear advantages to developers as they do not need to spend time thinking of meaningless keys and keeping track of them manually across different i3-label*.properties files, which in the long-run means saving time and automatizing the whole process of translating and localizing.

Download

You can download all sample files in this small talk here: i18n.tar.gz.



Copyright © Diego Pino, Igalia. This article is licensed under GNU Free Documentation License.