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

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 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 ;)

Changing model’s table from the controller

For one reason, or another you might wish to change your Model’s table on the fly…

It would seem that it should be quite easy to do with:


$this->Company->useTable = 'another_company_table';

…but it’s not going to work…

Instead, in your controller, use:


$this->Company->table = 'another_company_table';

I haven’t checked to see why this works the way it does, so if someone has an explanation, I’ll gladly update the post with your insight.

P.S. Phally pointed out that it is better is use the wrapper for setting the table variable (always better than setting vars directly), like so: $this->Company->setSource('another_company_table');

Get yourself a new home (alternative to home.ctp)

I’m sure you know that to modify your application’s homepage, one needs to edit/create the home.ctp file in app/views/pages/home.ctp.
That, however, leaves you dealing with a more or less static page…

One common option to add some other functionality to your otherwise static pages is to copy the Pages Controller from the core into your app, and make some required modifications.

On the other hand, you don’t have to be stuck with home.ctp as your default main page. All you need to do is simply designate a new root (/) route.

Let’s look at an example…

I have a Markets Controller (markets_controller.php), which has a nice action called summary(). It pulls data from various Market model methods and utilizes some features of the App Controller.
All of that is working quite nicely and in reality I’d prefer that page (summary) to be my new “home”.

So, rather than dealing with Pages Controller hackery, I simply replace the default route (in app/config/routes.php) with:

Router::connect('/', array('controller' => 'markets', 'action' => 'summary'));

That’s it, now I’ve got a new fully dynamic homepage without any pain whatsoever.

A little something about the Form Helper…

Just a quick pointer about the form helper usage…

If you don’t like the default output of $form->input();

echo $form->input('SomeModel.some_field');

//which produces:
<div class="input text">
   <label for="SomeModelSomeField">Some Field</label>
   <input name="data&#91;SomeModel&#93;&#91;some_field&#93;" type="text" value="" id="SomeModelSomeField" />
</div>

Mainly the div’s, it produces, you can of course turn them off by using:

echo $form->input('SomeModel.some_field', array('div'=>false));

//which produces:
<label for="SomeModelSomeField">Some Field</label>
<input name="data&#91;SomeModel&#93;&#91;some_field&#93;" type="text" value="" id="SomeModelSomeField" />

But you can also specify an alternative wrapper tag:

echo $form->input('SomeModel.some_field', array('div'=>array('tag'=>'li')));

//which produces:
<li class="input text">
   <label for="SomeModelSomeField">Some Field</label>
   <input name="data&#91;SomeModel&#93;&#91;some_field&#93;" type="text" value="" id="SomeModelSomeField" />
</li>

P.S I believe it was Mark Story, who’ve pointed this trick out to me. So thank him! ;)

Clearing up some confusion regarding the Security component

In the previous post, I’ve made a little “mistake” (if you wish to call it that) in the way I’ve setup the Security component…

So, here I’d like to shed some light on the way things really work.

This is the code I’ve been using in the past and…, I guess, didn’t fully investigate what exactly happens, when such setup is used:


class UsersController extends AppController {  

   var $name = 'Users';  
   var $components = array('Security');  
   
   function beforeFilter() {
        $this->Security->requireAuth('add');
   }
   
   //the rest of your controller code....
  //....
}  

First, Tarique Sani pointed out that $this->Security->requireAuth('add');, is not really necessary to make the Security component produce the hash and verify against the one sent with the form data.
So we can really easily protect our forms by just including: var $components = array('Security'); and nothing else.

After that, Nate explained that “adding $this->Security->requireAuth(’add’); adds a different type of form security. By default (without calling any methods) the Security component will make forms generate a hash to ensure that they haven’t been tampered with. Adding requireAuth(), on the other hand, writes a random hash to the session, which also gets written into the form. On POST, these hashes are compared. This protects the form from CSRF attacks, and is the only type of protection that interferes with Ajax or multiple tabs.”

The issue with forms not working with multiple tabs (or AJAX calls) was brought up by Reen and Jonah, and while I thought it was a nice, extra security feature, it is understandable that for some people it might be a drawback.

Well, now we’re all, hopefully, on the same page… and once again Nate and cake save the day :)

Make your CakePHP forms a lot more secure

Update: 11/06/2008
Tarique Sani pointed out that I had an extra line of code, which wasn’t necessary to make all this work (perhaps an old habit, but the post has been modified to reflect the change).
———————

My recent post started up some good conversations and I figured that a good follow-up would be an example of how to use the Security component to make your forms much more… secure.

We’ll start with a basic usage and then expand a little…

First let’s assume a basic model:

class User extends AppModel {
	
	var $name = 'User';
	
