Configuration
1. Create message files
Create one JSON file per locale inside src/i18n/messages/. You can organize keys into namespaces (nested objects).
{
"nav": {
"home": "Home",
"docs": "Documentation"
},
"hero": {
"title": "Hello, World!",
"description": "Welcome to <bold>astro-intl</bold>"
}
}2. Choose your approach
astro-intl offers three ways to load messages. Pick the one that fits your project best.
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.
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.
import { defineRequestConfig } from "astro-intl";
export default defineRequestConfig(async (locale) => {
return {
locale,
messages: (await import(`./messages/${locale}.json`)).default,
};
});astro.config.mjs with locales:
import { defineConfig } from "astro/config";
import astroIntl from "astro-intl";
export default defineConfig({
integrations: [
astroIntl({
defaultLocale: "en",
locales: ["en", "es"],
}),
],
});Option C — messagesDir (simplified JSON)
Set messagesDir to a directory path and the integration will automatically load {locale}.json files. No manual imports needed — perfect for JSON-based workflows.
import { defineConfig } from "astro/config";
import astroIntl from "astro-intl";
export default defineConfig({
integrations: [
astroIntl({
defaultLocale: "en",
locales: ["en", "es"],
messagesDir: "./src/i18n/messages",
}),
],
});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):
---
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):
---
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:
export const routing = {
locales: ["en", "es"],
defaultLocale: "en",
} as const;Create the middleware:
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:
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.
export function getStaticPaths() {
return [
{ params: { lang: "en" } },
{ params: { lang: "es" } },
];
}