Comienza con astro-intl
Una librería de internacionalización type-safe para Astro, inspirada en next-intl. Sin props drilling, carga dinámica y soporte completo de TypeScript.
Instalación
Instala astro-intl usando tu gestor de paquetes preferido.
npm install astro-intl pnpm add astro-intl yarn add astro-intl Opcionalmente registra la integración en tu astro.config.mjs:
import { defineConfig } from 'astro/config';
import astroIntl from 'astro-intl';
export default defineConfig({
integrations: [astroIntl()],
}); Estructura de Archivos
astro-intl espera una estructura de carpetas específica dentro de tu proyecto. Aquí está el layout recomendado:
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 [lang] para que Astro genere una ruta por locale. src/i18n/messages/. El nombre del archivo debe coincidir exactamente con el código del locale (ej. en.json, es.json). request.ts exporta una función que recibe el string del locale y retorna los mensajes para ese locale. Configuración
1. Crear archivos de mensajes
Crea un archivo JSON por locale dentro de src/i18n/messages/. Puedes organizar las claves en namespaces (objetos anidados).
{
"nav": {
"home": "Home",
"docs": "Documentation"
},
"hero": {
"title": "Hello, World!",
"description": "Welcome to <bold>astro-intl</bold>"
}
}2. Elige tu enfoque
astro-intl ofrece dos formas de cargar mensajes. Elige la que mejor se adapte a tu proyecto.
createIntlMiddleware, debes pasar defaultLocale y locales directamente en astro.config.mjs para que funciones como getLocales(), getDefaultLocale() e isValidLocale() funcionen correctamente. Opción A — Messages en config (recomendado)
Pasa tus imports de mensajes junto con defaultLocale y locales directamente en astro.config.mjs. No se necesitan archivos extra.
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"),
},
}),
],
});Opción B — Custom request.ts (avanzado)
Para control total, usa defineRequestConfig para crear un archivo request — similar a next-intl. Esto te permite agregar lógica personalizada como obtener mensajes desde un CMS. Aún necesitas pasar defaultLocale y locales en 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 con locales:
import { defineConfig } from "astro/config";
import astroIntl from "astro-intl";
export default defineConfig({
integrations: [
astroIntl({
defaultLocale: "en",
locales: ["en", "es"],
}),
],
});3. Configurar tu Layout
Mejor práctica: Llama a setRequestLocale una sola vez en un layout compartido (ej: src/layouts/Layout.astro) en lugar de repetirlo en cada página. Esto inicializa el contexto global de i18n antes de que cualquier componente lea traducciones.
Con Opción A (messages en 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>Con Opción 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. Enfoque con Middleware (alternativa)
En lugar de llamar a setRequestLocale en cada layout, puedes usar createIntlMiddleware para manejarlo automáticamente en todas las rutas. Esto también establece locales y defaultLocale en el store, por lo que no necesitas pasarlos en astro.config.mjs.
Crea un archivo de routing:
export const routing = {
locales: ["en", "es"],
defaultLocale: "en",
} as const;Crea el middleware:
import "@/i18n/request";
import { createIntlMiddleware } from "astro-intl/middleware";
import { routing } from "@/i18n/routing";
export const onRequest = createIntlMiddleware(routing);Tu astro.config.mjs se mantiene simple:
import { defineConfig } from "astro/config";
import astroIntl from "astro-intl";
export default defineConfig({
integrations: [astroIntl()],
});4. Exportar getStaticPaths
En cada página [lang], exporta getStaticPaths retornando todos los locales soportados para que Astro pueda pre-renderizarlos.
export function getStaticPaths() {
return [
{ params: { lang: "en" } },
{ params: { lang: "es" } },
];
}Uso
Traducción Básica
Una vez configurado tu layout, simplemente usa getTranslations(namespace) en cualquier página para obtener una función de traducción tipada para un namespace específico.
---
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>Interpolación
Usa placeholders {curlyOpen}varName{curlyClose} en tus strings de traducción y pasa un objeto de valores como segundo argumento de t(). Tipos aceptados: string, number, boolean. Los valores faltantes o null/undefined mantienen el placeholder sin cambios.
{
"hero": {
"greeting": "Hello, {name}!",
"info": "You have {count} new messages"
}
}---
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 -->Traducción con Markup
Usa t.markup(key, tags) para interpolar etiquetas HTML dentro de un string de traducción. El objeto tags mapea nombres de etiquetas a funciones que reciben el contenido interno.
{
"hero": {
"description": "A <bold>type-safe</bold> solution for <link>Astro</link>"
}
}---
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 + Interpolación
Combina interpolación de variables con reemplazo de tags pasando {curlyOpen} values, tags {curlyClose} a t.markup(). La interpolación se ejecuta primero, luego se procesan los tags.
{
"hero": {
"welcome": "Hello {name}, read our <link>docs</link> to get started"
}
}---
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" -->Componentes React
Usa getTranslationsReact(namespace) dentro de componentes React, luego llama a t.rich(key, tags) para interpolar nodos React.
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>
);
}Referencia de API
setRequestLocale
setRequestLocale(url: URL, getConfig: Function): Promise<void>
Extrae el locale del pathname de la URL (primer segmento) y llama a tu función getConfig para cargar los mensajes. Debe ser awaited antes de cualquier otra llamada.
runWithLocale
runWithLocale<R>(url: URL, fn: () => R | Promise<R>, getConfig?: GetRequestConfigFn): Promise<R>
Ejecuta una función dentro de un contexto aislado por request. Usa AsyncLocalStorage en Node.js para SSR concurrency-safe. Cae en fallback a variable global en runtimes sin soporte (Cloudflare Workers, Deno).
getLocale
getLocale(): string
Retorna el string del locale actual establecido por setRequestLocale. Lanza un error si se llama antes de establecer el locale.
getTranslations
getTranslations<T>(namespace?: string)
Retorna una función t(key, values?) tipada con scope al namespace dado. El objeto values opcional reemplaza placeholders {curlyOpen}varName{curlyClose}. También expone t.markup(key, tags | {curlyOpen} values?, tags {curlyClose}) para interpolación HTML con sustitución de variables opcional.
getTranslationsReact
getTranslationsReact<T>(namespace?: string)
Igual que getTranslations pero retorna una función con t.rich(key, tags) para interpolación de nodos React.
getDefaultLocale
getDefaultLocale(): string
Retorna el defaultLocale configurado via las opciones de la integración o createIntlMiddleware. Por defecto es "en" si no se establece.
getLocales
getLocales(): string[]
Retorna el array de locales configurados via las opciones de la integración o createIntlMiddleware. Retorna un array vacío si no se establece.
isValidLocale
isValidLocale(locale: string): boolean
Verifica si un string de locale está en la lista de locales configurada. Retorna true para cualquier string si no se han configurado locales.
getMessages
getMessages(): Record<string, unknown>
Retorna el objeto completo de mensajes para el request actual. Lanza un error si se llama antes de setRequestLocale.
createIntlMiddleware
createIntlMiddleware(options: { locales: string[]; defaultLocale?: string })
Crea un middleware de Astro que llama automáticamente a setRequestLocale en cada request. También establece locales y defaultLocale en el store de intl, por lo que no necesitas pasarlos en astro.config.mjs. Importar desde astro-intl/middleware.
RequestConfig Tipo retornado por tu función getRequestConfig. Contiene locale: string y messages: Record.
IntlConfig Tipo de configuración para las opciones de la integración. Contiene defaultLocale: string — el locale usado cuando la URL no tiene prefijo de idioma.
Primitive Tipo unión para valores de interpolación: string | number | boolean | null | undefined. Se usa como tipo de valor en el objeto values pasado a t() y t.markup().
DotPaths<T> Tipo utilitario que genera todos los paths válidos en notación de puntos para un objeto de mensajes dado. Se usa internamente para proveer autocompletado en las claves de traducción.
Ejemplos
Usando Namespaces
Los namespaces te permiten dividir archivos de traducción grandes en secciones lógicas. Pasa el nombre del namespace a getTranslations.
---
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>Interpolación
Reemplaza placeholders {curlyOpen}varName{curlyClose} en tus traducciones con valores dinámicos en tiempo de ejecución.
---
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>" -->Claves Type-Safe
Pasa el tipo de tus mensajes como genérico a getTranslations para obtener autocompletado completo y verificación de tipos en las claves de traducción.
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");Leyendo el Locale Actual
Usa getLocale() en cualquier lugar después de que setRequestLocale haya sido llamado — por ejemplo para construir URLs con locale.
---
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>