Language Switcher
このコンテンツはまだ日本語訳がありません。
This guide shows how to build a language switcher that lets users navigate between locales while staying on the equivalent page.
The switchLocalePath function
switchLocalePath converts a path from one locale to another, translating slugs in both directions:
import { switchLocalePath } from 'virtual:i18n';
switchLocalePath('/es/sobre/', 'en'); // "/about/"switchLocalePath('/about/', 'es'); // "/es/sobre/"switchLocalePath('/es/saunas/modelo-165/', 'en'); // "/saunas/model-165/"Language switcher component
Here’s a complete language switcher using the virtual:i18n exports:
---import { locales, localeLabels, switchLocalePath } from 'virtual:i18n';
const currentPath = Astro.url.pathname;const currentLocale = Astro.locals.locale;---
<nav aria-label="Language"> <ul> {locales.map(locale => ( <li> {locale === currentLocale ? ( <strong>{localeLabels[locale]}</strong> ) : ( <a href={switchLocalePath(currentPath, locale)}> {localeLabels[locale]} </a> )} </li> ))} </ul></nav>Adding hreflang alternate links
For SEO, add hreflang links in your layout’s <head>:
---import { locales, localeHtmlLang, switchLocalePath } from 'virtual:i18n';
const currentPath = Astro.url.pathname;---
<head> {locales.map(locale => ( <link rel="alternate" hreflang={localeHtmlLang[locale]} href={switchLocalePath(currentPath, locale)} /> ))} <link rel="alternate" hreflang="x-default" href={currentPath} /></head>This tells search engines about all available language versions of each page.
Language detection banner
You can go further and detect the user’s browser language to suggest switching. This example uses i18next-browser-languagedetector to detect the preferred language client-side and shows a banner if it doesn’t match the current page locale.
Translation keys
Add banner strings to your translation files:
{ "languageBanner": { "message": "This page is also available in your language.", "switch": "Switch", "dismiss": "Stay" }}{ "languageBanner": { "message": "Esta página también está disponible en tu idioma.", "switch": "Cambiar", "dismiss": "Quedarme" }}The component
---import { locales, localeLabels } from 'virtual:i18n';import en from '../i18n/en.json';import es from '../i18n/es.json';
const bannerStrings = { en: en.languageBanner, es: es.languageBanner,};---
<div id="language-banner" class="language-banner hidden" role="alert" data-locales={JSON.stringify(locales)} data-locale-labels={JSON.stringify(localeLabels)} data-banner-strings={JSON.stringify(bannerStrings)}> <p id="banner-message"></p> <div> <button id="banner-dismiss"></button> <a id="banner-switch" href="#"></a> </div></div>
<script> import LanguageDetector from 'i18next-browser-languagedetector';
const banner = document.getElementById('language-banner')!; const locales: string[] = JSON.parse(banner.dataset.locales!); const bannerStrings: Record< string, { message: string; switch: string; dismiss: string } > = JSON.parse(banner.dataset.bannerStrings!);
// Detect browser language (synchronous, no i18next init needed) const detector = new LanguageDetector(null, { order: ['navigator'], caches: [], }); const rawDetected = detector.detect();
// Normalize "en-US" → "en", "es-419" → "es" function toLocaleCode(lang: string | undefined): string | undefined { if (!lang) return undefined; const base = lang.split('-')[0].toLowerCase(); return locales.find((l) => l === base); }
const detectedLangs = Array.isArray(rawDetected) ? rawDetected : [rawDetected]; const detectedCode = detectedLangs.map(toLocaleCode).find(Boolean); const currentCode = toLocaleCode(document.documentElement.lang);
// Show banner if detected locale differs and user hasn't chosen if (detectedCode && currentCode && detectedCode !== currentCode) { const storedPreference = localStorage.getItem('preferred-locale');
if (!storedPreference) { // Find the alternate URL from hreflang links in <head> const alternateLink = document.querySelector( `link[rel="alternate"][hreflang="${detectedCode}"]` ) as HTMLLinkElement | null;
if (alternateLink) { const strings = bannerStrings[detectedCode]; document.getElementById('banner-message')!.textContent = strings.message;
const switchEl = document.getElementById( 'banner-switch' ) as HTMLAnchorElement; switchEl.textContent = strings.switch; switchEl.href = alternateLink.href;
document.getElementById('banner-dismiss')!.textContent = strings.dismiss;
// Show the banner banner.classList.remove('hidden');
// "Switch" — save preference and navigate switchEl.addEventListener('click', () => { localStorage.setItem('preferred-locale', detectedCode); });
// "Dismiss" — save current locale preference and hide document .getElementById('banner-dismiss')! .addEventListener('click', () => { localStorage.setItem('preferred-locale', currentCode); banner.classList.add('hidden'); }); } } }</script>How it works
- Detects browser language using
i18next-browser-languagedetector(navigator only, no cookies) - Compares the detected language against the current page’s
<html lang="..."> - Finds the alternate URL from the
<link rel="alternate" hreflang="...">tags you added in the hreflang section above - Shows the banner in the detected language so the user can read it
- Remembers the choice in
localStorageso the banner doesn’t reappear