Day 18 - I18N

Catalyst I18N and L10N using Catalyst::Plugin::I18N

Introduction

The task is fairly simple:

* Allow the user to specify a language of choice,
* Replace all messages with function calls to a translation module,
* Write translations for every language you want.

The last part (L10N) should be very simple (using a graphical PO editor is preferred) in order to allow non-technical people to localise your software.

Setting the URLs , using chained actions

For this exercise, let's just make our app available to both English and Romanian viewers.

http://localhost:3000/en will greet English speakers, while http://localhost:3000/ro will greet Romanian speakers.

A nice way to setup URLs like that is using Catalyst::DispatchType::Chained (see day 10):

 -=cut
 +sub language : PathPart('') Chained('/') CaptureArgs(1) {
 +       my ($self , $c, $language )  =@_;
 +}

 -sub index : Private {
 +sub cheer : PathPart('') Chained('language') Args(0) {

If you run the development server with debug enabled, you should see the chain:

 [debug] Loaded Chained actions:
 .-------------------------------------+--------------------------------------.
 | Path Spec                           | Private                              |
 +-------------------------------------+--------------------------------------+
 | /*                                  | /language (1)                        |
 |                                     | => /cheer                            |
 '-------------------------------------+--------------------------------------'

Setting up I18N

First step, make sure you have Catalyst::Plugin::I18N installed and add it to your plugin list:

 -use Catalyst qw/-Debug ConfigLoader Static::Simple/;
 +use Catalyst qw/ConfigLoader Static::Simple I18N/;

Then let Catalyst know what is your preffered language is:

 sub language : PathPart('') Chained('/') CaptureArgs(1) {
        my ($self , $c, $language )  =@_;
        $c->languages( [ $language ] );
 }

OK, that's it . Let's internationalise the messages now.

I18N with $c->loc

Just replace your messages with calls to $c->loc. Replace your interpolated variables with [_1] , [_2] etc. The patch below illustrates:

 --- hi_there.tt
 +++ hi_there.tt
 @@ -1,17 +1,17 @@
 -<html><head><title>Happy winter solstice</title></head>
 +<html><head><title>[% c.loc("Happy winter solstice") %]</title></head>
  <body>
 -[% IF c.stash.days_till_xmas > 0 %]
 -And best regards for the new year!  There are [% c.stash.days_till_xmas %]
 -days left until santa comes.
 -[% END %]
 +[% IF c.stash.days_till_xmas > 0 ;
 +c.loc("And best regards for the new year!  There are [_1]
 +days left until santa comes",   c.stash.days_till_xmas );
 +END %]

 -[% IF c.stash.days_till_xmas == 0 %]
 -It's already Christmas, go check your presents!
 -[% END %]
 +[% IF c.stash.days_till_xmas == 0 ;
 +c.loc("It's already Christmas, go check your presents!");
 +END %]

 -[% IF c.stash.days_till_xmas < 0 %]
 -You just missed it, but there's one next year too!
 -[% END %]
 +[% IF c.stash.days_till_xmas < 0 ;
 +c.loc("You just missed it, but there's one next year too!");
 +END %]

  </body>
  </html>

Everything should still be working, but now we are ready to localise it.

L10N with a little help from xgettext.pl

xgettext.pl comes with the Locale::Maketext::Lexicon distribution. You just pass it a list of files and it collects all localised strings and dumps them in a po file.

Catalyst::Plugin::I18N expects to find its messages in lib/Cheer/I18N/LANG.po, so here's the command:

 mkdir lib/Cheer/I18N
 /path/to/xgettext.pl -o lib/Cheer/I18N/ro.po root/hi_there.tt 

The file is generated:

 # SOME DESCRIPTIVE TITLE.
 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
 # This file is distributed under the same license as the PACKAGE package.
 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
 #
 #, fuzzy
 msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=CHARSET\n"
 "Content-Transfer-Encoding: 8bit\n"

 #: root/hi_there.tt:4
 #. (c.stash.days_till_xmas)
 msgid ""
 "And best regards for the new year!  There are %1 \n"
 "days left until santa comes"
 msgstr ""

 #: root/hi_there.tt:1
 msgid "Happy winter solstice"
 msgstr ""

 #: root/hi_there.tt:9
 msgid "It's already Christmas, go check your presents!"
 msgstr ""

 #: root/hi_there.tt:13
 msgid "You just missed it, but there's one next year too!"
 msgstr ""

Of course, in a real project you have tons of files. Here's what I do:

(find root/ -name '*.tt' ; find lib/MyApp/Controller -name '*.pm' )| xargs /path/to/xgettext.pl -o lib/MyApp/I18N/ro.po

Let's just translate the messages and we're done:

 --- ro.po
 +++ ro.po
 @@ -12,7 +12,7 @@
  "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
  "Language-Team: LANGUAGE <LL@li.org>\n"
  "MIME-Version: 1.0\n"
 -"Content-Type: text/plain; charset=CHARSET\n"
 +"Content-Type: text/plain; charset=utf-8\n"
  "Content-Transfer-Encoding: 8bit\n"

  #: root/hi_there.tt:4
 @@ -20,16 +20,16 @@
  msgid ""
  "And best regards for the new year!  There are %1 \n"
  "days left until santa comes"
 -msgstr ""
 +msgstr "Si La Multi Ani! Mai sunt %1\n zile pana vine Mos Craciun"

  #: root/hi_there.tt:1
  msgid "Happy winter solstice"
 -msgstr ""
 +msgstr "Craciun Fericit!"

  #: root/hi_there.tt:9
  msgid "It's already Christmas, go check your presents!"
 -msgstr ""
 +msgstr "E deja Craciun, desfaceti cadourile!"

  #: root/hi_there.tt:13
  msgid "You just missed it, but there's one next year too!"
 -msgstr ""
 +msgstr "Craciunul a trecut, dar mai e unul anul viitor!"

Restart your server, http://localhost:3000/ro should give you the localised version. Craciun fericit :)

AUTHOR

Bogdan Lucaciu bogdan@sns.ro