目次

Version 7, last updated by David Bernard at Nov 28 01:17 UTC

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.

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.

  • Text localization from property bundles
  • Full template localization
  • Custom resource bundle provider hook

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 (our default language), 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(request : Box[HTTPRequest]): Locale = 
      request.flatMap(r => {
        def localeCookie(in: String): HTTPCookie = 
          HTTPCookie("your.cookie.name",Full(in),
            Full(S.hostName),Full(S.contextPath),Full(2629743),Empty,Empty)
        def localeFromString(in: String): Locale = {
          val x = in.split("_").toList; new Locale(x.head,x.last)
        }
        def calcLocale: Box[Locale] = 
          S.findCookie("your.cookie.name").map(
            _.value.map(localeFromString)
          ).openOr(Full(LiftRules.defaultLocaleCalculator(request)))
        S.param("locale") match {
          case Full(null) => calcLocale
          case f@Full(selectedLocale) => 
            S.addCookie(localeCookie(selectedLocale))
            tryo(localeFromString(selectedLocale))
          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 _

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.?("mykey")

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:

  • Hebrew is a language that is written right-to-left (RTL) so generally the content / css / markup will be quite different
  • Within the languages that are left-to-right (LTR), French and English have a comparable number of character tokens, but german is typically more verbose and content takes up more room.

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 (that my friends is up to you) but this is how you pass the special resource bundles into Lift’s cycle:

LiftRules.resourceBundleFactories.prepend { 
  case (basename, locale) if localeAvalible_?(locale) => 
      CacheResourceBundle(locale)
  case _ => CacheResourceBundle(new Locale("en","GB"))
}

In the above example, localeAvalible_? checks if this locale is available (from a value in my application) and of course, CacheResourceBundle is a subclass of ResourceBundle and has the following signature:

case class CacheResourceBundle(loc: Locale) extends ResourceBundle

A Note on Resource Bundle Resolution

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

  • baseName + “_” + language1 + “_” + country1 + “_” + variant1
  • baseName + “_” + language1 + “_” + country1
  • baseName + “_” + language1
  • baseName + “_” + language2 + “_” + country2 + “_” + variant2
  • baseName + “_” + language2 + “_” + country2
  • baseName + “_” + language2
  • baseName

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:

  • Messages_fr_FR.properties
  • Messages_en_GB.properties
  • Messages.properties

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!