Create a Language Switcher
We have localized routing up and running, but we can only switch locales by manually updating the URL. That won't do! Instead, let's add a language switcher! Up here, next to our search form, we need a link to this page, but one for each of our supported locales. But hmm... how can we generate a link to a route for a different locale?
Peek into the web profiler for this request, and notice a bunch of underscored request attributes. These are mostly internal to Symfony, but this _locale
one is important. When using localized routes, Symfony sets this to the current locale of the request. What's cool, is that you can use this as a route parameter to generate URLs to different locales.
For instance, to generate a URL for app_homepage
, if you pass _locale:
'fr' as a route parameter, the generated URL will be /fr
, regardless of the current locale.
Onto the widget!
Building the Language Switcher
In the tutorial/
directory, open language_switcher.html.twig
, select everything, and copy it. Now, in templates/base.html.twig
, find the <form>
tag, and just above it, paste:
// ... line 1 | |
<html lang="{{ app.locale }}"> | |
// ... lines 3 - 17 | |
<body class="bg-[url('../images/bg-planet.png')] bg-contain bg-no-repeat"> | |
<nav class="flex items-center justify-between flex-wrap py-6 mx-auto px-4"> | |
// ... lines 20 - 27 | |
<div class="flex items-center"> | |
<div | |
data-controller="dropdown" | |
class="mx-auto mr-2 relative" | |
> | |
<a | |
href="#" | |
class="block w-full py-2 px-3 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" | |
data-action="dropdown#toggle click@window->dropdown#hide keydown.esc@window->dropdown#hide" | |
> | |
<span class="text-gray-400">Language:</span> | |
<strong class="ml-1 uppercase">en</strong> | |
<i class="ml-1 fa-solid fa-chevron-down text-gray-400"></i> | |
</a> | |
<ul | |
hidden | |
data-dropdown-target="content" | |
class="absolute overflow-hidden z-10 right-0 mt-1 w-full text-gray-900 border border-gray-300 rounded-lg bg-gray-50 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white" | |
> | |
<li class="uppercase hover:bg-gray-100 hover:text-gray-900 block px-4 py-2 text-sm"> | |
<a class="block" href="https://example.com">fr</a> | |
</li> | |
</ul> | |
</div> | |
// ... lines 52 - 71 | |
</div> | |
</nav> | |
// ... lines 74 - 81 | |
</body> | |
</html> |
Head back to our app and refresh, and here it is! It's just a stub right now, but you can see how it should work. This button will show the current locale, and when you click it, there's a drop-down with links to the other locales.
Let's wire this baby up!
Back in base.html.twig
, find the anchor tag with the text "Language:". This is the button. For this hard-coded text, en
, we want the current locale, we know how to do this already, {{ app.locale }}
:
// ... line 1 | |
<html lang="{{ app.locale }}"> | |
// ... lines 3 - 17 | |
<body class="bg-[url('../images/bg-planet.png')] bg-contain bg-no-repeat"> | |
<nav class="flex items-center justify-between flex-wrap py-6 mx-auto px-4"> | |
// ... lines 20 - 27 | |
<div class="flex items-center"> | |
<div | |
data-controller="dropdown" | |
class="mx-auto mr-2 relative" | |
> | |
<a | |
href="#" | |
class="block w-full py-2 px-3 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" | |
data-action="dropdown#toggle click@window->dropdown#hide keydown.esc@window->dropdown#hide" | |
> | |
// ... line 38 | |
<strong class="ml-1 uppercase">{{ app.locale }}</strong> | |
// ... line 40 | |
</a> | |
// ... lines 42 - 54 | |
</div> | |
// ... lines 56 - 75 | |
</div> | |
</nav> | |
// ... lines 78 - 85 | |
</body> | |
</html> |
Down in the ul
, which is the drop-down menu, before the li
tag, add {% for locale in app.enabled_locales %}
. I told you configuring enabled_locales
would come in handy! Under the li
, add {% endfor %}
and indent the guts.
This now loops though all the enabled locales, but also the current locale. We want to exclude this, so add a condition before the li
tag: {% if locale != app.locale %}
. Add {% endif %}
below, and indent:
// ... line 1 | |
<html lang="{{ app.locale }}"> | |
// ... lines 3 - 17 | |
<body class="bg-[url('../images/bg-planet.png')] bg-contain bg-no-repeat"> | |
<nav class="flex items-center justify-between flex-wrap py-6 mx-auto px-4"> | |
// ... lines 20 - 27 | |
<div class="flex items-center"> | |
<div | |
data-controller="dropdown" | |
class="mx-auto mr-2 relative" | |
> | |
// ... lines 33 - 41 | |
<ul | |
hidden | |
data-dropdown-target="content" | |
class="absolute overflow-hidden z-10 right-0 mt-1 w-full text-gray-900 border border-gray-300 rounded-lg bg-gray-50 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white" | |
> | |
{% for locale in app.enabled_locales %} | |
{% if locale != app.locale %} | |
// ... lines 49 - 51 | |
{% endif %} | |
{% endfor %} | |
</ul> | |
</div> | |
// ... lines 56 - 75 | |
</div> | |
</nav> | |
// ... lines 78 - 85 | |
</body> | |
</html> |
For the link text inside the li
, use {{ locale }}
. For the href
, we could send them to the homepage for that locale... but we can do better! I want them to stay on the same page, just with a different locale.
Check this out: {{ path(app.current_route,
app.current_route_parameters|merge({_locale: locale})) }}. app.current_route
gives us the current page's route name, and app.current_route_parameters
gives us the current page's route parameters. Then we merge these with a new parameter, _locale: locale
. This will generate a URL for the same page, but with a different locale:
// ... line 1 | |
<html lang="{{ app.locale }}"> | |
// ... lines 3 - 17 | |
<body class="bg-[url('../images/bg-planet.png')] bg-contain bg-no-repeat"> | |
<nav class="flex items-center justify-between flex-wrap py-6 mx-auto px-4"> | |
// ... lines 20 - 27 | |
<div class="flex items-center"> | |
<div | |
data-controller="dropdown" | |
class="mx-auto mr-2 relative" | |
> | |
// ... lines 33 - 41 | |
<ul | |
hidden | |
data-dropdown-target="content" | |
class="absolute overflow-hidden z-10 right-0 mt-1 w-full text-gray-900 border border-gray-300 rounded-lg bg-gray-50 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white" | |
> | |
// ... lines 47 - 48 | |
<li class="uppercase hover:bg-gray-100 hover:text-gray-900 block px-4 py-2 text-sm"> | |
<a class="block" href="{{ path(app.current_route, app.current_route_parameters|merge({_locale: locale})) }}">{{ locale }}</a> | |
</li> | |
// ... lines 52 - 53 | |
</ul> | |
</div> | |
// ... lines 56 - 75 | |
</div> | |
</nav> | |
// ... lines 78 - 85 | |
</body> | |
</html> |
Testing the Language Switcher
Moment of truth! Head back to our app and refresh. We're on the French homepage and our widget shows "Language: FR". So far, so good! Click the button, and here's our other locales, but with "FR" excluded. Click "ES" - Spanish. Boom, we're on the Spanish homepage! Switch to "EN", yep, we're on the un-prefixed, English homepage.
Click an article, and switch to "FR". Awesome! We're on the French version of the same article. Our widget works perfectly: no more late night customer support phone calls trying to walk a customer through changing the URL manually, and in German! Das ist ja verrückt!
Removing the Trailing Slash
One super minor gripe of mine, is that when you're on a locale-prefixed homepage, like /fr
, there's a trailing slash: /fr/
. There is no real technical issue with this, I just dislike it. And it's super easy to remove.
Open config/routes.yaml
, and under the controllers
key, add the option trailing_slash_on_root: false
:
controllers: | |
// ... lines 2 - 5 | |
trailing_slash_on_root: false | |
// ... lines 7 - 11 |
That's it!
Go back to our app... refresh... and the trailing slash is gone! Phew! I can sleep easy tonight!
Ok, enough prep work! Next, let's actually translate some content!