CakePHP URL-based language switching for i18n and l10n (internationalization and localization)

I should preface this post by saying that it does not cover the basics of i18n and l10n so, please, first take a look at the manual on how to get the basics going.

To better understand the goal and why some things were done the way they were, I’ll summarize the requirements:

  1. The app has to support two languages or more (in this case English and Russian)
  2. Default language is English
  3. The language switching is based on a URL param
  4. The URL format should be: example.com/eng/controller/action
  5. Language choice should persist in the session and a cookie

Just a note here… there are other ways to determine the language requested by the user, for example it could come from a domain name like eng.example.com or rus.example.com. Hopefully the approach outlined here will also be helpful if other methods of language switching are used in your app…

Also, worth while to mention, that having language name in the URL (as opposed to just reading it from the session or cookie) helps with SEO… I won’t bore you here with details, but basically it helps to ensure that a variation of each page, based on the language param in the URL, is properly indexed by the search engines. Thus, each indexed page can be found later in the native language of the user.

Last, but not least, CakePHP uses three letter language name abbreviation, based on this, so I figure, should be fine to use the same in the URL’s.

Alright, so looking at the URL format, instantly raises a question… how do we tack on the language name to the “front” of each URL?

Thankfully the Router accomplishes that pretty easily (in app/config/routes.php):


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

It takes a ‘language’ parameter and throws it to the front of the URL, just as we need it.

Now, we need to specify a default language to use, I just add this to my app/config/core.php


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

So, when someone comes to the http://example.com/users/home, the site is displayed in English by default. Then we usually see a link somewhere (with a little flag next to it :)), to switch to another language.

In cake we can make those language-switching links like this:


$html->link('Русский', array('language'=>'rus'));

Notice that we set the language param, which we’ll rely on to do our switching. Providing no other params, will simply reload the current page (in the new language) with the param tacked to the front of the URL (more about this later).

Side note, it’s not a good idea to use the __() translation function on language-switching links… If I get to the site and it’s displayed in the language I can’t understand even remotely, the only savior would be a link in my native language, which indicates that i can switch to it (and well, a little flag would help too :))

So now we actually need the code to switch the language, when a user clicks on the link, like above.

It’s best done in the App Controller, kinda like here:

    var $components = array('Session', 'Cookie'); 
    
    function beforeFilter() {
        $this->_setLanguage(); 
    } 
    
    function _setLanguage() {
                 
        if ($this->Cookie->read('lang') && !$this->Session->check('Config.language')) {           
            $this->Session->write('Config.language', $this->Cookie->read('lang'));
        }
        else if (isset($this->params['language']) && ($this->params['language'] 
                 !=  $this->Session->read('Config.language'))) {     
                
            $this->Session->write('Config.language', $this->params['language']); 
            $this->Cookie->write('lang', $this->params['language'], false, '20 days');
        }        
    } 

Let’s take a look at the code quickly and consider some scenarios…

I created a separate method _setLanguage();, the reason I like doing this is that it keeps the beforeFilter() cleaner, which already has enough crap in there usually.
Secondly, it can be overridden in the child controllers, if required.

So let’s consider some user-case scenarios:

  1. The user comes to the site for the very first time

    In this case the default language is read from the core.php file, so the site is set to English

  2. The user starts clicking around the site for the very first time in his native English

    Nothing really needs to be done, so we can happily skip that part

  3. The user comes to the site and has to switch the language to Russian

    Thankfully he sees a link to do so, and clicks on it. Now we check our else if, since no cookie or session with configured language exist yet. We see that the link has a /rus/ param in the URL and it is not yet stored in the session, therefore we write the new value of the default language to the session and the cookie.

  4. The above user browses around the site, leaves, and then comes back

    The session value is still present and therefore the site is automagically translated to Russian. This is good if the user forgot or doesn’t care to use links like example.com/rus/controller/action, because even plain links like example.com/controller/action will display the site in the right language because of the session value.

  5. The above user closes the browser, goes out hunting for wild boars, and comes to the site on some other day

    Now we rely on our previously stored cookie to read in the language and ensure we don’t override anything that might be in the session already. (the first if )

  6. Now if the user decides to read the site in English

    We pretty much follow through the same steps as above.

Now the last thing we need to do is to ensure that a URL param gets automatically added to all the links on the site, if a given language is chosen. Remember, that this is important to have such links for SEO as well.

Well, we’re sure as hell not going to supply the ‘language’ param manually to each link, so let’s override the cake’s default url() method to ensure the language param is now added to all links.

We create app_helper.php in /app/ (same place for app_controller.php and app_model.php), something like this:


class AppHelper extends Helper {

   function url($url = null, $full = false) {                     
        if(!isset($url['language']) && isset($this->params['language'])) {
          $url['language'] = $this->params['language'];
        }     
           
        return parent::url($url, $full);
   }

}

Basically we check if ‘language’ param is already in the URL if it is, we don’t need to worry about it.
If not, and $this->params['language'] is available we pre-pend the required language to the URL.
The rest of the site, and all standard links will now include that ‘language’ param at the front of the URL (again, good for SEO).

And that’s pretty much it, even though the post was a bit long-winded (and beer to you, if you’ve made through the whole thing) it is quite nice to be able to do i18n & l10n in just about 15 lines of code.

A little disclaimer: even though the code seems to work fine, it is still experimental… so if you find some problems I haven’t yet encountered, please be sure to let me know.

P.S. Here’s a sampe test view, from which you can generate your .po files (easily done with cake i18n console command, but this is a topic for another tutorial and there are plenty of them “out there”).


<?php    
    __('This is only a test message');    
?>

<p>
    <?php echo $html->link(__('Regular link', true), array('action'=>'test')); ?>
