Cambiar tema

Guía de configuración i18n de Astro: sitio multilingüe en 30 minutos (con selector de idioma)

Easton editorial illustration: monorepo project desk

Tienes un blog Astro listo para salir al mercado internacional y te encuentras con que no sabes montar el soporte multilingüe. locales, defaultLocale, prefixDefaultLocale: un montón de opciones que no terminan de encajar. No sabes cómo organizar el contenido multilingüe, Content Collections suena abstracto y un selector de idioma parece imposible.

La configuración i18n de Astro no es tan complicada; el problema es que la documentación oficial es muy técnica. En este artículo recorremos todo el flujo de configuración i18n de Astro, desde lo básico hasta el selector de idioma, con código completo en cada paso. En 30 minutos tu sitio puede soportar varios idiomas.

Configuración básica de Astro i18n (10 minutos)

Configuración detallada de astro.config.mjs

Empecemos por lo más importante: el archivo de configuración. Desde Astro v4.0, el soporte i18n viene integrado y es bastante sencillo. Abre tu astro.config.mjs y añade estas líneas:

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

export default defineConfig({
  i18n: {
    // Indica a Astro qué idiomas soporta tu sitio
    locales: ['en', 'zh-cn', 'ja'],

    // Idioma por defecto (debe ser uno de los locales)
    defaultLocale: 'en',

    // Si el idioma por defecto lleva prefijo en la ruta
    prefixDefaultLocale: false,
  }
});

Veamos cada opción:

locales — Todos los idiomas que soporta tu sitio. Usa códigos estándar como 'en' (inglés), 'zh-cn' (chino simplificado) o 'ja' (japonés). Si necesitas una distinción más fina, también puedes usar 'en-US', 'en-GB', etc.

defaultLocale — El idioma por defecto que verán los visitantes la primera vez. Debe ser uno de los valores del array locales; si no, Astro lanzará un error.

prefixDefaultLocale — Una opción con la que dudé bastante. Con false, las URL del idioma por defecto no llevan prefijo (por ejemplo, /about), y los demás sí (/zh-cn/about, /ja/about). Con true, todos los idiomas llevan prefijo (/en/about, /zh-cn/about).

En la mayoría de los casos, false basta: las URL del idioma principal quedan más limpias. Si te importa mucho la uniformidad de las URL o tienes requisitos SEO especiales, considera true.

Comparación de las tres estrategias de rutas

El enrutamiento i18n de Astro ofrece tres estrategias. Comparemos en una tabla cuál encaja mejor contigo:

EstrategiaConfiguraciónEjemplos de URLCasos de usoVentajas y desventajas
Estrategia 1: idioma por defecto sin prefijoprefixDefaultLocale: false/about
/zh-cn/about
/ja/about
La mayoría de sitios (recomendado)✅ URL limpias para el idioma por defecto
❌ Formato de URL no uniforme
Estrategia 2: todos los idiomas con prefijoprefixDefaultLocale: true/en/about
/zh-cn/about
/ja/about
Necesidad de URL uniformes
o requisitos SEO especiales
✅ Formato de URL uniforme
✅ Lógica de cambio de idioma más simple
❌ URL del idioma por defecto algo más largas
Estrategia 3: modo manualrouting: 'manual'Totalmente personalizableNecesidades multilingües complejas
Control total del enrutamiento
✅ Máxima flexibilidad
❌ Configuración compleja; mucha lógica manual

Mi recomendación: si el proyecto es relativamente simple (blog, documentación), usa la estrategia 1. Si quieres URL más ordenadas, la 2. La 3 es para proyectos con necesidades especiales, como elegir idioma según el comportamiento del usuario o usar subdominios distintos por idioma.

En este punto quizá pienses: solo quiero un blog bilingüe chino-inglés, ¿basta con copiar la configuración de arriba y adaptarla? Exacto, así de simple. Cambia locales a ['zh-cn', 'en'], pon defaultLocale en 'zh-cn' (si tu audiencia principal es china) y listo.

