コンテンツにスキップ

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:

src/components/LanguageSwitcher.astro
---
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>

For SEO, add hreflang links in your layout’s <head>:

src/layouts/BaseLayout.astro
---
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:

src/i18n/en.json
{
"languageBanner": {
"message": "This page is also available in your language.",
"switch": "Switch",
"dismiss": "Stay"
}
}
src/i18n/es.json
{
"languageBanner": {
"message": "Esta página también está disponible en tu idioma.",
"switch": "Cambiar",
"dismiss": "Quedarme"
}
}

The component

src/components/LanguageBanner.astro
---
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

  1. Detects browser language using i18next-browser-languagedetector (navigator only, no cookies)
  2. Compares the detected language against the current page’s <html lang="...">
  3. Finds the alternate URL from the <link rel="alternate" hreflang="..."> tags you added in the hreflang section above
  4. Shows the banner in the detected language so the user can read it
  5. Remembers the choice in localStorage so the banner doesn’t reappear