p28n, the top to bottom persistent internationalization tutorial.

By Jason Chow aka "p0windah"
For some developers, allowing a website to support multiple languages is essential. Luckily cakePHP 1.2 has the foundations available to make this possible.
Before forging ahead, I'd like a disclaimer. I don't claim this tutorial to be uniquely mine, it's an amalgamation of techniques from several pages and sites. Neither is it the most comprehensive and in-depth guide.

That said, I certainly hope after using this guide you can quickly and easily implement multiple languages in your cake app without needing to skip around the place. If I fall short of this and you have suggestions, leave a comment.

Once you complete this tutorial your site will be able to:
  1. display multiple languages
  2. allow users to switch languages
  3. store language settings in cookies, so returning visitors don't need to re-select their preferred language

The sites that I build typically require 3 languages:
  1. British English(en-gb)
  2. Simplified Chinese(zh-cn)
  3. Traditional Chinese(zh-tw)

So throughout this document, I'll be using them as my reference languages. Your site may support more or less languages.

Step 1: Setup the directories for your messages


$ cd cake/app/locale/

$ mkdir en_gb

$ mkdir en_gb/LC_MESSAGES

$ mkdir zh_tw

$ mkdir zh_tw/LC_MESSAGES

$ mkdir zh_cn

$ mkdir zh_cn/LC_MESSAGES


This will create the minimum folders required for each language our site needs to support.

To find your language code(s) refer to http://api.cakephp.org/1.2/l10n_8php-source.html#l00180
Download code //this is only a sample. don't add this to your code.
'nl' => array(
    'language' => 'Dutch (Standard)', 
    'locale' => 'dut', 
    'localeFallback' => 'dut', 
    'charset' => 'utf-8'
),
'pl' => array(
    'language' => 'Polish', 
    'locale' => 'pol', 
    'localeFallback' => 'pol', 
    'charset' => 'utf-8'
),
'sk' => array(
    'language' => 'Slovack', 
    'locale' => 'slo', 
    'localeFallback' => 'slo', 
    'charset' => 'utf-8'
),

By studying the sample above, we can see that directory names are actually the locale:
  • 'dut' is the correct directory name(locale) for 'nl'
  • 'pol' is the correct directory name(locale) for 'pl'
  • 'slo' is the correct directory name(locale) for 'sk'

Step 2: Write some strings to translate.


View Template:

Download code <?php $this->pageTitle __('pageTitle_home'true); ?>

<h1><?php __('welcome_heading'); ?></h1>
<?php __('lipsum'); ?><?php __('lipsum'); ?><?php __('lipsum'); ?><br/>
<?php __('lipsum'); ?><?php __('lipsum'); ?><?php __('lipsum'); ?><br/>
<?php __('footer_copyright'); ?>

note: The 2nd parameter controls whether a message should be returned or echo'd
So when you are working with a template, use:

PHP Snippet:

Download code <?php __('my_name');?>
And when you are working with code, use:

PHP Snippet:

Download code <?php $name __('my_name'true);?>

Step 3: Let's build a database from our PHP and templates.


$ cd cake/app/

$ cake extract


This will recursively go through all the folders and check both your .php and .ctp files for all of those __() functions you typed. Once it's complete, you should have a nice message template file named default.pot file inside cake/app/locale/

So let's copy this message template file into the right directories for each language.

$ cd cake/app/locale/

$ cp default.pot locale/en_gb/LC_MESSAGES/default.po

$ cp default.pot locale/zh_tw/LC_MESSAGES/default.po

$ cp default.pot locale/zh_cn/LC_MESSAGES/default.po



