Symfony 3, detect browser language

6.7k views Asked by At

I use Symfony 3. My website is in 2 languages, French and English and people can switch via a select form. Default language is French. Main URL are: example.com/fr for French version and example.com/en for English version

Well, now, I will like when the user arrives to the website to detect his browser language and redirect to the correct language automatically. Exemple, if the browser is in French, he is redirected to the French version : example.com/fr Else he is redirected to the English version: example.com/en

Is there a way to do that properly?

Thank you for your help

2

There are 2 answers

3
dbrumann On BEST ANSWER

If you don't want to rely on other bundles like JMSI18nRoutingBundle you have to make yourself familiar with Symfony's Event system, e.g. by reading up on the HttpKernel.

For your case you want to hook into the kernel.request event.

Typical Purposes: To add more information to the Request, initialize parts of the system, or return a Response if possible (e.g. a security layer that denies access).

In your custom EventListener you can listen to that event add information to the Request-object used in your router. It could look something like this:

class LanguageListener implements EventSubscriberInterface
{
    private $supportedLanguages;

    public function __construct(array $supportedLanguages)
    {
        if (empty($supportedLanguages)) {
            throw new \InvalidArgumentException('At least one supported language must be given.');
        }

        $this->supportedLanguages = $supportedLanguages;
    }

    public static function getSubscribedEvents()
    {
        return [
            KernelEvents::REQUEST  => ['redirectToLocalizedHomepage', 100],
        ];
    }

    public function redirectToLocalizedHomepage(GetResponseEvent $event)
    {
        // Do not modify sub-requests
        if (KernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
            return;
        }
        // Assume all routes except the frontpage use the _locale parameter
        if ($event->getRequest()->getPathInfo() !== '/') {
            return;
        }

        $language = $this->supportedLanguages[0];
        if (null !== $acceptLanguage = $event->getRequest()->headers->get('Accept-Language')) {
            $negotiator = new LanguageNegotiator();
            $best       = $negotiator->getBest(
                $event->getRequest()->headers->get('Accept-Language'),
                $this->supportedLanguages
            );

            if (null !== $best) {
                $language = $best->getType();
            }
        }

        $response = new RedirectResponse('/' . $language);
        $event->setResponse($response);
    }
}

This listener will check the Accept-Language header of the request and use the Negotiation\LanguageNegotiator to determine the best locale. Be careful as I didn't add the use statements, but they should be fairly obvious.

For a more advanced version you can just read the source for the LocaleChoosingListener from JMSI18nRoutingBundle.

Doing this is usually only required for the frontpage, which is why both the example I posted and the one from the JMSBundle exclude all other paths. For those you can just use the special parameter _locale as described in the documentation:

https://symfony.com/doc/current/translation/locale.html#the-locale-and-the-url

The Symfony documentation also contains an example how to read the locale and make it sticky in a session using a Listener: https://symfony.com/doc/current/session/locale_sticky_session.html This example also shows how to register the Listener in your services.yml.

0
Milan Švehla On

Slight changes to @dbrumann's answer to work with my use case and setup:

List of available locales are defined in services.yml file:

parameters:
  available_locales:
    - nl
    - en
    - cs

I wanted to determine the locale on any landing page of the website. In case the parsing fails, it fallbacks to _locale parameter or the default one.

class LocaleDetermineSubscriber implements EventSubscriberInterface
{
    private $defaultLocale;
    private $parameterBag;
    private $logger;

    public function __construct(ParameterBagInterface $parameterBag,
       LoggerInterface $logger, 
       $defaultLocale = 'en')
    {
        $this->defaultLocale = $defaultLocale;
        $this->parameterBag = $parameterBag;
        $this->logger = $logger;
    }

    public function onKernelRequest(RequestEvent $event)
    {
        $request = $event->getRequest();
        //do this on first request only
        if ($request->hasPreviousSession()) {
            return;
        }

        $allowedLocales = $this->parameterBag->get('available_locales'); //defined in services.yml
        $determinedLocale = null;

        // use locale from the user preference header
        $acceptLanguage = $event->getRequest()->headers->get('Accept-Language');
        if ($acceptLanguage != null) {
            $negotiator = new LanguageNegotiator();
            try {
                $best = $negotiator->getBest($acceptLanguage, $allowedLocales);

                if ($best != null) {
                    $language = $best->getType();
                    $request->setLocale($language);
                    $determinedLocale = $language;
                }
            } catch (Exception $e) {
                $this->logger->warning("Failed to determine language from Accept-Language header " . $e);
            }
        }

        //check if locale is set with _locale parameter if user preference header parsing not happened
        if($determinedLocale == null) {
            if ($locale = $request->attributes->get('_locale')) {
                if(in_array($locale, $allowedLocales)) {
                    $request->getSession()->set('_locale', $locale);
                } else {
                    $request->setLocale($request->getSession()->get('_locale', $this->defaultLocale));
                }
            } else {
                //fallback to default
                $request->setLocale($this->defaultLocale);
            }
        }
    }

    public static function getSubscribedEvents()
    {
        return [
            // must be registered before (i.e. with a higher priority than) the default Locale listener
            KernelEvents::REQUEST => [['onKernelRequest', 25]],
        ];
    }
}

It uses the willdurand/negotiation package, so it needs to be installed first:

composer require willdurand/negotiation

https://packagist.org/packages/willdurand/negotiation