I18n / localised routes in Yii

  1. Task Definition
  2. This How-To assumes the following
  3. Identifying the problem
  4. Solution
  5. In the end

Task Definition

So, you need localised urls made simple that follow the Yii framework guidelines?

  1. http://example.com/terms-and-conditions (english)
  2. http://example.com/impressum (german)
  3. http://example.com/mentions-legales (french)

It's actually pretty simple to do!

This How-To assumes the following

  1. You know how to use Yii framework
  2. You know how to use the translations generator
  3. You know how to use the Yii::t(...) function

Identifying the problem

The simplest solution to just translate them in the config UrlManager component right?

'urlManager' => array(
    'urlFormat' => 'path',
    'showScriptName' => false,
    'caseSensitive' => true,
    'rules'=>array(
        Yii::t('routes', '/terms-and-conditions') => '/site/index/termsAndConditions',
        // ..... more routing here....
        // .... and the default MVC routing rules here...
    ),
),

This way you will even get your routes translation file auto updated when you generate translations via the Yii cli script!

It sounds good doesn't it?!

However, this will NOT work because your translator is initialized after your configuration array is evaluated!

Solution

Bootstrap your Yii (as recommended by Qiang Xue)

Create a file Yii.php in the protected.components folder and create a class called Yii extending the YiiBase

class Yii extends YiiBase
{
}

At the moment Yii::createComponent allows you to pass in constructor arguments as extra arguments in the function. However we want to pass in extra arguments through our configuration array (we will need this for our super simplified UrlRule component later) This is perfectly fine since you can't have variable with numeric names in PHP.

Our overload looks like this (copy/paste from the createComponent source with a couple of extra lines of code).

class Yii extends YiiBase
{
    /**
     * Creates an object and initializes it based on the given configuration.
     *
     * The specified configuration can be either a string or an array.
     * If the former, the string is treated as the object type which can
     * be either the class name or {@link YiiBase::getPathOfAlias class path alias}.
     * If the latter, the 'class' element is treated as the object type,
     * and the rest of the name-value pairs in the array are used to initialize
     * the corresponding object properties.
     *
     * Any additional parameters passed to this method will be
     * passed to the constructor of the object being created.
     *
     * @param mixed $config the configuration. It can be either a string or an array.
     * @return mixed the created object
     * @throws CException if the configuration does not have a 'class' element.
     */
    public static function createComponent($config)
    {
        if(is_string($config))
        {
            $type=$config;
            $config=array();
        }
        elseif(isset($config['class']))
        {
            $type=$config['class'];
            unset($config['class']);
        }
        else
            throw new CException(Yii::t('yii','Object configuration must be an array containing a "class" element.'));

        if(!class_exists($type,false))
            $type=Yii::import($type,true);

        if(($n=func_num_args())>1)
        {
            $args=func_get_args();
            if($n===2)
                $object=new $type($args[1]);
            elseif($n===3)
                $object=new $type($args[1],$args[2]);
            elseif($n===4)
                $object=new $type($args[1],$args[2],$args[3]);
            else
            {
                unset($args[0]);
                $class=new ReflectionClass($type);
                // Note: ReflectionClass::newInstanceArgs() is available for PHP 5.1.3+
                // $object=$class->newInstanceArgs($args);
                $object=call_user_func_array(array($class,'newInstance'),$args);
            }
        }
        else
        {
            $args=array();
            foreach($config as $k => $v)
                if (is_int($k)) {
                    $args[] = $v;
                    unset($config[$k]);
                }

            $n = count($args);
            if ($n===0)
                $object=new $type;
            elseif ($n===1)
                $object = new $type($args[0]);
            elseif($n===2)
                $object = new $type($args[0], $args[1]);
            elseif($n===3)
                $object = new $type($args[0], $args[1], $args[2]);
        }

        foreach($config as $key=>$value)
            $object->$key=$value;

        return $object;
    }
}

Of course we now need to use our Bootstrapping class instead of the default empty class provided by the framework. To do this we need to go to our public folder and edit the index.php

// your $yii variable should point to our newly created bootstrapping file
$yii = dirname(__FILE__).'/protected/components/Yii.php';

Finally, in the protected.components create a new Url rule component as following

class LocalisedUrlRule extends CUrlRule {
    public function __construct($pattern, $route) {
        parent::__construct($route, Yii::t('routes', $pattern));
    }
}

In the end

If you modified everything correctly you can now write things like the following in your main.php config file

'urlManager' => array(
    'urlFormat' => 'path',
    'showScriptName' => false,
    'caseSensitive' => true,
    'rules'=>array(
        // FYI: translation here takes place just to have the script generate the translation string automatically
        array('class' => 'protected.components.LocalisedUrlRule', Yii::t('routes', '/terms-and-conditions'), '/site/termsAndConditions'),

        // ..... more routing here....
        // .... and the default MVC routing rules here...
    ),
),

The implementation is rather easy and by updating your translations you can always easily modify the localized routes (which are auto-generated) in the routes.php translations file.