Yii 1.1: SEO-conform Multilingual URLs + Language Selector Widget (i18n)

46 followers

You have a multilingual application, and you want the URL of a page to be different for different languages, to account for SEO. The URL for the contact page for example should look like http://something.com/en/contact in english, and http://something.com/de/contact in german. This tutorial describes how to make it happen.
Note that currently selected language is always a part of the URL, and thus available to the application through $_GET. You don't have to use sessions variables to keep track of the language if you don't want to.

The code is mainly from this article in spanish, to which I added some tweaks (thanks to sebas for the link).
Tip: If you don't want/need the language to be part of the URL, take a look at this article. That method may be preferrable if SEO is not an issue for your application (e.g. backend-type applications for authorized users only), since it is simpler.

Now to the code...

1. Extend the CUrlManager

Create the file 'components/UrlManager.php' with the content:

<?php
class UrlManager extends CUrlManager
{
    public function createUrl($route,$params=array(),$ampersand='&')
    {
        if (!isset($params['language'])) {
            if (Yii::app()->user->hasState('language'))
                Yii::app()->language = Yii::app()->user->getState('language');
            else if(isset(Yii::app()->request->cookies['language']))
                Yii::app()->language = Yii::app()->request->cookies['language']->value;
            $params['language']=Yii::app()->language;
        }
        return parent::createUrl($route, $params, $ampersand);
    }
}
?>

For this approach to work, the language has to be a part of the URL. That is, $_GET['language'] has to be defined. To ensure this, we override the createUrl() function of the class. If the desired language is not present in the URL, we look if it is stored in a session variable, or in a cookie and set the application language accordingly. After that, we add the language to the parameters of the URL.
For the sake of completeness:The array $params contains the part of the URL that comes after the '?', that is the GET parameters, as key-value pairs. For http://something.com/controller/action?language=en&id=1, $params would be equal to array('language'=>'en', 'id'=>1).

2. Edit your Controller

Add the following code to 'components/Controller.php':

<?php
public function __construct($id,$module=null){
    parent::__construct($id,$module);
    // If there is a post-request, redirect the application to the provided url of the selected language 
    if(isset($_POST['language'])) {
        $lang = $_POST['language'];
        $MultilangReturnUrl = $_POST[$lang];
        $this->redirect($MultilangReturnUrl);
    }
    // Set the application language if provided by GET, session or cookie
    if(isset($_GET['language'])) {
        Yii::app()->language = $_GET['language'];
        Yii::app()->user->setState('language', $_GET['language']); 
        $cookie = new CHttpCookie('language', $_GET['language']);
        $cookie->expire = time() + (60*60*24*365); // (1 year)
        Yii::app()->request->cookies['language'] = $cookie; 
    }
    else if (Yii::app()->user->hasState('language'))
        Yii::app()->language = Yii::app()->user->getState('language');
    else if(isset(Yii::app()->request->cookies['language']))
        Yii::app()->language = Yii::app()->request->cookies['language']->value;
}
public function createMultilanguageReturnUrl($lang='en'){
    if (count($_GET)>0){
        $arr = $_GET;
        $arr['language']= $lang;
    }
    else 
        $arr = array('language'=>$lang);
    return $this->createUrl('', $arr);
}
?>

We extend the constructor method of our controller class, in which we set the application language. Since all our invidiual controller classes extend from this one, the application language will be set explicitly upon each request.
Note: If we don't set Yii::app()->language explicitly for each request, it will be equal to its default value set in the confg file. If it is not set in the config file, it will be equal to the value Yii::app()->sourceLanguage, which defaults to 'en_us'.
You can set default values for the language and sourceLanguage of your application in your config file with

'sourceLanguage'=>'en',
'language'=>'de',

3. Build a Language Selector Widget

Create the file 'components/widgets/LanguageSelector.php' with the content:

<?php
class LanguageSelector extends CWidget
{
    public function run()
    {
        $currentLang = Yii::app()->language;
        $languages = Yii::app()->params->languages;
        $this->render('languageSelector', array('currentLang' => $currentLang, 'languages'=>$languages));
    }
}
?>

Create the file 'components/widgets/views/languageSelector.php' with the content:

