27

I have a problem with routing and the internationalization of my site built with Symfony2.
If I define routes in the routing.yml file, like this:

example: 
    pattern:  /{_locale}/example 
    defaults: { _controller: ExampleBundle:Example:index, _locale: fr } 

It works fine with URLs like:

mysite.com/en/example 
mysite.com/fr/example 

But doesn't work with

mysite.com/example 

Could it be that optional placeholders are permitted only at the end of an URL ?
If yes, what could be a possible solution for displaying an url like :

mysite.com/example  

in a default language or redirecting the user to :

mysite.com/defaultlanguage/example 

when he visits :

mysite.com/example. ? 

I'm trying to figure it out but without success so far.

Thanks.

Ka.
  • 1,189
  • 1
  • 12
  • 18
  • Could you solve this ? – Esteban Filardi Jan 31 '14 at 23:17
  • I did not implement any solution regarding default local in my website yet. I may have to do it soon as we are going international, I'll report the solution we'll implement here. I just read the answers and @mattexx one seems the best/cleaner one. I did not know you could define multiple route for one controller. – Ka. Feb 01 '14 at 13:45

12 Answers12

21

If someone is interested in, I succeeded to put a prefix on my routing.yml without using other bundles.

So now, thoses URLs work :

www.example.com/
www.example.com//home/
www.example.com/fr/home/
www.example.com/en/home/

Edit your app/config/routing.yml:

ex_example:
    resource: "@ExExampleBundle/Resources/config/routing.yml"
    prefix:   /{_locale}
    requirements:
        _locale: |fr|en # put a pipe "|" first

Then, in you app/config/parameters.yml, you have to set up a locale

parameters:
    locale: en

With this, people can access to your website without enter a specific locale.

dwitvliet
  • 7,242
  • 7
  • 36
  • 62
Pitchou
  • 255
  • 2
  • 8
  • 8
    Unfortunately this does not work: www.example.com//home/ url should be www.example.com/home/ and it wouldn't work this way. – amiroff Apr 03 '15 at 07:25
12

You can define multiple patterns like this:

example_default:
  pattern:   /example
  defaults:  { _controller: ExampleBundle:Example:index, _locale: fr }

example:
  pattern:   /{_locale}/example
  defaults:  { _controller: ExampleBundle:Example:index}
  requirements:
      _locale:  fr|en

You should be able to achieve the same sort of thing with annotations:

/**
 * @Route("/example", defaults={"_locale"="fr"})
 * @Route("/{_locale}/example", requirements={"_locale" = "fr|en"})
 */

Hope that helps!

