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!
I did this, but as soon as I add the 'merge...' part, i get: "An exception has been thrown during the rendering of a template ("Unable to generate a URL for the named route "app_homepage" as such route does not exist.") in base.html.twig at line 50." and I am stumped...