Search code examples
phpzend-frameworkurl-routingzend-translate

Zend Framework URL based translation routes


I am trying to implement URL based translation in Zend Framework so that my site is SEO friendly. This means that I want URLs like the below in addition to the default routes.

zend.local/en/module
zend.local/en/controller
zend.local/en/module/controller
zend.local/en/controller/action

The above are the ones I have problems with right now; the rest should be OK. I have added a controller plugin that fetches a lang parameter so that I can set the locale and translation object in the preDispatch method. Here are some of my routes (stored in a .ini file):

; Language + module
; Language + controller
resources.router.routes.lang1.type = "Zend_Controller_Router_Route_Regex"
resources.router.routes.lang1.route = "(^[a-zA-Z]{2})/(\w+$)"
resources.router.routes.lang1.defaults.controller = index
resources.router.routes.lang1.defaults.action = index
resources.router.routes.lang1.map.1 = "lang"
resources.router.routes.lang1.map.2 = "module"


; Language + module + controller
; Language + controller + action
resources.router.routes.lang2.type = "Zend_Controller_Router_Route_Regex"
resources.router.routes.lang2.route = "(^[a-zA-Z]{2})/(\w+)/(\w+$)"
resources.router.routes.lang2.defaults.module = default
resources.router.routes.lang2.defaults.action = index
resources.router.routes.lang2.map.1 = "lang"
resources.router.routes.lang2.map.2 = "controller"
resources.router.routes.lang2.map.3 = "action"

As the comments indicate, several URL structures will match the same route, which makes my application interpret the format incorrectly. For instance, the following two URLs will be matched by the lang1 route:

zend.local/en/mymodule
zend.local/en/mycontroller

In the first URL, "mymodule" is used as module name, which is correct. However, in the second URL, "mycontroller" is used as module name, which is not what I want. Here I want it to use the "default" module and "mycontroller" as controller. The same applies for the previous lang2 route. So I don't know how to distinguish between if the URL is of the structure /en/module or /en/controller.

To fix this, I experimented with the code below in my controller plugin.

// Get module names as array
$dirs = Zend_Controller_Front::getInstance()->getControllerDirectory();
$modules = array_keys($dirs);

// Module variable contains a module that does not exist
if (!in_array($request->getModuleName(), $modules)) {
   // Try to use it as controller name instead
   $request->setControllerName($request->getModuleName());
   $request->setModuleName('default');
}

This works fine in the scenarios I tested, but then I would have to do something similar to make the lang2 route work (which possibly involves scanning directories to get the list of controllers). This just seems like a poor solution, so if it is possible, I would love to accomplish all of this with routes only (or simple code that is not so "hacky"). I could also make routes for every time I want /en/controller, for instance, but that is a compromise that I would rather not go with. So, if anyone knows how to solve this, or know of another approach to accomplish the same thing, I am all ears!


Solution

  • I've reproduced your problem here and come out with the following (not using config files though):

    Router

    /**
     * Initializes the router
     * @return Zend_Controller_Router_Interface
     */
    protected function _initRouter() {
        $locale = Zend_Registry::get('Zend_Locale');
    
        $routeLang = new Zend_Controller_Router_Route(
            ':lang',
            array(
            'lang' => $locale->getLanguage()
            ),
            array('lang' => '[a-z]{2}_?([a-z]{2})?')
        );
    
        $frontController  = Zend_Controller_Front::getInstance();
        $router = $frontController->getRouter();
    
        // Instantiate default module route
        $routeDefault = new Zend_Controller_Router_Route_Module(
            array(),
            $frontController->getDispatcher(),
            $frontController->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);
    
        // Register plugin to handle language changes
        $frontController->registerPlugin(new Plugin_Language());
    
        return $router;
    }
    

    Plug-in

    /**
     * Language controller plugin
     */
    class Plugin_Language extends Zend_Controller_Plugin_Abstract
    {
        /**
         * @var array The available languages
         */
        private $languages = array('en', 'pt');
    
        /**
         * Check the URI before starting the route process
         * @param Zend_Controller_Request_Abstract $request
         */
        public function routeStartup(Zend_Controller_Request_Abstract $request)
        {
            $translate = Zend_Registry::get('Zend_Translate');
            $lang = $translate->getLocale();
    
            // Extracts the URI (part of the URL after the project public folder)
            $uri = str_replace($request->getBaseUrl() . '/', '', $request->getRequestUri());
            $langParam = substr($uri, 0, 3);
    
            // Fix: Checks if the language was specified (if not, set it on the URI)
            if((isset($langParam[2]) && $langParam[2] !== '/') || !in_array(substr($langParam, 0, 2), $this->languages)) { {
                $request->setRequestUri($request->getBaseUrl() . '/' . $lang . "/" . $uri);
                $request->setParam('lang', $lang);
            }
        }
    }
    

    Basically, there's the route chain for applying the language settings within the module default route. As a fix, we ensure that the URI will contain the language if the user left it out.

    You'll need to adapt it, as I'm using the Zend_Registry::get('Zend_Locale') and Zend_Registry::get('Zend_Translate'). Change it to the actual keys on your app.

    As for the lang route: [a-z]{2}_?([a-z]{2})? it will allow languages like mine: pt_BR

    Let me know if it worked for you.