Configurar soporte multilingüe i18n en Astro

Pasos completos para configurar un sitio Astro multilingüe en 30 minutos

  1. 1

    Step 1: Configurar astro.config.mjs

    Añade la configuración i18n en astro.config.mjs:
  2. 2

    Step 2: Organizar contenido multilingüe

    Elige un esquema de organización:
  3. 3

    Step 3: Crear diccionario de traducción UI

    Crea el diccionario en src/i18n/ui.ts:
  4. 4

    Step 4: Implementar selector de idioma

    Construye el componente con getRelativeLocaleUrl y Astro.currentLocale:
  5. 5

    Step 5: Optimización SEO

    Añade etiquetas hreflang en el Layout, configura el sitemap y localiza la meta información

Organización del contenido multilingüe (dos enfoques)

Con la configuración lista, toca organizar el contenido. Aquí mucha gente se atasca: ¿cómo colocar los archivos? Astro te da dos caminos; elige el que te resulte más natural.

Enfoque 1: carpetas por idioma (recomendado para principiantes)

Es la forma más intuitiva: una carpeta por idioma, así:

src/pages/
├── about.astro        # Idioma por defecto (supongamos chino)
├── blog.astro
├── index.astro
├── en/                # Versión en inglés
│   ├── about.astro
│   ├── blog.astro
│   └── index.astro
└── ja/                # Versión en japonés
    ├── about.astro
    ├── blog.astro
    └── index.astro

Si usas prefixDefaultLocale: false (idioma por defecto sin prefijo), los archivos del idioma principal van directamente en la raíz de pages. Solo los demás idiomas necesitan subcarpetas.

Ventaja: estructura clara; cada idioma es independiente y no se pisan al editar. Inconveniente: con muchas páginas hay mucha duplicación. Por ejemplo, 20 páginas y 5 idiomas = 100 archivos que mantener.

Enfoque 2: rutas dinámicas (recomendado para usuarios avanzados)

Si el enfoque 1 te parece pesado, prueba rutas dinámicas. Un solo archivo puede atender todos los idiomas:

src/pages/
└── [lang]/
    └── [...slug].astro

En [...slug].astro, renderiza el contenido según el parámetro lang:


---

// src/pages/[lang]/[...slug].astro
export function getStaticPaths() {
  const locales = ['zh-cn', 'en', 'ja'];
  const slugs = ['about', 'blog', 'contact'];

  return locales.flatMap((lang) =>
    slugs.map((slug) => ({
      params: { lang, slug },
    }))
  );
}

const { lang, slug } = Astro.params;
// Carga el contenido según lang y slug

---

Mayor reutilización de código y menor coste de mantenimiento, pero exige entender el enrutamiento dinámico de Astro. Si eres nuevo, empieza con el enfoque 1 y pasa al 2 cuando te sientas cómodo.

Content Collections para contenido multilingüe (imprescindible en blogs)

Para un blog, Content Collections es lo central. Es la forma recomendada por Astro de gestionar contenido, ideal para blogs y documentación.

La estructura de carpetas sería así:

src/content/
└── blog/
    ├── en/
    │   ├── post-1.md
    │   └── post-2.md
    ├── zh-cn/
    │   ├── post-1.md
    │   └── post-2.md
    └── ja/
        ├── post-1.md
        └── post-2.md

Define el schema en src/content/config.ts:

// src/content/config.ts
import { defineCollection, z } from 'astro:content';

const blogCollection = defineCollection({
  schema: z.object({
    title: z.string(),
    author: z.string(),
    date: z.date(),
    lang: z.enum(['en', 'zh-cn', 'ja']),  // Campo de idioma
  }),
});

export const collections = {
  blog: blogCollection,
};

Obtén artículos del idioma actual en la página:


---

// src/pages/blog/index.astro
import { getCollection } from 'astro:content';

const currentLang = Astro.currentLocale;  // Obtiene el idioma actual
const posts = await getCollection('blog', ({ data }) => {
  return data.lang === currentLang;  // Solo artículos del idioma actual
});

