24.6 Language Switcher in Templates
Right, so you’ve got a site that speaks multiple languages. Congratulations, you’ve just multiplied your complexity. The language switcher is the public-facing proof you’ve pulled it off. It’s the little widget that says, “Hey, we see you,” and it’s deceptively tricky to get right. It’s not just a dropdown; it’s a state machine with opinions about URLs, user sessions, and SEO. Let’s build one that doesn’t suck.
The Core Logic: It’s All About the URL
Forget sessions, forget cookies as your primary method. The single most important rule for a language switcher is this: the chosen language must be reflected in the URL. Why? Because a URL is a unique, shareable, bookmarkable representation of the resource. If I copy a URL for /fr/a-propos and send it to you, you should see the French page, not get redirected to English because your browser prefers it. This is non-negotiable for SEO and basic user sanity.
How you structure that URL is the first big decision. You’ve got three main options, and your headless CMS or framework probably has an opinion.
- Domain-based (
fr.example.com,example.fr): The gold standard for global brands. Cleanest separation, often best for SEO geo-targeting. Also the most complex to set up with DNS and certificates. - Path-based (
example.com/fr/a-propos): The most common approach. It’s straightforward, keeps everything on one domain, and is easily handled by most frameworks. - Query parameter-based (
example.com/a-propos?lang=fr): Please, just don’t. It’s messy, often poorly handled by search engines, and not shareable in the same way. It’s the “we built this in a weekend” option.
We’re going to focus on the path-based approach because it’s what you’re most likely using. The logic is universal.
Building a Simple Yet Robust Switcher
Your switcher needs to do two things: 1) show the current language, and 2) offer links to the same page in other languages. Here’s the kicker: it needs to be aware of the page it’s on. A link to the homepage in French is just /fr. A link to a blog post is /fr/blog/my-post. You need a template variable that gives you the current page’s “relative” URL without the language segment.
In a typical setup, you might have a variable like page.url which is /fr/blog/my-post/. You need to strip the /fr part. How you do this depends on your site generator.
Here’s a realistic example using Nunjucks syntax (similar to Jinja2 or Twig), which you might use in Eleventy or a similar tool. We assume our default language is English (no prefix) and our supported languages are English (en) and French (fr).
{# First, let's get our list of languages #}
{% set languages = [{
code: 'en',
label: 'English'
}, {
code: 'fr',
label: 'Français'
}] %}
{# This is the magic part. We get the current URL and remove the potential language prefix.
This gives us the "root-relative" path for the current page. #}
{% set currentUrl = page.url %}
{% set baseUrl = currentUrl | replace("/" + locale + "/", "/") %}
<nav aria-label="Language switcher">
<ul>
{% for language in languages %}
<li>
{% if language.code == locale %}
{# For the current language, we show it as a span, not a link #}
<span aria-current="true">{{ language.label }}</span>
{% else %}
{# For other languages, we build the new URL.
If the language isn't the default (en), we prepend the code.
Otherwise, we just use the baseUrl. #}
{% set translatedUrl = "/" + language.code + baseUrl if language.code != "en" else baseUrl %}
<a href="{{ translatedUrl | url }}" hreflang="{{ language.code }}">
{{ language.label }}
</a>
{% endif %}
</li>
{% endfor %}
</ul>
</nav>
The Devil’s in the Details: Edge Cases You Must Handle
The code above looks simple, right? Now watch as we break it. The replace filter is naïve. What if your default language isn’t en? What if you have a page whose slug starts with a language code, like /enigma-machine? Your switcher will happily strip the en out of that, turning it into /igma-machine. Fantastic.
A more robust method is to use a global variable or site configuration that defines your i18n structure. Your template should know that the first segment of the URL is the language code. You can split the URL and reassemble it.
// This is logic you might set in a global data file or your eleventy config.
// It's JavaScript, but the concept applies anywhere.
function getBaseUrl(fullUrl, currentLang, defaultLang) {
// Create a regex that matches the language segment at the start of the path
// The ^ anchor and \/ ensure we only match the first segment.
const langPrefixRegex = new RegExp(`^\/${currentLang}\/`);
const defaultLangPrefixRegex = new RegExp(`^\/${defaultLang}\/`);
// If we're on the default language, the URL might not have a prefix.
if (currentLang === defaultLang) {
// But what if it does? This handles both /en/page and /page.
return fullUrl.replace(defaultLangPrefixRegex, '/');
} else {
// If we're on a alternate language, strip its specific prefix.
return fullUrl.replace(langPrefixRegex, '/');
}
}
You’d then pass this baseUrl into your template. This handles the “enigma” edge case because it only removes the language code if it’s the first segment.
Best Practices: Beyond the Markup
hreflangAttribute: Always include it on your anchor tags. It tells search engines the relationship between these alternate URLs. Use the language code (fr) or, even better, the code with region (fr-CA).- ARIA Labels: The
aria-labelon the nav andaria-current="true"on the current language are not optional. This is basic accessibility. Screen reader users need to know what this widget is and where they are. - Don’t Use Flags. Just don’t. A flag represents a country, not a language. Spanish is spoken in dozens of countries besides Spain. Which flag do you use? Canadian French vs. French French? You’ll inevitably offend someone. Use the localized language name. It’s unambiguous and respectful.
- The Homepage Hole: Remember that the root URL (
/) is a special case. Your logic must correctly handle generating links to/frand/envs. just/. Test it. - CSS: Design for More Languages: Your beautiful two-item horizontal layout will look terrible the second someone adds a third language with a long name like “Ελληνικά”. Design your switcher to be flexible. A vertical stack or a dropdown are often safer bets.
Building a language switcher is one of those tasks that seems trivial until you actually do it. Then you realize it touches almost every part of your i18n strategy. Get the URL right, handle the edge cases with precision, and for heaven’s sake, don’t use flags.