La primavera pasada un cliente me entregó un brief que me habría hecho gruñir en 2022. Cinco idiomas. Once mercados. Un desarrollador. "Ya tenemos WPML configurado", dijo, con el tipo de confianza que te dice que nunca había mirado realmente sus puntuaciones de Lighthouse.
Eliminé la instalación de WordPress el segundo día. Reconstruí todo en Astro. Ocho semanas después, tres de esos locales estaban en la primera página de Google. El cuarto llegó dentro del mes. Esa experiencia básicamente terminó mi relación con WordPress para trabajo multilingüe.
Bien. Aquí está exactamente lo que hago ahora.
Por qué WordPress Multilingüe sigue fallando en rendimiento
Mira, no disfruto criticando WordPress. Seahawk ha construido cientos de sitios WP y probablemente construiremos cientos más. Pero la brecha de rendimiento en proyectos multilingües es genuinamente difícil de defender.
WPML y Polylang funcionan generando posts traducidos en la capa de base de datos y enrutando a través de PHP. Cada solicitud golpea el servidor. Incluso con caché de página completa (WP Rocket, W3TC, lo que prefieras), todavía estás lidiando con la sobrecarga de inicialización de PHP, fallos de caché en páginas de locale con bajo tráfico, y gestión de hreflang que se convierte en una pesadilla en el momento en que tienes más de dos idiomas.
Medí el sitio WP de cinco idiomas de un cliente en 1.3 segundos de Time to First Byte en la locale italiana. El mismo contenido, reconstruido en Astro como HTML completamente estático: 94ms. Eso no es una mejora marginal. Eso es una categoría diferente de producto.
Core Web Vitals son un factor de ranking confirmado, y los archivos estáticos servidos desde un nodo edge de CDN simplemente ganan esa batalla cada vez., and static files served from a CDN edge node simply win that battle every time.
Enrutamiento i18n Integrado en Astro (Y Por Qué Cambió Todo)
Astro lanzó enrutamiento i18n adecuado en v3 y lo refinó significativamente en v4. Para cuando estemos operando en 2026, la API es estable y genuinamente agradable de usar.
Aquí está la configuración básica que uso. Tu astro.config.mjs obtiene un bloque i18n:astro.config.mjs gets an i18n block:
`` i18n: { defaultLocale: 'en', locales: ['en', 'de', 'fr', 'it', 'es'], routing: { prefixDefaultLocale: false } } `` i18n: { defaultLocale: 'en', locales: ['en', 'de', 'fr', 'it', 'es'], routing: { prefixDefaultLocale: false } } ``
Esa única configuración le dice a Astro que sirva inglés en /, alemán en /de/, francés en /fr/, y así sucesivamente. Sin plugin. Sin paquete extra. Está integrado./, German at /de/, French at /fr/, and so on. No plugin. No extra package. It's baked in.
El prefixDefaultLocale: false es deliberado. La audiencia principal de la mayoría de mis clientes habla inglés, así que no quiero que /en/ contamine la estructura canónica. Google puede entender la relación a través de hreflang.prefixDefaultLocale: false is deliberate. Most of my clients' primary audience is English-speaking, so I don't want /en/ polluting the canonical structure. Google can figure out the relationship via hreflang.
La Estructura de Archivos Que Realmente Escala
Aquí es donde la mayoría de los tutoriales se equivocan. Te muestran un pages/[locale]/index.astro plano y dicen que listo. Bien para un sitio de presentación. Se desmorona en el momento en que tienes 40 rutas.pages/[locale]/index.astro and call it done. Fine for a brochure. Falls apart the moment you have 40 routes.
Lo que yo hago en su lugar:
`` src/ pages/ index.astro (English) about.astro (English) [locale]/ index.astro about.astro i18n/ en.json de.json fr.json it.json es.json `` src/ pages/ index.astro (English) about.astro (English) [locale]/ index.astro about.astro i18n/ en.json de.json fr.json it.json es.json ``
El directorio i18n/ contiene tus cadenas de traducción como archivos JSON planos. en.json es la fuente de verdad. Escribo todas las otras locales contra ella. ¿Falta una clave en de.json? La página en alemán vuelve al inglés para esa cadena en lugar de lanzar un error, porque agrego una pequeña función utilitaria t() que maneja el fallback con elegancia.i18n/ directory holds your translation strings as flat JSON files. en.json is the source of truth. I write every other locale against it. One key missing in de.json ? The German page falls back to English for that string rather than throwing an error, because I add a small t() utility function that handles fallback gracefully.
La Capa de Traducción: Mantenla Simple
He probado Paraglide JS, i18next y otros cuantos. Para construcciones estáticas de Astro mi recomendación honesta es: no sobre-ingenierices esto.
Un archivo utilitario simple que importe JSON de locales y retorne una función getter es suficiente para el 90% de los proyectos. Paraglide JS de Inlang vale la pena considerar si necesitas traducciones type-safe y tu equipo es lo suficientemente grande como para preocuparse por errores en tiempo de compilación en claves faltantes. Para trabajo en solitario o agencias pequeñas, es overhead que no necesitas.Paraglide JS from Inlang is worth considering if you need type-safe translations and your team is large enough to care about compile-time errors on missing keys. For solo work or small agencies, it's overhead you don't need.
Lo que sí me importa: mantener los archivos de traducción planos en lugar de profundamente anidados. "hero.cta.button.label" como clave suena organizado hasta que la buscas a las 11 de la noche. "heroCTALabel" es feo pero encontrable."hero.cta.button.label" as a key sounds organised until you're searching for it at 11pm. "heroCTALabel" is ugly but findable.
Manejando Plurales y Cadenas Dinámicas
La pluralización en alemán te pondrá a prueba. Lo mismo ocurre con árabe si vas por ahí (una vez ayudé a un cliente a expandirse a los EAU y pasé una tarde completa solo en formato de números).
Para plurales uso un pequeño helper en línea:
`` export function plural(count, one, other) { return count === 1 ? one : other; } `` export function plural(count, one, other) { return count === 1 ? one : other; } ``
¿Crudo? Sí. Pero cubre el 95% de los casos para idiomas europeos. Si te vas a Arabic, Russian, o Polish, necesitas reglas CLDR de plurales adecuadas. El proyecto Unicode CLDR las documenta exhaustivamente. Construye tu helper de plurales contra esas especificaciones o te equivocarás.The Unicode CLDR project documents these exhaustively. Build your plural helper against those specs or you will get it wrong.
hreflang: La Parte Que Todos Interpretan Mal
Correcto. Aquí es donde el SEO multilingüe realmente prospera o se desmorona.
hreflang le dice a Google cuál versión de una página servir a cuál locale. Si te equivocas, terminas canibalizando tus propios rankings o sirves la versión en alemán a hablantes de francés. He visto ambas cosas suceder en sitios que heredé.
Las reglas:
- Cada página debe incluir etiquetas hreflang para cada locale, incluida ella misma.
- Las etiquetas deben ser bidireccionales. Si /de/about referencia /fr/about, entonces /fr/about debe referenciar /de/about de vuelta.
/de/aboutreferences/fr/about, then/fr/aboutmust reference/de/aboutback. - Incluye una etiqueta x-default que apunte a tu locale predeterminado.
x-defaulttag pointing to your default locale. - Usa URLs absolutas. Siempre.
En Astro genero estas dentro de un componente BaseLayout.astro que cada página importa. Paso la ruta actual, recorro el array de locales, y genero automáticamente las etiquetas <link rel="alternate">. Sin mantenimiento manual.BaseLayout.astro component that every page imports. I pass in the current path, loop over the locale array, and spit out the <link rel="alternate"> tags automatically. No manual maintenance.
Perder la etiqueta x-default es el error más común que veo. La documentación de hreflang de Google es genuinamente clara al respecto. Léela una sola vez correctamente.x-default tag is the single most common mistake I see. Google's own hreflang documentation is genuinely clear on this. Read it once properly.
Sitemaps específicos por idioma
Un sitemap con todos los idiomas mezclados funciona. Pero sitemaps separados por idioma (enviados individualmente en Google Search Console) te dan datos de indexación más limpios y hacen mucho más fácil detectar errores de rastreo en un idioma específico.
Mi compilación de Astro genera cinco archivos: sitemap-en.xml, sitemap-de.xml, y así sucesivamente. @astrojs/sitemap soporta esto con la opción i18n lista para usar. Úsalo.sitemap-en.xml , sitemap-de.xml , and so on. @astrojs/sitemap supports this with the i18n option out of the box. Use it.
Estrategia de contenido para sitios estáticos multilingües
Esto es lo que la mayoría de desarrolladores omiten completamente: la traducción misma.
Puedes tener una configuración de Astro técnicamente perfecta con hreflang impecable y un sitemap que haría llorar de alegría a un empleado de Google. Si tu copia en alemán pasó por DeepL y nunca fue revisada por un humano, el sitio no tendrá buen ranking en Alemania.
No estoy siendo remilgado con esto. Uso DeepL constantemente. Es un borrador inicial excelente. Pero para cualquier página en la que realmente quieras rankear, un hablante nativo necesita leerla. No necesariamente reescribirla. Solo leerla, arreglar las partes raras, y confirmar que la redacción suena como algo que un alemán realmente diría.
Seahawk tuvo un proyecto fintech el año pasado donde el cliente quería siete idiomas y una línea de tiempo de cuatro semanas. Usamos DeepL como base, luego contratamos siete freelancers a través de Gengo para pasadas de revisión a aproximadamente £0.04 por palabra. El presupuesto total de traducción fue menor a £800. Ese sitio ahora ocupa el top cinco en cuatro de esos mercados. La economía funciona.
Metadatos Específicos por Locale
Los title tags y meta descriptions también necesitan localizarse. No solo traducirse. "Best accounting software" no se mapea directamente en búsquedas al equivalente en alemán porque los volúmenes de palabras clave difieren.
Guardo los metadatos específicos por locale en los archivos de traducción JSON bajo un namespace meta dedicado. La plantilla de página extrae de t('meta.title') y vuelve a un valor por defecto solo si la clave falta. Esto significa que un SEO puede editar un archivo JSON por locale sin nunca tocar el código.meta namespace. The page template pulls from t('meta.title') and falls back to a default only if the key is missing. This means an SEO can edit one JSON file per locale without ever touching the code.
Deployment: Dónde Alojar un Sitio Estático de 5 Idiomas
Vercel y Netlify manejan bien las compilaciones estáticas de Astro. Pero para multilingüe específicamente, quieres edge caching por locale y la capacidad de establecer headers específicos por locale.
He estandarizado en Cloudflare Pages. El nivel gratuito maneja la mayoría de presupuestos de clientes. Las solicitudes se sirven desde el nodo edge más cercano globalmente, lo cual importa enormemente cuando tu audiencia italiana está siendo servida desde un centro de datos en Virginia. La red de Cloudflare tiene nodos en Milán, Fráncfort, Ámsterdam. La diferencia en TTFB para visitantes europeos es medible.
Una cosa a configurar explícitamente: headers Cache-Control. Los archivos estáticos deben tener TTLs de caché largos (max-age=31536000, immutable para assets hasheados). Las páginas HTML deben tener TTLs más cortos o una política stale-while-revalidate, particularmente si estás haciendo compilaciones incrementales después de actualizaciones de contenido.Cache-Control headers. Static files should have long cache TTLs ( max-age=31536000, immutable for hashed assets). HTML pages should have shorter TTLs or a stale-while-revalidate policy, particularly if you're doing incremental builds after content updates.
Debugging de Compilaciones i18n Antes de que Vayan en Vivo
Algunas cosas que verifico en cada compilación de Astro multilingüe antes de hacer push a producción:
- Ejecuta astro build localmente e inspecciona el output del dist/. Cada carpeta de locale debe contener todas las páginas. Si falta una ruta, el paso de build es el lugar para detectarlo.
astro buildlocally and inspect thedist/output. Every locale folder should have every page. If a route is missing, the build step is the place to catch it. - Usa Screaming Frog para rastrear el deployment de preview. Filtra por hreflang. Cualquier página huérfana (páginas no referenciadas por ninguna etiqueta hreflang) se muestra inmediatamente.Screaming Frog to crawl the preview deployment. Filter by hreflang. Any orphaned pages (pages not referenced by any hreflang tag) show up immediately.
- Verifica el canonical x-default en DevTools del navegador al menos en la página de inicio y una página interna.
x-defaultcanonical in browser DevTools on at least the homepage and one inner page. - Visita manualmente una URL de locale en un navegador configurado en el idioma de ese locale. Parece obvio. Aun así detecta problemas.
El problema de build-time más común que encuentro es olvidar agregar un nuevo locale al array de locales en astro.config.mjs después de agregar el archivo JSON. El build no genera error. Simplemente ignora silenciosamente el nuevo locale. Molesto.astro.config.mjs locales array after adding the JSON file. The build doesn't error. It just silently ignores the new locale. Annoying.
FAQ
¿El i18n de Astro funciona con renderizado del lado del servidor (SSR) o solo con output estático?
Ambos. El enrutamiento i18n de Astro funciona también en modo SSR. Las funciones auxiliares getRelativeLocaleUrl() y getAbsoluteLocaleUrl() funcionan de forma idéntica independientemente del modo de output. Dicho esto, si vas completamente estático, los beneficios de rendimiento son significativamente mayores porque estás sirviendo HTML pre-construido sin costo de runtime.getRelativeLocaleUrl() and getAbsoluteLocaleUrl() helper functions work identically regardless of output mode. That said, if you're going fully static, the performance benefits are significantly greater because you're serving pre-built HTML with no runtime cost.
¿Cómo manejo la detección de locale y redirecciones para visitantes nuevos?
Para sitios estáticos hago esto en la capa de CDN/edge, no dentro de Astro. Cloudflare Workers puede leer el header Accept-Language y redirigir a la ruta de locale apropiada. No hagas esto dentro de tu app de Astro porque agrega un round trip y hace que el output estático sea no-determinístico.Accept-Language header and redirect to the appropriate locale path. Don't do this inside your Astro app because it adds a round trip and makes the static output non-deterministic.
¿Cuál es la mejor manera de gestionar archivos de traducción a medida que el sitio crece?
Mantenlos en control de versiones junto al código, no en un CMS separado ni en un sistema de gestión de traducciones de terceros, hasta que el proyecto realmente lo justifique. En el momento en que tengas más de tres idiomas y un equipo editorial activo, evalúa Tolgee o Localazy. Ambos se integran con archivos JSON planos y tienen sincronización con GitHub. Por debajo de ese umbral, un documento Notion compartido y actualizaciones JSON manuales funcionan perfectamente.Tolgee or Localazy. Both integrate with flat JSON files and have GitHub sync. Below that threshold, a shared Notion doc and manual JSON updates is honestly fine.
¿Puedo usar Astro Content Collections con i18n?
Sí, y es mi enfoque preferido para sitios con muchos blogs. Estructura tu colección de contenido con el idioma como segmento de ruta: src/content/blog/en/, src/content/blog/de/. Luego filtra por idioma en tu función getStaticPaths(). Cada idioma obtiene su propio HTML estático en tiempo de compilación. Limpio, rápido, y el equipo editorial puede gestionar archivos markdown sin tocar ningún componente.src/content/blog/en/, src/content/blog/de/. Then filter by locale in your getStaticPaths() function. Each locale gets its own static HTML at build time. Clean, fast, and the editorial team can manage markdown files without touching any components.
¿Tener múltiples páginas de idiomas afecta el SEO por contenido duplicado?
No, siempre que tu implementación de hreflang sea correcta. Google trata las variantes de idioma correctamente anotadas como páginas relacionadas pero distintas, no como duplicados. El riesgo viene de implementar hreflang incorrectamente, particularmente omitir anotaciones recíprocas o manejar incorrectamente x-default. Domina esos dos puntos y el contenido duplicado no es una preocupación.incorrectly implementing hreflang, particularly missing reciprocal annotations or incorrect x-default handling. Nail those two things and duplicate content is not a concern.
---
El cambio de sitios multilingües renderizados dinámicamente en PHP a sitios estáticos precompilados no se trata realmente de Astro en específico. Se trata de aceptar que la mayoría del contenido traducido no cambia de hora en hora. Compílalo una vez, cachéalo en todas partes, deja que la CDN haga el trabajo pesado. Astro simplemente hace que ese flujo de trabajo sea genuinamente agradable en lugar de una batalla contra el framework.
Cinco idiomas, un desarrollador, ocho semanas. Las herramientas están ahí. Úsalas.
