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.

src/i18n/routing.ts
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:

src/middleware.ts
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:

File structure
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
How it works When a user visits /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.

astro.config.mjs
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]" },
      },
    }),
  ],
});
Without middleware, there are no automatic rewrites. You need to create page files for each translated path. The simplest approach is to create thin wrapper pages that re-export the canonical one.

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):

src/pages/[lang]/about.astro
---
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:

src/pages/[lang]/sobre-nosotros.astro
---
// Re-export everything from the canonical page
export { default } from "./about.astro";
export { getStaticPaths } from "./about.astro";
---

Resulting file structure:

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)
Each wrapper file is just 2 lines. The canonical page contains all the logic. When Astro builds /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.

src/pages/[lang]/index.astro
---
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.

src/components/LanguageSelector.astro
---
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 -->
If the current path doesn't match any route template, 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.

Route with multiple params
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/123

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:

src/pages/[lang]/index.astro
---
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");
---
On Astro < 6.1, getFallbackRoutes() returns an empty array — no errors, fully backward compatible.