note: at this point in time, you can freely edit the default.po files(they're just text) and start translating strings. Changes made to these files will automatically be rendered in your views.

Here are some short snippets from my default.po files.

Download code // locale/zh_cn/LC_MESSAGES/default.po
msgid "footer_copyright"
msgstr "??? © 2007. ????"

// locale/zh_tw/LC_MESSAGES/default.po
msgid "footer_copyright"
msgstr "??? © 2007. ????"

// locale/en_gb/LC_MESSAGES/default.po
msgid "footer_copyright"
msgstr "Education Bureau © 2007. All rights reserved."

Step 4: Change the default language


A fresh install of cakePHP is set to use American English, so for the rest of us: we need that changed.
Download code // config/bootstrap.php
define(DEFAULT_LANGUAGE, 'zh-tw');

Step 5: Let users change the language


Component Class:

Download code <?php 
class P28nComponent extends Object {
    var 
$components = array('Session''Cookie');

    function 
startup() {
        if (!
$this->Session->check('Config.language')) {
            
$this->change(($this->Cookie->read('lang') ? $this->Cookie->read('lang') : DEFAULT_LANGUAGE));
        }
    }

    function 
change($lang null) {
        if (!empty(
$lang)) {
            
$this->Session->write('Config.language'$lang);
            
$this->Cookie->write('lang'$langnull'+350 day'); 
        }
    }
}
?>

Thanks Nasko for pointing out that Cookie->write() does not accept timestamps

Controller Class:

Download code <?php 
class P28nController extends AppController {
    var 
$name 'P28n';
    var 
$uses null;
    var 
$components = array('P28n');

    function 
change($lang null) {
        
$this->P28n->change($lang);

        
$this->redirect($this->referer(nulltrue));
    }

    function 
shuntRequest() {
        
$this->P28n->change($this->params['lang']);

        
$args func_get_args();
        
$this->redirect("/" implode("/"$args));
    }
}
?>

Controller Class:

Download code <?php 
//app_controller.php
class AppController extends Controller {
    var 
$components = array('P28n');
}
?>

The final piece of code, are some custom routes that need to be added to cake/app/config/routes.php
Download code <?php
//route to switch locale
Router::connect('/lang/*', array('controller' => 'p28n''action' => 'change'));

//forgiving routes that allow users to change the lang of any page
Router::connect('/eng?/*', array(
    
'controller' => "p28n",
    
'action' => "shuntRequest",
    
'lang' => 'en-gb'
));

Router::connect('/zh[_-]tw/*', array(
    
'controller' => "p28n",
    
'action' => "shuntRequest",
    
'lang' => 'zh-tw'
));

Router::connect('/zh[_-]cn/*', array(
    
'controller' => "p28n",
    
'action' => "shuntRequest",
    
'lang' => 'zh-cn'
));
?>

Step 6: Links to change language


View Template:

Download code <h1><?php __('welcome_heading'); ?></h1>
<?php __('lipsum'); ?><?php __('lipsum'); ?><?php __('lipsum'); ?><br/>
<?php __('lipsum'); ?><?php __('lipsum'); ?><?php __('lipsum'); ?><br/>
<?php __('footer_copyright'); ?>

<!-- these links will change the language, but allow the user to stay on this page //-->
<?php echo $html->link($html->image('en_gb.gif'), '/lang/en-gb'nullnullfalse); ?>
<?php 
echo $html->link($html->image('zh_tw.gif'), '/lang/zh-tw'nullnullfalse); ?>
<?php 
echo $html->link($html->image('zh_cn.gif'), '/lang/zh-cn'nullnullfalse); ?>

<!-- these links will change the language, then forward the user to the /news page //-->
<?php echo $html->link($html->image('en_gb.gif'), '/en-gb/news'nullnullfalse); ?>
<?php 
echo $html->link($html->image('zh_tw.gif'), '/zh-tw/news'nullnullfalse); ?>
<?php 
echo $html->link($html->image('zh_cn.gif'), '/zh-cn/news'nullnullfalse); ?>

Step 7: All done.


Assuming I have included all the right code and not forgotten anything, you should now be fully functional^^

Further reading


A popular cross-platform GUI tool for managing .po files is poEdit http://www.poedit.net/
Gettext and cakePHP supports much more than word for word, literal translations. Check out __(), __c(), __d(), __dc(), __dcn(), __dn(), __n() http://api.cakephp.org/1.2/basics_8php.html

..and finally, remember that utf-8 is your friend. treat it well, and it'll reciprocate.

Comments 528

CakePHP team comments Author comments

Comment

1 Extremely useful

Jason, this article just couldn't have come at a more appropriate time. I've been avoiding the tackling of the i18n aspect of my current project for the past week in favor of more urgent tasks. It was only today when I started digging around for tutorial, case studies or sample code. Enter the P28n tutorial. Well organized and to the point. Thank you!

Thanks Nasko for pointing out that Cookie->write() does not accept timestamps

It's not that difficult to get confused about this IMHO. Looking at the synopsis of CookieComponent::write():

Parameters:

mixed $key

mixed $value

boolean $encrypt

string $expires public


To me the term expires suggests a fixed point in time, so it's normal for someone to initially try passing a timestamp rather than a delta. When I initially implemented your code I didn't even get uncomfortable about passing a timestamp. It was only when my cookie didn't get set to expire when it was supposed to when I started looking through the sources.

Anyways, once again thanks for the nice write-up!
posted Wed, Sep 12th 2007, 18:12 by Atanas Vasilev

Comment

2 Things newbies need to be aware of before doing this tutorial

1.) The comments in the code snippets from the po files are *NOT* legal comments. The comment delimiter for po files afaict is "#:", maybe with a required leading unix newline. Do *not* use the shown "//" at the beginning of a comment line!

2.) There is no explaination given why localisation names diverge so much, even vertically across the stack (zh_cn vs. zh-cn or en_us vs. eng or deu vs. de_de) The default application stack lacks consitency here, newbies need to be aware of that and establish their own conventions if the need arises.

