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.


Comments (Login to leave comments)
Thoughts?
- Joe
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?
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!
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.
kblunkka did point out some things I could improve with my approach, so I may update soon with those changes.
Hope this helps!
However there seems to be some kind of issue... http://framework.zend.com/issues/browse/ZF-8812
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...
; 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"
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
}
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!
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' => 'ru'),
array(1 => '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.
@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!
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!
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.
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');
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.