Building a Language Switcher
A language switcher is the small menu in your header that lets visitors jump from the page they are reading to the same page in another language. The tricky part is not rendering the links — it’s computing where each link should point. A naive switcher that just links to /fr/ throws the reader back to the homepage every time they change language, which is frustrating. A good switcher maps the current path to its equivalent in every locale, so a reader on /fr/about lands on /es/about. Because Astro resolves all of this at build time, the switcher ships zero JavaScript and stays a plain set of <a> tags.
How the path mapping works
Astro exposes everything you need on the Astro global. Astro.currentLocale tells you which locale the current page belongs to, and Astro.url.pathname gives you the full path the visitor is on. To build a link for another locale, you strip the current locale prefix off the pathname to get the “logical” route, then re-prefix it for the target locale using the helpers from astro:i18n.
import { getRelativeLocaleUrl } from 'astro:i18n';
// Turn "/fr/about" into the bare route "about"
function stripLocale(pathname: string, locale: string): string {
const segments = pathname.split('/').filter(Boolean); // ["fr", "about"]
if (segments[0] === locale) segments.shift(); // drop the prefix
return segments.join('/'); // "about"
}
getRelativeLocaleUrl(targetLocale, route) then rebuilds a correct, config-aware URL. Because it respects your prefixDefaultLocale setting, the default locale’s link will resolve to /about or /en/about automatically without special-casing.
Build the route from
Astro.url.pathname, not fromAstro.url.href. The pathname excludes the origin and query string, which keeps the mapping clean and avoids leaking absolute URLs into your links.
A reusable switcher component
Put the logic in a single .astro component so every page can drop it into the header. The component reads the current locale and pathname, then iterates over your configured locales to emit one link each.
---
// src/components/LanguageSwitcher.astro
import { getRelativeLocaleUrl } from 'astro:i18n';
const locales = ['en', 'fr', 'es'] as const;
const labels: Record<(typeof locales)[number], string> = {
en: 'English',
fr: 'Français',
es: 'Español',
};
const current = Astro.currentLocale ?? 'en';
// Strip the active locale prefix to recover the logical route.
const segments = Astro.url.pathname.split('/').filter(Boolean);
if (segments[0] === current) segments.shift();
const route = segments.join('/');
---
<nav aria-label="Language" class="lang-switcher">
<ul>
{locales.map((locale) => (
<li>
<a
href={getRelativeLocaleUrl(locale, route)}
aria-current={locale === current ? 'true' : undefined}
hreflang={locale}
>
{labels[locale]}
</a>
</li>
))}
</ul>
</nav>
The aria-current attribute marks the active language for assistive technology and gives you a CSS hook to highlight it. The hreflang attribute tells crawlers which language each link leads to.
Drop it into a layout so it appears on every page:
---
// src/layouts/Base.astro
import LanguageSwitcher from '../components/LanguageSwitcher.astro';
---
<header>
<a href="/">Logo</a>
<LanguageSwitcher />
</header>
<slot />
For a reader on /fr/about, the rendered markup links each language back to the equivalent page:
Output:
<a href="/about" hreflang="en">English</a>
<a href="/fr/about" hreflang="fr" aria-current="true">Français</a>
<a href="/es/about" hreflang="es">Español</a>
Handling absolute URLs and crawlers
For SEO you should also emit <link rel="alternate" hreflang> tags in the document <head> so search engines know about every translated version. These need absolute URLs, so reach for getAbsoluteLocaleUrl instead.
---
// src/components/HreflangTags.astro
import { getAbsoluteLocaleUrl } from 'astro:i18n';
const locales = ['en', 'fr', 'es'] as const;
const segments = Astro.url.pathname.split('/').filter(Boolean);
if (segments[0] === (Astro.currentLocale ?? 'en')) segments.shift();
const route = segments.join('/');
---
{locales.map((locale) => (
<link rel="alternate" hreflang={locale} href={getAbsoluteLocaleUrl(locale, route)} />
))}
The two helpers differ only in what they return:
| Helper | Returns | Use it for |
|---|---|---|
getRelativeLocaleUrl | path only, e.g. /es/about | in-page <a> links |
getAbsoluteLocaleUrl | full URL incl. site origin | hreflang alternates, sitemaps, canonical tags |
Both require site to be set in astro.config.mjs for the absolute variant to resolve correctly.
Persisting the visitor’s choice
By default the switcher is purely link-based — no state, no JavaScript. If you want to remember a returning visitor’s preference, store the chosen locale in a cookie when a link is clicked and read it in middleware on the next visit. This requires on-demand rendering (an SSR adapter); keep it optional so static builds still work.
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
export const onRequest = defineMiddleware(({ url, cookies, redirect }, next) => {
const isRoot = url.pathname === '/';
const saved = cookies.get('locale')?.value;
if (isRoot && saved && saved !== 'en') {
return redirect(`/${saved}/`);
}
return next();
});
Best Practices
- Map the current pathname to each locale instead of always linking to the locale root — never strand the reader on the homepage.
- Always build links with
getRelativeLocaleUrl/getAbsoluteLocaleUrlso they survive changes toprefixDefaultLocale. - Keep the
localesarray in one place and import or mirror it from your config to avoid drift. - Add
hreflangto switcher links andrel="alternate"tags in the head so search engines index every translation. - Mark the active language with
aria-currentfor accessibility and styling. - Leave the switcher as plain
<a>tags so it works with zero client JavaScript; add cookie persistence only when you already have an SSR adapter.