Ir al contenido

Sistema de Traducción

Este sitio se publica en 17 idiomas. No gracias a un equipo de traductores, sino mediante un script de Python, un modelo Claude Opus y una estructura de archivos diseñada para que todo encaje en su lugar. Así es como funciona todo el sistema.

La documentación reside en src/content/docs/. El inglés es la raíz — todos los demás idiomas lo replican exactamente:

src/content/docs/
├── index.mdx ← English (root)
├── getting-started.md
├── features/
│ ├── play-chess.md
│ ├── multiplayer.md
│ └── ...
├── setup/
│ ├── tts-overview.md
│ └── ...
├── under-the-hood/
│ ├── architecture.md
│ └── ...
├── fr/ ← French
│ ├── index.mdx
│ ├── getting-started.md
│ ├── features/
│ │ ├── play-chess.md
│ │ └── ...
│ └── ...
├── ja/ ← Japanese
│ ├── index.mdx
│ ├── getting-started.md
│ └── ...
└── ... (16 language directories total)

Cada archivo traducido es un reflejo estructural de su fuente en inglés. Mismo nombre de archivo, misma ruta de subdirectorio, mismas claves de frontmatter. La única diferencia es que la prosa está en otro idioma.

Starlight (el framework de documentación) depende de esta simetría. Cuando un usuario cambia de idioma, Starlight intercambia /docs/getting-started/ por /fr/docs/getting-started/ — misma ruta, diferente prefijo de locale. Si el archivo en francés no existe exactamente en fr/getting-started.md, el selector de idioma falla o vuelve silenciosamente al inglés.

Los idiomas están ordenados por población global de jugadores de ajedrez, basándose en datos de Lichess, Chess.com y registros de la FIDE. El inglés aparece primero como idioma fuente; todo lo demás sigue el ranking ajedrecístico:

PosiciónCódigoIdiomaEstilo
1enInglésFuente de referencia
2esEspañolFormal estándar
3hiHindiEscritura devanagari
4ruRusoFormal estándar
5deAlemánFormal estándar
6frFrancésFormal estándar
7ptPortuguésPortugués europeo
11plPolacoFormal estándar
12itItalianoFormal estándar
13ukUcranianoFormal estándar
14trTurcoFormal estándar
17koCoreanoForma 합니다/습니다
18zhChino (Simplificado)Caracteres simplificados
zh-twChino (Tradicional)Caracteres tradicionales
23nbNoruego BokmålBokmål estándar
beBielorrusoBielorruso estándar
34jaJaponésForma です/ます

La columna de “estilo” es importante. El japonés y el coreano tienen opciones de registro formal que afectan a cada oración. El prompt de traducción incluye estas instrucciones para que el modelo produzca prosa natural y pulida — no una salida de máquina rígida.

Este orden también controla el menú desplegable de idiomas en la cabecera del sitio. Los idiomas con más hablantes que juegan al ajedrez aparecen primero, de modo que los usuarios tienen más probabilidades de encontrar el suyo sin tener que desplazarse.

Toda la documentación comienza como markdown en inglés en src/content/docs/. El frontmatter incluye un title y una description:

---
title: "Getting Started"
description: "Install En Parlant~ and play your first game."
---
Download the latest release...

Un script de Python (scripts/translate-docs.py) lee cada archivo fuente en inglés, lo envía a la API de Claude y escribe el markdown traducido:

Ventana de terminal
python3 scripts/translate-docs.py \
--anthropic-key $ANTHROPIC_API_KEY \
--model claude-opus-4-6 \
--workers 5

El script tarda aproximadamente 60–70 minutos en traducir los 28 archivos fuente a los 16 idiomas de destino (448 archivos en total). Ejecuta 5 llamadas paralelas a la API para mantenerse dentro de los límites de velocidad.

Para un solo idioma nuevo, son aproximadamente 4 minutos.

Ventana de terminal
pnpm build

