JalaModules/I18n

Internationalization Module

First of all, you must add the library to your application. This is done by simply adding the following line somewhere in a JavaScript file in Global directory, but outside any function declaration (so eg. add the following lines on top of a file Global.js):

// include the I18n library
app.addRepository("modules/jala/code/I18n.js");

This assumes that you placed the jala library package into the directory modules/jala of your Helma installation (this is where you should put it in any case if possible, otherwise you'd have to adjust quite some internal dependency paths in the different jala libraries).

How to Mark Messages in Code and Skins for Translation

First, all strings in the code and skins of an application that should be translated need to be surrounded by certain message calls. Jala I18n provides three methods to do this:

  1. gettext(key [, value1 [, value2 [, ...]]])
  2. markgettext(key)
  3. ngettext(singularKey, pluralKey [, value1 [, value2 [, ...]]])

plus a single Helma macro for internationalization of message parts in skins:

  1. The global <% message %> macro

In addition, the message parser that creates the PO file template recognizes all macro attributes called message, so the value of these attributes specified in skins will be part of the translation template.

gettext()

The gettext method accepts 1 to n arguments, where the first argument needs to be the message to be translated. A simple use of this method looks like this:

gettext("Hello, World!");

Depending on a specified locale Jala I18n will look for a translation of the key string "Hello, World!" and return any found translation. If no translation is found, the original message string will be returned. That's why it's best practise to create correct and meaningful message keys in the default language of the application.

But the gettext method also accepts additional arguments. These will be used by Jala I18n as "replacement parameters". Each message key can contain numerous placeholders looking like {number}. These placeholders will be replaced with the additional arguments to gettext, where the number inside the curly braces is the ordinal number of the additional argument. E.g. {0} is the first replacement argument, {1} the second, and so on. Using this mechanism one can add dynamic contents into an internationalized message:

gettext("Hello {0}!", session.user.name);
gettext("Hello {0}, it's about {1}!",
        session.user.name,
        (new Date()).format("HH:mm"));

But besides such simple, nevertheless extremely helpful replacements one can even use complex formatting patterns inside a message to internationalize date or number formats, currencies etc.:

gettext("It's now {0, time, short} o'clock", new Date());
gettext("It will be {0, date, EEEE, dd.MM.yyy, HH:mm} after the beep.", new Date());
gettext("The astonishing price of this product is just {0, number, currency}.", 4321);
gettext("You have {0, choice, 0#no new mail|#1one new mail|1<{0} new mails}",
        session.user.inbox.size());

As you can see, Jala I18n also allows you to add case switches into messages, so depending on the replacement value a certain part of a message will be used. In the last of the above examples the value passed to gettext as second argument triggers what will appear as message (eg. "You have one new mail" if the value is 1, but "You have 23 new mails" if the size of the inbox is 23). This also eases translation (although the syntax is a bit hard to read for reasonable people), as the different parts still stay in context.

Internally Jala I18n uses the standard Java class MessageFormat for these replacements and formattings. For a complete overview of the possibilities and accepted formatting patterns have a look at the of  Java API documentation

ngettext()

Different to gettext() this method can distinguish between singular and plural. Therefor nggettext() requires at least three arguments:

  1. The singular form of the message
  2. The plural form of the message
  3. A number determining whether to use the singular or plural form

A simple call of ngettext looks like this:

ngettext("Application",
         "Applications",
         countApps());

If the value returned by countApps() is 1, the singular form of the message will be used, for all other values ngettext will return a translated version of the plural form. ngettext() also accepts replacement arguments like gettext(), so the above example can be extended to a meaningful message:

ngettext("There is one application running",
         "There are {0} applications running",
         countApps());

So the first replacement parameter that can be referenced in a message is the amount passed to ngettext(). In addition to that you can pass more arguments to the method call and reference them using ordinal numbers starting with 1:

ngettext("Hello {1}, you have one new mail",
         "Hello {1}, you have {0} new mails",
         session.user.inbox.size(),
         session.user.name);

markgettext()

This is a special function in that it does basically nothing except marking a message for translation. Sometimes it's necessary to use this method, especially when you use messages stored in eg. variables and pass these variables to the gettext() or ngettext() methods:

var msgKey1 = markgettext("Hello {0}, wassup?");
var msgKey2 = markgettext("Pleased to meet you, {0}. What can i do for you?");
gettext(session.user.isConservative() ? msgKey1 : msgKey2, session.user.name);

The actual translation is done by gettext(), but it wouldn't be senseful to add the contents of the gettext call to the translation template, as the translator wouldn't have a clue how to translate "msgKey1". That's where markgettext() comes in handy, as the translation template will contain the messages passed. The only restriction of markgettext() is that it can only handle simple singular messages, any necessary plural distinction must be done manually in the code.

The "message" Macro

The global <% message %> macro makes it possible to translate certain parts of a skin. The macro itself supports both singular and plural forms and replacement parameters. The syntax of the macro is as follows:

<% message text="Hello, World!" %>
<% message text="Hello, {0}!" values="user.name" %>
<% message text="There's nobody online."
           plural="There are {0} users online."
           values="root.users" %>

For singular forms the values attribute is optional, for plural forms mandatory. You can specify multiple replacement values by separating them with commas, eg. values="user.name, root.users". For plural messages the first entry of the values attribute must be a number which will be used to determine whether to use the singular or the plural form of the message.

Within the values attribute you can access global properties as well as all default macro handlers, those objects present in the request path or custom defined macro handler objects. The resolving of the attribute value follows these rules:

  1. If a single parameter doesn't contain a dot, it will be replaced with the value of the corresponding global property, eg. values="currentTime". If the parameter contains a dot, the part left of the dot is interpreted as macro handler object, and the part on the right side of the dot is the name of the property which value should be used.
  2. With using request or response you can insert the value of a property of req.data or res.data into the message, eg. values="request.name, request.password.
  3. With using param as macro handler you can access the properties of the object that has been passed to the skin, eg. values="param.username, param.password"
  4. Any other value will be interpreted as a custom macro handler object, eg. values="list.size" would require a macro handler object res.handlers.list to be defined somewhere in the application code relevant to rendering the skin containing the message macro.

You cannot reference objects in the values attribute, only its properties -- with one exception: if the parameter references a HopObject collection or an Array, the macro will use the size resp. the length of the collection or Array as replacement value, eg.:

<% message text="Hello {1}! You have one mail in your mailbox."
           plural="Hello {1}! You have {0} mails in your mailbox"
           values="user.mailbox, user.name" %>

Assuming that user.mailbox is a HopObject collection containing mail objects, the message macro will use the size of the collection to determine whether to use the singular or plural form of the message plus as replacement parameter number 0.

As with Helma macros too you can only specify a handler object and a property name at maximum, so a replacement parameter user.site.title won't work.

The Macro Attribute "message"

Besides the global macro <% message %> the parser that creates the PO file template also respects a macro attribute message in any other macro tags. Using this attribute you can implement macros as usual while ensuring that any message passed to the macro will be part of the translation template:

<% story.online message="Publish this story!" %>

Of course the macro story.online is in this case responsible for using the macro attribute message, which could look like this:

Story.prototype.online_macro = function(param) {
   if (this.isOnline() == false) {
      res.write(gettext(param.message));
   }
   return;
}

Creating the Translation Template

For the creation of the PO file template which can be used to translate the messages for different languages, countries or variants ("dialects") the build script of the Jala HopKit has a task called "pot". The syntax is as follows:

build.bat pot

After pressing the enter key you'lll be asked for two arguments: first the name of the portable object (PO) file template that the parser should create. The parser will by default use the name "messages.pot" and create the file in the current working directory. Optionally you can specify a different path (which must include the name of the tempplate file), but make sure that the directory exists as it won't be created during the process.

The second part the script will ask you for is to specify the directories containing the application code (.js and .hac files) and/or the skins to search for messages that should be translated. You can specify more than one directory by separating them with spaces.

The parser works in recursive mode, so you only need to specify the toplevel directory where the parsing should start.

The result will be a  Gnu gettext PO template (or POT) file which can be used to create the necessary translations, preferably using a PO file editor, eg.  PoEdit.

Creating the JavaScript Message Files

After all translations have been done you need to convert them into JavaScript message files which will be used by the Jala I18n library to translate the messages in the application code and skins. For this the build script in Jala's HopKit provides a second task called "messages". Start the script using the following commandline:

build.bat messages

After issuing the command you'll be first asked to specify the directory containing the PO files with the translated messages. The file names must follow a certain naming convention:

language[_COUNTRY][_variant].po

"language" must be a two letter lowercase  ISO code. The country part of the file name is optional and must be a two letter uppercase  ISO Country Code. The variant is also optional and can be freely defined. In any case the extension of the file must be ".po" to be recognised by the script.

The second thing the script will ask you for is the path to the directory where the generated JavaScript message files should be placed.

Finally, you can specify an optional namespace where the object containing the messages will be placed. The script will create one JavaScript file for each locale, containing a plain object literal with the messages, where the message key is the property name, and the value of the property is the translation. Each of these message "catalogs" will be placed by default in a global object called "messages", with the locale key (eg. de_AT) as property name and the message catalog object as value. By specifying a different namespace when running the generator script you can place the messages object in a property of a different object. This way you can avoid cluttering the global namespace. Eg. when defining "jala" as namespace the message object containing the different catalogs will be located in global.jala.messages.

After that the script will start to parse and create the JavaScript message catalog files.

build.properties

Always answering the questions of the Jala HopKit script to create the PO file template or the JavaScript message catalog files can be quite tedious. To make things easier you can place a file called build.properties somewhere (preferably in the root of your application directory), containing pre-defined arguments to the scripts. A sample build.properties file looks like this:

##########################################
## Properties used by HopKit "pot" task ##
##########################################

## the name of the template file to create
## (including any relative or absolute path)
i18n.template     = ./i18n/messages.pot

## the directories to scan for gettext function
## and macro calls (separated with spaces)
i18n.scan         = ./code ./skins

###############################################
## Properties used by HopKit "messages" task ##
###############################################

## the directory containing the .po files to convert
i18n.poDirectory  = ./i18n/

## the path to the directory where to put the messages.jar
i18n.destination  = ./code/Global/i18n/

## the namespace to use for the message catalogs
i18n.namespace = jala

Adding the JavaScript message files to your application

Basically there are two ways to add the message catalog files to your application:

  1. You can put them into the Global directory of your application, or -- provided that you use a recent Helma version (>= 1.5) -- into a subdirectory of Global (eg. "i18n"),
  2. You can put them into a directory outside your application and add them using app.addRepository("/path/to/messagefiles")

By default the library expects the messages to be stored in the global property messages. If you specified a namespace during the generation of the message catalog files you'll need to tell jala.i18n where it should look for the message catalogs. Put the following beneath the line where you added the Jala I18n library to your application, eg. in the file myApp/Global/Global.js:

jala.i18n.setMessages(global.jala.messages);

The above example assumes that a namespace "jala" has been specified when generating the message catalog files. Please make sure to place this line after the one adding the I18n.js file, and also after the inclusion of the message files (if you didn't put the message files directly into the Global directory of your application code). A complete example:

// include the I18n library
app.addRepository("modules/jala/I18n.js");
// include the message catalog files
app.addRepository("apps/myApp/i18n/jala.de_AT.js");
app.addRepository("apps/myApp/i18n/jala.it_CH.js");
app.addRepository("apps/myApp/i18n/jala.rm_CH.js");
app.addRepository("apps/myApp/i18n/jala.fr_CH.js");
// tell jala.i18n where to find the messages
jala.i18n.setMessages(global.jala.messages);

How and where to define the locale used for translation

In addition you need to tell I18n how to retrieve the target locale for translations, so you should add the following line to the end of the above code block:

jala.i18n.setLocaleGetter(new Function("return res.meta.locale;"));

The method passed as argument must return an instance of java.util.Locale defining the target locale for the translation. This example assumes that the pre-request locale definition is stored in res.meta.locale, but of course you can also use any other location too. If no locale getter method is defined, I18n will use the default locale of the Java Virtual Machine running Helma.