Tag Archives: cakephp localization

CakePHP URL-based language switching for i18n and l10n (internationalization and localization)

Update (08/03/2012): Dorin M. has refactored this solution for 2.x series, look for the code here. For details of what’s going on, read on below.

———–

I should preface this post by saying that it does not cover the basics of i18n and l10n so, please, first take a look at the manual or one of the helpful cake books
on how to get the basics going.

To better understand the goal and why some things were done the way they were, I’ll summarize the requirements:

  1. The app has to support two languages or more (in this case English and Russian)
  2. Default language is English
  3. The language switching is based on a URL param
  4. The URL format should be: example.com/eng/controller/action
  5. Language choice should persist in the session and a cookie

Just a note here… there are other ways to determine the language requested by the user, for example it could come from a domain name like eng.example.com or rus.example.com. Hopefully the approach outlined here will also be helpful if other methods of language switching are used in your app…

Also, worth while to mention, that having language name in the URL (as opposed to just reading it from the session or cookie) helps with SEO… I won’t bore you here with details, but basically it helps to ensure that a variation of each page, based on the language param in the URL, is properly indexed by the search engines. Thus, each indexed page can be found later in the native language of the user.

Last, but not least, CakePHP uses three letter language name abbreviation, based on this, so I figure, should be fine to use the same in the URL’s.

Alright, so looking at the URL format, instantly raises a question… how do we tack on the language name to the “front” of each URL?

Thankfully the Router accomplishes that pretty easily (in app/config/routes.php):

Router::connect('/:language/:controller/:action/*',
                       array(),
                       array('language' => '[a-z]{3}'));

It takes a ‘language’ parameter and throws it to the front of the URL, just as we need it.

Now, we need to specify a default language to use, I just add this to my app/config/core.php

Configure::write('Config.language', 'eng');

So, when someone comes to the http://example.com/users/home, the site is displayed in English by default. Then we usually see a link somewhere (with a little flag next to it :)), to switch to another language.

In cake we can make those language-switching links like this:

$html->link('Русский', array('language'=>'rus'));

Notice that we set the language param, which we’ll rely on to do our switching. Providing no other params, will simply reload the current page (in the new language) with the param tacked to the front of the URL (more about this later).

Side note, it’s not a good idea to use the __() translation function on language-switching links… If I get to the site and it’s displayed in the language I can’t understand even remotely, the only savior would be a link in my native language, which indicates that i can switch to it (and well, a little flag would help too :))

So now we actually need the code to switch the language, when a user clicks on the link, like above.

It’s best done in the App Controller, kinda like here:

    var $components = array('Session', 'Cookie');

    function beforeFilter() {
        $this->_setLanguage();
    }

    function _setLanguage() {

        if ($this->Cookie->read('lang') && !$this->Session->check('Config.language')) {
            $this->Session->write('Config.language', $this->Cookie->read('lang'));
        }
        else if (isset($this->params['language']) && ($this->params['language']
                 !=  $this->Session->read('Config.language'))) {

            $this->Session->write('Config.language', $this->params['language']);
            $this->Cookie->write('lang', $this->params['language'], false, '20 days');
        }
    }

Let’s take a look at the code quickly and consider some scenarios…

I created a separate method _setLanguage();, the reason I like doing this is that it keeps the beforeFilter() cleaner, which already has enough crap in there usually.
Secondly, it can be overridden in the child controllers, if required.

So let’s consider some user-case scenarios:

  1. The user comes to the site for the very first time

    In this case the default language is read from the core.php file, so the site is set to English

  2. The user starts clicking around the site for the very first time in his native English

    Nothing really needs to be done, so we can happily skip that part

  3. The user comes to the site and has to switch the language to Russian

    Thankfully he sees a link to do so, and clicks on it. Now we check our else if, since no cookie or session with configured language exist yet. We see that the link has a /rus/ param in the URL and it is not yet stored in the session, therefore we write the new value of the default language to the session and the cookie.

  4. The above user browses around the site, leaves, and then comes back

    The session value is still present and therefore the site is automagically translated to Russian. This is good if the user forgot or doesn’t care to use links like example.com/rus/controller/action, because even plain links like example.com/controller/action will display the site in the right language because of the session value.

  5. The above user closes the browser, goes out hunting for wild boars, and comes to the site on some other day

    Now we rely on our previously stored cookie to read in the language and ensure we don’t override anything that might be in the session already. (the first if )

  6. Now if the user decides to read the site in English

    We pretty much follow through the same steps as above.

Now the last thing we need to do is to ensure that a URL param gets automatically added to all the links on the site, if a given language is chosen. Remember, that this is important to have such links for SEO as well.

Well, we’re sure as hell not going to supply the ‘language’ param manually to each link, so let’s override the cake’s default url() method to ensure the language param is now added to all links.

We create app_helper.php in /app/ (same place for app_controller.php and app_model.php), something like this:

class AppHelper extends Helper {

   function url($url = null, $full = false) {
        if(!isset($url['language']) && isset($this->params['language'])) {
          $url['language'] = $this->params['language'];
        }

        return parent::url($url, $full);
   }

}

Basically we check if ‘language’ param is already in the URL if it is, we don’t need to worry about it.
If not, and $this->params[‘language’] is available we pre-pend the required language to the URL.
The rest of the site, and all standard links will now include that ‘language’ param at the front of the URL (again, good for SEO).

And that’s pretty much it, even though the post was a bit long-winded (and beer to you, if you’ve made through the whole thing) it is quite nice to be able to do i18n & l10n in just about 15 lines of code.

A little disclaimer: even though the code seems to work fine, it is still experimental… so if you find some problems I haven’t yet encountered, please be sure to let me know.

P.S. Here’s a sampe test view, from which you can generate your .po files (easily done with cake i18n console command, but this is a topic for another tutorial and there are plenty of them “out there”).

<?php
    __('This is only a test message');
?>

<p>
    <?php echo $html->link(__('Regular link', true), array('action'=>'test')); ?>
</p>

<p>
    <?php echo $html->link(__('Regular link two', true), array('controller'=>'users', 'action'=>'test5', 'some stuff')); ?>
</p>

<p>
    <?php echo $html->link('English', array('language'=>'eng')); ?>
</p>

<p>
    <?php echo $html->link('Русский', array('language'=>'rus')); ?>
</p>

You’d probably want to switch your language, if you are not so good with Russian ;)

m4s0n501