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:
The sites that I build typically require 3 languages:
So throughout this document, I'll be using them as my reference languages. Your site may support more or less languages.
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
By studying the sample above, we can see that directory names are actually the locale:
note: The 2nd parameter controls whether a message should be returned or echo'd
So when you are working with a template, use:
And when you are working with code, use:
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.
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
A fresh install of cakePHP is set to use American English, so for the rest of us: we need that changed.
Download code
Thanks Nasko for pointing out that Cookie->write() does not accept timestamps
The final piece of code, are some custom routes that need to be added to cake/app/config/routes.php
Download code
Assuming I have included all the right code and not forgotten anything, you should now be fully functional^^
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.
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
The sites that I build typically require 3 languages:
- British English(en-gb)
- Simplified Chinese(zh-cn)
- 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', $lang, null, '+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(null, true));
}
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', 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.
Comments
Comment
1 Extremely useful
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!
Comment
2 Things newbies need to be aware of before doing this tutorial
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.
Comment
3 dynamic content
Comment
4 HowTo set the language
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.
Comment
5 Routes suggestions
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.
Comment
6 poedit
Comment
7 Search indexing problems
Comment
8 Search indexing problems
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!
Comment
9 Search indexing problems
Comment
10 Echoing question regarding dynamic content
Question
11 Why I cam not use cake extract
$ cake extract
The error is "-bash: cake: command not found"
Can someone help me.thanks so much.
Comment
12 cake shell
On linux and cake 1.2 its:
$ ./cake i18n
I think in 1.1 was:
$ ./cake extract
Hope it helps
Question
13 windows support
$ ./cake i18n
or
$ ./cake extract
how to run those commands under Windows?
Question
14 doesnt work
Comment
15 doesnt work for me either (FIXED)
Thanks for the great tutorial.
Comment
16 search indexing problems and database data
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.
Comment
17 cookie and l10n.php problem
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...
Bug
18 DEFAULT LANGUAGE
define(DEFAULT_LANGUAGE, 'xxxx');
by
define("DEFAULT_LANGUAGE", 'xxxx');
Comment
19 Avoid undefined property bug
Model Class:
<?phpclass P28nController extends AppController {
var $uses = null;
?>
I highly recommend applying an emtpy array instead of NULL.
;)
Comment
20 Caching is a real problem when writing and checking translations
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);
Comment
21 doesnt work
Comment
22 warning Cannot modify header information