Yii 1.1: Manage (Target) Language in Multilingual Applications + A Language Selector Widget (i18n)

24 followers

In case of a multilingual application, one might consider it a reasonable approach to store the preferred language of the user in a session variable, and after that, every time a page is requested, to check this session variable and render the page in the indicated language. This tutorial shows a Yii-way of doing this.
We implement an event handler for the onBeginRequest event; as the name of the event suggests, this event handler will be called at the beginning of each request, so its a good place to check whether a language is provided (via post, session or cookie) and set the application language accordingly.
We also implement a simple Language-Selector Widget, which can render the language options as ajax-links or as a drop-down list.

See also this post for an alternative.

Why is it necessary to set the application language for every request?


--->The requested page will be shown in the target language of the application, which can be set/get via Yii::app()->language.
--->If this property is not set explicitly, Yii assumes it to be equal to the source language of the application, which can be set/get via Yii::app()->sourceLanguage and defaults to 'en_us'.
--->These properties can also be set in the main config file, like
'sourceLanguage'=>'en',
'language'=>'de',
--->But hardcoding the target language in the main config file is not an option if you have multiple target languages. Therefore we store the current language in the session, and on the beginning of each request set the target language explicitly, like
Yii::app()->language = Yii::app()->user->getState('_lang').

Now to the implementation...


'components/widgets/LanguageSelector.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));
    }
}

I set the available languages in the main config file (see below) and retrieve it here via Yii::app()->params->languages.


'components/widgets/views/languageSelector.php':

<?php echo CHtml::form(); ?>
    <div id="language-select">
        <?php 
        if(sizeof($languages) < 4) {
            $lastElement = end($languages);
            foreach($languages as $key=>$lang) {
                if($key != $currentLang) {
                    echo CHtml::ajaxLink($lang,'',
                        array(
                            'type'=>'post',
                            'data'=>'_lang='.$key.'&YII_CSRF_TOKEN='.Yii::app()->request->csrfToken,
                            'success' => 'function(data) {window.location.reload();}'
                        ),
                        array()
                    );
                } else echo '<b>'.$lang.'</b>';
                if($lang != $lastElement) echo ' | ';
            }
        }
        else {
            echo CHtml::dropDownList('_lang', $currentLang, $languages,
                array(
                    'submit' => '',
                    'csrf'=>true,
                )
            ); 
        }
        ?>
    </div>
<?php echo CHtml::endForm(); ?>

If the number of available languages is smaller than four, the languages are displayed as ajax-links, separated with a '|'. When clicked, an ajax-request of type 'post' is sent to the current URL and the page is reloaded in case of success (so it is displayed in the newly selected language). Note that I have to send the 'YII_CSRF_TOKEN' in the request, because I have CSRF Validation for cookies enabled in my config file (see below).
If the number of languages is four or more, a dropDownList is generated.
You can of course always use a dropDownList if you prefer.


'views/layouts/main.php'
Put the widget inside the <div id="header">...</div>:

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


'config/main.php'
(add the following lines to the file, don't replace its contents):

return array(
    'sourceLanguage'=>'en',
 
    // Associates a behavior-class with the onBeginRequest event.
    // By placing this within the primary array, it applies to the application as a whole
    'behaviors'=>array(
        'onBeginRequest' => array(
            'class' => 'application.components.behaviors.BeginRequest'
        ),
    ),
 
    // application components
    'components'=>array(
        'request'=>array(
            'enableCookieValidation'=>true,
            'enableCsrfValidation'=>true,
        ),
                // ...some other components here...
    ),
    // application-level parameters
    'params'=>array(
        'languages'=>array('tr'=>'Türkçe', 'en'=>'English', 'de'=>'Deutsch'),
    ),
);


'components/behaviors/BeginRequest.php'

<?php
class BeginRequest extends CBehavior {
    // The attachEventHandler() mathod attaches an event handler to an event. 
    // So: onBeginRequest, the handleBeginRequest() method will be called.
    public function attach($owner) {
        $owner->attachEventHandler('onBeginRequest', array($this, 'handleBeginRequest'));
    }
 
    public function handleBeginRequest($event) {        
        $app = Yii::app();
        $user = $app->user;
 
        if (isset($_POST['_lang']))
        {
            $app->language = $_POST['_lang'];
            $app->user->setState('_lang', $_POST['_lang']);
            $cookie = new CHttpCookie('_lang', $_POST['_lang']);
            $cookie->expire = time() + (60*60*24*365); // (1 year)
            Yii::app()->request->cookies['_lang'] = $cookie;
        }
        else if ($app->user->hasState('_lang'))
            $app->language = $app->user->getState('_lang');
        else if(isset(Yii::app()->request->cookies['_lang']))
            $app->language = Yii::app()->request->cookies['_lang']->value;
    }
}


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

Total 10 comments

#16585 report it
Lukos at 2014/03/07 05:46pm
DropDown doesn't work

If you have too many languages or choose to use the DropDown code instead of the Ajax links, it doesn't seem to work at all. I don't know how the dropdown list is supposed to POST the page.

#12799 report it
Trejder at 2013/04/12 07:57am
Small notice

This is a very great Wiki article, that actually works out-of-the-box and like a charm! Thank you so much for it! :]

