Symfony2 wrong locale detection? - symfony

Following Symfony2 guide about translation i found that inferred locale from http headers (stored in $this->get('session')->getLocale()) is wrong (sent it, inferred en):
Host localhost User-Agent Mozilla/5.0 (Windows NT 6.1; WOW64;
rv:7.0.1) Gecko/20100101 Firefox/7.0.1
Accept text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8
Accept-Language it-it,it;q=0.8,en-us;q=0.5,en;q=0.3
Is this a normal behaviour? Or should i set something in order to get localization working out of the box?

I looked more thoroughly onto the code today, because I was experiencing the same problem as you, and it appears that the language comes from Session::getLocale(). But Symfony2 never calls Session::setLocale(), and sets the locale member of the Session object. A google search for "symfony2 session setlocale" leads to this § of the documentation
So I ended up putting this line on top of the controller I was working on, and it worked :
$this->getRequest()->getSession()->setLocale(
$this->getRequest()->getPreferredLanguage());
Now I guess this is not acceptable, because you're not going to add this on top of each and every controller. Plus, this should not be done for every request, it should only be done for the first one, when the user has no session yet. If anyone knows how to do this feel free to edit this answer.

per HTTP-Standard you should be using a different URL for each translated version of the page. What remains is a simple action that will infer the best-to-use locale from the request and redirect to the corresponding page:
/**
* #Route("/")
*/
public function localeRedirectAction() {
/* #var $request \Symfony\Component\HttpFoundation\Request */
/* #var $session \Symfony\Component\HttpFoundation\Session */
$req = $this->getRequest();
$session = $this->get('session');
$session->setLocale($req->getPreferredLanguage(array('de', 'en')));
return $this->redirect($this->generateUrl('home'));
}
if you need to do this for any page, you'll basically need to do the same, but within a Listener for the kernel.request-Event. In order to be reliably called after the route-matcher did it's work you should set the priority of the listener to a value < 0:
# services.yml
services:
my_locale_listener:
class: Namespace\LocaleListener
tags: [{ name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: -255 }]
arguments: [ #service_container, [ 'en', 'fr', 'de', 'es', 'it' ] ]
the listener would then look like this:
class LocaleListener {
public function __construct($container, $availableLocales) {
$this->container = $container;
$this->availableLocales = $availableLocales;
}
public function onKernelRequest(GetResponseEvent $e) {
$req = $e->getRequest();
$locale = $req->getPreferredLanguage($this->availableLocales);
// generate a new URL from the current route-name and -params, overwriting
// the current locale
$routeName = $req->attributes->get('_route');
$routeParams = array_merge($req->attributes->all(), array('_locale' => $locale));
$localizedUrl = $this->container->get('router')->generateUrl($routeName, $routeParams);
$e->setResponse(new RedirectResponse($localizedUrl));
}
}
P.S. i'm not entirely confident that this code actually works, but it should give a basic idea on how this could be done.

You can register listener like follow:
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
class LocaleListener
{
private $container;
private $defaultLocale;
public function __construct(ContainerInterface $container, $defaultLocale = 'nl')
{
$this->container = $container;
$this->defaultLocale = $defaultLocale;
}
public function onKernelRequest(GetResponseEvent $event)
{
if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
return;
}
if (!$this->container->has('session')) {
return;
}
$session = $this->container->get('session');
$session->setLocale($this->defaultLocale);
}
}
(gist)
Just after framework session setup stage:
<service id="my.event_listener.locale_listener" class="MyBundle\MyEventListener\LocaleListener">
<tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" priority="100" />
<argument type="service" id="service_container" />
</service>
(gist)

