Chaining language with default route

August 12, 2010

Zend Framework

There are several ways how to include language id in default route of Zend Framework. However, generally you end up with the solution not quite elegant and likely not totally trouble-free. I have seen people overwriting the default route by new one which mimics module route with additional language id. There is no need to throw the default module route away to do this. To get it right chain the plain language route with default route.

// application/Bootstrap.php

protected function _initRoutes() {
    $locale = $this->getResource('locale');

    // Create route with language id (lang)
    $routeLang = new Zend_Controller_Router_Route(
        ':lang',
        array(
            'lang' => $locale->getLanguage()
        ),
        array('lang' => '[a-z]{2}')
    );

    // Now get router from front controller
    $front  = $this->getResource('frontcontroller');
    $router = $front->getRouter();

    // Instantiate default module route
    $routeDefault = new Zend_Controller_Router_Route_Module(
        array(),
        $front->getDispatcher(),
        $front->getRequest()
    );
    
    // Chain it with language route
    $routeLangDefault = $routeLang->chain($routeDefault);

    // Add both language route chained with default route and
    // plain language route
    $router->addRoute('default', $routeLangDefault);
    $router->addRoute('lang', $routeLang);

    // Register plugin to handle language changes
    $front->registerPlugin(new My_Controller_Plugin_Language());
}

I think that the code above has enough comments to clarify it, so lets forward to the controller plugin. If you are not familiar with chaining routes, refer to reference guide Zend_Controller_Router_Route_Chain.

In Language controller plugin we have to take care of actions, which have to be done if language has been changed. Setting the language id as a global router parameter is the most important. The another common task is to check, if there is available translation for the selected language.

// library/My/Controller/Plugin/Language.php

class My_Controller_Plugin_Language
    extends Zend_Controller_Plugin_Abstract
{
    public function routeShutdown(Zend_Controller_Request_Abstract $request)
    {
        $lang = $request->getParam('lang', null);

        $translate = Zend_Registry::get('Zend_Translate');

        // Change language if available
        if ($translate->isAvailable($lang)) {
            $translate->setLocale($lang);
        } else {
            // Otherwise get default language
            $locale = $translate->getLocale();
            if ($locale instanceof Zend_Locale) {
                $lang = $locale->getLanguage();
            } else {
                $lang = $locale;
            }
        }

        // Set language to global param so that our language route can
        // fetch it nicely.
        $front = Zend_Controller_Front::getInstance();
        $router = $front->getRouter();
        $router->setGlobalParam('lang', $lang);
    }
}

In our Language plugin, we check if user selected language is available. In spite of its availability, we have valid language id in $lang, which is set as a global param. To achieve a better user experience you may want to store the user selected language in to session to be retrieved on later visits.

Update (2011-03-08): Updated the plugin to change the method “routeStartup” to “routeShutdown”, as the intention is to update the route with the default language when unmatched.