</p>
    
<p>
    <?php echo $html->link(__('Regular link two', true), array('controller'=>'users', 'action'=>'test5', 'some stuff')); ?>
</p>

<p>
    <?php echo $html->link('English', array('language'=>'eng')); ?>
</p>

<p>
    <?php echo $html->link('Русский', array('language'=>'rus')); ?>
</p>

You’d probably want to switch your language, if you are not so good with Russian ;)

144 thoughts on “CakePHP URL-based language switching for i18n and l10n (internationalization and localization)

  1. Now, ain’t this a perspective that most I18N/L10N tutorials are completely lacking on? I’ve read numerous articles about gettext in general, and i18n in the CakePHP context, but was completely lost as to how exactly I could automate the generation of links w/ corresponding language parameters. As a result I now have 2 projects, deployed with static /controller/action links, relying on the Config/Cookie setting for the language selection and with no SEO happiness at all…

    This couldn’t have come at a more convenient time. Thanks for this article, tekno! :-)

  2. I did something similar in one of my cakePHP-apps.
    The reason I switchted to URLs like http://en.domain.com instead of http://domain.com/en/ is:
    You don’t have to mess araound with AppHelper::url().
    (On the other hand you have to take special care of your cookies: set them for *.domain.com not just en.domain.com….)

  3. Hi,

    A couple of things:

    You should also override Controller::redirect in your AppController in the same fashion you override Helper::url in AppHelper. This way redirects work transparently too.

    About SEO.. In your example you will be able to reach the same page on at least two urls:
    1. domain.com/controller/action
    2. 1. domain.com/eng/controller/action

    This is not good for SEO as you might get penalized for duplicate content. What you should do is you should check in beforeFilter if supplied url language param equals the default language set in core.php, and if it does do a 301 (permanent) redirect to domain.com/controller/action (See, the language param has been removed)

    Of course, I also have an opinion on using flags for language representatives: Don’t do it! ;-)

    Flags represents countries; Not languages!

    In Canada spoken languages include French and English – What flag are you going to use? Someone will feel insulted / less comfortable whatever you choose.

  4. @Kim Biesbjerg
    I’m not quite sure about any penalties for duplicate content… I don’t have any concrete support that it’s going to have any impact on page ranking. Not having a param in the URL by default, ensures simplicity for the user, which, IMO is quite more important. Either way it’s a simple change and is really up to the requirements.

    Flags… that’s up to the user, hence, as you’ve noticed, I was more or less kidding about it :)

    @andreas
    That approach is just fine as well, but may not be available to some users. Secondly I’m not sure how well that plays with SEO.

    @Nasko
    That’s exactly why I wrote this one up, I saw lots of other tutorials that deals with the basics (some better than others), but nothing that actually showed and explained the functional aspect of this in your app.
    Well, either way… glad to hear it was helpful ;)

  5. @powtac
    Cool, you’re welcome :)

    @leo
    That was an interesting read, thanks.
    It to me it seems that we are lucky to have such an easy alternative in cake, the similar approach on his list required possibly modified dir structure on the server, which would be horrible to maintain.
    Regarding the semantics and simplicity (or as he says aesthetics), having a language URL param seems quite reasonable and clean to me. That being said, my second choice would be language specific domains, as eng.example.com, as IMO, it is quite a nice and simple option, but unfortunately not always available for simple hosts or those who don’t wish to get into DNS management.

  6. I have a very similar approach, but to avoid the problem mentioned by Kim Biesbjerg and to make url shorter for some readers I set one primary language for my app and then use urls without language param for the main language.

    For example, if my site is in English, Russian and French i will have urls like:

    – domain.com/controller/action for English
    – domain.com/fr/controller/action for French
    – domain.com/ru/controller/action for Russian

    BTW, I prefer using 2-letters code as more intuitive for most users.

  7. thanks! great post.

    as Kim said, it’s better to use sub-domains url based translation:
    1- less code
    2- better for SEO, Search engines don’t like duplication.

  8. @holooli

    Unfortunately some users cannot control the domain name to that extend (which was the case for this client). Secondly I’m not positive that it’s better for SEO, and in in all honestly it is good and consistent marketing that wins the SE rankings, SEO from the developer’s perspective can only take you so far.

  9. Nice article. Definitely an under-documented aspect of language management.

    Just a small note on language codes:
    Just as using a Canadian flag might offend a large part of the population, Using “English” is likely to offend a lot of people. That’s because English is not a language. English is not A language. There is en_gb and en_us for example with differences in spelling like colour vs. color.

    Cake supports these types of language names as well as the simpler eng and en. Using eng or en will select a default version of English if your app does not require the more detailed nuances of the language. Cake recognizes these versions of English (as an example): English (Australian), English (Belize), English (Canadian), English (British), English (Ireland), English (Jamaica), English (New Zealand), English (Trinidad), English (United States), English (South Africa)

    The same setup is of-course available for French and any other language spoken in more than one country. I guess the more slang you want to use the more pronounced the differences are likely to be.

    I just thought it was an aspect worth mentioning in this context since quite a few languages have these kinds of variations.

  10. @Martin
    Thanks for your insight. You are absolutely right about that, but taking a more simplistic approach, let’s consider some other scenarios… For example if I have a Russian flag and a US flag, it accomplishes two things. One is the availability of another language is indicated by a static icon, which could be a good thing if somehow the actual link text did not display properly (due to encoding, etc, etc,). Secondly, the US flag shows further that we are dealing with information for US-based content (rather than British or Irish) and thus, it plays a powerful, yet a simple UI distinction. So again, depending on the context and the application itself, it could be a positive design move or a completely wrong one.

  11. He Teknoid! Very good and complete article. The way you determine which language to serve to the client is pretty much like I do in a couple of projects: http://www.cake-toppings.com/2008/08/29/language-detection-and-appropriate-redirects/

    I agree that language flag in domain (fra.domain.com, nl.domain.com) is better for SEO IF (and only if) you place that domain on a server in that country (France or Netherlands) and get content from that server. Then it will count as really language specific and get some extra points in find-ability.

  12. Excellent post, thanks!

    I’m considering going with the sub-domain approach though, anyone got tips how to do that?

    Also, couldn’t HTTP Accept-Language be used to do the initial selection automatically as well? I think I read somewhere this could be done directly via Routes, but never found any more info on how to do it.

  13. @Oscar

    Thank you.

    As far as different types of language switching approaches, they are quite nicely outlined in the link posted by leo… please see above.

  14. @primeminister

    Sounds good.
    I do feel that domain named based switching is easier, but this was a better exercise and at some point we might switch to another approach for this client. That, however, becomes much simpler once the foundation is laid out.

  15. Right, figured out a pretty nice solution. prettyUrl() now returns an array with parameters fit for Router::url instead of passing them to Router::url itself. So, instead of returning a string with the url, an array is built and returned which then should be passed to Router::url, easiest via HtmlHelper::link for example. This will then automatically go through our custom url() method which will add the language parameters.

  16. Hi, I’m trying to localize the Pages controller, but I’ve a little problem when I create $html->link(‘English’, array(‘language’=>’eng’)); and I’m on http://example.com/pages/home it points to http://example.com/pages/display. It seems that passed params get lost… is it right or I’ve to change something in routes? For the moment I fixed the problem manually adding the pass params: $html->link(‘English’, array(‘language’=>’eng’,join(‘/’,$this->params[‘pass’])))
    Is there a better solution? thank yuo

  17. Hey teknoid,

    I set this up as you suggested and it seems to work well with a couple of caveats. I changed the regex for languageto just be a list of acceptable three letter languages (otherwise, the add action was being interpreted as a language. I also added a route for the index page of each other language (like /fre/ for example).

    However, I’m having trouble with the html::link helper, specifically

    $html->link(‘Français’,array(‘language’=>’fre’))

    This works unless there’s a paramater in the current url, so /posts/view/5 should link to ‘/fre/posts/view/5’ but it actually just links to ‘/fre/posts/view’ which in turn redirects.

    Theories? Solutions?

    Thanks again.

  18. Well, it’s provisionally fixed. Instead of

    $html->link(’Français’,array(’language’=>’fre’))

    I use

    $html->link(‘English’,array(‘language’=>”)+$this->params[‘pass’])

    which adds the unnamed paramaters located in params->pass (typically [0] => 3 etc) to the link. I also used

    function _setLanguage() {
    if (isset($this->params[‘language’])) {
    $this->Session->write(‘Config.language’, $this->params[‘language’]);
    } else if (isset($this->params[‘named’][‘language’])) {
    $this->params[‘language’]=$this->params[‘named’][‘language’];
    $this->Session->write(‘Config.language’, $this->params[‘language’]);
    } else {
    $this->Session->write(‘Config.language’, ‘eng’);
    }
    $this->set(‘language’,$this->Session->read(‘Config.language’));
    }

    as my set language function so I can use $language in my views if necessary (I use it to suffix sql field names where tables have multiple languages)

  19. @visskiss
    i’m with you. keeping parameter in the url while changing languages is nice. thx for sharing the code.

    @teknoid
    i use $language in the view as a css id for the flags. this way i can modify the current one so the user knows it is already “clicked”. but i don’t see why one would need it for tables suffix there too…
    btw. thanks for the article.

  20. i must say that the 3-letter language name abbreviation based on the standard iso 639-2 is no good:
    pick brazil. it is commonly abbreviated as “bra”.
    the standard points “bra” to the braj country.
    the 2-letter iso 639-1? no good either:
    in brazil one speaks portuguese. in portugal too. but that’s 2 different languages. this standard points it to “pt” – for portugal only.
    using “pt_br” like “en_us” or “en_gb” would be the solution imho, although it is not always possible.

  21. @bberg

    I’m glad you’ve found the article helpful.
    Also, you are definitely right that the given ISO abbreviation may not be applicable in some cases (for me it worked fine, so I figured to use it)… but hopefully the post shows how to use any type of URL-based language switching to base upon for your specific need.

  22. Hi,

    For redirect method I overloaded it in the App_controller.php file like this:

    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);
    }

    Although after saving session information any URL (with or without language) will work this make sure that things like $this->redirect(array(…)); will not result in a URL without language. This could annoy the user since his URL will have or not the language set after some action. To prevent this is better to have it since first time user set and not change until he sets another language.

    Regards,
    Benito.

  23. @teknoid

    Yes, yes, for me it works perfectly well too!

    I am just curious because virtually all I18n articles for CakePHP (and even CookBook) mention the need of L10n component but just set the session “Config.lang” or configure “Config.language ” do the job with no pain. I am wondering what is the difference between use or not the L10n since, as per my understand, the unique function it has is to get locale messages for a given valid locale (what, per my understand, is happening with your simple, few lines, solution).

    Regards,

  24. @Josenivaldo Benito Junior

    L10n will let you to set locale manually amongst other things, but I don’t see any need to use it, when (as you’ve mentioned) the setting can be obtained from Config and Session.

  25. thanks for this post teknoid, I found t to be very helpful.

    I just had a problem with the login form not working anymore with the Auth component, but I fixed it adding the language param to loginAction.

    $this->Auth->loginAction = array( ‘controller’=>’users’,
    ‘action’=>’login’,
    ‘language’=>$this->Session->read(‘Config.language’));

    I also overloaded the flash function in app_controller so I can keep the language param (the same Josenivaldo did with the redirect function)

    function flash($message, $url, $pause = 1) {
    if (!isset($url[‘language’]) && $this->Session->check(‘Config.language’)) {
    $url[‘language’] = $this->Session->read(‘Config.language’);
    }
    parent::flash($message,$url,$pause);
    }

    Thanks again for this wonderful post.

  26. Isn’t it easier to get the language always out the url instead of Cookies/Sessions? If no language was found, redirect to the same page, but with the language in the url. It is not that hard to do, I’ve done it myself. Takes care of duplicate content too.

  27. @Phally

    If I went to http://www.example.com, switched to my native language, it is best to store my preference in the cookie. Because when I come back the next day to http://www.example.com, there is no language param in the URL to extract the language setting from.
    The thing is the language parm may not always be available, so using session/cookie is a perfect fall-back and improves usability as well as SEO.

  28. Hi teknoid,

    thanks for this pretty code.
    it works great with my “normal” pages.

    Im trying to combine this language switching with multiple pagination (http://debuggable.com/posts/how-to-have-multiple-paginated-widgets-on-the-same-page-with-cakephp:48ad241e-b018-4532-a748-0ec74834cda3) but i go in circle…

    i always get the problem that my paginator links look like this:
    /controller/action/params/page:2/language:deu/model:Project

    and not like desired:
    /deu/controller/action/params/page:2/model:Project

    I think this depends on some function that needs to be overridden in the app_controler, possibly right? But i think the paginate function would get its url from $this->params …i am helpless :/

    hopefully,

    Robin

  29. @Robin

    Have you tried asking at the google group or IRC? I see where the problem is, but don’t have a solution off top of my head.
    I am actually using AJAX-based pagination for this project and do not seem to have a similar issue.

  30. A truly great article. All comments are also very insightful. Perhaps I should combine all these into a tutorial and post it on my blog in my native language (Persian) or even put an English translation out there for others to use. Are you OK with that ?

  31. Hey Teknoid:

    Once again you’ve come thru in a huge way!

    Running into a small issue with the routing though. Seems like I lose url parameters when I try using $html->link(‘eng’, array(‘language’ => ‘eng’))

    So if I’m on this page:
    /rus/posts/view/3

    I end up on
    /eng/posts/view

    after clicking the link above. Would be great to be able to just drop this in my default layout and not have to worry about passing those params.

    Either way, thanks again for this super clear and helpful tutorial I wish I found months ago!

    1. Argh, sorry, see others have already posted that question (thought I checked thoroughly)! Disregard my post.

      Thanks!

  32. Maybe You know, why when use admin routing cake switch me to default language, and i can’t switch to other languages?

  33. Great tutorial!
    As many other people I was having a hard time understanding the whole multilanguage thing!
    Thanks to you I finally achieved a working multilingual site.
    great work!

  34. Thank you!! Thank you!!
    Very useful tutorial (and a lot of suggestions in comments too!!!)

    But… what about POST data?
    if I load a page trough a form using POST method and then I click on the language link to switch lang i’ll get the right page with “no data to search ” message.
    Is there any trick to bring all “$this->data” back??

    Thanks again !!

  35. Great post and nice approach!
    Unfortunately I don’t get part of its to work – especially overriding the url method does not work here.

    Two things: the !isset($url[‘language’]) is never true, because when there is no language set, $url[‘language’] is not empty but ‘\’ instead

    If I then append the language with $url[‘language’] = $this->params[‘language’]; the URL is broken, e.g. started out with ‘/vouchers/index’, add language ‘eng’ -> url is ‘evouchers/index’ and not ‘/eng/vouchers/index’

    I have no clue where this comes from! Any ideas?

    Cheers,
    tobi_one

  36. Hi Teknoid,
    thanks for the article. I am getting ready to implement internationalization into my application. I am confused about one thing though. All the examples (not just yours) show that the views are written in English and wrapped with _(). Would it not be better to have a central file with all your system’s messaging?

    For instance if I need to show the string “Thanks for your submission” I would not want to have to write it out several times throughout my views. It makes it harder to make changes and keep messaging consistent.

    Do you achieve this by creating a .po file for the base language? Hmm, the more I write the more I realize that I am quite unclear on the strategy I need.

    Any advice would be appreciated.
    Thanks.
    Steve

  37. @Steven Wright

    I am not aware of global setting for something like this. Not to say that there isn’t one, I just haven’t come across it.
    Perhaps you could check with the google group or IRC.

    … or maybe someone will chime in with a response here. At any rate, it seems reasonable to have something like this available.

    Oh, and if you do find out… please be kind to share :)

    1. I think I am completely ass backwards actually. I believe what I have to do is use the language files correctly to achieve this.

  38. Hi,

    I still have the problem that by just adding $url[‘language’] the URL is broken. I used this workaround:

    $urlRoute = Router::parse($url);
    $urlRoute[‘named’][‘language’] = $this->params[‘language’];

    if (!empty($urlRoute[‘controller’])){
    $url = array_merge(array_intersect_key($urlRoute, array_flip(array(‘admin’, ‘controller’, ‘action’))), $urlRoute[‘pass’], $urlRoute[‘named’]);
    }

    I also override the redirect function as Josenivaldo Benito Junior pointed out. I have the same problem with named parameters showing up as /language:eng instead of /eng/… as Robin, but can live with that.

    But one more thing I’m wondering about: how do you handle forms? With all the submit buttons the language parameter gets lost currently at my site. Any ideas?

    Cheers,
    tobi_one

  39. @tobi_one

    Thanks for sharing your solution.

    I’ve had forms on this particular app working without any problem. Nothing, that I did not write about here. I am wondering if some things have changed in the core since the writing of this article, but since I really haven’t worked on this project in a while, it’s pretty hard to guesstimate what could be the issue.

    If by any chance you find a solution, do share as it might help others. Thanks :)

  40. What could work with the forms as a global solution is to redeclare the “create” method of the form helper.

    Or as a quick work around I added the language parameter to all form creation calls like this, e.g.:

    $form->create(‘User’, array(‘action’ => ‘login’,’url’ => array(‘language’ => $html->getLanguage())));

    with $html->getLanguage() being a custom defined helper to return the current language string.

    Cheers,
    tobi_one

  41. @tobi_one

    I’ve just tested again, and the form URL has the parameter passed-in as expected.
    If I use either:

    $form->create();

    or

    $form->create(‘Article’, array(‘action’ => ‘test’));

    The language param appears as expected:

    … action=”/eng/articles/test”

    Perhaps there is a problem, with url() override, in your app. I can’t imagine what else it could be.

  42. Hi,

    Brilliant tutorial thanks a lot!!!

    I would like to use this but also route all controllers to the one controller.

    Any suggestions? Something along the lines of.

    Router::connect(‘/*’, array(‘controller’ => ‘home’, ‘action’ => ‘index’));

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

  43. I am creating a dynamic website that the user can manage through our custom content management system. So the user can add new pages and the navigation and content will be dynamic from the database including the language. I could do it through parameters but I want the url to look like a new page (Controller).

    This is what I have come up with so far, but if you have a better solution I would appreciate it.

    Router::connect(‘/:language/*’,
    array(‘controller’ => ‘home’, ‘action’ => ‘index’),
    array(‘language’ => ‘[a-z]{2}’));

    Router::connect(‘/*’, array(‘controller’ => ‘home’, ‘action’ => ‘index’));

    Then I would use the $this->params[‘url’] and split it based on the ‘/’ to get the page the user is looking to access from the database.

    Thanks for your help.
    Peter

  44. I ran into a lot of problems with this approach.

    1) If you have the auth component in use and a users/login action you get in some trouble. I still don’t have any good solution.
    2)you need to modify the url method in your app_helper
    3)you need to modify the rediret method in your app_controller
    4) you need to modify the router::url method. I use router::url e.g. in the activation mail, to avoid hardcoded urls.

    But the login and logout actions are really confusing. Does anyone have a good solution for that. It seems that the auth redirects just dont really work with the language param

    Otto

  45. Hi teknoid and thanks for a great article!

    Some people mentioned overloading the app_controller with redirect and flash functions, does that mean that each time a redirect/flash will occur, those functions will be executed instead of the ones in cake/libs/controller/controller.php?

    I just want to make sure I understand to help me debug my app.

    Thanks in advance!

    Nicolas

  46. @Nicolas

    Since AppController extends Controller any method with the same name as in the parent Controller, will be overridden. If you need to use some functionality from the parent method as well, be sure to use parent::whateverMethodNameYouOverride()

  47. @Otto

    1. I use Auth in the app as well, no problems here.
    2. Yes, that’s how it is meant to work…
    3. Why?
    4. Use FULL_BASE_URL or the relevant method to the get full URL, instead of Router::url()

    1. Hi teknoid,

      1) so you’re lucky. If i try to login using http://www.example.com/eng/login it won’t work. Only works if I use http://www.example.com/login (i have some routes changed…)
      2) yep, just to mention :-)
      3) tried again, works now without modifying redirect
      4) I use $html->url() now, with the second parameter set to “true”. Then $html->url() returns the full address like http://www.example.com/whatever. $html->url() is already modified, so this is perfect.

      5) The links that are actually switching the language:

      You use: link(‘Русский’, array(‘language’=>’rus’)); ?>

      But this won’t work, if you have some parameters in the url like here:

      http://www.example.com/eng/profiles/view/123

      If you have link(‘Русский’, array(‘language’=>’rus’)); ?> on top of your site, then this links to:

      http://www.example.com/rus/profiles/view

      So the parameter is missing.

      I changed the way the language switching links work like that:

      $u=array(‘language’=>’rus’);

      foreach($this->params[‘pass’] as $p){
      $u[]=$p;
      }

      echo $html->link(‘Русский’, $u);

      Is there a better solution than this? Would be great, looks ugly.

      By the way. If I forgot to mention: This tutorial is great. Thanks a lot.

      Otto

  48. teknoid, thanks for this great post. Everything is working as expected. (BTW I tested on the last two stable releases 1.2.5 and 1.2.4.8284)

    I’m using also the Translate behavior (to translate content using the i28n table). The translate behavior needed the language to be written on the Config.language so I added

    – Configure::write(‘Config.language’, $this->Cookie->read(‘lang’));
    – Configure::write(‘Config.language’, $this->params[‘language’]);

    to the if..else statement on the _setLanguage() method.

    Without this two lines, the translation using the po files was done but the content stay on the language readed from Configure::read(‘Config.language’).

    Thanks again for your help, hope this helps others using the Translate Behavior.

  49. @Kim Biesbjerg

    You wrote: “This is not good for SEO as you might get penalized for duplicate content. What you should do is you should check in beforeFilter if supplied url language param equals the default language set in core.php, and if it does do a 301 (permanent) redirect to domain.com/controller/action (See, the language param has been removed)”

    How can I use the Controller::redirect method to perform this redirection?

  50. @Leonel Galan

    Something like ‘language’ => false, should probably take care of removing the param. It should be similar to the way you handle ‘admin’ or any other prefix.

  51. @teknoid

    Thanks again for your help, CakePHP amaze me every day, using: $this->redirect(array(‘language’ => false) + $this->params[‘pass’], 301); did the trick. I used the ‘pass’ to keep the last part of the URL, as others pointed out.

    I’m having ‘trouble’ when using CakePHP admin routes. I put a link on my default layout to change language to english: link(‘English’, array(‘language’=>’eng’) + $this->params[‘pass’]); ?> and it works well on any other pages, but in admin pages the link returned is http://localhost/fc.com.gt/admin/tags/index/language:eng and not http://localhost/fc.com.gt/eng/admin/tags/index as expected.

    I’m new to CakePHP but suspect it has something to do with the “Router::connect(‘/:language/:controller/:action/*’, array(), array(‘language’ => ‘[a-z]{3}’));” not considering the admin part.

  52. Hi again,

    I’ m trying to figure how to redo some of my routes, that were using paramaters, with the language parameter, maybe you will have a solution.

    I used this route before:

    Router::connect(‘/bands/:id/:suffixe’, array(‘controller’=>’bands’, ‘action’=>’detail’), array(‘id’=>$ID, ‘suffixe’=>'[a-zA-Z0-9_-]+’));

    I thought that I just had to add the language parameter like this:
    Router::connect(‘/:language/:controller/:action/*’, array(), array(‘language’ => ‘fr|en’));

    Router::connect(‘/:language/bands/:id/:suffixe’, array(‘controller’=>’bands’, ‘action’=>’detail’), array(‘id’=>$ID, ‘suffixe’=>'[a-zA-Z0-9_-]+’, ‘language’=>’fr|en’));

    The link is ok

    localhost/en/bands/1/band_name

    But when I click it, cake return an error because it sees the ID 1 as the controller action instead of “detail” that was declared in my route…

    Any idea where I’m doing something wrong?

  53. @Nicolas, please post all the routes defined on routes.php. I had a similar problem (read my post above and the solution below)

    @teknoid
    Hey teknoid I found a solution for the problem I post before. I found the solution on a ticket on cakephp site: https://trac.cakephp.org/ticket/6173.

    I had to add this Router:connect, BEFORE the existing one.

    Router::connect( ‘/:language/admin/:controller/:action/*’,
    array(‘admin’ => true, ‘prefix’ => ‘admin’),
    array(‘language’ => ‘[a-z]{3}’));

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

    If the route was added after, admin was sought as a controller and the controller as the action. With this I’m almost finish with my complete i18n and l10n website with: URL translation, layout translation, content translation, language in URLs (thanks to teknoid), and slugs on each language.

    1. @Leonel Galan

      Placing my route before

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

      as you did worked for me!!

      Thanks for the tip :)

    2. It seems that every custom routes, for the routing to working correctly, has to be put before this one :

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

      1. I’m not sure about that affirmation. For me, it looks like Cake evaluates routes in order, and routes need to be defined from specific to general.

        I’m glad it worked!

  54. @Leonel Galan

    Thanks for your help and sharing your solutions.

    Teamwork at its best! :)

    @Nicolas

    Glad you were able to solve it, surely it will help others in the future.

  55. Hey teknoid
    I used plugins in my application. URL for plugins is “/pluginname/controllername/action”
    Router::connect( ‘/:language/:controller/:action/*’, array(), array(’language’ => ‘[a-z]{3}’)); don’t work for plugins.
    Router::connect( ‘/:language/:plugin/:controller/:action/*’, array(), array(’language’ => ‘[a-z]{3}’)); don’t work.
    Any idea where I’m doing something wrong?

    1. Sorry,
      I found solution.
      I had to add this Router:connect, in this sequence:
      Router::connect( ‘/:language/:controller/:action/*’,
      array(‘plugin’ => null),
      array(’language’ => ‘[a-z]{3}’));
      Router::connect( ‘/:language/:plugin/:controller/:action/*’,
      array(),
      array(’language’ => ‘[a-z]{3}’));

  56. Hey, great article, just what I was looking for! One thing, I’m not seeing where the value in core.php is used, am I overlooking something (probably obvious)?

    Toby

  57. @toby1kenobi

    I guess, the core sets the default value for the language at “start-up”… to be honest with you I haven’t bothered to examine the code. (It ain’t broke… don’t fix it). But, I’m sure someone has a better answer.

  58. Do you use the same models for both English and Russian or do you create separate tables for each language? When a user tries to register and doesn’t pass the model validation, I am wondering how to display different error messages in either English or Chinese.

  59. @sbefort

    The validation messages (at least in 1.2) need to be translated in the view. So use the __() function and create appropriate translations in the “po” files.

    There is also a translatable behavior, but that is used for translating content from the DB.

    This is not the cleanest approach, and AFAIK, it is being reworked in future versions of cake.

  60. Hey,

    I enjoyed reading your article. Everything sounded very promising. But after the first steps I just got frustrated.
    I apply the route given above: Router::connect(‘/:language/:controller/:action/*’, array(), array(‘language’ => ‘[a-z]{3}’));
    I reload the page (http://localhost/) and get this error:
    Missing Controller

    Error: Controller could not be found.

    Error: Create the class Controller below in file: app/controllers/controller.php

    When I use this URL ‘http://localhost/eng/pages/display/home’ it works. Am I missing something?

  61. Ok, whatever it was, it’s gone. I just did a fresh start. But I still have an issue. The application seems to ignore the app_helper. Do I have to declare it somewhere?
    It doesn’t put the language in front of the other stuff.
    I kinda worked around it with link(__(‘Start’, true), array(‘language’ => Configure::read(‘Config.language’))); ?>

    And here is another thing. The base path ‘/’ is connected to the pages controller and displays the home.ctp. After changing to a different language the “Home-Link” isn’t for example http://localhost/deu/
    it’s http://localhost/deu/pages/display which doesn’t look pretty at all. Any hints how I can fix this?

  62. @Mathias

    1. No, app helper should be found if you named everything correctly.

    2. Haven’t seen this problem, not sure if your routes are setup properly, if so the URL should be parsed by router and displayed without /pages/display.

  63. OMG this post is a live saver!! this post and some of its comments should be in book.cakephp.org its very very good! Thank you very much!

  64. for the duplicate content : I just did this…

    /* app_controller */
    function _setLanguage(){
    if ($this->Cookie->read(‘lang’) && !$this->Session->check(‘Config.language’)) {
    $this->Session->write(‘Config.language’, $this->Cookie->read(‘lang’));
    $this->L10n->get($this->Cookie->read(‘lang’));
    }elseif(isset($this->params[‘language’]) && ($this->params[‘language’] != $this->Session->read(‘Config.language’))) {
    $this->Session->write(‘Config.language’, $this->params[‘language’]);
    $this->Cookie->write(‘lang’, $this->params[‘language’], null, ’20 days’);
    $this->L10n->get($this->params[‘language’]);
    }else{
    $this->params[‘language’] = Configure::read(‘Config.language’);
    }
    }

    /* routes */

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

    /* index of the site */

    echo $html->meta(
    ‘canonical’,
    $html->url(‘/’.Configure::read(‘Config.language’)),array(),false
    );

    (for canonical link)

  65. Hi Teknoid,

    I just want to share the change a made. I use your procedure for a long time now and need to implement dynamic content translate for some models. During my research I found the article “i18n in CakePHP 1.2 – database content translation, Part 2” (http://www.palivoda.eu/2008/04/i18n-in-cakephp-12-database-content-translation-part-2/#comment-1283) from Rostislav and decided to follow his approach for dynamic data i18n.

    However, I lately realized the method beforeFilter (where _setLanguage is originally called) is executed after models being instantiated. But sadly true, beforeFilter is the only callback where all things are already done for use (components, models, etc) and we cannot execute _setLanguage properly from class constructor method. The issue is that models have their locale property ($this->Model->locale) set to default language and after that beforeFilter sets the language chosen by user. The result is no dynamic translation.

    I temporally workaround it by parsing the $this->param[‘language’] property and using it to set L10n locale at constructClasses method from app_controller. This ensures that every language passed as a get parameter in URL will be used as locale to models. Another way I foreseen to it is to use loadModels function and do same approach in it but this will be called as many times as your controller have models to attach so I prefered to use constructClasses at app_controller.php which is called once. It is yet necessary to use some mechanism to check available language and prevent user from passing a non-available language to the system which could cause query errors.

    I am still not entirely comfortable with this solution and need to understand better the problem to be confident on it or find another way. Thus I am wide open for suggestions and comments.

    Thanks and regards,
    Benito

  66. @Josenivaldo Benito

    Hi, thank you for sharing your solutions. This is an important part of i18n, and I’ll be exploring new approaches for DB content translation in the near future, so it’ll be good to come back to review this in more detail.

  67. I am so lost:
    I have done everything in this article and the modifications offered in the comments, but it doesn’t work, and there are things I don’t understand:

    1) What does it mean if the second parameter here is an empty array?
    Router::connect(‘/:language/:controller/:action/*’,array(),array(‘language’ => ‘[a-z]{3}’));

    2) What does this do exactly: array(‘language’=>’fre’)+$this->params[‘pass’]
    Does the plus sign concatenate arrays? It seems to be doing that, but I can’t find this syntax on any php site.

    3) Could someone explain what’s going on in the AppHelper::url function? And what’s the difference between $url[‘language’] and $this->params[‘language’]. Aren’t they the same?

    I am going nuts

  68. @un_pro

    1. Did you look at the API?

    2. Syntax doesn’t seem to be right. I cannot vouch for someone else’s code, there’d be little time for me to do anything if I could test every suggestion :)

    3. It automatically rewrites all your links, so you don’t have to manually specify param in each link. It takes an argument from the URL if one is present.
    Also, please check the API for details on how the method works.

  69. When using redirect, i get urls like /home/index/language:est/ instead of /est/home/index/

    and i have the following overriding in my app_controller.php

    function redirect($url, $status = NULL, $exit = true ) {
    if(!isset($url[‘language’]) && isset($this->params[‘language’])) {
    $url[‘language’] = $this->params[‘language’];
    }
    parent::redirect($url, $status, $exit);
    }

  70. Thank you very much for this great article. It is the most helpful one out there.

    One question, would it be better if you don’t check if url has language parameter, and prepend it every link?

  71. Awesome article. I have a question about view caching that no one has mentioned yet.

    I want to implement view caching for my multi-lingual site. Cake does this based on URL so I really like your prefix implementation. HOWEVER, your implementation makes it so if they are sessioned w/ rus and go to /controller/action it will display in russian right?

    This wont work as desired if view cach is enabled for the /controller/action – as the lang used to cache the page will always be displayed til it expires.

    I’ve been trying to think of ways to make it so omitting the 3 letter lang code will always default to ‘eng’ (and therefor controller/actions w/o lang prefix will display eng view cache). Was thinking of redirecting to /3letterprefix/controller/action if language param does not exist and lang in session is not eng.

    Do you see any problems with this approach? do you not do any view caching in your apps?

  72. @Robin

    I had the same problem. Add ‘?’ attribute to your paginator ‘url’:
    $paginator->options[‘url’][‘?’] = ‘?name1=value1&name2=value2’;

    @teknoid

    Thank you for the great article!

  73. Hey Teknoid,

    Do you have a fix for prefix routing and the :language ‘type’ of parameter in CakePHP 1.3? The route specified in comment #2216 by Leonel Galan does not work with 1.3

    Thanks,

  74. @ion

    Hi, yes I can share it, however I am in a business trip in South Korea. For a Brazilian guy this means the other side of the world. My personal computer is in Brazil and now I only have the company computer with limited access to my stuff. I will be back to Brazil in next week probably becoming online again at April 3 or 4.

    Is it ok to you?

    Regards,
    Benito

  75. @Josenivaldo Benito

    Actually if you could describe the procedure that would be great. Unless it is a solution with a lot of different places to put code.
    What I’ve found is that there is a function in the i18n behavior called _getLocale which if
    I manage to make it listen to the params[‘language’] or the Cookie or the Session I should get the desired result.
    However I still don’t manage to change the language. I think it is mainly because I have set it up in bootstrap.php (DEFAULT_LANGUAGE). The Config.language paramater does not do anything …

    Thank for the reply anyway
    ion

  76. @ion

    To be honesty I can’ t remember :()

    I remember I did something but can’ t remember what. Frankly speaking I even can’t remember just out of my head if I get a final solution or just did a workaround. Really need to deeply dive into that code again.

    My job is related with mobile communication development, not directly related with web design/development. Now I only do some pretty ugly stuff just for fun in my spare time (which is getting rare day to day – sad!). The translation (static and dynamic) was implemented for local HAM club contest I am part of. We need a simple web system to disclose the stations preparing to participate in our contest (annually conducted). I keep a darn simple system up for 4 years now and about 3 years ago I have rewrote it using CakePHP. My intention was to keep it even simple for further updates and new modules development. One year and half things grew up due to Argentina coming to compose our organization and now we have an international contest. That setup the need for two languages living together, portuguese and spanish. At that time I found this article and later final last year I needed to ad the dynamic translation finding for Rostislav’s solution. To summarize things: from last year until now I did not took a look to that source code and many things became fog in my head.

    Sorry, I will not be able to help you before getting in touch with my personal computer/source code and verify what I did. What I can see from the website is that I can setup the language but it stills have problem in showing the language at first access to the first page. I will try to access the provider management area this night from hotel, inside company it is strictly prohibited by the firewall.

    Regards,
    Benito.

  77. @Josenivaldo Benito

    Thank you Josenivaldo very much for your prompt reply. I am so deeply involved with the issue at the moment that I’m getting results every 30′ !!!
    I must say it has been a pain in the ass for me to get both static and dynamic content translation with cakephp. The system is great, even its core localization functions work great out of the box however things get really messy when dealing with dynamic content from a database and really really messy when it comes to HABTM relationships between the models. That was the main reason why I switched to the i18n component and forgot about the core translate behavior in cakephp.
    Right now I’ve managed to switch between languages for both dynamic and static content with a few tweaks but still I get loads of errors when changing urls….
    I’ve been struggling for more than a year on and off with this thing and I hope this one is the last one …

    Anyways thank for your reply
    Regards
    Ion

    1. @ion

      Just checking, when did you downloaded the i18n component code? I have exchanged some e-mails with Rostislav (the author) and he made some code improvements making it work correct with containable, etc. If you don’ t have the most recent version it may worthy try update. For sure it will not solve all your issues but it is better to have a new code right?

      Late I send you updates if I can access my provider management area

      1. @Josenivaldo Benito

        Well I got the code a week ago. I’ll download it again. Rostislav has done a great job keeping up with his posts.

        Thanks
        ion

  78. I implemented your code and changed:

    $this->Cookie->write(‘lang’, $this->params[‘language’], null, ’20 days’);

    to:

    $this->Cookie->write(‘lang’, $this->params[‘language’], false, ’20 days’);

    Because the third argument is the encryption key (or false) and null will be seen as the encryption key.

  79. @Maurits

    Thanks for bringing this up, there’s a good chance that the API had changed slightly since the writing of this article. (I’ve updated the code to reflect the change).

  80. for cakephp 1.3 using this for set language

    function _setLanguage() {
    @$lang = $this->params['named']['language'];
    if ($this->Cookie->read('lang') && !$this->Session->check('Config.language')) {
    $this->Session->write('Config.language', $this->Cookie->read('lang'));
    }
    else if (isset($lang) && ($lang != $this->Session->read('Config.language'))) {
    $this->Session->write('Config.language', $lang);
    $this->Cookie->write('lang', $lang, false, '20 days');
    print($lang);
    }

    }

  81. For any one struggling to use the Cake Core TranslateBehaviour with multilingual database content and the language is not being set correctly. Try altering the _getLocale cake/libs/models/behaviours/translate.php as follows. (cakephp 1.3)


    function _getLocale(&$model) {

    if (!isset($model->locale) || is_null($model->locale)) {
    if(!class_exists('CakeSession')) {
    App::import('Component', 'CakeSession');
    }
    $CakeSession =& new CakeSession;
    if($CakeSession->check('Config.language')) {
    $this->locale = $CakeSession->read('Config.language');;
    } else {
    $this->locale = Configure::read('Config.language');
    }
    if (!class_exists('I18n')) {
    App::import('Core', 'i18n');
    }
    $I18n =& I18n::getInstance();
    $I18n->l10n->get($this->locale);
    $model->locale = $I18n->l10n->locale;
    }

    return $model->locale;
    }

  82. @Chris

    Thank you for sharing. One thing to note, is that instead of making modifications directly in the core you can move the behavior to your app, and, if you submitted a ticket, wait for the fix ;)

  83. What’s up to every , because I am really keen of reading this blog’s post to be updated regularly.
    It consists of pleasant information.

Leave a reply to fredi Cancel reply