p28n, the top to bottom persistent internationalization tutorial.
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:
To find your language code(s) refer to http://api.cakephp.org/1.2/l10n_8php-source.html#l00180
So when you are working with a template, use:
So let's copy this message template file into the right directories for each language.
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.
..and finally, remember that utf-8 is your friend. treat it well, and it'll reciprocate.
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:
- display multiple languages
- allow users to switch languages
- store language settings in cookies, so returning visitors don't need to re-select their preferred language
- British English(en-gb)
- Simplified Chinese(zh-cn)
- Traditional Chinese(zh-tw)
Step 1: Setup the directories for your messages
This will create the minimum folders required for each language our site needs to support.$ 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
To find your language code(s) refer to http://api.cakephp.org/1.2/l10n_8php-source.html#l00180
//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:
<?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'dSo when you are working with a template, use:
PHP Snippet:
<?php __('my_name');?>
And when you are working with code, use: PHP Snippet:
<?php $name = __('my_name', true);?>
Step 3: Let's build a database from our PHP and templates.
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/$ cd cake/app/
$ cake extract
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.
// 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.
// config/bootstrap.php
define(DEFAULT_LANGUAGE, 'zh-tw');
Step 5: Let users change the language
Component Class:
<?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', $lang, null, '+350 day');
}
}
}
?>
Thanks Nasko for pointing out that Cookie->write() does not accept timestamps
Controller Class:
<?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(null, true));
}
function shuntRequest() {
$this->P28n->change($this->params['lang']);
$args = func_get_args();
$this->redirect("/" . implode("/", $args));
}
}
?>
Controller Class:
<?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
<?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:
<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', null, null, false); ?>
<?php echo $html->link($html->image('zh_tw.gif'), '/lang/zh-tw', null, null, false); ?>
<?php echo $html->link($html->image('zh_cn.gif'), '/lang/zh-cn', null, null, false); ?>
<!-- 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', null, null, false); ?>
<?php echo $html->link($html->image('zh_tw.gif'), '/zh-tw/news', null, null, false); ?>
<?php echo $html->link($html->image('zh_cn.gif'), '/zh-cn/news', null, null, false); ?>
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.

