Howto: Simple multilingual routes in Zend Framework
Posted on: Neděle, Čer 27, 2010
Nowadays, many people want their website to be multilingual. With the equipment which is provided by Zend Framework, it’s not a big deal to translate anything on the website. But when it comes to URLs and routes, one can read hundreds and thousands of articles and still doesn’t have a clue how to do that in a really simple way.
If I am talking about something really simple, I mean something that detects the locale that should be used to translate the website based on the URL. I want the router to detect the locale because my routes contains words in the language connected to the locale and those have to be translated before the route is evaluated.
Let’s have a route:
‘:locale/:@controller/:@action/*’
The “:” character before the segment name means that the segment is variable, segments beginning with the sequence “:@” are variable and meant to be translated. There is an example of matching URLs:
cs/ubytovani
en/accomodation
de/unterkunft
All the routes should invoke dispatching of the same controller and the same action (e.g. AcommodationController::indexAction) but the language of the generated pages should be different – based on the locale used in the URL.
Many people came up with the idea of a controller plugin, which looks for the locale parameter of the request. That’s ok, but if and only if we don’t need the router to translate the segments in the URL. If so, the routeStartup event is too early, because there are no request parameters yet. The routeShutdown event is too late, because the router has already done all its work and doesn’t care of the following actions.
With the plugin, only the main page with URLs “/” or “/en” or “/de” works well. The route is matched, the plugin detects the language and then the controller is called. The rest of the pages works only for the default locale, because the router matches the URL “/de/unterkunft” to be dispatched at the unterkunftController instead of accomodationController, because there is no such word as “unterkunft” in the default locale (cs).
Got it? We would need to override something in the router to detect the locale in the URL and use it in the rest of the URL. And that’s quite simple, we will override the match method in the route.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | /** /** * This class has been inherited to bring an additional feature to the * Zend_Controller_Router_Route class. It detects a route segment named * locale and sets a default locale for the runnig application based on * the segment's value * * If the value is not a valid Locale identifier the locale is not set * into the registry * * @author Jiri Helmich <jiri @HELMICH.CZ> */ class MyApp_Controller_Router_Route extends Zend_Controller_Router_Route { /** * Matches a user submitted path with parts defined by a map. Assigns and * returns an array of variables on a successful match. * * @param string $path Path used to match against this routing map * @return array|false An array of assigned values or a false on a mismatch * @author Jiri Helmich <jiri @HELMICH.CZ> */ public function match($path, $partial = false) { //make a copy of that for the parental class $originalPath = $path; //if the path is empty, there is no locale :-) if ($path !== '') { //path begins with a delimiter, so trim that and explode the path $path = trim($path, $this->_urlDelimiter); $path = explode($this->_urlDelimiter, $path); //loop over each part of the path foreach ($path as $pos => $value) { //a simple test if this could be a matching route if (!array_key_exists($pos, $this->_variables)) break; //the route is probably longer than the path, not our business //get a name of current route segment $name = $this->_variables[$pos]; //locale segment, that's the interesting stuff if ($name === 'locale' && !empty($value)) { try { //if the given value is not a valid locale identifier //an exception is thrown $locale = Zend_Locale::findLocale($value); //otherwise, we construct a new locale instance based on the identifier ... $locale = new Zend_Locale($locale); // ... and set that into the registry Zend_Registry::set("Zend_Locale",$locale); //BUT the default translator already has a locale set, //so we need to override that $this->getTranslator()->setLocale($locale); //we would also like if the assemble method of the //router would have the locale value automatically Zend_Controller_Front::getInstance()->getRouter()->setGlobalParam('locale',$locale); //that's all we need to do, the rest is parent's job break; }catch(Zend_Locale_Exception $e) { //this could throw an exception //but without doing that, the standard match method //is executed the and default locale is used } } } } //let the parental class to do its job return parent::match($originalPath, $partial); } } |
That’s really it. We detect the locale in the URL, set it into the registry and run the standard match method to do the rest of the job, now with the right locale.
Now, you just need to add the following lines of code into the application bootstrap:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | $translator = new Zend_Translate('tmx',APPLICATION_PATH.'/../data/langs/translation.xml','cs'); Zend_Registry::set('Zend_Translate', $translator); $router = Zend_Controller_Front::getInstance()->getRouter(); $router->removeDefaultRoutes(); $pages = new MyApp_Controller_Router_Route( ':locale/:@controller/:@action/*', array( 'controller' => 'index', 'action' => 'index', 'locale' => 'cs' ) ); $router->addRoute('pages',$pages); |
Pedro Padron
Prosinec 18th, 2010 at 9.03
Permalink this comment
1
This is insanely useful! You have saved me quite some time. Thanks!