Ir al contenido
Acecore

Cómo hacer que un sitio Astro 7 soporte 9 idiomas ― Traducción del blog y arquitectura multilingüe

by Gui
Índice
Cómo hacer que un sitio Astro 7 soporte 9 idiomas ― Traducción del blog y arquitectura multilingüe

Actualizamos el sitio web oficial de Acecore de solo japonés a soporte para 9 idiomas. En el despliegue inicial se tradujeron 21 artículos a 8 idiomas, generando 168 archivos. A 29 de julio de 2026, el repositorio contiene 29 artículos en japonés y 208 traducciones, 237 archivos de artículo en total, y el build genera 652 páginas. Solo se publican URLs localizadas cuando existe el archivo de traducción.

Estrategia multilingüe

Definición del alcance

Abordamos el soporte multilingüe en tres fases:

  1. Base i18n: Configuración de enrutamiento i18n integrado de Astro, utilidades de traducción y archivos JSON de traducción para 9 idiomas
  2. Traducción de textos UI: Textos de componentes en encabezado, pie de página, barra lateral y todas las páginas
  3. Traducción de artículos: 21 artículos traducidos a 8 idiomas en el despliegue inicial (168 archivos generados)

Diseño de URLs

Adoptamos prefixDefaultLocale: false de Astro, sirviendo japonés en la raíz (/blog/...) y otros idiomas con prefijos (/en/blog/..., /zh-cn/blog/..., etc.).

# Japonés (predeterminado)
/blog/astro-performance-tuning/

# Inglés
/en/blog/astro-performance-tuning/

# Chino simplificado
/zh-cn/blog/astro-performance-tuning/

Usar el mismo slug en todos los idiomas mantiene simple el mapeo de URLs al cambiar de idioma.

Implementación de la base i18n

Configuración i18n de Astro

Se configura el enrutamiento i18n en astro.config.mjs.

// astro.config.mjs
export default defineConfig({
  i18n: {
    defaultLocale: 'ja',
    locales: ['ja', 'en', 'zh-cn', 'es', 'pt', 'fr', 'ko', 'de', 'ru'],
    routing: {
      prefixDefaultLocale: false,
    },
  },
})

Utilidades de traducción

Los archivos de configuración, funciones utilitarias y archivos JSON de traducción se consolidan en src/i18n/.

// src/i18n/utils.ts
export function t(locale: Locale, key: string): string {
  return translations[locale]?.[key] ?? translations[defaultLocale][key] ?? key
}

Los archivos de traducción están en formato JSON bajo src/i18n/locales/, gestionando aproximadamente 100 claves para navegación, pie de página, UI del blog y metadatos.

Patrón View Component

La implementación de páginas usa el Patrón View Component. El diseño y la lógica se centralizan en src/views/, mientras los archivos de ruta (src/pages/) son wrappers ligeros que simplemente pasan el locale.

---
// src/pages/[locale]/about.astro (archivo de ruta)
import AboutPage from '../../views/AboutPage.astro'
const { locale } = Astro.params
---

<AboutPage locale={locale} />

Este diseño elimina la duplicación de lógica entre la ruta japonesa (/about) y las rutas multilingües (/en/about).

Soporte multilingüe del contenido del blog

Estructura de directorios

