Documentation

Get Started with astro-intl

A type-safe internationalization library for Astro, inspired by next-intl. No props drilling, dynamic loading, and full TypeScript support.

Installation

Install astro-intl using your preferred package manager.

npm
npm install astro-intl
pnpm
pnpm add astro-intl
yarn
yarn add astro-intl

Optionally register the integration in your astro.config.mjs:

astro.config.mjs
import { defineConfig } from 'astro/config';
import astroIntl from 'astro-intl';

export default defineConfig({
  integrations: [astroIntl()],
});
The integration is optional — it only logs a startup message. The core API works without it.

File Structure

astro-intl expects a specific folder structure inside your project. Here is the recommended layout:

Project structure
src/
├── i18n/
│   ├── messages/
│   │   ├── en.json       ← English translations
│   │   └── es.json       ← Spanish translations
│   └── request.ts        ← Locale resolver
├── layouts/
│   └── Layout.astro      ← Shared layout
├── pages/
│   └── [lang]/           ← Dynamic locale segment
│       ├── index.astro
│       └── docs.astro
└── components/
    └── MyComponent.astro
pages/[lang]/ Pages must be nested under a [lang] dynamic segment so Astro can generate one route per locale.
messages/ Each locale gets its own JSON file inside src/i18n/messages/. The filename must match the locale code exactly (e.g. en.json, es.json).
request.ts The request.ts file exports a function that receives the locale string and returns the messages for that locale.

Configuration

1. Create message files

Create one JSON file per locale inside src/i18n/messages/. You can organize keys into namespaces (nested objects).

src/i18n/messages/en.json
{
  "nav": {
    "home": "Home",
    "docs": "Documentation"
  },
  "hero": {
    "title": "Hello, World!",
    "description": "Welcome to <bold>astro-intl</bold>"
  }
}

2. Choose your approach

astro-intl offers two ways to load messages. Pick the one that fits your project best.

If you are not using createIntlMiddleware, you must pass defaultLocale and locales directly in astro.config.mjs so that functions like getLocales(), getDefaultLocale(), and isValidLocale() work correctly.

Option A — Messages in config (recommended)

Pass your message imports along with defaultLocale and locales directly in astro.config.mjs. No extra files needed.

astro.config.mjs
import { defineConfig } from "astro/config";
import astroIntl from "astro-intl";

export default defineConfig({
  integrations: [
    astroIntl({
      defaultLocale: "en",
      locales: ["en", "es"],
      messages: {
        en: () => import("./src/i18n/messages/en.json"),
        es: () => import("./src/i18n/messages/es.json"),
      },
    }),
  ],
});

Option B — Custom request.ts (advanced)

For full control, use defineRequestConfig to create a request file — similar to next-intl. This lets you add custom logic like fetching messages from a CMS. You still need to pass defaultLocale and locales in astro.config.mjs.

src/i18n/request.ts
import { defineRequestConfig } from "astro-intl";

export default defineRequestConfig(async (locale) => {
  return {
    locale,
    messages: (await import(`./messages/${locale}.json`)).default,
  };
});

astro.config.mjs with locales:

astro.config.mjs
import { defineConfig } from "astro/config";
import astroIntl from "astro-intl";

export default defineConfig({
  integrations: [
    astroIntl({
      defaultLocale: "en",
      locales: ["en", "es"],
    }),
  ],
});

3. Set up your Layout

Best practice: Call setRequestLocale once in a shared layout (e.g., src/layouts/Layout.astro) instead of repeating it on every page. This initializes the global i18n context before any component reads translations.

With Option A (messages in config):

src/layouts/Layout.astro
---
import { setRequestLocale } from "astro-intl";

// No need to import anything — messages come from astro.config
await setRequestLocale(Astro.url);
---

<!DOCTYPE html>
<html>
  <head>
    <title>My Site</title>
  </head>
  <body>
    <slot />
  </body>
</html>

With Option B (defineRequestConfig):

src/layouts/Layout.astro
---
import { setRequestLocale } from "astro-intl";
import "../i18n/request"; // registers defineRequestConfig

await setRequestLocale(Astro.url);
---

<!DOCTYPE html>
<html>
  <head>
    <title>My Site</title>
  </head>
  <body>
    <slot />
  </body>
</html>

3b. Middleware approach (alternative)

Instead of calling setRequestLocale in every layout, you can use createIntlMiddleware to handle it automatically for all routes. This also sets locales and defaultLocale in the store, so you don't need to pass them in astro.config.mjs.

