Most i18n implementations are over-engineered. You add a third-party library (next-intl, react-i18next, @formatjs), configure context providers, wire up fallback logic, and suddenly your bundle is 40kb heavier. You're shipping i18n overhead to users in Iceland who speak Icelandic and don't need English at all. It's backwards. Astro fixes this.
Astro 5 ships native i18n routing. Locale-prefixed routes (/en/, /es/, /fr/). JSON dictionaries per locale. Server-side language detection via the Accept-Language header. No runtime overhead. No third-party libs. You write two files — the routing config and one JSON file per language — and suddenly you're serving 12 locales to 12 countries with zero client-side i18n code. That's the move.
Why Most i18n Is Over-Engineered
The standard approach: bundle a lightweight i18n library (200–400kb with deps), wire it into your app, pass locale context down from the root. On the client, it detects navigator.language, falls back to a default, and re-renders content. It works. It's also inefficient. You're shipping i18n logic to every user, every page, every request. If a Spanish user lands on your site, they still download English copy, German copy, French copy — the library just picks which one to render. You're serving dead weight.
Astro's approach: the server knows the user's language before they load the page. The browser sends an Accept-Language header on every request. Astro reads it, picks the best locale match, and routes them to /es/ or /en/ or /fr/. The page that renders already contains the correct language. No client-side language detection. No re-render. No library overhead. The user downloads one locale's copy — exactly what they need.
Astro i18n Config in 3 Steps
Step 1: Configure routing in astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
i18n: {
defaultLocale: 'en',
locales: ['en', 'es', 'fr', 'de', 'it', 'pt'],
routing: {
prefixDefaultLocale: true,
},
},
});
That's it. Tell Astro which locales exist, set the default, and enable prefix routing. Every route becomes locale-aware. /about becomes /en/about, /es/about, /fr/about. Build once, deploy once, serve all locales from a single site.
Step 2: Structure pages by locale
src/pages/
en/
index.astro
about.astro
pricing.astro
es/
index.astro
about.astro
pricing.astro
fr/
index.astro
about.astro
pricing.astro
Astro picks the right folder automatically. A request to /es/about serves src/pages/es/about.astro. A request to /fr/pricing serves src/pages/fr/pricing.astro. If a locale is missing a page, Astro 404s (you can customize this). No fancy routing utilities. No magical getStaticPaths. Just folders.
Step 3: Create JSON dictionaries
// src/i18n/en.json
{
"nav.home": "Home",
"nav.about": "About",
"cta.button": "Get Started",
"hero.headline": "Build faster"
}
// src/i18n/es.json
{
"nav.home": "Inicio",
"nav.about": "Acerca de",
"cta.button": "Comenzar",
"hero.headline": "Construir más rápido"
}
Dot-notation keys, structured by component or section. Import the locale's JSON in the page and pass it down. Or use a utility to grab the current locale from Astro.currentLocale and load the right JSON automatically. One JSON per locale, no shared config overhead.
The Locale Switcher Component
Users need to switch languages. A simple React component with a dropdown, client:load so it's interactive immediately:
// src/components/LocaleSwitcher.tsx
import { useEffect, useState } from 'react';
export default function LocaleSwitcher() {
const currentPath = typeof window !== 'undefined' ? window.location.pathname : '';
const currentLocale = currentPath.split('/')[1];
const locales = ['en', 'es', 'fr', 'de'];
const switchLocale = (locale: string) => {
const newPath = currentPath.replace(`/${currentLocale}`, `/${locale}`);
window.location.href = newPath;
};
return (
<select value={currentLocale} onChange={(e) => switchLocale(e.target.value)}>
{locales.map((locale) => (
<option key={locale} value={locale}>
{locale.toUpperCase()}
</option>
))}
</select>
);
}
Drop this in your header, and users can switch locales. On change, the browser navigates to the same page in a different locale (/en/pricing → /es/pricing). Server handles the rest.
Six FAQs
How does Astro detect the user's language?
Via the Accept-Language header sent by every browser. Astro reads it, finds the best locale match (exact match first, then fallback to parent locale, then default). Works on first request, before the page loads. Zero client-side detection needed.
Can I handle locale switching without a full page reload?
Not natively with Astro's i18n routing — locale changes require a navigation to a different URL (/en/ to /es/). If you want instant switches without reload, that's a React context + client-side pattern, which defeats the purpose. The reload is fast; the server sends the correct HTML immediately.
What if a locale is missing a page?
Astro 404s by default. You can customize this by creating a fallback page or programmatically generating missing pages in getStaticPaths. For marketing sites, the simpler approach: ensure every locale has every page. Use a build check (CI script) to enforce it.
How do I handle locale-specific assets (fonts, images)?
Store them by locale in src/assets/: src/assets/en/hero.webp, src/assets/es/hero.webp. Import them in the locale-specific page. Astro optimizes each automatically. Or use a single global asset if it's truly language-agnostic.
Can I do SEO per locale?
Yes. Astro's <Layout> accepts title and description props. Set them per page, per locale. In your es/about.astro, use a different title/description than en/about.astro. Each page gets its own og:tags, hreflang headers, sitemap entries. Full SEO localization out of the box.
What about locale-specific currency, dates, or number formatting?
Use JavaScript's native Intl API: new Intl.DateTimeFormat('es-ES').format(date), new Intl.NumberFormat('de-DE').format(1234.56). No library needed. It's part of every modern browser. For static content, include the formatted version directly in your JSON dictionary.
The Bottom Line
Astro's native i18n routing is the right move for any marketing site serving multiple locales. No third-party libraries. No bundle bloat. No client-side language detection. The server handles it all before the page loads. You scale to 12 locales with the same effort as 2. Your Core Web Vitals stay clean because you're shipping zero i18n overhead per locale. Your SEO improves because each locale gets its own URL, its own sitemap entry, its own hreflang tag.
If you're building a global brand, this is the pattern. Folder structure + JSON dictionaries + one config line. Done. Stop reaching for heavy i18n libraries and start using Astro's native approach. Your users (and your analytics team) will thank you.
Ready to build a global site? Aidxn Design specializes in multi-locale architecture, or dive deeper into Astro Islands and performance optimization for the full picture.