I wrote a LocaleListener which is not redirecting you to a locale-specific url, it does however set the locale settings for you ;)
Code in the services.yml
services:
acme.demobundle.listener.request:
class: Namespace\LocaleListener
tags: [{ name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: -255 }]
arguments: [ #service_container, [ 'en', 'fr', 'de', 'it', 'es' ] ]
and the actual listener:
class LocaleListener
{
protected $container;
protected $availableLocales;
public function __construct(\Symfony\Component\DependencyInjection\Container $container, $availableLocales) {
$this->container = $container;
$this->availableLocales = $availableLocales;
}
public function onKernelRequest(GetResponseEvent $e) {
$req = $e->getRequest();
$locale = $req->getPreferredLanguage($this->availableLocales);
$session = $this->container->get('session');
$session->setLocale($locale);
}
}

Related

Call container inside ExceptionListener

I am using Symfony and i have created custom ExceptionListener to handle error.
class ExceptionListener
{
protected $templating;
protected $kernel;
public function __construct(EngineInterface $templating, $kernel)
{
$this->templating = $templating;
$this->kernel = $kernel;
}
public function onKernelException(GetResponseForExceptionEvent $event)
{
// exception object
$exception = $event->getException();
// new Response object
$response = new Response();
$response->setContent(
// create you custom template AcmeFooBundle:Exception:exception.html.twig
$this->templating->render(
'Exception/exception.html.twig',
array('exception' => $exception)
)
);
// HttpExceptionInterface is a special type of exception
// that holds status code and header details
if ($exception instanceof HttpExceptionInterface) {
$response->setStatusCode($exception->getStatusCode());
$response->headers->replace($exception->getHeaders());
} else {
$this->container->get('monolog.logger.db')->info('something happened 34', [
'foo' => 'bar'
]);
$response->setStatusCode(500);
}
if($exception instanceof FatalThrowableError){
return $this->templating->render(
'Exception/exception.html.twig'
);
}
// set the new $response object to the $event
$event->setResponse($response);
}
}
and in service
kernel.listener.acme_foo_exception_listener:
class: AppBundle\Listener\ExceptionListener
arguments: [#templating, #kernel]
tags:
- { name: kernel.event_listener, event: kernel.exception, method: onKernelException }
My aim is to when symfony throws exception i need to log error in database so i have created Logger event as per below link and it works fine when i called in controller but this event doesn't work when i called inside ExceptionListener.
I got following error
Notice: Undefined property:
AppBundle\Listener\ExceptionListener::$container in
can any one help me how i can pass container inside Listener
As said by geoforce your service doesn't know about the container. Quick fix for this by changing the service arguments:
arguments: [#templating, #container]
While changing the listener constructor to:
public function __construct(EngineInterface $templating, ContainerInterface $container)
{
$this->container = $container;
// ...
This should work, but injecting the entire container is quite an overkill and should definitely be done differently. Inject just what you need:
arguments: [#templating, '#monolog.logger.db']
And your constructor:
public function __construct(EngineInterface $templating,
LoggerInterface $logger)
{
$this->logger = $logger;
// ...
Log with $this->logger->info(...).
Since you've said that you're new to Symfony, I'd heavily recommend reading the DI component (http://symfony.com/doc/current/components/dependency_injection.html) docs. Understanding what DI does and how it works is mandatory to work with MVC frameworks like Symfony.
Like the error says, you are trying to access a property that does not exist:
$this->container->get('monolog.logger.db')->info('something happened 34', [
'foo' => 'bar'
]);
The container property is never declared nor assigned. If you want to access your logging service inject it in your service definition, like you did with the templating and kernel services.
Updated service definition:
kernel.listener.acme_foo_exception_listener:
class: AppBundle\Listener\ExceptionListener
arguments: [#templating, #kernel, #monolog.logger.db]
tags:
- { name: kernel.event_listener, event: kernel.exception, method: onKernelException }
And update your class constructor to accept the log service as the third argument.

Soanta Admin Bundle Locale

I have a problem with translation. I use
symfony 2.7
sonata admin-bundle 2.3
I have create the interactive login listener, when the user log in the application I get the user locale and set the session _locale, but this is ignore in sonata.
Listener is
class UserLocaleListener {
/**
* #var Session
*/
private $container;
public function __construct(Session $session)
{
$this->session = $session;
}
public function onInteractiveLogin(InteractiveLoginEvent $event)
{
$request = $event->getRequest();
$user = $event->getAuthenticationToken()->getUser();
if (null !== $user->getLocale()) {
$this->session->set('_locale', $user->getLocale());
//$request->setLocale($user->getLocale());
var_dump($request->getSession()->get('_locale'));
}
}
}
in service.yml add
app.user_locale_listener:
class: xxxxxx\xxxxxxxx\EventListener\UserLocaleListener
arguments: ["#session"]
tags:
- { name: kernel.event_listener, event: security.interactive_login, method: onInteractiveLogin }
Where is my mistake ?
the locale is set on the request, and will not "stick" , so each request it will be the default again, unless you do something like this:
http://symfony.com/doc/current/cookbook/session/locale_sticky_session.html
Wich will on each request, take the locale from the session, and set it on the current request.
(make sure that LocaleListener has a lower priority then your UserLocaleListener, so that it runs after it)

Symfony2 locale detection: not considering _locale in session

I'm trying to implement a LocaleListener that detects user's preferred language (considering Accept-Language header) and stores it in session to avoid checking it every request. I've developed the code below to accomplish this:
public function onKernelRequest(GetResponseEvent $event) {
$request = $event->getRequest();
if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
return;
}
$preferredLocale = $request->getPreferredLanguage($this->availableLocales);
if ($this->container->has('session')) {
$session = $this->container->get('session');
if (!$session->has('_locale')) {
$session->set('_locale', $preferredLocale);
}
} else {
$request->setLocale($preferredLocale);
}
}
The code is working, the preferred language is being stored in session, but symfony isn't considering the locale stored in session to translate strings. In my case, my preferred language was 'pt_BR' and when I escape:
{{ app.request.locale }}
symfony is escaping 'en'. Shouldn't symfony be considering the value stored in session('_locale') to define request locale? Is this a correct behavior? How can I accomplish that?
Here is a working language listener. the second method is to change the language to the users preferences, which the user chooses. You can omit this method, if your user haven't the facility to define their language.
<?php
namespace Acme\UserBundle\EventListener;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
class LanguageListener
{
private $session;
public function setSession(Session $session)
{
$this->session = $session;
}
/**
* kernel.request event. If a guest user doesn't have an opened session, locale is equal to
* "undefined" as configured by default in parameters.ini. If so, set as a locale the user's
* preferred language.
*
* #param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
*/
public function setLocaleForUnauthenticatedUser(GetResponseEvent $event)
{
if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
return;
}
$request = $event->getRequest();
if ('undefined' == $request->getLocale()) {
if ($locale = $request->getSession()->get('_locale')) {
$request->setLocale($locale);
} else {
$request->setLocale($request->getPreferredLanguage());
}
}
}
/**
* security.interactive_login event. If a user chose a language in preferences, it would be set,
* if not, a locale that was set by setLocaleForUnauthenticatedUser remains.
*
* #param \Symfony\Component\Security\Http\Event\InteractiveLoginEvent $event
*/
public function setLocaleForAuthenticatedUser(InteractiveLoginEvent $event)
{
$user = $event->getAuthenticationToken()->getUser();
if ($lang = $user->getLanguage()) {
$this->session->set('_locale', $lang);
}
}
}
in your services.yml:
services:
acme.language.interactive_login_listener:
class: Acme\UserBundle\EventListener\LanguageListener
calls:
- [ setSession, [#session] ]
tags:
- { name: kernel.event_listener, event: security.interactive_login, method: setLocaleForAuthenticatedUser }
acme.language.kernel_request_listener:
class: Acme\UserBundle\EventListener\LanguageListener
tags:
- { name: kernel.event_listener, event: kernel.request, method: setLocaleForUnauthenticatedUser }
Oh, and you have to define an undefined fallback_language in config.yml to get it work.
framework:
translator: { fallback: "undefined" }
default_locale: "en"
As of symfony 2.1, the locale is not stored in the session, but in the request. What you can do to solve it:
restore the old way of saving the locale. You can read how to do this in the upgrade file
edit the LocaleListener to store the locale in the request:
if (!$request->attributes->has('locale')) {
$request->setLocale(...);
}

SonataAdminBundle use Event Subscriber

I try to set the current authenticated user within a SonataAdminBundle Form:
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('title')
->add('content')
->add('slug')
//->add('user')
;
}
Since I don't want the user to select/choose it's own user, I guess I'd like to set the username during the procedure the form was sent by the user (without any user object).
(Note: I'd like to use this in a couple of forms, so I'll need a general solution.)
What I've done now was setting up an EventListener after I've read this: http://symfony.com/doc/current/cookbook/service_container/event_listener.html
class PostListener
{
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function prePersist(LifeCycleEventArgs $args)
{
$entity = $args->getEntity();
//Get user
$securityContext = $this->container->get('security.context');
$user = $securityContext->getToken()->getUser();
//Set authenticated user as autor
$entity->setUser($user);
}
}
Including service:
post.listener:
class: Backender\BlogBundle\Listener\PostListener
calls:
- [ setContainer, [ #service_container ] ]
tags:
- { name: doctrine.event_listener, event: prePersist }
Not sure now, if this is the right approach, because I wan't to specify the form where I want to set the user. (This one will want to set user on every form right?)
With some more research it seems like I've to use an Event Subscriber like this: http://symfony.com/doc/2.0/cookbook/form/dynamic_form_generation.html
In this example they use FormEvents::PRE_SET_DATA, I guess in my case I have to use POST_SET_DATA.
Here I've some problems!:
1: I'm quiet new to SonataAdminBundle and there we use protected function configureFormFields(FormMapper $formMapper)... where ->addEventSubscriber() isn't available?
2: Is this the right way?, I really didn't find any examples like what I need here for sonata-admin.
I'm thankful for every help!
Best regards...
Try with $formMapper->getFormBuilder()->addEventSubscriber($subscriber);
In the end I found 2 possible solutions for my problem. I'd like to describe these possibilities here because there isn't that much stuff to find about.
Event listener with query of instance of
Call as service:
blog.post.listener:
class: Acme\BlogBundle\Listener\PostListener
arguments: ['#service_container']
tags:
- { name: doctrine.event_listener, event: prePersist }
The listener class:
class PostListener
{
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function prePersist(LifeCycleEventArgs $args)
{
$entity = $args->getEntity();
//Get user
$securityContext = $this->container->get('security.context');
$user = $securityContext->getToken()->getUser();
if ($entity instanceof Post) {
//Set authenticated user as autor
$entity->setUser($user);
}
}
}
The second way (and maybe the easier) is to use prePersist in Admin Class of SonataAdminBundle
Call as service (use service container or security context as argument):
blog.admin.post:
class: Acme\BlogBundle\Admin\PostAdmin
tags:
- { name: sonata.admin, manager_type: orm, group: Article Handling, label: Posts }
arguments: [null, Acme\BlogBundle\Entity\Post, SonataAdminBundle:CRUD, #service_container]
PostAdmin:
(Note, i use container because i need further stuff in the future)
class PostAdmin extends Admin
{
protected $securityContext;
public function __construct($code, $class, $baseControllerName, ContainerInterface $container)
{
parent::__construct($code, $class, $baseControllerName);
$this->container = $container;
}
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('title')
->add('content')
;
}
public function prePersist($post)
{
$user = $this->container->get('security.context')->getToken()->getUser();
$post->setUser($user);
}
}
I solved adding events to the Admin Class by injecting the Event Dispatcher:
<service id="acme.listener.contract"
class="Acme\AppBundle\Event\ContractListener">
<argument type="service" id="twig"></argument>
<argument type="service" id="mailer"></argument>
<tag name="kernel.event_listener" event="contract.created" method="onContractCreated" />
</service>
Then in my admin class I add the event to an existing method:
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Security\Core\SecurityContextInterface;
class ContractAdmin extends Admin
{
protected $dispatcher;
/**
* Security Context
* #var \Symfony\Component\Security\Core\SecurityContextInterface
*/
protected $securityContext;
public function setEventDispatcher(EventDispatcherInterface $dispatcher)
{
$this->dispatcher = $dispatcher;
}
public function setSecurityContext(SecurityContextInterface $securityContext)
{
$this->securityContext = $securityContext;
}
public function prePersist($contract)
{
$user = $this->securityContext->getToken()->getUser();
$contract->setUser($user);
$event = new ContractEvent($contract);
$dispatcher = $this->dispatcher;
$dispatcher->dispatch(
ContractEvents::CONTRACT_CREATED,
$event
);
}
This allows me to add extra events I am sharing across my app at other places too.
In your case you can simply inject the Security Context only:
sonata.admin.contract:
class: Acme\AppBundle\Admin\ContractAdmin
tags:
- { name: sonata.admin, manager_type: orm, group: "Contracts", label_catalogue: "messages", label: "Auftragsübersicht" }
arguments:
- ~
- Acme\AppBundle\Entity\Contract
- ~
calls:
- [ setSecurityContext, [#security.context]]
- [ setEventDispatcher, [#event_dispatcher]]

Symfony2 default locale in routing

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.
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.
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!
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:
{{ targetLanguage }}
Clean & simple!
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)),
);
}
}
Symfony3
app:
resource: "#AppBundle/Controller/"
type: annotation
prefix: /{_locale}
requirements:
_locale: en|bg| # put a pipe "|" last
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 %}>
{{ language.locale }}
</li>
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 :)
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
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...
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
root:
pattern: /
defaults:
_controller: FrameworkBundle:Redirect:urlRedirect
path: /en
permanent: true
How to configure a redirect to another route without a custom controller
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.

Resources