How to localise a Play framework 1.0 web application
This article is about
web application
internationalisation
(internationalization, I18N
) and language
localisation
(localization, L10N
). This article shows you how to localise a Play
framework 1.0 web application, with an example based on the 'Yet Another
Blog Engine' example application, used in the Play
tutorial.
Internationalisation and localisation
There are two separate steps to perform: internationalisation and localisation. Both are mostly about text.
Internationalisation, in programming terms, is a refactoring to remove locale-specific code from the application code. In a web application, this is almost entirely about replacing user-interface text in view templates with references to messages. It also includes formatting non-text data types: dates, currency and other numbers.
Localisation is making a locale-specific version of an application. If the application is internationlised, this means having one or more selectable locale-specific versions. In a web application, this localisation is mostly about translating the user-interface text into the chosen natural language. Language selection is typically a combination of language preferences set in the web browser, and a language selection user-interface in the application itself.
In practice, the two steps go together: you both internationalise and localise one part of the application at a time.
Play framework example
The rest of this article covers how to localise the 'Yet Another Blog
Engine' example application, used in the
Play tutorial.
The starting point is the code in the Play distribution’s
samples-and-tests/yabe
directory. The goal is to fully
internationalise the application, and add several language
localisations.
To get started, see the instructions in the Play manual’s
Setting up i18n
cookbook, which covers the basic cases. First, edit
conf/application.conf
and uncomment (in the default configuration
file) or add a line with three supported languages:
# Localisations for English, Dutch and French.
application.langs=en,nl,fr
If you load a page in the application now, the Play console will show three warnings because you do not have any locale-specific message files yet:
16:19:04,728 WARN ~ Messages file missing for locale en 16:19:04,729 WARN ~ Messages file missing for locale nl 16:19:04,729 WARN ~ Messages file missing for locale fr
UTF-8 message files
The warnings above mean that you need to replace the existing
conf/messages
file with one message file for each language:
-
messages.en
-
messages.nl
-
messages.fr
At this point we encounter the first improvement over the normal Java way of doing things. These files use the same syntax as Java properties files, but they are not properties files because they must use UTF-8 encoding. Java Properties, on the other hand, specifies ISO-8859-1 'Latin-1' character encoding for streaming to and from text files.
Being able to use UTF-8 message files is a big deal for language localisation, because it means that you can write localised language messages in 'plain text'. For example, this means that for a Greek localisation, instead of:
hello.morning = \u0152\u222b\u0152\u00b1\u0152\u00aa\u0152\u2211\u0152\u00ba\u0152\u2260\u0153\u00c5\u0152\u00b1
hello.informal = \u0152\u2265\u0152\u00b5\u0152\u03c0\u0152\u00b1 \u0153\u00c9\u0152\u00f8\u0153\u00d6
you can use Greek letters instead of those Unicode character escapes:
hello.morning = καλημέρα
hello.informal = γεια σου
For the rest of this tutorial, code samples will either define messages in one of these files, or show internationalised mark-up in one of the HTML view templates.
Simple messages
The simple case is a text string that does not change, and that is not
interrupted by other markup. For example, the first such text in the
yabe/app/views/main.html
template, in the tools
list:
<ul id="tools">
<li>
<a href="@{Admin.index()}">Log in to write something</a>
</li>
</ul>
To internationalise this, we replace the text with a message look-up,
using the &{'key'
} syntax:
<ul id="tools">
<li>
<a href="@{Admin.index()}">&{'views.main.tools.login'}</a>
</li>
</ul>
To add the localisations, add the corresponding line in each of the
three message files. In conf/messages.en
views.main.tools.login = Log in to write something
In conf/messages.nl
views.main.tools.login = Inloggen om iets te schrijven
In conf/messages.fr
views.main.tools.login = Connectez-vous pour écrire quelque chose
The message key can be anything you like; in this example I have used a
key to indicate the location views/main.html#tools
Once you have saved these changes, you can see the different language
versions in your web browser by changing the setting that results in a
different Accept-Language
HTTP request header. In Firefox, select
Preferences » Content » Languages » Choose…, add French [fr] and
Dutch [nl] if they are not already in the list, change which one is at
the top of the list, close the dialogue box and reload the page.
Application model localisation
If you use that link to log in to the blog’s 'admin' pages, you can access lists of posts, tags, comments and users. These pages are provided by the CRUD module. For each of these pages, the title (light pink) and the column headers are terms from the application’s model, i.e. JavaBean class and property names.
The CRUD module internationalises these names using the JavaBean class or property name as the message key, which means you can localise them with messages such as the following.
In conf/messages.nl
post = artikel
Post = Artikel
posts = artikelen
Posts = Artikelen
comment = reactie
Comment = Reactie
comments = reacties
Comments = Reacties
user = gebruiker
User = Gebruiker
users = gebruikers
Users = Gebruikers
In conf/messages.fr
post = article
Post = Article
posts = articles
Posts = Articles
comment = commentaire
Comment = Commentaire
comments = commentaires
Comments = Commentaires
user = utilisateur
User = Utilisateur
users = utilisateur
Users = Utilisateurs
You will notice that this does not change the rounded purple navigation links:
Those are defined views/admin.html
which you can internationalise to
use the same localisations simply by surrounding the existing text with
&{'…'
} as follows:
<a href="@{Posts.list()}">&{'Posts'}</a>
…
<a href="@{Tags.list()}">&{'Tags'}</a>
…
<a href="@{Comments.list()}">&{'Comments'}</a>
…
<a href="@{Users.list()}">&{'Users'}</a>
Parameterised messages
As well as simple messages, our application includes messages that contain a variable, such as Posts tagged with Play
To localise a message that contains a single parameter, use a Java format string to insert the parameter value in the message:
views.Application.listTagged.title = Posts tagged with %s
and in the template, add the parameter like this:
&{'views.Application.listTagged.title', tag}
When a message contains multiple parameters, add an index to the format string to allow for different word order in another language:
views.Admin.index.welcome = Welcome %1s, <span>you have written %2s posts so far</span>
… with a list in the template:
&{'views.Admin.index.welcome', user, posts.size()}
In this example, we would also like to use the correct plural form for the word 'post', so make that word a parameter too:
views.Admin.index.welcome = Welcome %1s, <span>you have written %2s %3s so far</span>
… and use the pluralize
extension in the template
&{'views.Admin.index.welcome', user, posts.size(), posts.pluralize(messages.get('post'), messages.get('posts'))}
Note that we have to use messages.get
to look up the localised
singular and plural.
Play module localisation
Play module localisation works the same was as localisation within your
application. This application uses the CRUD and Secure modules, which
means that we must localise the messages in
play/modules/crud/conf/messages
and
play/modules/secure/conf/messages
that our application uses.
In conf/messages.nl
# play/modules/crud (administration)
crud.title = Beheer
crud.home = Home
crud.blank = Nieuw
crud.index.title = Kies het te bewerken object
crud.index.objectType = Type object
crud.index.action =
crud.index.add = Voeg toe
crud.add = &{{ "{%s" }}} toevoegen
crud.list.title = &{{ "{%s" }}}
crud.list.size = %d &{{ "{%s" }}}
crud.list.totalSize = %d totaal
crud.pagination.previous = « Vorige
crud.pagination.next = Volgende »
crud.pagination.last = Laatste »»
crud.pagination.first = «« Eerste
crud.show.title = &{{ "{%s" }}} bewerken
crud.save = Opslaan
crud.saveAndContinue = Opslaan en verder bewerken
crud.cancel = Annuleren
crud.hasErrors = Corrigeer fouten a.u.b.
crud.blank.title = &{{ "{%s" }}} toevoegen
crud.saveAndAddAnother = Opslaan en nogmaals creëren
crud.delete = &{{ "{%s" }}} verwijderen
crud.created = &{{ "{%s" }}} is aangemaakt
crud.saved = &{{ "{%s" }}} is opgeslagen
crud.deleted = &{{ "{%s" }}} is verwijderd
crud.delete.error = Kan dit object niet verwijderen
crud.search = Zoeken
crud.none = (Geen)
crud.help.required = Verplicht.
crud.help.minlength = Min. lengte is %d.
crud.help.maxlength = Max. lengte is %d.
crud.help.email = Geldig e-mailadres
crud.help.dateformat = In de vorm YYYY-MM-DD.
crud.help.numeric = Numeriek.
crud.help.min = Moet groter daan %d zijn.
crud.help.future = In de toekomst.
crud.help.past = In het verleden.
crud.help.after = Na %s.
crud.help.before = Voor %s.
crud.help.range = Tussen %d en %d
# play/modules/secure
secure.username = Uw e-mailadres:
secure.password = Uw wachtwoord:
secure.signin = Nu inloggen
In conf/messages.fr
# play/modules/crud (administration)
crud.title = Administration
crud.home = Home
crud.blank = Nouveau
crud.index.title = Choisissez l'objet à modifier
crud.index.objectType = Type objet
crud.index.action = XXX
crud.index.add = Ajouter
crud.add = Ajouter &{{ "{%s" }}}
crud.list.title = &{{ "{%s" }}}
crud.list.size = %d &{{ "{%s" }}}
crud.list.totalSize = %d total
crud.pagination.previous = « Précédent
crud.pagination.next = Suivant »
crud.pagination.last = Dernier »»
crud.pagination.first = «« Premier
crud.show.title = Modifier &{{ "{%s" }}}
crud.save = Enregistrer
crud.saveAndContinue = Enregistrer et continuez à modifier
crud.cancel = Annuler
crud.hasErrors = Corrigez les erreurs s.v.p.
crud.blank.title = Ajouter &{{ "{%s" }}}
crud.saveAndAddAnother = Enregistrer et ajouter un autre
crud.delete = Supprimer &{{ "{%s" }}}
crud.created = &{{ "{%s" }}} a été crée
crud.saved = &{{ "{%s" }}} est enregistré
crud.deleted = &{{ "{%s" }}} est supprimé
crud.delete.error = Ne peut pas supprimer l’objet
crud.search = Chercher
crud.none = (aucun)
crud.help.required = Obligatoire.
crud.help.minlength = Longeur minimum est %d.
crud.help.maxlength = Longeur maximum est %d.
crud.help.email = Adresse e-mail valide
crud.help.dateformat = En format YYYY-MM-DD.
crud.help.numeric = Numerique.
crud.help.min = Doit être plus grand que %d.
crud.help.future = Dans le futur.
crud.help.past = Dans le passé.
crud.help.after = Après %s.
crud.help.before = Avant %s.
crud.help.range = Entre %d et %d
# play/modules/secure
secure.username = Votre adresse e-mail:
secure.password = Votre mot de passe:
secure.signin = Connectez-vous maintenant
Of course, once you have done this it is also a good idea to contribute the localisations back to the module.
Special cases compared to JavaServer Faces (JSF)
In 2008 I was localising an application that was built using Seam 2 and JavaServer Faces (JSF) 1.2. There turned out to have three special cases that were awkward to implement in JSF, which I reported as javaserverfaces-spec-public issue 517.
-
Parameterised message used in an attribute value
-
Formatted message parameter
-
Link within message
All three cases turn out to be straightforward in Play.
The first case happens when you want to use a phrase with a parameter in an attribute value in the template, such as:
<a href="@\{Application.show(_post.id)}" title="By Bob">
This is a problem in JSF, because you would normally use an XML tag to perform the parameter replacement, which you cannot do in an attribute value. The Play syntax simply avoids this problem, and you can just do:
<a href="@\{Application.show(_post.id)}" title="&{'views.tags.display.author', _post.author.fullname}">
The second case is when you want to format a value, such as a date, for
use as a message parameter in a phrase like By Bob on 2009-06-14
.
Again, the problem in JSF is caused by having to an XML tag to format
the value, while needing to be able to use the result in an XML
attribute value. In Play the formatting extensions do not get in the way
of the message parameter syntax, so you can do:
<span>&{'views.tags.display.author', _post.author.fullname, comment.postedAt.format('yyyy-MM-dd')}"}</span>
You can, of course, localise the format pattern as well:
<span>&{'views.tags.display.author', _post.author.fullname, comment.postedAt.format(messages.get('views.dateFormat'))}"}</span>
The third case typically occurs when you want part of a localised
message to be a hyperlink, as in the message
Log in to write something
. This is a problem in JSF because the
hyperlink is a JSF component that is rendered in a way that means the
link’s mark-up cannot be in the message file. Play on the other hand,
lets you use plain HTML in your templates, so you can just put the
mark-up in your message with a parameter for the URL:
logIn = <a href="%s">Log in</a> to write something
&{'logIn', '/admin'}
Our application was using the <a href="@{Admin.index()}">
syntax in
the hyperlink to get the framework to generate the URL based on the
routes file. To do this in the message parameter, do:
&{'logIn', actionBridge.Admin.index()}
The localised 'Yet Another Blog Engine' example
The end result of applying the above steps is a localised version of the 'Yet Another Blog Engine' example that works in English, Dutch and French: yabe-l10n-1.0.2.zip
The 'Yet Another Blog Engine' admin interface in Dutch (above) and French (below).