<div id="language-select">
<?php 
    if(sizeof($languages) < 4) {
        // Render options as links
        $lastElement = end($languages);
        foreach($languages as $key=>$lang) {
            if($key != $currentLang) {
                echo CHtml::link(
                     $lang, 
                     $this->getOwner()->createMultilanguageReturnUrl($key));
            } else echo '<b>'.$lang.'</b>';
            if($lang != $lastElement) echo ' | ';
        }
    }
    else {
        // Render options as dropDownList
        echo CHtml::form();
        foreach($languages as $key=>$lang) {
            echo CHtml::hiddenField(
                $key, 
                $this->getOwner()->createMultilanguageReturnUrl($key));
        }
        echo CHtml::dropDownList('language', $currentLang, $languages,
            array(
                'submit'=>'',
            )
        ); 
        echo CHtml::endForm();
    }
?>
</div>

If the number of available languages is smaller than four, the languages are displayed as links, separated with a '|'. Otherwise a dropDownList is generated.
Note: When this post-request is processed in Controller.php (see above), the application will be redirected to one of the urls provided via hidden fields in this form, because at that point in time, the controller id will not exist and the call to $this->createUrl('') will throw an error.

4. Put the Widget on your Site

Add the following code into the header-div in 'views/layouts/main.php'

<div  id="language-selector" style="float:right; margin:5px;">
    <?php 
        $this->widget('application.components.widgets.LanguageSelector');
    ?>
</div>

5. Edit your Config File

Apply the following changes/additions to the file 'config/main.php':

<?php
'components'=>array(
    ...
    'request'=>array(
        'enableCookieValidation'=>true,
        'enableCsrfValidation'=>true,
    ),
    'urlManager'=>array(
        'class'=>'application.components.UrlManager',
        'urlFormat'=>'path',
        'showScriptName'=>false,
        'rules'=>array(
            '<language:(de|tr|en)>/' => 'site/index',
            '<language:(de|tr|en)>/<action:(contact|login|logout)>/*' => 'site/<action>',
            '<language:(de|tr|en)>/<controller:\w+>/<id:\d+>'=>'<controller>/view',
            '<language:(de|tr|en)>/<controller:\w+>/<action:\w+>/<id:\d+>'=>'<controller>/<action>',
            '<language:(de|tr|en)>/<controller:\w+>/<action:\w+>/*'=>'<controller>/<action>',
        ),
    ),
),
'params'=>array(
    'languages'=>array('tr'=>'Türkçe', 'en'=>'English', 'de'=>'Deutsch'),
),
?>

We declare our new class 'UrlManager' as the class to be used by the urlManager component, and prefix <language:(de|tr|en)>/ to the keys of our rules array.

That's it. If something is unclear, wrong or incomplete, please let me know.

Total 20 comments

#18434 report it
mfecteau at 2014/10/28 08:21pm
for yii 2.0

This thread could help someone figure out what to do in yii2 to make this work as we wait for an update in the wiki.

#12756 report it
freshyiiuser at 2013/04/10 05:11am
file translation

good tut. i follow these steps to create a web app in english and arabic. i made a switch case in the controller to render to arindex in case if lang=arabic and index if it's english.

the page content change but not the main menu and the header. how do i change them?

#12731 report it
freshyiiuser at 2013/04/08 07:09pm
folder directories

i follow the same steps for creating a multilingual app, i tried to press a link in the app, for example the home/index page: the link was: localhost/yii/ireport/ar. i put the index.php folder under the following directory: webroot\protected\views\ar\site\index.php, i got the following error: object not found. i am beginning in using yii, can any one help me in this issue?

#11832 report it
Mike at 2013/02/06 03:09pm
Thanks

Your wiki inspired me to create an extension - even though i take a different approach there. It requires much less configuration but it has no widget included, but that would be easy to create.

Maybe you want to check it out here: http://www.yiiframework.com/extension/localeurls/

#11606 report it
Chris83 at 2013/01/22 06:54pm
Universal url rule

Here's an universal rule that can be used to rewrite urls to begin with the locale:

'<lang:([a-z]{2}(?:_[a-z]{2})?)>/<route:[\w\/]+>'=>'<route>'
#11037 report it
ItsYii at 2012/12/11 08:41am
Now working

So making a folder for en/index.php doesn't work but ar/index.php does. I guess English is default and cannot be placed in a folder? I also tried the ID en_us. Not important anyway as this is good enough. Furthermore as someone mentioned in the comments on the Yii-Guide link you supplied, category/index.php as well as category/ar/index.php must exist to work.

Thanks for the help

:)

