FreshMarker I18N

“Those who know nothing of foreign languages know nothing of their own.”

Johann Wolfgang von Goethe

Internationalization, or I18N for short, is a skill that every modern software must master. Long gone are the days when software could only offer an English only interface.

Localizable values such as dates and numbers can always be output in FreshMarker using the language-specific formatter. The situation is different with terms that need to be adapted to the language of the document.

Multilingual templates can be realized with FreshMarker via alternatives. Depending on the effort required, separate templates, bricks or switch cases can be used for each language. The variant with different cases for the different languages is shown here.

<table>
<#switch .locale?lang>
<#case de>
<tr><th>Artikelnummer</th><th>Name</th></tr>
<#case fr>
<tr><th>Numéro d'article</th><th>Nom</th></tr>
<#default>
<tr><th>Article number</th><th>Name</th></tr>
</#switch>
<#list articles as article>
<tr><td>${article.number}</td><td>${article.name}</td></tr>
</#list>
</table>

Depending on the language in the .locale variable, one of the cases de, fr or the default case is selected. This approach may be practicable for a few language adaptations, but it is cumbersome and error-prone for frequent adaptations.

Instead of repeating the entire structure several times, it would be more elegant to make the terms to be translated flexible. The following model uses Interpolations with a new i18n Build-In.

<table>
<tr><th>${'articleNumber'?i18n}</th><th>${'articleName'?i18n}</th></tr>
<#list articles as article>
<tr><td>${article.number}</td><td>${article.name}</td></tr>
</#list>
</table>

The Built-In works with String values and uses these as the key for the translation. The translation is then returned as a result based on the key and the current locale.

The Built-In is realized in the StringPluginAdapter with the i18n BuiltIn implementation.

builtIns.put(BUILDER.of("i18n"), (x, y, e) -> i18n((TemplateString) x, e));

The implementation uses the ResourceBundle class from the JDK for the translations. The translations for each language are each stored in a separate properties file.

Here are the German translations in a file articles_de.properties.

articleNumber=Artikelnummer
articleName=Name

Here are the English translations in a file articles_en.properties.

articleNumber=Article number
articleName=Name

The name of the ResourceBundle and a Locale are stored in the ProcessContext. This loads the corresponding ResourceBundle instance. The value of the variable is then used as the key and the entry from the ResourceBundle is returned as a TemplateString.

private TemplateObject i18n(TemplateString x, ProcessContext e) {
  try {
    ResourceBundle bundle = ResourceBundle.getBundle(e.getResourceBundle(), e.getLocale());
    return new TemplateString(bundle.getString(x.getValue()));
  } catch (RuntimeException ex) {
    return TemplateNull.NULL;
  }
}

In the event of an error, NULL is returned. An error can occur if no ResourceBundle name has been stored, the ResourceBundle has not been found or the key does not exist in the ResourceBundle.

This implementation makes it possible to react to exceptions within the Interpolation. If ${'articleDescription'?i18n} returns the value NULL and therefore an exception, this can be avoided using the default operator. The variant ${'articleDescription'?i18n!'Description'} always returns a translation or the value Description.

In order for the ResourceBundle to be known for processing, the name must first be specified using the Template#setResourceBundle method. This currently means that only one ResourceBundle can be used for a template. How this can be changed is the subject of another blog post on the topic of I18N.

Leave a Comment