Create a routing config:

src/i18n/routing.ts
export const routing = {
  locales: ["en", "es"],
  defaultLocale: "en",
} as const;

Create 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 astro.config.mjs stays simple:

astro.config.mjs
import { defineConfig } from "astro/config";
import astroIntl from "astro-intl";

export default defineConfig({
  integrations: [astroIntl()],
});

4. Export getStaticPaths

In every [lang] page, export getStaticPaths returning all supported locales so Astro can pre-render them.

src/pages/[lang]/index.astro
export function getStaticPaths() {
  return [
    { params: { lang: "en" } },
    { params: { lang: "es" } },
  ];
}

Usage

Basic Translation

Once your layout is configured, simply use getTranslations(namespace) in any page to get a typed translation function for a specific namespace.

src/pages/[lang]/index.astro
---
import { getTranslations } from "astro-intl";

// No need to call setRequestLocale here - it's already in the layout!
const tHero = getTranslations("hero");
const tNav  = getTranslations("nav");
---

<h1>{tHero("title")}</h1>
<p>{tHero("description")}</p>
<a href="/">{tNav("home")}</a>

Interpolation

Use {curlyOpen}varName{curlyClose} placeholders in your translation strings and pass a values object as the second argument to t(). Accepted types: string, number, boolean. Missing or null/undefined values keep the placeholder unchanged.

en.json — message with variables
{
  "hero": {
    "greeting": "Hello, {name}!",
    "info": "You have {count} new messages"
  }
}
Component.astro — t() with values
---
const tHero = getTranslations("hero");
---

<p>{tHero("greeting", { name: "John" })}</p>
<!-- "Hello, John!" -->

<p>{tHero("info", { count: 5 })}</p>
<!-- "You have 5 new messages" -->

<p>{tHero("greeting")}</p>
<!-- "Hello, {name}!" — placeholder kept when no values -->

Markup Translation

Use t.markup(key, tags) to interpolate HTML tags inside a translation string. The tags object maps tag names to functions that receive the inner content.

en.json — message with tags
{
  "hero": {
    "description": "A <bold>type-safe</bold> solution for <link>Astro</link>"
  }
}
Component.astro — t.markup()
---
const tHero = getTranslations("hero");
---

<p set:html={tHero.markup("description", {
  bold: (chunks) => `<strong>${chunks}</strong>`,
  link: (chunks) => `<a href="https://astro.build">${chunks}</a>`,
})} />

Markup + Interpolation

Combine variable interpolation with tag replacement by passing {curlyOpen} values, tags {curlyClose} to t.markup(). Interpolation runs first, then tags are processed.

en.json — variables + tags
{
  "hero": {
    "welcome": "Hello {name}, read our <link>docs</link> to get started"
  }
}
Component.astro — t.markup() with values
---
const tHero = getTranslations("hero");
---

<p set:html={tHero.markup("welcome", {
  values: { name: "John" },
  tags: {
    link: (chunks) => `<a href="/docs">${chunks}</a>`,
  }
})} />
<!-- "Hello John, read our <a href="/docs">docs</a> to get started" -->

React Components

Use getTranslationsReact(namespace) inside React components, then call t.rich(key, tags) to interpolate React nodes.

MyComponent.tsx — t.rich()
import { getTranslationsReact } from "astro-intl";

export default function MyComponent() {
  const t = getTranslationsReact("hero");

  return (
    <p>
      {t.rich("description", {
        bold: (chunks) => <strong>{chunks}</strong>,
        link: (chunks) => <a href="https://astro.build">{chunks}</a>,
      })}
    </p>
  );
}

API Reference

function setRequestLocale

                  
                    setRequestLocale(url: URL, getConfig: Function): Promise<void>
                  
                

Extracts the locale from the URL pathname (first segment) and calls your getConfig function to load messages. Must be awaited before any other call.

function runWithLocale

                  
                    runWithLocale<R>(url: URL, fn: () => R | Promise<R>, getConfig?: GetRequestConfigFn): Promise<R>
                  
                

Runs a function inside an isolated per-request context. Uses AsyncLocalStorage on Node.js for concurrency-safe SSR. Falls back to a global variable on runtimes without it (Cloudflare Workers, Deno).

function getLocale

                  
                    getLocale(): string
                  
                

Returns the current locale string set by setRequestLocale. Throws if called before setting the locale.

function getTranslations

                  
                    getTranslations<T>(namespace?: string)
                  
                

