Zend Framework URL based translation routes
Asked Answered
T

1

9

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!

Tramontane answered 29/8, 2012 at 13:52 Comment(3)
It will always have the lang in the URI? I mean, no default language for something like localhost/module/controller = deafult-lang = enRutter
@Keyne If no language is specified in the URL, a default language will be set within my controller plugin. In that case, the default routes are used and everything should work.Tramontane
I've posted an answer, but I'm not sure about your lang2 route. On my answer I've just added the lang part in one route and let the default module routes doing its job. Let me know if I got it right.Rutter
R
1

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.

Rutter answered 3/9, 2012 at 23:57 Comment(6)
Thank you for your answer. I hope you don't mind that I wrote my comment on pastebin; I found it impossible to fit a proper explanation in so few characters. Please see my comment here.Tramontane
No problem, I've read it. The part where we include the language parameter in the url (if not specified) is transparent to the end user. It's the same as if it were optional (we need it so that it can work). The user will not see it being injected, the URL on the browser will remain the same. IIRC, there only two missing parts: One is to override the Zend_Locale language and the other is to "translate" this code into a configuration file. Got it?Rutter
It seems like everything is working properly here. I'll figure out how to put this into a configuration file and will post ASAP.Rutter
I used to have simple custom routes such as /register, which would default to /default/user/register. These routes are stored in a config file. However, with the new language routes in place, this does not work anymore. I suspected at it had something to do with the order in which the routes were loaded. Adding the routes in the bootstrap after your routes will make it work if I change it to /:lang/register with a requirement of 'lang' => '[a-z]{2}_?([a-z]{2})?' (because all requests will have a language now). Any idea on how I can avoid this for all my routes and to use config?Tramontane
Have you achieved it? Still looking for help?Rutter
I have not yet found a solution for my last comment above, so if you have any ideas, that would be much appreciated.Tramontane

© 2022 - 2024 — McMap. All rights reserved.