My Blog

CakePHP Locale and Routing

7.31.2012 | Blog, cakephp, PHP

CakePHP

When I was working on the cakephp localization ( l10n ) and internationalization ( i18n ), I came across a problem so I decided to let the world know about it if anyone had got the same issue.

What we want here is that a user can go to the localized version of the website according to the URL identified, for instance we want the website to load the English locale when we pass a URL like this http://example.tld/eng/Controller/Action

The problem is that while implementing the routes for the language detection, the plugins made a conflict with the rules in the routes file. Language routing is easy but sometimes can be tricky.

Why Use Language Routes?

Simply because its much better for the SEO.

Setting Up A Default Locale

Next we have to define a default language for the website in Bootstrap.php. I’m setting it as ‘eng’ since cakephp uses the 3 letter code

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

This will load and use default.po located in app/Locale/eng/LC_MESSAGES/ for translation. It’s called default since its the default domain.
Any call to the function __() will load the key from the loaded PO file. You can also translate from another domain using __d($domain,$key). For instance to load a city called ‘cairo’ from a domain called ‘cities’ you can call __d(‘cities’,’cairo’) which will load app/Locale/eng/LC_MESSAGES/cities.po and translate the key ‘cairo’ to the adjacent translation since we set the locale to be ‘eng’.

Language Detection From The URL

Let’s first detect the locale or language key using cakephp routes in the app/Config/routes.php.

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

This line says that match the language regular expression, the controller, the action and any parameters beyond, to overcome the default behavior that if we put the language key (eg. eng) it will try to find a controller with the same name for instance it will try to find a controller named ‘eng’ which is not what we want.

Next we need to set the captured locale/language key to load the appropriate language file (po file).
In the AppController.php we can implement a method to set the language from the url as the following

class AppController extends Controller {
    public function beforeFilter() {
    	$this->_setLanguage();
    }

    private function _setLanguage() {
    	if(isset($this->params['language']) &&
    			($this->params['language'] != $this->Cookie->read('Config.language')))
    	{
    		$this->Cookie->write('Config.language', $this->params['language']);
    		// set the application language
    		Configure::write('Config.language',$this->params['language']);
    	}elseif(!isset($this->params['language']) && $this->Cookie->read('Config.language')){
    		// set the application language
    		Configure::write('Config.language',$this->Cookie->read('Config.language'));
    	}
    }
}

Above we are storing the language in a cookie, if the language was not sent in the URL them we load it from the cookie,if the language was sent in the URL but different from the cookie value then we set the cookie value to the value passed from the URL. if none was sent, then we have got the default language set in the bootstrap as mentioned earlier.

The Problem (Router Conflicts)

This method works perfectly but what happens if you want to access a plugin?

There is no distinction between the controller name and the plugin name, they are both strings and have undetermined number of characters and nothing is unique about any of them. With a url like this: http://example.tld/eng/PluginName/ControllerName/ActionName it will match the plugin name as the controller which is not really what we want here.

The Solution

So in order to solve this issue, we need to match the plugin names so that it wont consider it as a controller. I have implemented the following code in order to solve this issue.

// make an array of loaded plugins
$loaded = CakePlugin::loaded();
array_walk($loaded, function(&$item,$key){
	$item = Inflector::underscore($item);
});
$loaded = implode('|', $loaded);

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

What are we doing here? We are getting an array of loaded plugins using the loaded() method, then we convert the Multiword plugin (CamelCase) to underscored lowercases using the Inflector::underscore() method and then implode them with the pipe generating an ORed regular expression to match the plugin key in the route. So now since we hard coded the rules dynamically in the routes so there wont be any conflicts in cakephp routing.

Hope that helps someone

Cheers!

Be Sociable, Share!

Responses

Andre
2.28.2014

the problem with thsi solution is reverse linking… it just doesnt work, how do you make $this->Html->link(…..); prin for example “/en/pages/view/1”, instead of just printing “/pages/view/1”

Gonzalo
8.13.2015

Andre: I think you must pass the language code as an argument. Something like:
Html->link(‘Some Text’,
array(
‘controller’ => ‘pages’,
‘action’ => ‘view’,
‘id’ => 1,
‘language’ => ‘en’
)
);

But you should refer to the official CakePHP documentation to be sure.

Comments