Returns a typed t(key, values?) function scoped to the given namespace. The optional values object replaces {curlyOpen}varName{curlyClose} placeholders. Also exposes t.markup(key, tags | {curlyOpen} values?, tags {curlyClose}) for HTML interpolation with optional variable substitution.

function getTranslationsReact

                  
                    getTranslationsReact<T>(namespace?: string)
                  
                

Same as getTranslations but returns a function with t.rich(key, tags) for React node interpolation.

function getDefaultLocale

                  
                    getDefaultLocale(): string
                  
                

Returns the defaultLocale configured via the integration options or createIntlMiddleware. Defaults to "en" if not set.

function getLocales

                  
                    getLocales(): string[]
                  
                

Returns the array of locales configured via the integration options or createIntlMiddleware. Returns an empty array if not set.

function isValidLocale

                  
                    isValidLocale(locale: string): boolean
                  
                

Checks whether a locale string is in the configured locales list. Returns true for any string if no locales have been configured.

function getMessages

                  
                    getMessages(): Record<string, unknown>
                  
                

Returns the full messages object for the current request. Throws if called before setRequestLocale.

function createIntlMiddleware

                  
                    createIntlMiddleware(options: { locales: string[]; defaultLocale?: string })
                  
                

Creates an Astro middleware that automatically calls setRequestLocale on every request. Also sets locales and defaultLocale in the intl store, so you don't need to pass them in astro.config.mjs. Import from astro-intl/middleware.

type RequestConfig

Type returned by your getRequestConfig function. Contains locale: string and messages: Record.

type IntlConfig

Configuration type for the integration options. Contains defaultLocale: string — the locale used when the URL has no language prefix.

type Primitive

Union type for interpolation values: string | number | boolean | null | undefined. Used as the value type in the values object passed to t() and t.markup().

type DotPaths<T>

Utility type that generates all valid dot-notation paths for a given messages object. Used internally to provide autocomplete for translation keys.

Examples

Using Namespaces

Namespaces let you split large translation files into logical sections. Pass the namespace name to getTranslations.

Multiple namespaces in one page
---
import { setRequestLocale, getTranslations } from "astro-intl";
import getRequestConfig from "../../i18n/request";

await setRequestLocale(Astro.url, getRequestConfig);

// Each call is scoped to its namespace
const tNav    = getTranslations("nav");
const tHero   = getTranslations("hero");
const tFooter = getTranslations("footer");
---

<nav>{tNav("home")}</nav>
<h1>{tHero("title")}</h1>
<footer>{tFooter("description")}</footer>

Interpolation

Replace {curlyOpen}varName{curlyClose} placeholders in your translations with dynamic values at runtime.

Interpolation with variables
---
import { setRequestLocale, getTranslations } from "astro-intl";
import getRequestConfig from "../../i18n/request";

await setRequestLocale(Astro.url, getRequestConfig);

const tHero = getTranslations("hero");
// en.json: { "hero": { "greeting": "Hello, {name}! You have {count} items." } }
---

<!-- Simple interpolation -->
<p>{tHero("greeting", { name: "John", count: 3 })}</p>
<!-- "Hello, John! You have 3 items." -->

<!-- Markup + interpolation combined -->
<p set:html={tHero.markup("welcome", {
  values: { name: "John" },
  tags: {
    link: (chunks) => `<a href="/docs">${chunks}</a>`,
  }
})} />
<!-- en.json: "welcome": "Hi {name}, check our <link>docs</link>" -->
<!-- Result: "Hi John, check our <a href="/docs">docs</a>" -->

Type-Safe Keys

Pass your messages type as a generic to getTranslations to get full autocomplete and type checking on translation keys.

Type-safe with generics
import type en from "../i18n/messages/en.json";

// Pass the type of your messages JSON
type HeroMessages = typeof en["hero"];

const tHero = getTranslations<HeroMessages>("hero");

// ✅ Autocomplete works — "title", "description", etc.
tHero("title");

// ❌ TypeScript error — key doesn't exist
tHero("nonExistentKey");

Reading the Current Locale

Use getLocale() anywhere after setRequestLocale has been called — for example to build locale-aware URLs.

Locale-aware links
---
import { getLocale } from "astro-intl";

const locale = getLocale(); // "en" | "es"
---

<!-- Build locale-prefixed URLs -->
<a href={`/${locale}/docs`}>Documentation</a>
<a href={`/${locale}/about`}>About</a>