Astro lee el markdown fuente, lo renderiza a través de las plantillas de Starlight y genera HTML estático en dist/. La compilación tarda unos 30 segundos para las ~500 páginas.

Ventana de terminal
pnpm run deploy

Publica el sitio compilado en Cloudflare Workers.

El script de traducción es intencionalmente simple — unas 300 líneas de Python. Este es el flujo:

┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Read English │────▶│ Claude API │────▶│ Write target │
│ source file │ │ (Opus 4.6) │ │ language file │
│ │ │ │ │ │
│ getting- │ │ "Translate into │ │ fr/getting- │
│ started.md │ │ French..." │ │ started.md │
└─────────────────┘ └──────────────────┘ └─────────────────┘
× 5 parallel
× 16 languages
× 28 files

El prompt le indica a Claude exactamente qué traducir y qué dejar intacto:

  • Traducir: prosa, encabezados, etiquetas de tablas, título y descripción del frontmatter
  • Mantener en inglés: bloques de código, código en línea, ejemplos de comandos, rutas de archivos, URLs, nombres de productos, nombres de personas, diagramas ASCII

Esta separación es fundamental. Un comando como pnpm build debe permanecer como pnpm build en todos los idiomas. Un nombre de producto como “En Parlant~” o “Stockfish” no se traduce. Pero “Getting Started” se convierte en “はじめに” en japonés y “Начало работы” en ruso.

Claude Opus 4.6 produce traducciones notablemente mejores que los modelos más rápidos. La diferencia se manifiesta en:

  • Fraseo natural — Opus escribe como un hablante nativo, no como un traductor. Reestructura las oraciones cuando el orden de palabras del inglés sonaría extraño en el idioma de destino.
  • Precisión técnica — La terminología de ajedrez, la jerga de TTS y los conceptos de software se traducen utilizando los términos correctos del dominio específico.
  • Consistencia — El registro formal se mantiene coherente en todo momento. El japonés usa la forma です/ます en todas partes, sin alternar entre casual y cortés a mitad de párrafo.
  • Manejo de MDX — Opus preserva correctamente las etiquetas de componentes JSX (<Card>, <CardGrid>) y las sentencias import en archivos .mdx sin corromperlas.

La diferencia de coste es real — aproximadamente $28 por una traducción completa del sitio con Opus frente a $5 con Sonnet — pero para 448 archivos que los usuarios realmente leerán, la calidad lo vale.

Esta es la parte que más tiempo llevó hacer funcionar correctamente.

Starlight detecta el idioma de una página comprobando el primer segmento de su slug de contenido. Cuando ve fr/docs/getting-started, sabe que es francés porque fr es el primer segmento. Pero la implementación inicial producía slugs como docs/fr/getting-started — el locale enterrado debajo de docs/. Starlight veía docs como el primer segmento, trataba todo como inglés y generaba más de 7.000 páginas duplicadas en lugar de ~500.

Una función personalizada generateId en src/content.config.ts controla cómo las rutas de archivos se convierten en slugs de contenido:

generateId({ entry }) {
const slug = entry.replace(/\.[^.]+$/, "");
const firstSeg = slug.split("/")[0];
if (firstSeg && localeKeys.includes(firstSeg)) {
return `${firstSeg}/docs/${slug.slice(firstSeg.length + 1)}`;
}
return `docs/${slug}`;
}

Esto coloca el prefijo de locale antes de docs/:

Ruta del archivoSlugURL
getting-started.mddocs/getting-started/docs/getting-started/
fr/getting-started.mdfr/docs/getting-started/fr/docs/getting-started/
ja/features/puzzles.mdja/docs/features/puzzles/ja/docs/features/puzzles/

El inglés usa defaultLocale: "root", lo que significa que no tiene prefijo — reside directamente en /docs/, no en /en/docs/.

Un array localeKeys en el mismo archivo debe listar cada locale que no sea inglés. Si un locale existe en la configuración de Astro pero no en este array, su contenido traducido se trata como contenido en inglés — el selector de idioma falla y el recuento de páginas se dispara.