	var $validate = array(
	    'name'=>array('rule'=>'notEmpty'),
	    'email'=>array('rule'=>'email'),
	    'password'=>array(	            
	            'Cannot be empty' => array('rule'=>'notEmpty'),
	    	    'Must be at least 4 chars' => array('rule'=>array('minLength', 4))
	    )
	);		
}

We’ve got some basic validation rules defined (and note, there is no 'required'=>true, which was the point of confusion and problems for some people).

Now we’ll assume that we have an evil user, who wants to tamper with our form fields and bypass the ‘name’ field, for example, to save blank data into the DB. If we leave our form unsecured, it is very easy to remove the field from the data array (by using some basic hacking tool) and since it won’t be present in the array, the validation will be skipped and the form data will be saved with a blank name.

So how can we avoid this problem, by using the Security component?

Let’s build our controller:

class UsersController extends AppController {

    var $name = 'Users';
    var $components = array('Security');
        
    function add() {
        if(!empty($this->data)) {
            $this->User->save($this->data);
        }
    }
}

And a basic view for the add action:

echo $form->create();
echo $form->inputs(array('name', 'email', 'password'));
echo $form->end('Register');

Pretty simple, right?
We’ve made our form quite secure with only one “extra” line of code:
var $components = array('Security');

Let me explain exactly what happens behind the scenes…

The Security component will create a hash based on the form fields produced by our Form Helper. If someone tampers with the form fields (by adding or removing or changing any field), the hash is not going to match with the expected one and the add() action will fail.

Yep, it’s that simple. You are welcome to try to mess around with the form by using your favorite POST-modifier tool (maybe: https://addons.mozilla.org/en-US/firefox/addon/1290, thanks to Jonah for providing the link).

You’ll notice that the action will fail without any error message (you’ll just get a blank screen in most cases). In my opinion, that’s just fine for any evil user… why make the app user-friendly for them?

Well, let’s be nice and make it a bit more user-friendly. It’s a good exercise, if anything…

First, we’ll extend our controller a little by creating a beforeFilter() method, let’s add one more line of code to it:

  function beforeFilter() {
        $this->Security->blackHoleCallback = 'fail';        
  }

The $this->Security->blackHoleCallback = 'fail'; tells the Security component to call our custom fail() method, in case the action gets black-holed, which it will if someone tampers with the form.

So let’s create the fail() method:

 function fail() {
        $this->cakeError('youSuck');
    }

Alright, as you can see this method will in turn trigger the youSuck() error method, which we’ll need to build in our custom app_error.php handler:

So, if you don’t have one, create app_error.php in your /app/ root directory (this is where you define custom error methods, or override existing ones).

class AppError extends ErrorHandler {
    function youSuck() {
        $this->controller->set(array(
             'name' => __('You are an evil person', true)
         ));
        
        $this->__outputMessage('bad_user');
    }
}

And now we need a new view bad_user.ctp, which is placed in /app/views/errors/bad_user.ctp

<h2><?php echo $name; ?></h2>
<p class="error">
	<strong><?php __('Error'); ?>: </strong>
	<?php  __('Do not try that again!'); ?>
</p>

Now, when someone tampers with the form, they’ll get a nice error page suggesting what we really think of hackers.

Hopefully you can see how Security component can make your app a lot more secure with just 2-3 extra lines of code. And at the same time we’ve covered a little example of how to best handle custom errors.

P.S. You can make your form even more secure by supplying additional params to save().

‘required’=>true… the source of major confusion

In all the time I’ve been following cake on IRC (almost daily), it has become very clear that other than ACL and maybe Auth, ‘required’=>true in the model validation is the most confusing part of CakePHP for many people.

On IRC it comes up, probably, once a week… even from people who’ve “been around”.
And I can imagine that there are a lot of apps out there, where ‘required’=>true is used, without fully understanding as to what it really does.

The problem is that people are quick to assume that ‘required’ means ‘notEmpty’.
Even after pointing people to RTFM, they still come back confused as to the real meaning of ‘required’.

Who cares, right?

Well… we often hear a complain about save() failing (very often) without any apparent reason.
The issue boils down to people having ‘required’=>true in the validation rules, while in reality they only wanted ‘rule’=>’notEmpty’.

When first creating the Model such setup has no ill effects, because all fields are in the data array and the record can be successfully added and even edited (assuming some basic forms).
Then at some point the app becomes a little more complex and people decide to save only a few fields at a time… and of course the validation fails.

This leads to debauchery on IRC, dents in the wall from banging the head, and other nasty side-effects.

My proposal… rename ‘required’ to something else. Doesn’t matter what, just not ‘required’.

The reality of the situation is that, if the key was named ‘mustBeInDataSet’, people would never even bother to include it, unless it was really needed… and lots of mess and confusion would be avoided.

Looking at the code, it doesn’t seem like a major change… although I’ve not done any testing (but I promise that I will).
Just wanted to get some feedback on this issue first….