Making the Locale "Sticky" during a User's Session

Making the Locale "Sticky" during a User's Session

Symfony stores the locale setting in the Request, which means that this setting is not available in subsequent requests. In this article, you'll learn how to store the locale in the session, so that it'll be the same for every subsequent request.

Creating a LocaleListener

To simulate that the locale is stored in a session, you need to create and register a new event listener. The listener will look something like this. Typically, _locale is used as a routing parameter to signify the locale, though it doesn't really matter how you determine the desired locale from the request:

 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
// src/AppBundle/EventListener/LocaleListener.php
namespace AppBundle\EventListener;

use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class LocaleListener implements EventSubscriberInterface
{
    private $defaultLocale;

    public function __construct($defaultLocale = 'en')
    {
        $this->defaultLocale = $defaultLocale;
    }

    public function onKernelRequest(GetResponseEvent $event)
    {
        $request = $event->getRequest();
        if (!$request->hasPreviousSession()) {
            return;
        }

        // try to see if the locale has been set as a _locale routing parameter
        if ($locale = $request->attributes->get('_locale')) {
            $request->getSession()->set('_locale', $locale);
        } else {
            // if no explicit locale has been set on this request, use one from the session
            $request->setLocale($request->getSession()->get('_locale', $this->defaultLocale));
        }
    }

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

Then register the listener:

  • YAML
    1
    2
    3
    4
    5
    6
    services:
        app.locale_listener:
            class: AppBundle\EventListener\LocaleListener
            arguments: ['%kernel.default_locale%']
            tags:
                - { name: kernel.event_subscriber }
    
  • XML
    1
    2
    3
    4
    5
    6
    <service id="app.locale_listener"
        class="AppBundle\EventListener\LocaleListener">
        <argument>%kernel.default_locale%</argument>
    
        <tag name="kernel.event_subscriber" />
    </service>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    use AppBundle\EventListener\LocaleListener;
    use Symfony\Component\DependencyInjection\Definition;
    
    $container
        ->setDefinition('app.locale_listener', new Definition(
            LocaleListener::class,
            array('%kernel.default_locale%')
        ))
        ->addTag('kernel.event_subscriber')
    ;
    

That's it! Now celebrate by changing the user's locale and seeing that it's sticky throughout the request. Remember, to get the user's locale, always use the Request::getLocale method:

1
2
3
4
5
6
7
// from a controller...
use Symfony\Component\HttpFoundation\Request;

public function indexAction(Request $request)
{
    $locale = $request->getLocale();
}

Setting the Locale Based on the User's Preferences

You might want to improve this technique even further and define the locale based on the user entity of the logged in user. However, since the LocaleListener is called before the FirewallListener, which is responsible for handling authentication and setting the user token on the TokenStorage, you have no access to the user which is logged in.

Suppose you have defined a locale property on your User entity and you want to use this as the locale for the given user. To accomplish this, you can hook into the login process and update the user's session with this locale value before they are redirected to their first page.

To do this, you need an event listener for the security.interactive_login event:

 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
// src/AppBundle/EventListener/UserLocaleListener.php
namespace AppBundle\EventListener;

use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;

/**
 * Stores the locale of the user in the session after the
 * login. This can be used by the LocaleListener afterwards.
 */
class UserLocaleListener
{
    /**
     * @var Session
     */
    private $session;

    public function __construct(Session $session)
    {
        $this->session = $session;
    }

    /**
     * @param InteractiveLoginEvent $event
     */
    public function onInteractiveLogin(InteractiveLoginEvent $event)
    {
        $user = $event->getAuthenticationToken()->getUser();

        if (null !== $user->getLocale()) {
            $this->session->set('_locale', $user->getLocale());
        }
    }
}

Then register the listener:

  • YAML
    1
    2
    3
    4
    5
    6
    7
    # app/config/services.yml
    services:
        app.user_locale_listener:
            class: AppBundle\EventListener\UserLocaleListener
            arguments: ['@session']
            tags:
                - { name: kernel.event_listener, event: security.interactive_login, method: onInteractiveLogin }
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <!-- app/config/services.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="app.user_locale_listener"
                class="AppBundle\EventListener\UserLocaleListener">
    
                <argument type="service" id="session"/>
    
                <tag name="kernel.event_listener"
                    event="security.interactive_login"
                    method="onInteractiveLogin" />
            </service>
        </services>
    </container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    // app/config/services.php
    use AppBundle\EventListener\UserLocaleListener;
    use Symfony\Component\DependencyInjection\Reference;
    
    $container
        ->register('app.user_locale_listener', UserLocaleListener::class)
        ->addArgument(new Reference('session'))
        ->addTag(
            'kernel.event_listener',
            array('event' => 'security.interactive_login', 'method' => 'onInteractiveLogin')
        );
    

Caution

In order to update the language immediately after a user has changed their language preferences, you need to update the session after an update to the User entity.

This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.