mattexx
  • 6,456
  • 3
  • 36
  • 47
  • 1
    It helps. However, lower one, with annotations, does not work for me. Why? – mario May 18 '14 at 14:52
  • 1
    Could be you forgot `use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;` – Jhonne Mar 13 '15 at 14:40
  • Make sure you use different route names for them – amiroff Apr 03 '15 at 07:51
  • 1
    **For those using annotations**, put everything on one line as documented on last paragraphe of [this page](http://symfony.com/doc/current/book/routing.html#adding-requirements "Symfony locale routing") – Marc Perrin-Pelletier Dec 15 '15 at 14:00
  • is there a way to do this, but where the /example automatically goes to the locale version of the default locale of the user? so if the default locale of the browser was spanish (es), it redirects to es/example? – Joseph Astrahan Jan 11 '16 at 06:15
7

This is what I use for automatic locale detection and redirection, it works well and doesn't require lengthy routing annotations:

routing.yml

The locale route handles the website's root and then every other controller action is prepended with the locale.

locale:
  path: /
  defaults:  { _controller: AppCoreBundle:Core:locale }

main:
  resource: "@AppCoreBundle/Controller"
  prefix: /{_locale}
  type: annotation
  requirements:
    _locale: en|fr

CoreController.php

This detects the user's language and redirects to the route of your choice. I use home as a default as that it the most common case.

public function localeAction($route = 'home', $parameters = array())
{
    $this->getRequest()->setLocale($this->getRequest()->getPreferredLanguage(array('en', 'fr')));

    return $this->redirect($this->generateUrl($route, $parameters));
}

Then, the route annotations can simply be:

/**
 * @Route("/", name="home")
 */
public function indexAction(Request $request)
{
    // Do stuff
}

Twig

The localeAction can be used to allow the user to change the locale without navigating away from the current page:

<a href="{{ path(app.request.get('_route'), app.request.get('_route_params')|merge({'_locale': targetLocale })) }}">{{ targetLanguage }}</a>

Clean & simple!

Pier-Luc Gendreau
  • 13,553
  • 4
  • 58
  • 69
  • But this way automatically redirected to the link with the locale: www.example.com/en www.example.com/fr but www.example.com not displayed. What we are trying to do is exactly the same as running www.symfony.com site: if I go to www.example.com is used the default locale if I go to www.example.com/fr is used fr locale if I go to www.example.com/en is used locale en Looking at the symfony website, and is also specified in the documentation Chapter routes, if I go to www.example.com and all other routes without specifying the place he uses the default locale without changing the url! – Lughino Jul 16 '13 at 20:27
  • 1
    Fair enough. This method makes it so the locale is always in the URL, which I personally don't mind. Especially that it keeps annotations simple and totally decoupled from locale settings. – Pier-Luc Gendreau Jul 16 '13 at 22:12
  • But does not solve the problem posed .. However optimal solution! – Lughino Jul 16 '13 at 22:18
  • how do you do this in symfony 3? – Joseph Astrahan Jan 11 '16 at 00:50
  • You should probably create a new question for Symfony3 – Pier-Luc Gendreau Jan 12 '16 at 16:21
4

The Joseph Astrahan's solution of LocalRewriteListener works except for route with params because of $routePath == "/{_locale}".$path)

Ex : $routePath = "/{_locale}/my/route/{foo}" is different of $path = "/{_locale}/my/route/bar"

I had to use UrlMatcher (link to Symfony 2.7 api doc) for matching the actual route with the url.

I change the isLocaleSupported for using browser local code (ex : fr -> fr_FR). I use the browser locale as key and the route locale as value. I have an array like this array(['fr_FR'] => ['fr'], ['en_GB'] => 'en'...) (see the parameters file below for more information)

The changes :

  • Check if the local given in request is suported. If not, use the default locale.
  • Try to match the path with the app route collection. If not do nothing (the app throw a 404 if route doesn't exist). If yes, redirect with the right locale in route param.

Here is my code. Works for any route with or without param. This add the locale only when {_local} is set in the route.

Routing file (in my case, the one in app/config)

app:
    resource: "@AppBundle/Resources/config/routing.yml"
    prefix:   /{_locale}/
    requirements:
        _locale: '%app.locales%'
    defaults: { _locale: %locale%}

The parameter in app/config/parameters.yml file

locale: fr
app.locales: fr|gb|it|es
locale_supported:
    fr_FR: fr
    en_GB: gb
    it_IT: it
    es_ES: es

services.yml

app.eventListeners.localeRewriteListener:
    class: AppBundle\EventListener\LocaleRewriteListener
    arguments: ["@router", "%kernel.default_locale%", "%locale_supported%"]
    tags:
        - { name: kernel.event_subscriber }

LocaleRewriteListener.php

<?php
namespace AppBundle\EventListener;

use Symfony\Component\HttpFoundation\RedirectResponse;

use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;

class LocaleRewriteListener implements EventSubscriberInterface
{
    /**
     * @var Symfony\Component\Routing\RouterInterface
     */
    private $router;

    /**
    * @var routeCollection \Symfony\Component\Routing\RouteCollection
    */
    private $routeCollection;

    /**
    * @var urlMatcher \Symfony\Component\Routing\Matcher\UrlMatcher;
    */
    private $urlMatcher;

    /**
     * @var string
     */
    private $defaultLocale;

    /**
     * @var array
     */
    private $supportedLocales;

    /**
     * @var string
     */
    private $localeRouteParam;

    public function __construct(RouterInterface $router, $defaultLocale = 'fr', array $supportedLocales, $localeRouteParam = '_locale')
    {
        $this->router = $router;
        $this->routeCollection = $router->getRouteCollection();
        $this->defaultLocale = $defaultLocale;
        $this->supportedLocales = $supportedLocales;
        $this->localeRouteParam = $localeRouteParam;
        $context = new RequestContext("/");
        $this->matcher = new UrlMatcher($this->routeCollection, $context);
    }

    public function isLocaleSupported($locale)
    {
        return array_key_exists($locale, $this->supportedLocales);
    }

    public function onKernelRequest(GetResponseEvent $event)
    {
        //GOAL:
        // Redirect all incoming requests to their /locale/route equivalent when exists.
        // Do nothing if it already has /locale/ in the route to prevent redirect loops
        // Do nothing if the route requested has no locale param

        $request = $event->getRequest();
        $baseUrl = $request->getBaseUrl();
        $path = $request->getPathInfo();

        //Get the locale from the users browser.
        $locale = $request->getPreferredLanguage();

        if ($this->isLocaleSupported($locale)) {
            $locale = $this->supportedLocales[$locale];
        } else if ($locale == ""){
            $locale = $request->getDefaultLocale();
        }

        $pathLocale = "/".$locale.$path;

        //We have to catch the ResourceNotFoundException
        try {
            //Try to match the path with the local prefix
            $this->matcher->match($pathLocale);
            $event->setResponse(new RedirectResponse($baseUrl.$pathLocale));
        } catch (\Symfony\Component\Routing\Exception\ResourceNotFoundException $e) {

        } catch (\Symfony\Component\Routing\Exception\MethodNotAllowedException $e) {

        }
    }

    public static function getSubscribedEvents()
    {
        return array(
            // must be registered before the default Locale listener
            KernelEvents::REQUEST => array(array('onKernelRequest', 17)),
        );
    }
}
4

Symfony3

app:
resource: "@AppBundle/Controller/"
type:     annotation
prefix: /{_locale}
requirements:
    _locale: en|bg|  # put a pipe "|" last
Marko Krustev
  • 109
  • 2
  • 3
2

There is my Solution, it makes this process faster!

Controller:

/**
 * @Route("/change/locale/{current}/{locale}/", name="locale_change")
 */
public function setLocaleAction($current, $locale)
{
    $this->get('request')->setLocale($locale);
    $referer = str_replace($current,$locale,$this->getRequest()->headers->get('referer'));

    return $this->redirect($referer);
}

Twig:

<li {% if app.request.locale == language.locale %} class="selected" {% endif %}>
    <a href="{{ path('locale_change', { 'current' : app.request.locale,  'locale' : language.locale } ) }}"> {{ language.locale }}</a>
</li>
Vadim
  • 21
  • 1
2

I have a full solution to this that I discovered after some research. My solution assumes that you want every route to have a locale in front of it, even login. This is modified to support Symfony 3, but I believe it will still work in 2.

This version also assumes you want to use the browsers locale as the default locale if they go to a route like /admin, but if they go to /en/admin it will know to use en locale. This is the case for example #2 below.

So for example:

1. User Navigates To ->  "/"         -> (redirects)    -> "/en/"
2. User Navigates To ->  "/admin"    -> (redirects)    -> "/en/admin"
3. User Navigates To ->  "/en/admin" -> (no redirects) -> "/en/admin"

In all scenarios the locale will be set correctly how you want it for use throughout your program.

You can view the full solution below which includes how to make it work with login and security, otherwise the Short Version will probably work for you:

Full Version

Symfony 3 Redirect All Routes To Current Locale Version

Short Version

To make it so that case #2 in my examples is possible you need to do so using a httpKernal listner

LocaleRewriteListener.php

<?php
//src/AppBundle/EventListener/LocaleRewriteListener.php
namespace AppBundle\EventListener;

use Symfony\Component\HttpFoundation\RedirectResponse;

use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\RouteCollection;

class LocaleRewriteListener implements EventSubscriberInterface
{
    /**
     * @var Symfony\Component\Routing\RouterInterface
     */
    private $router;

    /**
    * @var routeCollection \Symfony\Component\Routing\RouteCollection
    */
    private $routeCollection;

    /**
     * @var string
     */
    private $defaultLocale;

    /**
     * @var array
     */
    private $supportedLocales;

    /**
     * @var string
     */
    private $localeRouteParam;

    public function __construct(RouterInterface $router, $defaultLocale = 'en', array $supportedLocales = array('en'), $localeRouteParam = '_locale')
    {
        $this->router = $router;
        $this->routeCollection = $router->getRouteCollection();
        $this->defaultLocale = $defaultLocale;
        $this->supportedLocales = $supportedLocales;
        $this->localeRouteParam = $localeRouteParam;
    }

    public function isLocaleSupported($locale) 
    {
        return in_array($locale, $this->supportedLocales);
    }

    public function onKernelRequest(GetResponseEvent $event)
    {
        //GOAL:
        // Redirect all incoming requests to their /locale/route equivlent as long as the route will exists when we do so.
        // Do nothing if it already has /locale/ in the route to prevent redirect loops

        $request = $event->getRequest();
        $path = $request->getPathInfo();

        $route_exists = false; //by default assume route does not exist.

        foreach($this->routeCollection as $routeObject){
            $routePath = $routeObject->getPath();
            if($routePath == "/{_locale}".$path){
                $route_exists = true;
                break;
            }
        }

        //If the route does indeed exist then lets redirect there.
        if($route_exists == true){
            //Get the locale from the users browser.
            $locale = $request->getPreferredLanguage();

            //If no locale from browser or locale not in list of known locales supported then set to defaultLocale set in config.yml
            if($locale==""  || $this->isLocaleSupported($locale)==false){
                $locale = $request->getDefaultLocale();
            }

            $event->setResponse(new RedirectResponse("/".$locale.$path));
        }

        //Otherwise do nothing and continue on~
    }

    public static function getSubscribedEvents()
    {
        return array(
            // must be registered before the default Locale listener
            KernelEvents::REQUEST => array(array('onKernelRequest', 17)),
        );
    }
}

To understand how that is working look up the event subscriber interface on symfony documentation.

To activate the listner you need to set it up in your services.yml

services.yml

# Learn more about services, parameters and containers at
# http://symfony.com/doc/current/book/service_container.html
parameters:
#    parameter_name: value

services:
#    service_name:
#        class: AppBundle\Directory\ClassName
#        arguments: ["@another_service_name", "plain_value", "%parameter_name%"]
     appBundle.eventListeners.localeRewriteListener:
          class: AppBundle\EventListener\LocaleRewriteListener
          arguments: ["@router", "%kernel.default_locale%", "%locale_supported%"]
          tags:
            - { name: kernel.event_subscriber }

Finally this refers to variables that need to be defined in your config.yml

config.yml

# Put parameters here that don't need to change on each machine where the app is deployed
# http://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
parameters:
    locale: en
    app.locales: en|es|zh
    locale_supported: ['en','es','zh']

Finally, you need to make sure all your routes start with /{locale} for now on. A sample of this is below in my default controller.php

<?php

namespace AppBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;

/**
* @Route("/{_locale}", requirements={"_locale" = "%app.locales%"})
*/
class DefaultController extends Controller
{

    /**
     * @Route("/", name="home")
     */
    public function indexAction(Request $request)
    {
        $translated = $this->get('translator')->trans('Symfony is great');

        // replace this example code with whatever you need
        return $this->render('default/index.html.twig', [
            'base_dir' => realpath($this->container->getParameter('kernel.root_dir').'/..'),
            'translated' => $translated
        ]);
    }

    /**
     * @Route("/admin", name="admin")
     */
    public function adminAction(Request $request)
    {
        $translated = $this->get('translator')->trans('Symfony is great');

        // replace this example code with whatever you need
        return $this->render('default/index.html.twig', [
            'base_dir' => realpath($this->container->getParameter('kernel.root_dir').'/..'),
            'translated' => $translated
        ]);
    }
}
?>

Note the requirements requirements={"_locale" = "%app.locales%"}, this is referencing the config.yml file so you only have to define those requirements in one place for all routes.

Hope this helps someone :)

Community
  • 1
  • 1
Joseph Astrahan
  • 8,659
  • 12
  • 83
  • 154
1

We created a custom RoutingLoader that adds a localized version to all routes. You inject an array of additional locales ['de', 'fr'] and the Loader adds a route for each additional locale. The main advantage is, that for your default locale, the routes stay the same and no redirect is needed. Another advantage is, that the additionalRoutes are injected, so they can be configured differently for multiple clients/environments, etc. And much less configuration.

partial_data                       GET      ANY      ANY    /partial/{code}
partial_data.de                    GET      ANY      ANY    /de/partial/{code}
partial_data.fr                    GET      ANY      ANY    /fr/partial/{code}

Here is the loader:

<?php

namespace App\Routing;

use Symfony\Component\Config\Loader\Loader;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;

class I18nRoutingLoader extends Loader
{
const NAME = 'i18n_annotation';

private $projectDir;
private $additionalLocales = [];

public function __construct(string $projectDir, array $additionalLocales)
{
    $this->projectDir = $projectDir;
    $this->additionalLocales = $additionalLocales;
}

public function load($resource, $type = null)
{
    $collection = new RouteCollection();
    // Import directly for Symfony < v4
    // $originalCollection = $this->import($resource, 'annotation')
    $originalCollection = $this->getOriginalRouteCollection($resource);
    $collection->addCollection($originalCollection);

    foreach ($this->additionalLocales as $locale) {
        $this->addI18nRouteCollection($collection, $originalCollection, $locale);
    }

    return $collection;
}

public function supports($resource, $type = null)
{
    return self::NAME === $type;
}

private function getOriginalRouteCollection(string $resource): RouteCollection
{
    $resource = realpath(sprintf('%s/src/Controller/%s', $this->projectDir, $resource));
    $type = 'annotation';

    return $this->import($resource, $type);
}

private function addI18nRouteCollection(RouteCollection $collection, RouteCollection $definedRoutes, string $locale): void
{
    foreach ($definedRoutes as $name => $route) {
        $collection->add(
            $this->getI18nRouteName($name, $locale),
            $this->getI18nRoute($route, $name, $locale)
        );
    }
}

private function getI18nRoute(Route $route, string $name, string $locale): Route
{
    $i18nRoute = clone $route;

    return $i18nRoute
        ->setDefault('_locale', $locale)
        ->setDefault('_canonical_route', $name)
        ->setPath(sprintf('/%s%s', $locale, $i18nRoute->getPath()));
}

private function getI18nRouteName(string $name, string $locale): string
{
    return sprintf('%s.%s', $name, $locale);
}
}

Service definition (SF4)

App\Routing\I18nRoutingLoader:
    arguments:
        $additionalLocales: "%additional_locales%"
    tags: ['routing.loader']

Routing definition

frontend:
    resource: ../../src/Controller/Frontend/
    type: i18n_annotation #localized routes are added

api:
    resource: ../../src/Controller/Api/
    type: annotation #default loader, no routes are added
Fabian Schmick
  • 1,616
  • 3
  • 23
  • 32
HKandulla
  • 1,101
  • 12
  • 17
0

Maybe I solved this in a reasonably simple way:

example:
    path:      '/{_locale}{_S}example'
    defaults:  { _controller: 'AppBundle:Example:index' , _locale="de" , _S: "/" }
    requirements:
        _S: "/?"
        _locale: '|de|en|fr'

Curious about the judgement of the critics ... Best wishes, Greg

Gregor
  • 1
  • 1
0
root:
    pattern: /
    defaults:
        _controller: FrameworkBundle:Redirect:urlRedirect
        path: /en
        permanent: true

How to configure a redirect to another route without a custom controller

slk500
  • 703
  • 1
  • 8
  • 13
  • For SF 4 : https://symfony.com/doc/4.2/routing/redirect_in_config.html#redirecting-using-a-path – Pierre Oct 06 '19 at 09:21
0

I use annotations, and i will do

/**
 * @Route("/{_locale}/example", defaults={"_locale"=""})
 * @Route("/example", defaults={"_locale"="en"}, , requirements = {"_locale" = "fr|en|uk"})
 */

But for yml way, try some equivalent...

webda2l
  • 6,686
  • 2
  • 26
  • 28
  • 1
    For information, some changes about the locale are in comming for the v2.1. Locale will be only in the request, no more in session https://github.com/symfony/symfony/commit/74bc699b270122b70b1de6ece47c726f5df8bd41 – webda2l Oct 26 '11 at 15:51
  • 1
    I don't want to use the same URL to display a resource in many different languages based on the user's locale. I want to use a default URL to display a resource in the website default language. For example, that is what Apple do in their website : http://apple.com => US "default" version ; http://apple.com/uk : uk version ; http://apple.com/fr : French version, etc. – Ka. Oct 26 '11 at 16:36
-3

I think you could simply add a route like this:

example: 
pattern:  /example 
defaults: { _controller: ExampleBundle:Example:index } 

This way, the locale would be the last locale selected by the user, or the default locale if user locale has not been set. You might also add the "_locale" parameter to the "defaults" in your routing config if you want to set a specific locale for /example:

example: 
pattern:  /example 
defaults: { _controller: ExampleBundle:Example:index, _locale: fr }

I don't know if there's a better way to do this.

alghimo
  • 2,899
  • 18
  • 11