3.) The tutorial mostly just offers codesnippets and hardly says where to put what - newbies will have to look up the Cake filename/classdirectory convetions. As this tutorial is for 1.2 but the manual only covers 1.1 (as of now Sebtember 2007) there are a few differences to expect.

4.) Everything under /app/view/ (templates) uses '.ctp' as ending as of CakePHP version 1.2. Trying to figure out CakePHP templating standards using this tutorial and the manual can be confusing, keep the above in mind.

5.) The 'en_gb.gif' files (presumably images of flags)
don't exist within the framework and it isn't said where they belong. I presume studying the class(es) used to instance the $html object or studying the manual may bring some insight here.

6.) Bottom line: This is NOT a tutorial for people who aren't safe in navigating around in unknow application frameworks and learning their methodologies. Nor is it for those who are easyly confused by object oriented concepts. The scarcity of detail information in this tutorial can only be met by doing own further research on CakePHP! ... However it does give a nice strategy for dealing with persistant internationalisation for those willing to dive into the details to get it working for their application.
posted Fri, Sep 21st 2007, 10:43 by Phillip

Comment

3 dynamic content

This looks very nice, but this is just static content, how would one go about saving dynamic content in different languages ?
posted Fri, Sep 21st 2007, 14:10 by chris

Comment

4 HowTo set the language

After reading this tutorial I was a bit confused where the language was actually changed, as it's a bit hidden in the code.

As far as I can see there are 2 possiblities to change the language:


<?php
Configure
::write('Config.language''fre');
?>

This will change the language just for the current page.


<?php 
$this
->Session->write('Config.language''eng'); 
?>

This will change the language for the current user. This setting has a higher priority than Configure::write(), so changing the language in this way would overwrite the code posted above.
posted Sun, Oct 7th 2007, 11:39 by Florian S.

Comment

5 Routes suggestions

Hi,
The routing can make some problems with some robots as Googlebot since when you change language, you get a redirect to the exact same page as you had but in another language ( transparent for the user). Since google will see that you try to redirect to the same page that he already indexed the content, he won't charge page, and he won't index the other language content.
I would strongly suggest that instead of using the route:
Router::connect('/eng?/*', array(
'controller' => "p28n",
'action' => "shuntRequest",
'lang' => 'en-gb'
));
you use something like :
$Route->connect('/en-gb/:controller/:action/*', array('lang' => 'en-gb'));
and inside your app controler have this function:

function beforeFilter(){
if(isset($this->params['lang'])){
$this->Session->write('Config.language', $this->params['lang']);
}
}
Like this, google will interprete this as a nother page and will index the content.

posted Sun, Oct 7th 2007, 15:59 by Charles GUimont

Comment

6 poedit

Is there any way to use something like poedit to make life a little easier with this scheme?
posted Tue, Oct 9th 2007, 20:41 by Lindsey Simon

Comment

7 Search indexing problems

Correct me if I'm wrong, but I don't think that Googlebot (or most other crawlers) understands cookies... and as such it won't understand sessions. So in fact, all it sees is the page with default language in that case. There should also be a handler in the pages controller to parse language code in the URL and show correct version of the page with all links pointing to pages with the right localization.
posted Wed, Oct 10th 2007, 09:09 by Artem Gluvchynskyj

Comment

8 Search indexing problems

Yes, it won't read cookies from what I know. But ne thiung is that using the session to pass the language to the page can be a good idea and the system is able to read the entire page. After that, you may ajust your link to keep the language in the url??
Maybe modifying the html->link function could be an idea.
Also, what I did is that I published a sitemap with all languages link, like this google will treat them as different pages and will index it's content!


posted Wed, Oct 10th 2007, 09:15 by Charles GUimont

Comment

9 Search indexing problems

Of course in terms of usability it is a good idea to pass the locale through session. Sitemap will help to keep pages in the index but I doubt that pages with non-default locales will receive much link weight especially from external pages. So there should be some kind of URL-based handler in order to keep the site optimized.
posted Wed, Oct 10th 2007, 09:28 by Artem Gluvchynskyj

Comment

10 Echoing question regarding dynamic content

I'd just like to echo chris' comment above regarding dynamic content. Wondering if there is anything in the pipeline for us to look forward to pulling translated labels from a DB?
posted Mon, Nov 5th 2007, 17:08 by Jeff Smith

Question

11 Why I cam not use cake extract

Hi everyone,Why I can not use the follow script:
$ cake extract
The error is "-bash: cake: command not found"
Can someone help me.thanks so much.
posted Wed, Nov 28th 2007, 04:02 by andy

Comment

12 cake shell

Hi everyone,Why I can not use the follow script:
$ cake extract
The error is "-bash: cake: command not found"
Can someone help me.thanks so much.


On linux and cake 1.2 its:
$ ./cake i18n

