Skip to content

Redirect visitors to their preferred language ​

This recipe redirects visitors arriving at the language-neutral / URL to a supported localized URL.

The language is selected in this order:

  1. the site_language cookie, when it contains a supported locale;
  2. the Accept-Language request header;
  3. the language selection page, when no supported locale matches.

If no supported locale can be selected, the template displays a language selection page instead.

Define the language-selection route ​

Create a route for / that renders a dedicated static template:

yaml
emsch_language_selection:
    config:
        path: /
    template_static: template/view/language_select.html.twig

The localized routes remain responsible for rendering the actual website pages. For example, the language-selection page can link to emsch_path.fr and emsch_path.nl.

Select and redirect in Twig ​

This template reads request-specific data. Keep the following apparently unused assignment: it forces eMS to treat the response as private, which is required because the result depends on the visitor cookie and request headers.

twig
{%- set user = app.user -%}

In the language-selection template, read the cookie first. When it is missing or unsupported, let Symfony match the Accept-Language header against the supported locales:

twig
{%- set supportedLocales = ['fr', 'nl'] -%}
{%- set preferredLocale = app.request.cookies.get('site_language') -%}

{%- if preferredLocale not in supportedLocales -%}
    {%- set preferredLocale = app.request.getPreferredLanguage(['']|merge(supportedLocales)) -%}
{%- endif -%}

{%- if preferredLocale in supportedLocales -%}
    {%- do emsch_http_error(302, headers: {
        'Location': "/#{preferredLocale}",
    }) -%}
{%- endif -%}

Request::getPreferredLanguage() parses the Accept-Language header and returns the best match from the locales passed to it. Symfony returns the first locale from that list when the header does not match any supported language, so the empty first entry is intentional: it prevents an automatic redirect to the first supported locale and leaves the language chooser visible.

The redirect is relative to the current host. If the localized entry point is generated by a route, you can use path() instead of constructing the Location value manually.

It might be useful to disable the redirection within a eMS channel context:

twig
{%- if user and preferredLocale in supportedLocales -%}
    {%- do emsch_http_error(302, headers: {
        'Location': path('emsch_path', {_locale: preferredLocale, path: 'home'}),
    }) -%}
{%- endif -%}

Display a language chooser ​

When there is no matching cookie or Accept-Language value, render links for all supported locales:

twig
{%- for locale in supportedLocales -%}
    <a href="{{ path("emsch_path.#{locale}") }}" lang="{{ locale }}">
        {{- 'locale'|trans({}, trans_default_domain, locale) -}}
    </a>
{%- endfor -%}

The language-selection template only reads the site_language cookie; it does not need a PHP controller. Keep the cookie-writing part of your existing language-choice mechanism, and validate the locale against the supported-locale list before storing it. On later visits, a supported cookie takes precedence over the browser preference.

If the cookie is not available, expired, or contains an unsupported value, the Accept-Language fallback is used again.

Caching considerations ​

The root response depends on both a cookie and the Accept-Language header. Do not serve this language-selection response from a shared cache without varying the cache key accordingly. A private response, or a no-store/no-cache policy, avoids redirecting all visitors to the language selected by the first cached request.