指南, 教程 & 参考手册

Browse

Contents

好书推荐

国内书籍(将于2012年出版)

国外书籍(电子图书)

Internationalisation and localisation

Having built our fully-functional blog engine, we will now consider an optional extra: web application internationalization and language localisation. Although we could have done this from the start, it is more realistic to build the first version of an application in a single language, and add multiple languages later.

Internationalisation and localisation

There are two 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.

Yet Another Blog Engine

The starting point for this section is the finished tutorial code, which you can find in the Play distribution’s samples-and-tests/yabe directory. The goal is to fully internationalise the application, and add French and Dutch language localisations.

To get started, 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 %1$s, <span>you have written %2$s 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 %1$s, <span>you have written %2$s %3$s 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

When you are localising a web application, there are a few special cases that be awkward to implement if you are using a component-based web application framework, such as JavaServer Faces:

  1. Parameterised message used in an attribute value
  2. Formatted message parameter
  3. 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.

The ‘Yet Another Blog Engine’ admin interface in Dutch (above) and French (below).

Next: Now you have finished the tutorial, move on to The Essential Documentation - Main concepts.

By Peter Hilton, originally published on the Lunatech Research blog.