Add Multi-Language Support
Astro's built-in i18n routing lets you adapt your project for an international audience. Configure a default language, compute relative page URLs, accept the preferred languages from your visitor's browser, and set per-language fallbacks so visitors are always directed to existing content.
Set Up i18n Routing
Configure your locales and start building localized pages
Configure Your Locales
Open astro.config.mjs and add an i18n object. List every supported language in locales and pick one as your defaultLocale:
// astro.config.mjs
import { defineConfig } from "astro/config";
export default defineConfig({
i18n: {
locales: ["es", "en", "pt-br"],
defaultLocale: "en",
},
});💡 Note: defaultLocale must be one of the languages listed in locales.
Create Localized Folders
Organize content into /[locale]/ folders inside src/pages/. Folder names must match your locales exactly. With the default prefixDefaultLocale: false, your default language lives at the root:
src/
└── pages/
├── about.astro → example.com/about/
├── index.astro → example.com/
├── es/
│ ├── about.astro → example.com/es/about/
│ └── index.astro → example.com/es/
└── pt-br/
├── about.astro → example.com/pt-br/about/
└── index.astro → example.com/pt-br/✨ The localized folders don't need to sit at the root of /pages/: they can be nested anywhere.
Create Localized Links
Use the helper functions from the astro:i18n module to compute correct, localized routes. getRelativeLocaleUrl() always returns the right path for a given locale:
---
// src/pages/es/index.astro
import { getRelativeLocaleUrl } from "astro:i18n";
const aboutURL = getRelativeLocaleUrl("es", "about");
---
<a href="/get-started/">¡Vamos!</a>
<a href={getRelativeLocaleUrl("es", "blog")}>Blog</a>
<a href={aboutURL}>Acerca</a>🎉 You're Multi-Language!
Astro now generates localized routes from your folder structure and the i18n middleware verifies that every localized URL maps to a valid route.
Default Language URLs
Choose whether your default language gets a URL prefix
prefixDefaultLocale: false
Your default language has no prefix. Its files live at the root of src/pages/.
- •
pages/about.astro→/about/ - •
pages/fr/about.astro→/fr/about/
prefixDefaultLocale: true
Every language gets a prefix. All content files, including the default locale, live in their own folder.
- •
pages/en/about.astro→/en/about/ - •A root
pages/index.astrois always required
// astro.config.mjs
import { defineConfig } from "astro/config";
export default defineConfig({
i18n: {
locales: ["es", "en", "fr"],
defaultLocale: "en",
routing: {
prefixDefaultLocale: true,
// Optional: also redirect "/" to "/en/"
redirectToDefaultLocale: true,
},
},
});⚠️ With prefixDefaultLocale: true, URLs without a locale prefix return a 404 unless you set a fallback. The home page / stays unprefixed by default, add redirectToDefaultLocale: true to redirect it to /[defaultLocale]/.
Fallback Languages
Show existing content instead of a 404 for missing translations
Map languages to a fallback locale with i18n.fallback, then choose how the fallback is served with routing.fallbackType: "redirect" (default) sends the visitor to the fallback route, while "rewrite" serves the fallback content without changing the URL.
// astro.config.mjs
import { defineConfig } from "astro/config";
export default defineConfig({
i18n: {
locales: ["es", "en", "fr"],
defaultLocale: "en",
fallback: {
fr: "es", // missing /fr/ pages fall back to /es/
},
routing: {
fallbackType: "rewrite", // serve es content at the fr URL
},
},
});With this config, a visitor opening example.com/fr/my-page/ sees the content from /es/my-page/: no 404, even when src/pages/fr/my-page.astro doesn't exist yet.
Custom Locale Paths
Map several browser language codes onto a single, custom URL path
Instead of a plain string, pass an object with a path (the URL prefix and folder name) and codes (the browser language codes it covers). Useful for grouping variants like fr, fr-CA, and fr-BR under one /french/ URL:
// astro.config.mjs
export default defineConfig({
i18n: {
locales: [
"es",
"en",
{
path: "french", // folder name + URL prefix, no slashes
codes: ["fr", "fr-BR", "fr-CA"],
},
],
defaultLocale: "en",
routing: {
prefixDefaultLocale: true,
},
},
});📁 Your /[locale]/ folder must be named to match path (here src/pages/french/), and you pass path: not a code, as the locale to helpers like getRelativeLocaleUrl("french", "about").
Routing Logic & Custom Middleware
Extend or replace Astro's i18n routing for full control
🧭 Astro implements i18n as a middleware placed first in the chain. It awaits every response from your own middleware and page routes, then runs its logic, such as verifying that a localized URL maps to a valid route. You can add your own logic alongside or instead of it while still using the astro:i18n helpers.
Manual Routing
astro@4.6.0+Set routing: "manual" to disable Astro's i18n middleware and write your own. No other routing options can be combined with it. Astro exposes redirectToDefaultLocale(), notFound(), and redirectToFallback() for your middleware:
// src/middleware.js
import { defineMiddleware } from "astro:middleware";
import { redirectToDefaultLocale } from "astro:i18n"; // available with "manual" routing
export const onRequest = defineMiddleware(async (ctx, next) => {
if (ctx.url.pathname.startsWith("/about")) {
return next();
}
return redirectToDefaultLocale(302);
});The middleware() Function
To extend rather than replace Astro's i18n routing, create its middleware manually with middleware() and order it against your own using sequence():
// src/middleware.js
import { defineMiddleware, sequence } from "astro:middleware";
import { middleware } from "astro:i18n"; // Astro's own i18n routing
const userMiddleware = defineMiddleware(async (ctx, next) => {
const response = await next();
// render /about even if i18n would have 404'd it
if (ctx.url.pathname.startsWith("/about")) {
return new Response("About page", { status: 200 });
}
return response;
});
export const onRequest = sequence(
userMiddleware,
middleware({
redirectToDefaultLocale: false,
prefixDefaultLocale: true,
fallbackType: "redirect",
}),
);Per-Language Domains
astro@4.9.0+Serve specific locales from their own domains on server-rendered sites
For server output (with the @astrojs/node or @astrojs/vercel adapter and a configured site), map locales to custom domains with i18n.domains. Unmapped locales keep your prefixDefaultLocale behavior:
// astro.config.mjs
export default defineConfig({
site: "https://example.com",
output: "server", // required, with no prerendered pages
adapter: node({ mode: "standalone" }),
i18n: {
locales: ["es", "en", "fr", "ja"],
defaultLocale: "en",
routing: { prefixDefaultLocale: false },
domains: {
fr: "https://fr.example.com",
es: "https://example.es",
},
},
});
// /fr/about.astro → https://fr.example.com/about
// /es/about.astro → https://example.es/about
// /ja/about.astro → https://example.com/ja/about
// /about.astro → https://example.com/about⚙️ Requirements & limitations
- •
siteis mandatory andoutputmust be"server"with no prerendered pages. - •Your proxy/host must forward
X-Forwarded-Host(orHost) andX-Forwarded-Proto; missing headers cause a 404. - •These URLs are also returned by
getAbsoluteLocaleUrl()andgetAbsoluteLocaleUrlList().
Browser Language Detection
On pages rendered on demand, Astro combines the browser's Accept-Language header with your locales to detect a visitor's preferences.
Astro.preferredLocale
The best-matching locale for the visitor, or undefined if none match.
Astro.preferredLocaleList
Every locale supported by both the site and the browser, as an array.
Astro.currentLocale
The locale from the current URL. Available on all pages, including static ones.
---
// Any on-demand rendered page
const locale = Astro.preferredLocale ?? Astro.currentLocale;
---
<p>Showing content for: {locale}</p>Ready to Go Global! 🌍
Your site now speaks multiple languages with clean URLs and smart fallbacks.