18 Responses to “Chaining language with default route”

  1. zonta Says:

    i’ve added
    resources.locale.default = "en_US"

    and

    $this->bootstrap(‘locale’);
    $this->bootstrap(‘FrontController’);

    because of the same problem.
    Now the error is

    Message: No entry is registered for key ‘Zend_Translate’

    which refers to

    $translate = Zend_Registry::get(‘Zend_Translate’);

    inside plugin file.

  2. kblunkka Says:

    @zonta

    You have to bootstrap your locale resource. Try to add this into your application.ini …

    resources.locale.default = "en_US"

    In addition, you may have to add …
    $this->bootstrap(‘locale’);

    before the line …
    $locale = $this->getResource(‘locale’);

  3. zonta Says:

    I’m quite a beginner with Zend Framework but i can’t get your code to work.

    I’ve created the _initRoutes() in my Bootstrap and created the plugin, but "$locale = $this->getResource(‘locale’);" returns NULL to me, so $locale->getLanguage() ends with a fatal error.
    Same with "$front = $this->getResource(‘frontcontroller’);"

    Thanks for your article.

  4. essentiallogic Says:

    Hi – I’ve followed the example in the tutorial but need some help with getting the view url helper to automatically add the lang parameter to whatever href it generates.

    I can see the ‘default’ route contains chained :lang but it’s not being output in my href.

    E.g. if i navigate to page http://localhost/en/news/list and my list.phtml view contains this:

    <? echo $this->url(array(‘controller’ => ‘news’, ‘action’ => ‘item’, ‘id’ => 999)) ?>More</a>

    I would expect to see output /en/news/item/999 – however, i don’t get the /en/ part output – am i missing something?

    Thanks!

  5. jgornick Says:

    @alex505 – I don’t know what you mean by "rewriting", but all I’m doing is simply creating routes with and without the language specified, then adding them to the router. I’m simply manipulating strings here so it still should be pretty speedy. The only thing I worry about is adding many routes to the router and having the router parse them. But, as I understand it, the router will go through the list of routes and then stop processing as soon as it finds a match. This means that if you have LOTS of routes and it just so happens the one that matches is the last in the list, it might take a little bit longer to parse.

    @satyrius – If you take a look at my solution, I support having a default language and create routes that use the default language when a language isn’t specified. A link to my blog post can be found here: http://joegornick.com/2009/12/02/zend-framework-best-practices-part-2-i18n/

    I plan on doing some speeds tests soon to see how my approach performs to the approach described by @kblunkka. I also want to provide a pros/cons list for each solution and speed will be a big thing.

    Good discussions here!

  6. satyrius Says:

    @kblunkka

    Really like your solution, I have used it in my current project. But I have a problem I cannot solve. I want to language route matches the url with no lang parameter. Using your routes config with one chain route and plain lang route.

    The following works ok
    /en/foo/bar – ‘en’ language, ‘foo’ controller, ‘bar’ action
    /ru/foo/bar – ‘ru’ language, ‘foo’ controller, ‘bar’ action

    But my site have a default ‘ru’ language and I do not want to pass lang information for russian urls. I want following:
    /foo/bar – ‘ru’ language (by default), ‘foo’ controller, ‘bar’ action

    But chain fails. I have tried to use RegExp route for language:

    $routeLang = new Zend_Controller_Router_Route_Regex(
    "(ru|en)?",
    array(‘lang’ =&gt; ‘ru’),
    array(1 =&gt; ‘lang’),
    ‘%s’
    );

    Lang part route matches, but next chained route fails. After debugging I have found that problem is inside Zend_Controller_Router_Route_Chain. See $separator setup and check in match() method.

  7. alex505 Says:

    @kblunkka

    Thanks for your reply! I currently have it working with all the routes in my bootstrap. I will defenitly try your method out. Thanks!

    It isn’t really a big deal. But i think that having your routes in application.ini is more clean than having to add everything in your bootstrap.

    Thanks!

  8. kblunkka Says:

    @alex505,

    Here is my latest experiment. Just keep your routes in application.ini as is and add this routeStartup to auto chain every route with language.

    // library/My/Controller/Plugin/Language.php
    public function routeStartup(Zend_Controller_Request_Abstract $request)
    {
    $front = Zend_Controller_Front::getInstance();
    $locale = Zend_Registry::get(‘Zend_Locale’);
    $router = $front->getRouter();

    $routeLang = new Zend_Controller_Router_Route(
    ‘:lang’,
    array(
    ‘lang’ => $locale->getLanguage()
    ),
    array(‘lang’ => ‘[a-z]{2}’)
    );

    $newDefaultRoutes = array();
    $oldDefaultRoutes = array();
    foreach ($router->getRoutes() as $name => $route) {
    $newDefaultRoutes[$name] = $routeLang->chain($route);
    $oldDefaultRoutes[$name.$name] = $route;
    $router->removeRoute($name);
    }

    $router->addRoutes($oldDefaultRoutes + $newDefaultRoutes);
    }

    public function routeShutdown(Zend_Controller_Request_Abstract $request)
    {
    $lang = $request->getParam(‘lang’);
    $this->_setLanguage($lang);
    }

    protected function _setLanguage($lang)
    {
    // code
    }

  9. kblunkka Says:

    This is my initial solution how to chain language route in application.ini

    ; Routes
    resources.router.routes.module.type = Zend_Controller_Router_Route_Module

    resources.router.routes.lang.type = Zend_Controller_Router_Route
    resources.router.routes.lang.route = ":lang"
    resources.router.routes.lang.reqs.lang = "[a-z]{2}"
    resources.router.routes.lang.abstract = On

    resources.router.routes.default.type = Zend_Controller_Router_Route_Chain
    resources.router.routes.default.chain = "lang, module"

  10. alex505 Says:

    @jgornick,

    I had a look at your solution, but i’m kinda concerned about the fact that there is so much "rewriting". I have the feeling there might be a easier way to do this.

    I have solved it almost using the method from this article, but now i have to chain the new route (the one with the parameter) to the lang route as well. But for now, i still cannot get the parameter.

    Frustrating…

  11. kblunkka Says:

    Now when you asked I did some researching and found that somebody has already made this. The solution is quite similar what proposed here, but with ini configurations… http://www.m4d3l-network.com/developpement/php/zend-framework/add-language-route-to-your-zend-framework-project/

    However there seems to be some kind of issue… http://framework.zend.com/issues/browse/ZF-8812

  12. jgornick Says:

    @alex505 – If you take a look at my link I posted above, my approach uses application.ini for routes.

    kblunkka did point out some things I could improve with my approach, so I may update soon with those changes.

    Hope this helps!

  13. alex505 Says:

    This approach isn’t trouble free either. With this method, i am unable to simply add parameters (with a new route) from the application.ini file.

    Also, when i want to add this route, i’d have to chain it to the lang route as well. And since I cannot do this in the application.ini, i would have to do this in the bootstrap. So i end up with a load of routes in the bootstrap.

    So i wouldn’t call this method trouble free. Unless you provide some solutions for this? Because on IRC and the forum, nobody knows it.

  14. kblunkka Says:

    @xerionth Nice spot! It should be routeShutdown and NOT routeStartup as mentioned in the post.

  15. xerionth Says:

    Very nice and simple code !!

    I just integrated it and found a minor bug (i think):
    In the routeStartup method the lang param doesn’t exist yet. Changing the routeStartup method to routeShutdowm fixes this :)

    Thx!

  16. alex505 Says:

    So would this make the URL’s like

    website.com/en/controller/action/param

    or

    website.com/controller/action/en

    ?

    And would: website.com/controller/action still be valid?

    And is this SEO friendly?

  17. codedreamer Says:

    I spend a lot of time to find a good solution. My solution was very similar to your’s but your’s looks very clean. Next project, I will test your solution!

  18. jgornick Says:

    A while ago, I wrote an article on this which takes a slightly different approach: http://joegornick.com/2009/12/02/zend-framework-best-practices-part-2-i18n/

    Thoughts?

    – Joe