---

Así se muestran automáticamente los artículos del idioma elegido. La documentación oficial de Astro usa este esquema para contenido multilingüe; es un enfoque fiable.

Gestión de archivos de traducción UI

Además del contenido, necesitas traducir textos fijos de la interfaz: navegación, botones, etiquetas de formulario, etc. Recomiendo un diccionario de traducción:

// src/i18n/ui.ts
export const ui = {
  'en': {
    'nav.home': 'Home',
    'nav.about': 'About',
    'nav.blog': 'Blog',
    'btn.readMore': 'Read More',
  },
  'zh-cn': {
    'nav.home': '首页',
    'nav.about': '关于',
    'nav.blog': '博客',
    'btn.readMore': '阅读更多',
  },
  'ja': {
    'nav.home': 'ホーム',
    'nav.about': '概要',
    'nav.blog': 'ブログ',
    'btn.readMore': '続きを読む',
  },
} as const;

Y dos funciones auxiliares:

// src/i18n/utils.ts
import { ui } from './ui';

// Obtiene el idioma actual desde la URL
export function getLangFromUrl(url: URL) {
  const [, lang] = url.pathname.split('/');
  if (lang in ui) return lang as keyof typeof ui;
  return 'zh-cn';  // Idioma por defecto
}

// Devuelve la función de traducción
export function useTranslations(lang: keyof typeof ui) {
  return function t(key: keyof typeof ui[typeof lang]) {
    return ui[lang][key] || ui['zh-cn'][key];
  }
}

Uso en un componente:


---

// Algún componente
import { getLangFromUrl, useTranslations } from '@/i18n/utils';

const lang = getLangFromUrl(Astro.url);
const t = useTranslations(lang);

---

<nav>
  <a href="/">{t('nav.home')}</a>
  <a href="/about">{t('nav.about')}</a>
  <a href="/blog">{t('nav.blog')}</a>
</nav>

Simple y práctico: todas las traducciones en un archivo, fácil de mantener. Si el volumen crece mucho, puedes dividirlo en varios JSON por módulo.

Implementar el selector de idioma (código completo)

Con configuración y contenido listos, llega la parte clave: el selector de idioma. Fue lo que más me costó al principio; tras revisar varios sitios, con las funciones helper de Astro resulta sencillo.

Entender las funciones helper i18n de Astro

Astro ofrece varias funciones muy útiles para URL multilingües:

getRelativeLocaleUrl(locale, path) — Obtiene la ruta relativa de un idioma.

import { getRelativeLocaleUrl } from 'astro:i18n';

// URL de la página About en inglés
const url = getRelativeLocaleUrl('en', 'about');
// Devuelve: '/en/about' o '/about' (según tu configuración)

getAbsoluteLocaleUrl(locale, path) — Obtiene la URL absoluta (con dominio).

import { getAbsoluteLocaleUrl } from 'astro:i18n';

const url = getAbsoluteLocaleUrl('en', 'about');
// Devuelve: 'https://example.com/en/about'

Astro.currentLocale — Obtiene el idioma de la página actual.


---

const currentLang = Astro.currentLocale;
// Devuelve: 'zh-cn', 'en', etc.

---

Astro.preferredLocale — Obtiene el idioma preferido del navegador (si tu sitio lo soporta).


---

const browserLang = Astro.preferredLocale;
// Devuelve: idioma del navegador (si está en tus locales)

---

Con estas herramientas ya puedes montar el selector.

Construir el componente de cambio de idioma

Aquí tienes un selector sencillo y listo para copiar:


---

// src/components/LanguageSwitcher.astro
import { getRelativeLocaleUrl } from 'astro:i18n';

// Todos los idiomas soportados (mejor leerlos de la config; aquí van fijos para el ejemplo)
const locales = {
  'zh-cn': '简体中文',
  'en': 'English',
  'ja': '日本語',
};