#11035 report it
c@cba at 2012/12/11 07:26am
@ItsYii - folder placement

Hi,
in your comment further down you mention that you tried protected/en/views.
That should be protected/views/category/en.
Just emphasizing it, sou you can make sure you tried it the correct way.

Since I didn't use file translation like this, I don't know the common problems that may arise. So I cannot say where the problem lies exactly without knowing the specific error messages etc..

According to my experience with Yii it would pay off to try and make the "Yii-way", as described in the guide, work.

#11033 report it
ItsYii at 2012/12/11 07:12am
folders for views

C@cba i already did try what you mentioned. I basically made two folders inside my view. Ar and en inside say views/category/ar/index.php is the exact folder structure. That will give EXception ..... Cannot find request etc. The switch statement is all that works. If what you are saying is correct that will certainly save much time. Maybe some settings need to be done?

Thanks

#11032 report it
c@cba at 2012/12/11 06:39am
@ItsYii - Where do the views and controllers go?

Hi, according to the Yii-Guide (see topic file translation) ):
"" if the target language is zh_cn while the source language is en_us, rendering a view named edit would resulting in searching for the view file protected/views/ControllerID/zh_cn/edit.php. If the file is found, this translated version will be used for rendering; otherwise, the file protected/views/ControllerID/edit.php will be rendered instead. ""

For example for your PostsController.php you could make two folders:
views/posts/en/
views/posts/ar/
In the controller, if you say $this->render('view'), depending on the application language, Yii would automatically use the correct view file, i.e. if Yii::app()->language == 'ar', the view file under views/posts/ar/view.php would be rendered.

Did you try this already?

#11029 report it
ItsYii at 2012/12/11 04:07am
Cookies ans sessions

SO as mosity70 sugessted I made a switch statement do create different views,

$url = Yii::app()->getRequest()->requestUri;
 $match = "default";
 
$english = "(/en/|/en)";
$arabic = "(/ar/|/ar)";
if (preg_match($english, $url)){
global $match;
$match = "en";
 
 
}else if (preg_match($arabic, $url)){
global $match;
 $match = "ar";
}
 
//echo $match;
switch ($match){
 
case "en":
 
$this->render('index');
  break;
 
case "ar":
 
$this->render('arindex');
  break;
 
  default:
 
  //What to place here
 
 // $this->render('index');
}

If the user just hits mysite.com it will give them the stored cookie request, so if it's stored as ar how do I make it so it forces mysite.com/ar. So in other words how do I obtain the cookie here or how do I just remove storing cookies all together, so I don't have the problem.

Or is the a better way to have different views for each language? the last time I made an arabic/ english site I literally created two yii applications with it's models and everything. Thanks

EDIT. Never mind, I didn't realize you can obtain the language by Yii::app()->language which gives you the stored language. I guess there was no need for me to get the url after all :|

I changed my code now

$languages = Yii::app()->language;
 
switch ($languages){
 
case "en":
 
$this->render('index');
  break;
 
case "ar":
 
$this->render('arindex');
  break;
 
default:
 
$this->render('index'); // which will only be called if there's nothing stored for lagnuage
}
#11007 report it
moisty70 at 2012/12/10 05:54am
different view for each language

If you want to have a different view for each language you need to use another strategie. The easiest could be change the controller function to include a switch-case

#11004 report it
ItsYii at 2012/12/10 04:50am
Where do the views and controllers go?

@moisty70 Thanks for the reply I see, but I'm a little unclear on how to do this. My question is how do I make the index view for say de and one for en. Once I have these views where do I place them or how do I name them?

#11003 report it
moisty70 at 2012/12/10 04:45am
No need to create any folders

11002 No need to create any folders, htacces and Yii urlManager component do the job.

#11002 report it
ItsYii at 2012/12/10 04:37am
folder settings

Trying to figure where to place the folders, whichever language I choose it goes to the same page. How do I place the folders? do I make an en, de and tr folder and do I place them inside protected/en/views. That way brings errors. I'm a little confused here where is the URL looking?

Thanks

#10693 report it
MtlMike at 2012/11/15 01:53pm
Base install with Yii-user module

Hi,

** Update **

After setting up a base yii install with the user module I tried to add the rights module... no dice.

the function call createMultilanguageReturnUrl() will cause a fail when trying to activate the rights module...

Anyone experience this and have any comments... Thanks in advance.

** Update **

Thanks for a great post - helped me a lot.

