7
votes

Since 2012, this post appears to be the most definitive resource on how to do localized routes in CakePHP (code copied below).

It works great, with one exception: it doesn't redirect requests that are missing the language prefix. For example, http://example.com will show the same content as http://example.com/eng (if English is the default language). And similarly, if it's not the homepage: http://example.com/foo/bar/ => http://example.com/eng/foo/bar. There is some mention of this problem in the comments, but no conclusive solution, which is what I'm looking for.

Code.

// Step 1: app/Config/routes.php
Router::connect('/:language/:controller/:action/*',
                  array(),
                  array('language' => 'eng|fra'));

Router::connect('/:language/:controller',
                  array('action' => 'index'),
                  array('language' => 'eng|fra'));  

Router::connect('/:language',
                  array('controller' => 'welcome', 'action' => 'index'),
                  array('language' => 'eng|fra'));

//Step 2: app/Config/core.php
Configure::write('Config.language', 'eng');

//Step 3: create app/View/Helper/MyHtmlHelper.php
App::uses('HtmlHelper', 'View/Helper');
class MyHtmlHelper extends HtmlHelper {
    public function url($url = null, $full = false) {
        if(!isset($url['language']) && isset($this->params['language'])) {
          $url['language'] = $this->params['language'];
        }
        return parent::url($url, $full);
   }
}

//Step 4: app/Controller/AppController.php
class AppController extends Controller {
    public $components = array('Cookie','Session');
    //set an alias for the newly created helper: Html<->MyHtml
    public $helpers = array('Html' => array('className' => 'MyHtml'));

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

    private function _setLanguage() {
    //if the cookie was previously set, and Config.language has not been set
    //write the Config.language with the value from the Cookie
        if ($this->Cookie->read('lang') && !$this->Session->check('Config.language')) {
            $this->Session->write('Config.language', $this->Cookie->read('lang'));
        } 
        //if the user clicked the language URL 
        else if (   isset($this->params['language']) && 
        ($this->params['language'] !=  $this->Session->read('Config.language'))
                ) {
            //then update the value in Session and the one in Cookie
            $this->Session->write('Config.language', $this->params['language']);
            $this->Cookie->write('lang', $this->params['language'], false, '20 days');
        }
    }

    //override redirect
    public function redirect( $url, $status = NULL, $exit = true ) {
        if (!isset($url['language']) && $this->Session->check('Config.language')) {
            $url['language'] = $this->Session->read('Config.language');
        }
        parent::redirect($url,$status,$exit);
    }
}

//add the links to the languages:
//Step 5: app/View/...
echo $this->Html->link('English', array('language'=>'eng')); 
echo $this->Html->link('Français', array('language'=>'fra')); 

UPDATE 1

I tried the suggestion by user221931, but it doesn't seem to work. Here is what I added to my routes:

/* Add default language param */
Router::redirect('/:controller/:action/*', 
                    array('language' => 'fra'), 
                    array('persist' => false) );
Router::redirect('/:controller/', 
                    array('language' => 'fra'), 
                    array('persist' => false) );
Router::redirect('/', 
                    array('controller'=>'pages', 'action'=>'display', 'language' => 'fra', 'home'), 
                    array('persist' => false) );

It seems to have no effect. The following URLs are not redirected: http:example.com/, http://example.com/controller/, http://example.com/controller/action/

UPDATE 2 As requested, here is my complete routes file:

<?php
/**
 * Routes configuration
 *
 * In this file, you set up routes to your controllers and their actions.
 * Routes are very important mechanism that allows you to freely connect
 * different URLs to chosen controllers and their actions (functions).
 *
 * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
 * @link          http://cakephp.org CakePHP(tm) Project
 * @package       app.Config
 * @since         CakePHP(tm) v 0.2.9
 * @license       http://www.opensource.org/licenses/mit-license.php MIT License
 */

#http://book.cakephp.org/2.0/en/views/json-and-xml-views.html
Router::parseExtensions('json');

/**
 * Here, we are connecting '/' (base path) to controller called 'Pages',
 * its action called 'display', and we pass a param to select the view file
 * to use (in this case, /app/View/Pages/home.ctp)...
 */
    Router::connect('/', array('controller' => 'pages', 'action' => 'display', 'home'));
/**
 * ...and connect the rest of 'Pages' controller's URLs.
 */
    Router::connect('/pages/*', array('controller' => 'pages', 'action' => 'display'));


/**
 * LOCALIZED URLs
 * See: http://colorblindprogramming.com/multiple-languages-in-a-cakephp-2-application-in-5-steps
 */
Router::connect('/:language/:controller/:action/*',
                       array(),
                       array('language' => 'eng|fra'));

Router::connect('/:language/:controller',
                   array('action' => 'index'),
                   array('language' => 'eng|fra')); 

Router::connect('/:language',
                   array('controller' => 'pages', 'action' => 'display', 'home'),
                   array('language' => 'eng|fra'));

# prevent routing conflicts with plugins...
# http://www.omaroid.com/cakephp-locale-language-routing/
// 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' => 'eng|fra','plugin' => "($loaded)"));

/* HIDE /index */
//Router::connect('/:controller/', array('action'=>'index') );
Router::connect('/:language/:controller/', array('action'=>'index') );

/**
 * Load all plugin routes. See the CakePlugin documentation on
 * how to customize the loading of plugin routes.
 */
    CakePlugin::routes();

/**
 * Load the CakePHP default routes. Only remove this if you do not want to use
 * the built-in default routes.
 */
    require CAKE . 'Config' . DS . 'routes.php';
2

2 Answers

1
votes

That is pretty simple to do from AppController.php:

const DEFAULT_LANGUAGE = 'eng';
public function beforeFilter() {
    parent::beforeFilter();
    if (empty($this->params['language'])) {
        $this->redirect(array('language' => self::DEFAULT_LANGUAGE));
    }
}
1
votes

You added the very specific language routing (if first part of url path is eng or fra) but didn't remove the default routing (Router::connect('/:controller/:action/*') so if these two words are not found the more generic route of /:controller/:action/* will match.

You can do any of the next three things:

  1. Remove the default routes which will make /:controller/:action/* and /:controller/* unroutable and noone will be able to form urls that will access the content without the language in there.
  2. Add hardcoded routes that redirect to eng i.e. /users -> /eng/users, /posts/ -> /eng/posts, etc). This might seem as a lot of work, but while pattern match is ok for development, hardcoding dramatically improves the speed of routing so you'll probably need to go that way some point in the future. The added advantage of it is that you can add a persist rule that will make the browser remember the redirect and not hit the server anymore.
  3. Add code in your AppController that will check to see if language is included in the path and if not try to guess the users language from the browser settings so to redirect to the most appropriate url or the default.

Number 1 is the easiest, cleanest and fastest but might not be appropriate if you have controllers that don't need to have language in front.

Number 2 doesn't leave you with a choice on what language you redirect to as it will be hardcoded. Depending on the app it might be perfectly ok.

Number 3 is the most elegant solution from UX perspective but is causing some slight overhead in each page load (by checking the language).