Routing
astro-intl provides a routing system that lets you define translated URL paths for each locale. This enables localized URLs like /es/sobre-nosotros instead of /es/about.
1. Define your routes
Add a routes map to your routing config. Each key is a route name, and each value maps locales to their URL templates. Use [param] for dynamic segments.
export const routing = {
locales: ["en", "es"],
defaultLocale: "en",
routes: {
home: { en: "/", es: "/" },
about: { en: "/about", es: "/sobre-nosotros" },
blog: { en: "/blog/[slug]", es: "/blog/[slug]" },
shop: { en: "/shop/[category]/[id]", es: "/tienda/[category]/[id]" },
},
} as const;2a. With Middleware (recommended)
When using createIntlMiddleware, pass your routes in the config. The middleware will automatically rewrite translated URLs to their canonical filesystem paths — no extra page files needed.
Pass routes to the middleware:
import "@/i18n/request";
import { createIntlMiddleware } from "astro-intl/middleware";
import { routing } from "@/i18n/routing";
export const onRequest = createIntlMiddleware(routing);Your filesystem only needs the default locale paths:
src/pages/
└── [lang]/
├── index.astro ← /en/ and /es/
├── about.astro ← /en/about and /es/sobre-nosotros
├── blog/
│ └── [slug].astro ← /en/blog/my-post and /es/blog/mi-post
└── shop/
└── [category]/
└── [id].astro ← /en/shop/clothing/42 and /es/tienda/clothing/42/es/sobre-nosotros, the middleware matches it against the routes map, finds the canonical template (/about), and rewrites the request to /es/about — which maps to your [lang]/about.astro file. No duplicate pages needed. 2b. Without Middleware
If you prefer not to use middleware, you can still use path() and switchLocalePath() for URL generation. Configure routes via the integration options instead.
import { defineConfig } from "astro/config";
import astroIntl from "astro-intl";
export default defineConfig({
integrations: [
astroIntl({
defaultLocale: "en",
locales: ["en", "es"],
routes: {
about: { en: "/about", es: "/sobre-nosotros" },
blog: { en: "/blog/[slug]", es: "/blog/[slug]" },
},
}),
],
});Example: Astro native routing for translated paths
Create a page file for each translated URL. These "wrapper" pages simply re-export everything from the canonical (default locale) page — zero duplication.
Your canonical page (default locale paths):
---
import Layout from "@/layouts/Layout.astro";
import { getTranslations } from "astro-intl";
const t = getTranslations("about");
export function getStaticPaths() {
return [
{ params: { lang: "en" } },
{ params: { lang: "es" } },
];
}
---
<Layout title={t("title")}>
<h1>{t("heading")}</h1>
<p>{t("description")}</p>
</Layout>Create the translated route as a thin wrapper:
---
// Re-export everything from the canonical page
export { default } from "./about.astro";
export { getStaticPaths } from "./about.astro";
---Resulting file structure:
src/pages/[lang]/
├── about.astro ← Canonical page (all the logic)
├── sobre-nosotros.astro ← Wrapper (2 lines, re-exports about.astro)
├── blog/
│ └── [slug].astro ← Same URL structure, no wrapper needed
└── shop/
└── [category]/
└── [id].astro ← Canonical for /shop/:category/:id
src/pages/[lang]/tienda/
└── [category]/
└── [id].astro ← Wrapper (re-exports ../shop/[category]/[id].astro)/es/sobre-nosotros, it renders the same component as /es/about — the translated URL works without middleware. 3. Generating URLs with path()
Use path(routeKey, options?) to generate localized URLs. It picks the correct template for the target locale and substitutes any params.
---
import { path } from "astro-intl/routing";
---
<!-- Simple route -->
<a href={path("about")}>About</a>
<!-- Current locale "en" → /en/about -->
<!-- Current locale "es" → /es/sobre-nosotros -->
<!-- Explicit locale -->
<a href={path("about", { locale: "es" })}>Sobre nosotros</a>
<!-- → /es/sobre-nosotros -->
<!-- With params -->
<a href={path("blog", { params: { slug: "hello-world" } })}>Read post</a>
<!-- → /en/blog/hello-world -->
<!-- Multiple params -->
<a href={path("shop", { locale: "es", params: { category: "ropa", id: "42" } })}>
Ver producto
</a>
<!-- → /es/tienda/ropa/42 -->4. Switching locales with switchLocalePath()
Use switchLocalePath(currentPath, nextLocale) to convert the current URL to its equivalent in another locale. It matches the current path against route templates, extracts params, and rebuilds the URL with the target locale's template.
---
import { switchLocalePath } from "astro-intl/routing";
const currentPath = Astro.url.pathname;
---
<nav>
<a href={switchLocalePath(currentPath, "en")}>English</a>
<a href={switchLocalePath(currentPath, "es")}>Español</a>
</nav>
<!-- On /en/about → switches to /es/sobre-nosotros -->
<!-- On /es/tienda/ropa/42 → switches to /en/shop/ropa/42 -->
<!-- On /en/unknown/page → falls back to /es/unknown/page -->switchLocalePath falls back to simply swapping the locale prefix. Dynamic Parameters
Route templates support dynamic segments with [param] syntax. You can have multiple params per route — they are extracted and substituted automatically.
import { path } from "astro-intl/routing";
// Route defined as:
// shop: { en: "/shop/[category]/[id]", es: "/tienda/[category]/[id]" }
const url = path("shop", {
locale: "es",
params: { category: "electronics", id: "123" },
});
// → /es/tienda/electronics/123Fallback Routes (Astro 6.1+)
Starting with Astro 6.1, the integration automatically detects fallbackRoutes from the astro:routes:resolved hook. When you configure i18n.fallbackType: 'rewrite' in your Astro config, Astro generates fallback routes for locales that don't have their own content. astro-intl collects these and makes them available via getFallbackRoutes().
Query fallback routes at runtime:
---
import { getFallbackRoutes } from "astro-intl";
const fallbacks = getFallbackRoutes();
// [
// { pattern: "/fr/about", pathname: "/fr/about/", locale: "fr" },
// { pattern: "/fr/blog/[...slug]", locale: "fr" },
// ]
// Example: check if the current page has a fallback for a locale
const hasFrenchFallback = fallbacks.some(fb => fb.locale === "fr");
---getFallbackRoutes() returns an empty array — no errors, fully backward compatible.