Astro almacena en caché las asignaciones de slugs en .astro/data-store.json. Después de cualquier cambio en la configuración de locales, este archivo debe eliminarse antes de recompilar, o la compilación se completa con éxito pero con enrutamiento obsoleto (incorrecto).

La barra lateral se define en astro.config.mjs. Los elementos que apuntan a una página (propiedad slug) utilizan automáticamente el título del frontmatter de la página traducida — no se necesita traducción manual:

{ slug: "docs/getting-started" }
// English: "Getting Started" (from English frontmatter)
// French: "Premiers pas" (from French frontmatter)
// Japanese: "はじめに" (from Japanese frontmatter)

Pero las etiquetas de grupo y los enlaces externos necesitan traducciones explícitas:

{
label: "Features",
translations: {
fr: "Fonctionnalités",
es: "Características",
de: "Funktionen",
ja: "機能",
// ... all 16 languages
},
items: [
{ slug: "docs/features/play-chess" },
{ slug: "docs/features/multiplayer" },
// ...
],
}

Siete elementos de la barra lateral necesitan traducciones manuales: Welcome, Features, App Menus, Setup Guides, Under the Hood, Credits y Accessibility. Añadir un nuevo idioma significa agregar una entrada a cada uno de estos siete bloques.

En Parlant~ enlaza a esta documentación desde su menú de Ayuda. El enlace es sensible al locale — si estás usando la aplicación en francés, se abre la documentación en francés:

const docsLocalePrefix = useMemo(() => {
const lang = i18n.language; // e.g. "fr_FR", "zh_TW"
if (!lang || lang.startsWith("en")) return "";
if (lang === "zh_TW") return "/zh-tw";
return `/${lang.slice(0, 2)}`;
}, [i18n.language]);

Cada idioma disponible en la aplicación tiene una traducción correspondiente en el sitio. La asignación es automática — fr_FR se mapea a /fr/docs/, ja_JP se mapea a /ja/docs/. El único caso especial es el chino tradicional: zh_TW se mapea a /zh-tw/docs/ (con guion).

Las partes difíciles ya están resueltas:

  1. La arquitectura de enrutamiento está solucionada. La función generateId, el array localeKeys y la configuración defaultLocale: "root" trabajan en conjunto para que Starlight genere la estructura de URLs correcta. Este fue el mayor punto de dolor — requirió rastrear más de 6 archivos fuente en Starlight y Astro para encontrar y corregir el problema.

  2. El script de traducción se encarga de todo. Retraducción completa del sitio, adición de un solo idioma, actualizaciones de archivos individuales — todo es el mismo script con diferentes flags. Reintenta ante límites de velocidad, paraleliza entre workers e informa los errores con claridad.

  3. Añadir un nuevo idioma son cuatro ediciones de configuración y un comando. Agregar el locale a astro.config.mjs, content.config.ts y translate-docs.py, añadir las traducciones de la barra lateral, ejecutar el script. Aproximadamente 10 minutos de trabajo más 4 minutos de tiempo de traducción.

  4. Actualizar contenido es aún más sencillo. Editar el fuente en inglés, ejecutar el script con --files y --overwrite solo para los archivos modificados, recompilar. O para correcciones menores de prosa, editar directamente los archivos traducidos.

  5. Todo el pipeline está capturado en un skill. Ejecutar /translate_docs guía a través de todo el proceso — qué modo usar, qué flags pasar, comprobaciones previas, verificación posterior a la traducción. No se requiere conocimiento institucional.

La traducción total de 448 archivos en 16 idiomas costó aproximadamente $28 y tomó alrededor de 70 minutos. Un solo idioma nuevo cuesta aproximadamente $1,70. Un solo archivo retraducido a todos los idiomas cuesta aproximadamente $1. Estos son los costes continuos de mantener la documentación actualizada en 17 idiomas.