“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.