Internationalizing a CakePHP application can be tricky when it comes to deal with i18n urls. We will see in this article how the Custom route classes introduced by CakePHP 1.3 could be used to add the current language to your urls in a few lines of code.
EDIT: This proof of concept has now been improved and a better version of the code below can be found in CakeDC's I18n plugin on Github
Requirements
This article will not go too deep in internationalizing an application as many resources already exist about it. We suppose the following:
- Your application defines the current language on given the language code passed in the url
- The available languages are configured via Configure::write('Config.languages', array('eng', 'fre', 'deu'));
- You use the CakePHP array syntax for defining urls:
- $this->Html->link('link', array('controller' => 'posts', 'action' => 'view', $post['Post']['id']));
- $this->redirect(array('controller' => 'posts', 'action' => 'index'));
- Router::url(array('controller' => 'posts', 'action' => 'index'), true);
Custom routes were already introduced by Mark Story on his blog, so we will not do it again here... before continuing be sure you have read "Using custom Route classes in CakePHP"
Show me some code!
I18nRoute
As I said (or not), routes are probably the best place for customizing your urls and add information in them... much more better at least than overriding the Helper::url() method in an AppHelper class!
Custom routes introduced a way to customize how routes are processed in a very easy and powerful way (i.e ~20 lines of code). It is a bit like wrapping the Router class in CakePHP 1.2, a good example of this was the CroogoRouter.
First, we are going to create an I18nRoute class extending CakeRoute in the "/libs/routes/i18n_route.php" file. Here is its code:
<?php class I18nRoute extends CakeRoute { /** * Constructor for a Route * Add a regex condition on the lang param to be sure it matches the available langs * * @param string $template Template string with parameter placeholders * @param array $defaults Array of defaults for the route. * @param string $params Array of parameters and additional options for the Route * @return void * @access public */ public function __construct($template, $defaults = array(), $options = array()) { $options = array_merge((array)$options, array( 'lang' => join('|', Configure::read('Config.languages')) )); parent::__construct($template, $defaults, $options); } /** * Attempt to match a url array. If the url matches the route parameters + settings, then * return a generated string url. If the url doesn't match the route parameters false will be returned. * This method handles the reverse routing or conversion of url arrays into string urls. * * @param array $url An array of parameters to check matching with. * @return mixed Either a string url for the parameters if they match or false. * @access public */ public function match($url) { if (empty($url['lang'])) { $url['lang'] = Configure::read('Config.language'); } return parent::match($url); } }
The most important part of the code is in the "match()" method. We just add the current language to the url "lang" named param if it was not set. The constructor was also overriden to add a regex pattern for the "lang" param. Thus, only lang prefixes defined in your list of available languages will be parsed by the route.
Define your routes
It is now time to use this custom route in your application. Here is how the default route for pages could be defined in "/config/routes.php":
App::import('Lib', 'routes/I18nRoute'); Router::connect('/:lang/pages/*', array('controller' => 'pages', 'action' => 'display'), array('routeClass' => 'I18nRoute'));
- import the library file containing the custom route
- add a ":lang" param in where you want the language code appear in the url
- tell the Router you want to use this custom class (third param)
Link from everywhere!
Now you won't have to worry about the language code transmitted in your urls... every generated link will contain the current language code. If you want to switch the language (for instance switching to the French version of your application), you will just have to add the "lang" param to the url array.
Here are some examples of urls which would be generated on the "/eng/posts/index" page:
$this->Html->link(__('French', true), array_merge($this->passedArgs, array('lang' => 'fre'))); // /fre/posts/index
$this->Html->link('link', array('controller' => 'posts', 'action' => 'view', $post['Post']['id'])); // /eng/posts/view/2
Disclaimer
This code is experimental and the article shows you how to use CustomRoutes to implement this basic feature. Many improvements could be added to fit your needs (no language code for the default application lang, short languages code...)
Even if the tests we made were successful, we have not used this code in production yet so there may be "real word" use cases that are not handled correctly with this solution... if you find one, please tell us in the comments!