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";
import en from "./src/i18n/messages/en.json" with { type: "json" };
import es from "./src/i18n/messages/es.json" with { type: "json" };
export default defineConfig({
integrations: [
astroIntl({
defaultLocale: "en",
locales: ["en", "es"],
messages: { en, es },
routes: {
about: { en: "/about", es: "/sobre-nosotros" },
blog: { en: "/blog/[slug]", es: "/blog/[slug]" },
},
}),
],
});Example: Astro native routing for translated paths
Start from the static tutorial: keep messages in the integration and add an about object with title, heading and description to both JSON files. The canonical page initializes the language before translating; the localized page renders it as a component.
Your canonical page (default locale paths):
---
import { setRequestLocale, getTranslations } from "astro-intl";
await setRequestLocale(Astro.url);
const t = getTranslations("about");
export function getStaticPaths() {
return [
{ params: { lang: "en" } },
{ params: { lang: "es" } },
];
}
---
<h1>{t("heading")}</h1>
<p>{t("description")}</p>Create a page that renders the canonical component:
---
import About from "./about.astro";
export function getStaticPaths() {
return [{ params: { lang: "es" } }];
}
---
<About />Resulting file structure:
src/pages/[lang]/
├── about.astro ← Canonical page (all the logic)
├── sobre-nosotros.astro ← Wrapper (renders 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 (renders the matching shop page)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/123Astro 7 Advanced Routing
astro-intl 2.2.2 works with Astro 7's complete request pipeline and explicitly composed src/fetch.ts pipelines. No Astro 7-specific astro-intl API is required.
Keep Astro's complete pipeline, including src/middleware.ts:
import { astro, FetchState } from "astro/fetch";
export default {
fetch(request: Request) {
return astro(new FetchState(request));
},
};Or compose only middleware and page rendering:
import { FetchState, middleware, pages } from "astro/fetch";
export default {
fetch(request: Request) {
const state = new FetchState(request);
return middleware(state, (nextState) => pages(nextState));
},
};middleware() for createIntlMiddleware() to initialize request locale and messages. Sessions, Actions, cache, and other handlers must also be added explicitly when your app needs them.Fallback 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.