// Idioma y ruta actuales
const currentLang = Astro.currentLocale || 'zh-cn';
const currentPath = Astro.url.pathname
  .replace(`/${currentLang}/`, '/')  // Quita el prefijo de idioma
  .replace(/^\//, '');  // Quita la barra inicial

---

<div class="language-switcher">
  <button class="lang-button">
    {locales[currentLang]} ▼
  </button>
  <div class="lang-dropdown">
    {Object.entries(locales).map(([lang, label]) => {
      const url = getRelativeLocaleUrl(lang, currentPath);
      return (
        <a
          href={url}
          class={lang === currentLang ? 'active' : ''}
        >
          {label}
        </a>
      );
    })}
  </div>
</div>

<style>
  .language-switcher {
    position: relative;
    display: inline-block;
  }

  .lang-button {
    padding: 8px 16px;
    background: #f3f4f6;
    border: 1px solid #d1d5db;
    border-radius: 6px;
    cursor: pointer;
  }

  .lang-button:hover {
    background: #e5e7eb;
  }

  .lang-dropdown {
    display: none;
    position: absolute;
    top: 100%;
    right: 0;
    margin-top: 4px;
    background: white;
    border: 1px solid #d1d5db;
    border-radius: 6px;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
    min-width: 150px;
  }

  .language-switcher:hover .lang-dropdown {
    display: block;
  }

  .lang-dropdown a {
    display: block;
    padding: 10px 16px;
    color: #374151;
    text-decoration: none;
    transition: background 0.2s;
  }

  .lang-dropdown a:hover {
    background: #f3f4f6;
  }

  .lang-dropdown a.active {
    background: #dbeafe;
    color: #1e40af;
    font-weight: 500;
  }
</style>

<script>
  // Alternar en móvil al pulsar
  document.querySelector('.lang-button')?.addEventListener('click', (e) => {
    e.stopPropagation();
    const dropdown = document.querySelector('.lang-dropdown');
    dropdown?.classList.toggle('show');
  });

  // Cerrar al pulsar fuera
  document.addEventListener('click', () => {
    document.querySelector('.lang-dropdown')?.classList.remove('show');
  });
</script>

Úsalo en tu Layout o barra de navegación:


---

// src/layouts/Layout.astro
import LanguageSwitcher from '@/components/LanguageSwitcher.astro';

---

<header>
  <nav>
    <!-- Otros enlaces de navegación -->
    <LanguageSwitcher />
  </nav>
</header>

La lógica principal:

  1. Obtener idioma y ruta actuales
  2. Generar la URL de cada idioma soportado
  3. Resaltar el idioma activo
  4. Cambiar al pulsar el enlace correspondiente

Fíjate en getRelativeLocaleUrl(lang, currentPath): al cambiar de idioma el usuario permanece en la misma página en otra versión lingüística, no vuelve al inicio.

Detección del idioma del navegador (opcional pero recomendable)

A veces quieres que la primera visita redirija al idioma del navegador. Puedes hacerlo con middleware:

// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';

export const onRequest = defineMiddleware((context, next) => {
  const url = context.url;
  const currentLocale = context.currentLocale;
  const preferredLocale = context.preferredLocale;

  // Si entra en la raíz y el idioma del navegador no coincide, redirige
  if (url.pathname === '/' && preferredLocale && preferredLocale !== currentLocale) {
    return context.redirect(`/${preferredLocale}/`);
  }

  return next();
});

Así la primera visita va al idioma familiar del usuario. No fuerces la redirección en cada visita: si el usuario cambió de idioma manualmente y lo reviertes, la experiencia empeora.

Mejor combinar con una cookie que recuerde la elección:

// Middleware mejorado
export const onRequest = defineMiddleware((context, next) => {
  const url = context.url;
  const currentLocale = context.currentLocale;
  const preferredLocale = context.preferredLocale;
  const savedLang = context.cookies.get('user-lang')?.value;

  // Prioridad a la preferencia guardada
  if (savedLang && savedLang !== currentLocale && url.pathname === '/') {
    return context.redirect(`/${savedLang}/`);
  }

  // Si no hay cookie, usa el idioma del navegador
  if (!savedLang && url.pathname === '/' && preferredLocale && preferredLocale !== currentLocale) {
    context.cookies.set('user-lang', preferredLocale, {
      path: '/',
      maxAge: 31536000,  // 1 año
    });
    return context.redirect(`/${preferredLocale}/`);
  }

  return next();
});

Y al cambiar idioma en el selector, actualiza la cookie:

<script>
  document.querySelectorAll('.lang-dropdown a').forEach((link) => {
    link.addEventListener('click', (e) => {
      const lang = e.target.getAttribute('data-lang');
      document.cookie = `user-lang=${lang}; path=/; max-age=31536000`;
    });
  });
</script>

Así se recuerda la preferencia y en la próxima visita se muestra el idioma elegido.

Técnicas avanzadas (fallback, dominios, SEO)

Con lo básico cubierto, unas técnicas más avanzadas. Si el proyecto es simple, puedes saltar esta sección y volver cuando la necesites.

Configuración de estrategia fallback

Imagina que traduces el sitio poco a poco y aún no existe la versión japonesa de algunas páginas. Si un usuario entra en una URL japonesa inexistente, quizá quieras mostrar la versión en inglés. Eso es fallback:

// astro.config.mjs
export default defineConfig({
  i18n: {
    locales: ['en', 'zh-cn', 'ja'],
    defaultLocale: 'en',
    fallback: {
      ja: 'en',      // Si no hay japonés, muestra inglés
      'zh-cn': 'en', // Si no hay chino, muestra inglés
    },
  }
});

Si /ja/some-page no existe, Astro mostrará el contenido de /en/some-page en lugar de un 404. Muy útil mientras el contenido sigue en traducción.

Mapeo de dominios personalizado

Algunos productos internacionales usan dominios distintos por idioma:

  • Inglés: example.com
  • Chino: example.cn
  • Japonés: example.jp

Astro lo soporta, pero el mapeo de dominios solo funciona en modo SSR:

// astro.config.mjs
export default defineConfig({
  output: 'server',  // Hay que activar SSR
  adapter: node(),   // Y configurar un adaptador
  i18n: {
    locales: ['en', 'zh-cn', 'ja'],
    defaultLocale: 'en',
    domains: {
      'zh-cn': 'https://example.cn',
      ja: 'https://example.jp',
    },
  }
});

El contenido en chino se despliega en example.cn y el japonés en example.jp. En sitios estáticos (modo por defecto) esta opción no está disponible.

Puntos clave de SEO

En sitios multilingües, el SEO consiste sobre todo en decir a los buscadores qué versiones lingüísticas existen. Lo más importante son las etiquetas hreflang.

Astro facilita el SEO i18n: con buena configuración, mucho es automático. Aun así, conviene hacer manualmente lo siguiente:

1. Añadir etiquetas hreflang en el Layout


---

// src/layouts/Layout.astro
import { getAbsoluteLocaleUrl } from 'astro:i18n';

const locales = ['en', 'zh-cn', 'ja'];
const currentPath = Astro.url.pathname
  .replace(/^\/(en|zh-cn|ja)\//, '')
  .replace(/^\//, '');

---

<html>
<head>
  <!-- Etiqueta hreflang por idioma -->
  {locales.map((lang) => (
    <link
      rel="alternate"
      hreflang={lang}
      href={getAbsoluteLocaleUrl(lang, currentPath)}
    />
  ))}

  <!-- Etiqueta de idioma por defecto -->
  <link
    rel="alternate"
    hreflang="x-default"
    href={getAbsoluteLocaleUrl('en', currentPath)}
  />

  <!-- Meta description localizada -->
  <meta name="description" content={description[currentLang]} />

  <!-- URL canónica -->
  <link rel="canonical" href={getAbsoluteLocaleUrl(currentLang, currentPath)} />
</head>
</html>

2. sitemap.xml multilingüe

Si usas @astrojs/sitemap, genera un sitemap por idioma automáticamente. Asegúrate de configurar site:

// astro.config.mjs
import sitemap from '@astrojs/sitemap';

export default defineConfig({
  site: 'https://example.com',  // Obligatorio
  integrations: [sitemap()],
});

3. Localizar meta información

Traduce también title, description, keywords y demás meta por idioma:


---

const meta = {
  'en': {
    title: 'Welcome to My Blog',
    description: 'A blog about web development',
  },
  'zh-cn': {
    title: '欢迎来到我的博客',
    description: '一个关于 Web 开发的博客',
  },
};

const currentLang = Astro.currentLocale || 'en';

---

<head>
  <title>{meta[currentLang].title}</title>
  <meta name="description" content={meta[currentLang].description} />
</head>

Con estos pasos, los buscadores indexarán correctamente tu sitio multilingüe.

Conclusión

Repasemos el flujo completo de configuración i18n en Astro:

Paso 1: configurar astro.config.mjs (5 minutos)

  • Define el array locales (idiomas soportados)
  • Define defaultLocale (idioma por defecto)
  • Elige la estrategia prefixDefaultLocale según tus necesidades

Paso 2: organizar el contenido multilingüe (elige el enfoque que te encaje)

  • Principiantes: carpetas por idioma
  • Avanzados: rutas dinámicas + Content Collections
  • No olvides el diccionario de traducción UI

Paso 3: implementar el selector de idioma (copia y pega el código)

  • Usa getRelativeLocaleUrl para generar URL
  • Usa Astro.currentLocale para el idioma actual
  • Opcional: detección del navegador y cookie de preferencia

Tras usar Astro i18n un tiempo, la verdad es que resulta muy cómodo: configuración simple, helpers útiles y buen rendimiento (todas las rutas por idioma se pregeneran en el build). Si tu sitio necesita varios idiomas, la solución integrada de Astro merece la pena.

¡Pon manos a la obra y añade soporte multilingüe a tu sitio Astro! Si te atascas, consulta la documentación oficial i18n de Astro; ahí están los detalles de la API.

Si tienes experiencia práctica o algún tropiezo que quieras compartir, déjalo en los comentarios; aprendemos entre todos.

FAQ

¿Cuánto tiempo lleva configurar i18n en Astro?
La configuración básica solo requiere 5-10 minutos:
• Incluye definir locales, defaultLocale y prefixDefaultLocale en astro.config.mjs

Un sitio multilingüe completo (con selector de idioma y optimización SEO) lleva unos 30 minutos.
¿Debo poner prefixDefaultLocale en true o false?
En la mayoría de los casos, false es suficiente:
• Así las URL del idioma por defecto son más limpias (por ejemplo, /about)

Si necesitas un formato de URL uniforme o requisitos SEO especiales, puedes usar true (por ejemplo, /en/about).
¿Cómo organizar el contenido multilingüe?
Hay dos enfoques:

Enfoque 1: carpetas por idioma (recomendado para principiantes):
• Estructura clara, pero más archivos

Enfoque 2: rutas dinámicas (recomendado para usuarios avanzados):
• Mayor reutilización de código y menor coste de mantenimiento

Para blogs, se recomienda gestionar artículos multilingües con Content Collections.
¿Cómo implementar un selector de idioma?
Usa la función getRelativeLocaleUrl de Astro para generar URL multilingües y Astro.currentLocale para obtener el idioma actual.

Puedes combinarlo con detección del idioma del navegador y cookies para recordar la preferencia del usuario.
¿Cómo optimizar el SEO de un sitio multilingüe?
Los pasos principales incluyen:
• Añadir etiquetas hreflang en el Layout para indicar a los buscadores las versiones en cada idioma
• Configurar el plugin sitemap para generar automáticamente un sitemap multilingüe
• Localizar title, description y demás meta información

Astro ofrece buen soporte para SEO i18n; gran parte del trabajo es automático.

13 min de lectura · Publicado el: 2 dic 2025 · Actualizado el: 21 ago 2026

Comentarios

Inicia sesión con GitHub para dejar un comentario

Easton BlogEaston Blog