With the user module in a base install I did have to change the rules above and make a small change in the createMultilanguageReturnUrl function to get this working.

Here are the rules. (I am new to this so perhaps they can be done better)

PHP
    '<language:(fr|en)>/' => 'site/index',

                '<language:(fr|en)>/<action:(contact)>/*' => 'site/<action>',

                '<language:(fr|en)>/<controller:(user)>' => '<controller>',

                '<language:(fr|en)>/<controller:\w+>/<id:\d+>'=>'<controller>/view',

                '<language:(fr|en)>/<module:(user)>/<controller:(admin|profileField)>/<id:\d+>' => '<module>/<controller>/view',

                '<language:(fr|en)>/<module:(user)>/<controller:(admin|profileField)>/<action:\w+>/<id:\d+>' => '<module>/<controller>/<action>',

                '<language:(fr|en)>/<module:(user)>/<controller:\w+>/<action:\w+>' => '<module>/<controller>/<action>',

                '<language:(fr|en)>/<controller:\w+>/<action:\w+>/<id:\d+>'=>'<controller>/<action>',

                '<language:(fr|en)>/<controller:\w+>/<action:\w+>/*'=>'<controller>/<action>',

And here is the small change to the function: not the use of $this->route

PHP
  public function createMultilanguageReturnUrl($lang='en')
        {

            if (count($_GET) > 0)
            {
                $arr = $_GET;
                $arr['language']= $lang;

            } else {

                $arr = array('language'=>$lang);

            }

            return Yii::app()->createUrl($this->route, $arr);

        }

#10270 report it
yasen at 2012/10/16 03:54pm
Yii::app()->language

Hi, nice tutorial. Been working on with it for some time and extended it to include languages from the database. However I've been thinking lately that Yii::app()->language is meant to be in format "en_us". I think it would be useful later on in i18 use, e.g. CApplication::getLocale($localeID) would expect parameter in that format.

#9865 report it
Zugluk at 2012/09/17 05:45am
Working with Yii::t() ??

Hi all, are these changes working with Yii::t(...) translation function + /component/messages/lang/translation_lang_file.php in controllers, view etc... in order to customize translation and not use only those give by yii framwork ??

Thanks a lot for sharing tuto !!

#9719 report it
Tpoxa at 2012/09/05 05:27pm
Module url structure

To support the same url structure for modules - add

'<language:(est|ru|en)>/<module:\w+>/<controller:\w+>/<action:\w+>/*' => '<module>/<controller>/<action>',
#9050 report it
moisty70 at 2012/07/16 01:24pm
default language en .htaccess

yes, thats right.

Your code helped with the urls, but now images and css are not shown (404)

my problem is that I don't know how rewritecond rules work, I'll check futher

Thanks very much

Edit:

Why images,css,etc are not 404 if I repeat the no file no dir lines?

RewriteEngine on

# if a directory or a file exists, use it directly
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d

# Redirect example.com/*/ to /en/*/
# The condition makes sure that the rule is only applied if language 
# is not part of the URL in form of e.g. "/en/"
RewriteCond %{REQUEST_URI} !^.*/(en|de|tr)/.*$
RewriteRule ^(.*)$ /en/$1 [L,R]

# if a directory or a file exists, use it directly
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d


# otherwise forward it to index.php
RewriteRule . index.php
#9044 report it
c@cba at 2012/07/16 07:55am
reply@moisty70

Do I understand this correctly: If some user follows an external link "www.example.com/contact" to your website, or enters that url manually:
1 - you want the website to appear in e.g. english (with the above approach it does, if english is set as the default language in the main config file)
2 - and additionally you want the URL in the address-bar of the browser to be "www.example.com/en/contact"

Is this right? I'm not sure, but I think this would probably best be done in the .htaccess file with a RewriteRule.

The .htaccess file would have to look something like the following (tested it locally, did work):

RewriteEngine on

# if a directory or a file exists, use it directly
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d

# Redirect example.com/*/ to /en/*/
# The condition makes sure that the rule is only applied if language 
# is not part of the URL in form of e.g. "/en/"
RewriteCond %{REQUEST_URI} !^.*/(en|de|tr)/.*$
RewriteRule ^(.*)$ /en/$1 [L,R]

# otherwise forward it to index.php
RewriteRule . index.php

See also both answers here.

Best regrads...

Leave a comment

Please to leave your comment.

Write new article