I think in 1.1 was:
$ ./cake extract

Hope it helps
posted Thu, Dec 13th 2007, 06:55 by Mariano Guezuraga

Question

13 windows support

bash commands:
$ ./cake i18n
or
$ ./cake extract

how to run those commands under Windows?
posted Wed, Dec 19th 2007, 13:49 by Artem K

Question

14 doesnt work

Hi everything goes ok and it runs without error, but locale is not changed when i click on lang/xx links. Looking at the code, i see it writes new setting to cookie and session, but where does it set the new value for the default_language?
posted Thu, Jan 3rd 2008, 09:53 by Kiril Zvezda

Comment

15 doesnt work for me either (FIXED)

My problem was that I did not change the name of default.pot to default.po Now it works fine.

Thanks for the great tutorial.
posted Thu, Jan 10th 2008, 11:00 by mike stivaktakis

Comment

16 search indexing problems and database data

Search indexing problem can easily been solved by adding virtal directories and url rewritng transforming any /en/ into an ending ?lang=en and overriding the $html->link so it outputs any link with a begining /en/ if en is the current language or implementing a helper that does the same job without touching to $html->link.
but i really wonder after reading this article how you do to translate your database data ? it seems it is available and i have that impression listening to the dedicated show though my poor english doesn't allow me to get the how-to...
i only need a kick-off here: what's the beginning of it ? how do i start to be able to translate the data in my tables ?
anyway thanks for the great job !!! cakephp rocks.
posted Fri, Jan 18th 2008, 04:48 by Raphaele Giordan

Comment

17 cookie and l10n.php problem

I am using Beta: 1.2.0.6311.

Problem 1.

In cake/lib/l10n.php, line 376 (caused by the fix: https://trac.cakephp.org/ticket/3382), it has:

$this->languagePath[2] = $this->__l10nCatalog[$this->__l10nMap[$this->default]]['localeFallback'];

because zh-tw is not defined in the __l10nMap, it causes problem. In fact, because the __l10nMap table does not have the en_us record. It will fail with all the sub locale as well.

Problem 2.

I put the following in app_controller.php:
var $components = array('P28n');

the UI output:
warning: Cannot modify header information - headers already sent by (output started at C:cygwinhometerencesrcbuzitrunksrccakephpappcontrollerscomponentsp28n.php:20) [COREcakelibscontrollercomponentscookie.php, line 384]

Code | Context

$name = "[lang]"
$value = "en"


Warning: implode() [function.implode]: Invalid arguments passed in C:cygwinhometerencesrcbuzitrunksrccakephpcakelibsdebugger.php on line 497

setcookie - [internal], line ??
CookieComponent::__write() - COREcakelibscontrollercomponentscookie.php, line 384
CookieComponent::write() - COREcakelibscontrollercomponentscookie.php, line 233
P28nComponent::change() - APPcontrollerscomponentsp28n.php, line 15
P28nComponent::startup() - APPcontrollerscomponentsp28n.php, line 8
Dispatcher::start() - COREcakedispatcher.php, line 319
Dispatcher::dispatch() - COREcakedispatcher.php, line 226
[main] - APPwebrootindex.php, line 84

look like something is output some thing before it can put the cookie in the header...

posted Fri, Feb 8th 2008, 12:33 by Terence Kwan

Bug

18 DEFAULT LANGUAGE

It works fine for me replacing in bootstrap.php:
define(DEFAULT_LANGUAGE, 'xxxx');
by
define("DEFAULT_LANGUAGE", 'xxxx');
posted Tue, Feb 26th 2008, 04:32 by Clement

Comment

19 Avoid undefined property bug

Model Class:

<?php 
class P28nController extends AppController {
    var 
$uses null;
?>


I highly recommend applying an emtpy array instead of NULL.

;)
posted Sun, Mar 2nd 2008, 12:58 by Felix

Comment

20 Caching is a real problem when writing and checking translations

I had a lot of problems trying to get this to work until I switched off all caching in Cake. i18n caches the whole .po-file and doesn't update very often.

I would love to have a more specific way to do this but when writing localizations and checking them in the browser uncomment this line in config/core.php

Configure::write('Cache.disable', true);


posted Fri, Mar 14th 2008, 10:35 by Martin Westin

Comment

21 doesnt work

The localized string like __('lorem') in Controller:: beforeFilter() will make language switch malfunction.
posted Thu, Mar 27th 2008, 03:07 by Rostislav Palivoda

Comment

22 warning Cannot modify header information

Check out if you have a space after the closing ?> tag.


...
Problem 2.

I put the following in app_controller.php:
var $components = array('P28n');

the UI output:
warning: Cannot modify header information - headers already sent by (output started at ...)

posted Mon, May 5th 2008, 18:16 by Cristina

Login to Submit a Comment