Anyone succeeded?
thanks in advance
class P28nComponent extends Object {
I did
class P28nComponent extends Component {
and then some inherited methods were reflectively invoked which spoiled the url.
I tried to use this code, but If do something like
http://localhost/mycakeapp/lang/eng
I get a cyclic link error
If I try
http://localhost/mycakeapp/eng/mycontroller/myaction
I get
Undefined index: lang [APP/controllers/p28n_controller.php, line 20]
has anyone succeeded in using this code please?
When I use them at msgstr, even if the file is with UTF-8 encoding, that does not work...
Solutions?
Any help would be appreciated.
Nevermind... just needed the command line to extract and poedit to update.
However I am doing this in a more essential level
http://www.the-di-lab.com/?p=72
May I ask you one little question?
I have defalut.po, view-1.po and view-2.po files.
There are some common translations in default.po
view-1.po and view-2.po have some conflicting translations, I have to separate them.
Now I use function _d() in my programs for every i18n design.
I hope view-1.po and view-2.po just do their special translations.
How can view-1.po or view-2.po include default.po for the common items?
Thank you very much.
app/app_controller.php :
---
class AppController extends Controller {
...
var $domain = 'default';
...
function beforeFilter() {
...
$this->domain = 'default';
}
function beforeRender(){
...
$this->set('domain', $this->domain);
}
}
In app/controllers/xxx_controller.php :
class XxxController extends AppController {
function yyy($index = 0) {
...
$this->set('domain', '-views-xxxs-yyy');
...
}
}
In app/views/xxxs/yyy.ctp :
...
__d($domain, "SENTENCE_TO_TRANSLATE");
...
In app/locale/eng/LC_MESSAGES/-views-xxxs-yyy.po :
...
msgid "SENTENCE_TO_TRANSLATE"
msgstr "sentence translated"
...
In app/locale/fre/LC_MESSAGES/-views-xxxs-yyy.po :
...
msgid "SENTENCE_TO_TRANSLATE"
msgstr "phrase traduite"
...
-controllers-news_controller.pot
-views-news-add.pot
-views-news-edit.pot
(And so on for each News action)
All of which you have to rename to .po
In /app/app_controller.php, add these lines (or modify your existing methods accordingly):
Just remember that if you have controller-specific beforeFilter and beforeRender you need to add the parent::beforeFilter and parent::parent::beforeController somewhere in them.function beforeFilter(){
//Sets the domain to the current controller automatically
$this->domain = '-controllers-' . low($this->name) . '_controller';
}
function beforeRender(){
//Sets the domain to the current view for the current controller automatically
$this->set('domain', '-views-' . low($this->name) . '-' . $this->action);
}
class P28nController extends AppController {
var $name = 'P28n';
var $uses = null;
var $components = array('P28n');
function change($lang = null) {
$this->P28n->change($lang);
$url = $this->referer(null, true);
switch ($lang)
{
case 'bul':
$url = preg_replace('/\/[bg|en]{2}\//', '/bg/', $url, 1);
break;
case 'eng':
$url = preg_replace('/\/[bg|en]{2}\//', '/en/', $url, 1);
break;
}
$this->redirect($url, null, true);
}
function shuntRequest() {
$this->P28n->change($this->params['lang']);
$args = func_get_args();
$this->redirect("/" . implode("/", $args), null, true);
}
}
But I'm wondering how can I modify my links easily around the whole my site? Now I use the regular HTML helper:
$html->link('title', array('controller' => 'some', 'action' => 'doThat'));// config/bootstrap.php
define("DEFAULT_LANGUAGE", 'en-ca');
It should use the three letter fallback language code, like
// config/bootstrap.php
define("DEFAULT_LANGUAGE", 'eng');
However, it would be nice to be able to use a .po file by view. It would be more optimized and clearer.
How do it? An idea?
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);
Model Class:
<?phpclass P28nController extends AppController {
var $uses = null;
?>
I highly recommend applying an emtpy array instead of NULL.
;)
define(DEFAULT_LANGUAGE, 'xxxx');
by
define("DEFAULT_LANGUAGE", 'xxxx');
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:\cygwin\home\terence\src\buzi\trunk\src\cakephp\app\controllers\components\p28n.php:20) [CORE\cake\libs\controller\components\cookie.php, line 384]
Code | Context
$name = "[lang]"
$value = "en"
Warning: implode() [function.implode]: Invalid arguments passed in C:\cygwin\home\terence\src\buzi\trunk\src\cakephp\cake\libs\debugger.php on line 497
setcookie - [internal], line ??
CookieComponent::__write() - CORE\cake\libs\controller\components\cookie.php, line 384
CookieComponent::write() - CORE\cake\libs\controller\components\cookie.php, line 233
P28nComponent::change() - APP\controllers\components\p28n.php, line 15
P28nComponent::startup() - APP\controllers\components\p28n.php, line 8
Dispatcher::start() - CORE\cake\dispatcher.php, line 319
Dispatcher::dispatch() - CORE\cake\dispatcher.php, line 226
[main] - APP\webroot\index.php, line 84
look like something is output some thing before it can put the cookie in the header...
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.
Thanks for the great tutorial.
$ ./cake i18n
or
$ ./cake extract
how to run those commands under Windows?
$ 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
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!
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.
As far as I can see there are 2 possiblities to change the language:
This will change the language just for the current page.<?php
Configure::write('Config.language', 'fre');
?>
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.<?php
$this->Session->write('Config.language', 'eng');
?>
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.
It's not that difficult to get confused about this IMHO. Looking at the synopsis of CookieComponent::write():
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!