If it is not working, one should check in first place, if language-specific settings in his or her application doesn't use two-code language specification (i.e. en_gb instead of just gb). As two-code is default in Yii, while this solution uses one-code.

Just a small notice, to article author. My Netbeans claims for the line $user = $app->user; in protected/components/behaviours/beginBehaviour.php that varialbe $user seems to be unsued. As well as for $event in the same function. I think, you can easily remove them.

#6313 report it
c@cba at 2011/12/28 07:36am
okay...

Yesterday, I had tried the correct version of rules too, but it still didn't work.

This morning I started with a clean slate of code (the joys of using Git), implemented the article (in the comment #6302) and changed the rules in config, and now it works!

Previously I must have had some leftover code somewhere (from my playing around), that messed things up.

Thanks again for your help!

#6310 report it
sebas at 2011/12/27 03:33pm
little correction

I was telling to replace <_c>/<_a> by <language:(en|es|pt)>/<_c>/<_a>

Sorry for my bad explanation

#6309 report it
c@cba at 2011/12/27 02:18pm
Hmm...

Didn't work. In my 'config/main.php' I put:

'rules'=>array(
     ...
     '<controller:\w+>/<id:\d+>'=>'<language:(en|tr|de)>/<controller>/view',
),

When I request for example the url
http://blabla.com/article/1
I get the error:
Unable to resolve the request "<language:(en|tr|de)>/article/view".

I wonder... In the createUrl() Method of the CUrlManager class of yii, there is the following code:

if(isset($params['#']))
{
    $anchor='#'.$params['#'];
    unset($params['#']);
}
...
        return $url.$anchor

I have the feeling, that we have to do semething similar somewhere:

if(isset($params['language']))
{
    $language=$params['language'];
    unset($params['language']);
}
...
        return $language.'/'.$url.$anchor;

and add rules to the config file like:

'rules'=>array(
     ...
     '<language:(en|tr|de)>/<controller:\w+>/<id:\d+>'=>'<controller>/view',
),

This rule says, that when the url looks like
---> en/controller/id
yii should call
---> controller/view
and pass on 'language' and 'id' as GET-parameter, right?

I played around quite a bit with this idea, but couldn't make it work... yet! I'm optimistic...
Would I need to change/extend the parseUrl() method too?

#6305 report it
sebas at 2011/12/27 09:36am
Hey

Just add the following to your urlRules:

'<_c>/<_a>' ====> '<language:(en|es|fr|pt)>/<_c>/<_a>'

#6304 report it
c@cba at 2011/12/27 09:25am
Thanks for the link!

I implemented it, and it does work, but the URLs look like
http://blabla.com/site/contact?language=de

I could not make them look like
http://blabla.com/de/site/contact

I will need this later for the public part of my application, so I'm still interested in a solution. Any ideas?

#6302 report it
sebas at 2011/12/27 08:34am
Yes... You can also see this post:
#6295 report it
c@cba at 2011/12/26 06:25pm
SEO in a roundabout way... would it work?

You're right. Do you think that the following scenario would work (in terms of SEO):
1. I let the language management for users as it is described above.
2. I configure the URL-Manager in such a way, that for example the url
http://blabla.com/de/controller/action
renders the page
http://blabla.com/controller/action
in the german language.
(See the Yii-Guide for details)
3. I let search engines know about these urls via a sitemap.

Update: I realize that this wouldn't work. There has to be a holistic solution, where parametrized URLs and links can be used throughout the application. I found bits and pieces of information (here, here and here), but nothing I could make work the way I want it to. Any hints and information in that regard would be appreciated.

For my current application, the solution in this article is ideal, since all its pages can be accessed by authorized users only and SEO is not an issue.

#6292 report it
sebas at 2011/12/26 03:29pm
nice writting but...

You don't take care about multilanguage SEO. it's always nice to have multiple URL for same pages for language optimization.

Leave a comment

Please to leave your comment.

Write new article