Internationalization

This wiki entry was originally written by Timothy Perrett and was lifted with permission from his blog which can be found at blog.getintheloop.eu

One of the best things about Lift is its amazingly flexible template and resource localization system. This article discusses the mechanisms that you can use to localize your application.

Lift’s New Resource Bundles

As of version 2.2, Lift supports a new type of resource bundle for I18N, covered in Localization. The new resource bundles can be global and per-page, and support direct UTF-8 encoding. Java resource bundles, covered here, uses ISO8859-1 for their encoding and so require unicode escapes for many non-latin codepages. Additionally, Lift’s new resource bundles are XML, and allow you to directly embed XHTML elements in the translations.

Overview

Out of the box, Lift gives you the following options – items 1 and 2 require zero boiler plate, whilst the 3rd option gives you the flexibility to extend the localization however you need.

We will now take a look at each localization option in turn, in relative detail stopping to investigate how they work.

Assumed environment

For the length of this article we’ll assume that we have the need to localize into English , French, German and Hebrew. These languages have the following locale codes:

Language Code
English en_GB
French fr_FR
German de_DE
Hebrew he_IL

These are standard ISO codes used by the Java localization system, irrespective of Lift et al. As a bit of background for those of you not familiar with locale codes and their purpose, you can see that the first part of the locale code denotes the spoken language – for example, en is English – and the second part after the underscore is the country code. This is amazingly helpful for languages like English and Arabic which are spoken in many countries as it gives you a specific language and then culture on which you can account for language nuances etc. Salutations are a classic one – UK English might have a salutation of “Good afternoon” whilst Australian English might have “Good ay’”.

In order to tell Lift that this application will be localized into several languages, we have to set a customized localeCalculator. In our application it will look like this:

    def localeCalculator: Locale = 
      request.flatMap(r => {
        def localeCookie: HTTPCookie = 
          HTTPCookie("your.cookie.name",Full,
            Full,Full,Full,Empty,Empty)
        def localeFromString: Locale = {
          val x = in.split.toList; new Locale
        }
        def calcLocale: Box[Locale] = 
          S.findCookie.map(
            _.value.map
          ).openOr(Full(LiftRules.defaultLocaleCalculator))
        S.param match {
          case Full => calcLocale
          case f@Full => 
            S.addCookie(localeCookie)
            tryo(localeFromString)
          case _ => calcLocale
        }
      }).openOr(Locale.getDefault())

Add this function someplace in your application – here we’ll assume its just in the Boot class, then set it in LiftRules like so:

LiftRules.localeCalculator = localeCalculator _

Comet

The most important thing to know about comet actors is that they live outside of the page request cycle.
Given our localeCalculator function above and a call to S.?, S.??, S.locale, … inside a comet actor will always lead to the “Locale.getDefault()” result. This may confuse at first sight but the reason is quite simple: The incoming parameter request in the function localeCalculator will always be Empty if it has been called from a comet actor.

Therefore, your best bet is to use SessionVars when it comes to internationalization & comet. Unlike Requests they are accessible from the comet actors scope.

Text localization from property bundles

So, given our working languages we have several issues at hand and several strategies we could choose. Lets start with the most basic form of localization that will be familiar with most users of the java platform… properties files loaded as a ResourceBundle.

So lets assume that we have our translations already completed, we just need to put them into key-value pairs in properties files located in:

${project.basedir}/src/main/resources/i18n/mybundlename_*locale*.properties

So for us, that looks like:

mybundlename_en_GB.properties
mybundlename_fr_FR.properties
mybundlename_de_DE.properties
mybundlename_he_IL.properties

This is pretty standard stuff that is well documented in the javadocs from sun. So, you need to know how to specify value keys within lift. There are two use cases, one is in your code, and the other is in your HTML templates.

Then you should inform Lift to use this bundle add into Boot.boot() :

LiftRules.resourceNames = “i18n/mybundlename” :: LiftRules.resourceNames 

In code:

S.?

In template:

<lift:loc locid="mykey">Default Text</lift:loc>

This is all well and good, but there are use cases that simple key/value replacement doesnt take care of – with our use case languages we have a great example here:

So, to deal with such cases Lift brings you comprehensive template localization which we’ll now discuss.

Template file localization

Given this conundrum about language direction and content length lift can assert different template names. For example, lets assume that in your webapp directory you have a file called index.html – based on the LiftRules.localeCalculator Locale that is returned, it can choose the right template. With the problems we face here, we might have:

index_en_GB.html
index_en_AU.html
index_de.html
index_he.html

This would then yield different content templates for German and Hebrew, and gives us two distinctly different english templates for UK English and Australian English… for arguments sake the imagery in the two templates could be different because Australian culture is somewhat more casual than compared to England.

This exact same scheme also applies for CSS resources if you do not need the possibly side effect of code duplication in this system.

Custom resource bundle provider hook

One of the driving mantras of Lift is that everything, and we mean everything has sensible defaults, but you can hook right into the core lift lifecycle and add your own stuff. Localization is no different and it is of course a common idiom to need to load localization content from a database backed cache. I wont delve into exactly how you handle the caching or similar but this is how you pass the special resource bundles into Lift’s cycle:

LiftRules.resourceBundleFactories.prepend { 
  case  if localeAvalible_? => 
      CacheResourceBundle
  case _ => CacheResourceBundle(new Locale)
}

In the above example, localeAvalible_? checks if this locale is available and of course, CacheResourceBundle is a subclass of ResourceBundle and has the following signature:

case class CacheResourceBundle extends ResourceBundle

A Note on Resource Bundle Resolution

Per Java’s documentation on ResourceBundle, resolution of property files is done in this order:

where “language1”, “country1”, and “variant1” are the requested locale parameters, and “language2”, “country2”, “variant2” are the default locale parameters.

For example, if the default locale for your computer is “en_GB”, someone requests a page for “ja”, and you have the following property files defined:

then the Messages_en_GB.properties file, and not Messages.properties will be used. If you want to change this behavior, set your default Locale to the ROOT locale in your Boot.scala with the following code:

import java.util.Locale
Locale.setDefault(Locale.ROOT)

Conclusion

Thats pretty much it folks – by way of a mix of these schemes its possible to build up a very rich localization methodology which works for pretty much all locale needs. In this example we have discussed language lengths, RTL languages and given you all the code you need to get going with localization in Lift – go forth and localize!