Los artículos traducidos se colocan en subdirectorios con código de idioma. El loader glob de Astro los detecta automáticamente de forma recursiva con el patrón **/*.md.

src/content/blog/
  astro-performance-tuning.md          # Japonés (base)
  website-renewal.md
  en/
    astro-performance-tuning.md        # Versión en inglés
    website-renewal.md
  zh-cn/
    astro-performance-tuning.md        # Versión en chino simplificado
    website-renewal.md
  es/
    ...

Utilidades de resolución de contenido

Se implementaron 3 funciones en src/utils/blog-i18n.ts.

// Determinar si es un artículo base (sin barra en el ID = base)
export function isBasePost(post: CollectionEntry<'blog'>): boolean {
  return !post.id.includes('/')
}

// Eliminar prefijo de locale del ID para obtener el slug base
export function getBaseSlug(postId: string): string {
  const idx = postId.indexOf('/')
  return idx !== -1 ? postId.slice(idx + 1) : postId
}

// Obtener la versión localizada de un artículo base (fallback al original)
export function localizePost(
  post: CollectionEntry<'blog'>,
  allPosts: CollectionEntry<'blog'>[],
  locale: Locale,
): CollectionEntry<'blog'> {
  if (locale === defaultLocale) return post
  return allPosts.find((p) => p.id === `${locale}/${post.id}`) ?? post
}

localizePost() sigue devolviendo el artículo original como medida de seguridad, pero las rutas públicas y los listados usan isPostAvailableInLocale() y filtros para seleccionar solo traducciones reales. No se genera una URL localizada cuando falta la traducción.

El punto clave es no modificar el esquema existente de la colección de contenido. El loader glob de Astro reconoce automáticamente los archivos en subdirectorios con IDs como en/astro-performance-tuning, sin necesidad de cambios de configuración.

Reglas de los archivos de traducción

Los archivos de traducción se generaron siguiendo estas reglas:

  • Las claves del frontmatter permanecen en inglés (title, description, date, etc.)
  • Los valores de etiquetas se mantienen en japonés (['技術', 'Astro'], etc.)
  • URLs, rutas de imágenes, bloques de código y HTML no se modifican
  • Fecha y autor permanecen sin cambios
  • Texto del cuerpo y valores de texto del frontmatter (title, description, callout, FAQ, etc.) se traducen

Flujo de trabajo de traducción

El proceso de traducción sigue estos pasos:

  1. Crear inglés como idioma intermedio: Traducir del japonés original al inglés
  2. Traducir del inglés a cada idioma: Expandir desde el inglés a 7 idiomas
  3. Procesamiento por lotes: Procesar 5-6 artículos a la vez con GitHub Copilot

La traducción en dos etapas (japonés → inglés → idiomas destino) reduce la variación de calidad. Pasar por el inglés como idioma intermedio produce calidad más estable que traducir directamente del japonés a cada idioma.

View Components multilingües

Implementación de BlogPostPage

La página de artículos obtiene la versión locale del contenido usando localizePost() y la asigna a una variable de plantilla.

---
// src/views/BlogPostPage.astro
const localizedPost = localizePost(basePost, allPosts, locale)
const post = localizedPost // las referencias existentes de la plantilla funcionan tal cual
---

Este enfoque permite el soporte multilingüe sin cambiar ninguna referencia a post.data.title o post.body en la plantilla.

Implementación de páginas de lista

Las listas de blog, etiquetas, autores y archivos filtran solo artículos base con isBasePost(), y luego sustituyen con versiones traducidas usando localizePost() al momento de mostrar.

---
const allPosts = await getCollection('blog')
const basePosts = allPosts.filter(isBasePost)
const displayPosts = basePosts.map((p) => localizePost(p, allPosts, locale))
---

Consideraciones de build

Escape en frontmatter YAML

Las traducciones al francés causaron problemas donde los apóstrofos (l'atelier, qu'on, etc.) conflictuaban con las comillas simples de YAML.

# NG: Error de análisis YAML
title: 'Le métavers est plus proche qu'on ne le pense'

# OK: Cambiar a comillas dobles
title: "Le métavers est plus proche qu'on ne le pense"

Se usó un script Node.js para corregir todos los archivos en lote. Texto en inglés como Acecore's tiene el mismo problema, por lo que el tipo de comillas debe considerarse al generar archivos de traducción.

Filtrado de rutas de imágenes OG

/blog/og/[slug].png.ts también capturaba slugs de artículos traducidos (en/aceserver-hijacked, etc.), causando errores de parámetros. Se resolvió filtrando con isBasePost().

export const getStaticPaths: GetStaticPaths = async () => {
  const allPosts = await getCollection('blog')
  const posts = allPosts.filter(isBasePost)
  return posts.map((post) => ({
    params: { slug: post.id },
    props: { title: post.data.title },
  }))
}

Soporte multilingüe de Pages CMS

Pages CMS (.pages.yml) solo apunta a archivos directamente bajo el directorio path especificado, por lo que los subdirectorios de traducción se registraron como colecciones individuales.

content:
  - name: blog
    label: ブログ(日本語)
    path: src/content/blog
  - name: blog-en
    label: Blog(English)
    path: src/content/blog/en
  - name: blog-zh-cn
    label: 博客(简体中文)
    path: src/content/blog/zh-cn
  # ... configurado para cada idioma

Las etiquetas se escriben en cada idioma para que sea inmediatamente claro qué colección corresponde a qué idioma en el CMS.

UI de cambio de idioma

Se añadió un componente LanguageSwitcher al encabezado, proporcionando una UI de cambio de idioma para escritorio y móvil. Al cambiar de idioma, los usuarios navegan al locale correspondiente de la misma página. En la primera visita, se detecta el navigator.language del navegador para redirección automática.

Visualización multilingüe de etiquetas

Las etiquetas de los artículos mantienen sus slugs en japonés en las URLs mientras solo se traduce el nombre visible. Esto evita la complejidad de enrutamiento mientras muestra las etiquetas en el idioma nativo del usuario.

Las definiciones de etiquetas se centralizan en src/content/tags/{tagId}.json, con cada etiqueta conteniendo un campo i18n.name. Esto traslada la fuente de verdad de las traducciones de etiquetas de los JSON de traducción a la colección de etiquetas.

{
  "id": "technology",
  "name": "技術",
  "i18n": {
    "en": { "name": "Technology" },
    "fr": { "name": "Technologie" }
  }
}

Las tarjetas de artículos, la barra lateral, el listado de etiquetas y los detalles de artículos consultan la colección de etiquetas para cambiar el nombre visible, garantizando que la visualización de etiquetas esté unificada en el idioma correspondiente.

Datos de autor multilingües

Los nombres, biografías y listas de habilidades de los autores también cambian según el idioma. Se añadió un campo i18n a src/content/authors/{authorId}.json para almacenar las traducciones de cada idioma.

{
  "id": "hatt",
  "name": "ハット",
  "bio": "代表取締役。Web制作・サーバー運用…",
  "skills": ["TypeScript", "Astro", "..."]
  "i18n": {
    "en": {
      "name": "Hatt",
      "bio": "CEO and representative director. Web development...",
      "skills": ["TypeScript", "Astro", "..."]
    }
  }
}

La utilidad getLocalizedAuthor() obtiene la información del autor apropiada para el locale.

// src/utils/blog-i18n.ts
export function getLocalizedAuthor(author: Author, locale: Locale) {
  const localized = author.i18n?.[locale]
  return localized ? { ...author, ...localized } : author
}

SEO para sitio multilingüe

Para maximizar los beneficios SEO del soporte multilingüe, implementamos mecanismos para que los motores de búsqueda identifiquen e indexen correctamente cada versión de idioma.

Soporte hreflang en sitemap

La opción i18n de @astrojs/sitemap se combina con un filtro que comprueba si existe el archivo de traducción. El sitemap incluye solo versiones reales y genera automáticamente sus etiquetas xhtml:link rel="alternate".

// astro.config.mjs
sitemap({
  filter(page) {
    return !isMissingLocalizedBlogPost(page)
  },
  i18n: {
    defaultLocale: 'ja',
    locales: {
      ja: 'ja',
      en: 'en',
      'zh-cn': 'zh-CN',
      es: 'es',
      pt: 'pt',
      fr: 'fr',
      ko: 'ko',
      de: 'de',
      ru: 'ru',
    },
  },
})

Los artículos disponibles en los 9 idiomas reciben un clúster hreflang de 9 idiomas. Un artículo disponible solo en japonés permanece como una entrada japonesa independiente, sin enlazar URLs localizadas inexistentes.

Soporte de idioma en datos estructurados JSON-LD

Se añadió el campo inLanguage a los datos estructurados BlogPosting de los artículos, informando a los motores de búsqueda en qué idioma está escrito cada artículo.

// BlogPostPage.astro (extracto JSON-LD)
{
  "@type": "BlogPosting",
  "inLanguage": htmlLangMap[locale],  // "ja", "en", "zh-CN", etc.
  "headline": post.data.title,
  // ...
}

Feeds RSS multilingües

Además del /rss.xml en japonés, se generan feeds RSS para cada versión de idioma (/en/rss.xml, /zh-cn/rss.xml, etc.). Los títulos y descripciones de los feeds se traducen por idioma, y la etiqueta <language> genera códigos de idioma compatibles con BCP47.

// src/pages/[locale]/rss.xml.ts
export const getStaticPaths = () =>
  locales
    .filter((l) => l !== defaultLocale)
    .map((l) => ({ params: { locale: l } }))

El <link rel="alternate" type="application/rss+xml"> en BaseLayout.astro también configura automáticamente la URL RSS apropiada para el locale.

Resumen

El sitio utiliza actualmente las funciones i18n integradas de Astro 7.1.3 para generar la versión estática multilingüe.

  • Base i18n: Sin prefijo para japonés con prefixDefaultLocale: false de Astro
  • Traducción de UI: Cero duplicación de lógica mediante el Patrón View Component
  • Traducción de contenido: Enfoque de subdirectorios sin cambios de esquema
  • Traducción de etiquetas: Slugs en japonés en URLs, nombres visibles traducidos por idioma
  • Traducción de datos de autor: Bio y habilidades cambian según el idioma
  • SEO: Hreflang en sitemap, inLanguage en JSON-LD, feeds RSS multilingües
  • Traducciones ausentes: No se genera una URL localizada; el artículo japonés permanece en su URL original
  • Soporte CMS: Los artículos de cada idioma son editables individualmente en Pages CMS

Los archivos de traducción se seguirán añadiendo de forma incremental. Hasta que exista una traducción, solo se publica el artículo japonés; al añadir el archivo del locale se habilitan su URL, su entrada de sitemap y su relación hreflang.

Flujo de trabajo multilingüe

Base i18n

Configurar el enrutamiento i18n integrado de Astro y las utilidades de traducción.

Traducción de textos UI

Traducir los textos de encabezado, pie de página y todos los componentes.

Traducción de artículos

Generar 168 archivos de traducción en el despliegue inicial (21 artículos × 8 idiomas).

CMS y verificación de build

Configurar Pages CMS multilingüe y verificar la generación de todas las páginas.

Comparación antes y después

Solo japonés

  • Solo 1 idioma (japonés)
  • 23 artículos de blog
  • 523 páginas generadas (tras soporte multilingüe de UI)
  • Pages CMS con 1 colección de blog
  • Etiquetas y datos de autor solo en japonés
  • 1 solo feed RSS

9 idiomas (despliegue inicial)

  • Japonés + 8 idiomas (en, zh-cn, es, pt, fr, ko, de, ru)
  • 23 artículos + 168 traducciones = 191 en total
  • 621 páginas generadas en el despliegue inicial
  • Pages CMS con 9 colecciones por idioma
  • 25 etiquetas y datos de autor traducidos por idioma
  • Feeds RSS multilingües (9 idiomas)
Preguntas frecuentes
¿Por qué se eligieron 9 idiomas?
Para maximizar el alcance global, cubrimos los principales mercados lingüísticos. Inglés, chino, español y portugués cubren la mayoría de los usuarios de internet, mientras que francés, alemán, ruso y coreano complementan los mercados principales restantes.
¿Cómo se garantiza la calidad de la traducción?
Usamos traducción por IA con GitHub Copilot. Primero se crea la versión en inglés como idioma intermedio, luego se traduce desde el inglés a cada idioma destino para reducir la variación de calidad. Los valores de etiquetas en frontmatter se mantienen en japonés, y las URLs, bloques de código y rutas de imágenes no se modifican.
¿Qué ocurre cuando no existe un artículo traducido?
Si falta el archivo de traducción de un locale, no se genera una URL localizada para el artículo. El artículo japonés sigue disponible en su URL original y el selector de idioma enlaza al índice del blog del locale de destino.
¿Es necesario traducir al añadir un nuevo artículo?
No es necesario traducir para publicar el artículo japonés. Añadir un archivo Markdown con el mismo nombre al directorio del idioma habilita la URL, la entrada de sitemap y la relación hreflang de ese locale.

Comentarios

Cargando comentarios...

No se pueden publicar enlaces, correos ni textos promocionales.

G

Gui

CEO de Acecore. Lidera sistemas de negocio, web, bases de datos e infraestructura, calidad y adopción de IA desde la definición de problemas de negocio hasta el diseño, la puesta en marcha y la mejora posterior. Se apoya en capacidad práctica con C#/.NET y también cubre PHP/JavaScript, SQL Server/PostgreSQL/MySQL y Linux/Windows Server, diseñando requisitos, selección tecnológica, estándares de calidad y operaciones de desarrollo basadas en GitHub como un flujo coherente. Incorpora la IA generativa en procesos de desarrollo, verificación y organización de información, como una base práctica para que equipos pequeños entreguen más rápido y con mayor fiabilidad.

Definición de problemas de negocioSelección tecnológicaDiseño de sistemasC#/.NETDiseño de bases de datos/infraestructuraOperaciones de desarrollo en GitHubIA generativaDiseño de flujos de IADiseño de calidadIntegración en sitio

¿Quiere saber más sobre nuestros servicios?

Ofrecemos soporte integral en desarrollo de sistemas, diseño web, operaciones de servidores y diseño gráfico.

Artículos relacionados

Buscar en el sitio

Al introducir al menos dos caracteres, Contenido relacionado envía automáticamente sus términos de búsqueda a Cloudflare Workers AI. No introduzca información personal ni confidencial.Tratamiento de los datos de búsqueda