i18n Web
La internacionalización es arquitectura, no una pasada de traducción al final. El objetivo es una interfaz de usuario que se renderice correctamente para cualquier configuración regional sin casos especiales: sin oraciones concatenadas, sin formatos hardcodeados, sin diseños que se rompan cuando el alemán es un 35% más largo o el árabe invierte la dirección.
Cuándo usar esta habilidad
Úsala cuando el usuario pida:
- Agregar un idioma, un selector de idioma o enrutamiento por configuración regional
- Traducir una aplicación, extraer cadenas o configurar un flujo de trabajo de traducción
- Corregir errores de pluralización, género o "1 elemento / 2 elementos"
- Dar formato a fechas, números, moneda, tiempo relativo o listas por configuración regional
- Soporte RTL (árabe, hebreo, persa, urdu)
- Agregar hreflang, metadatos localizados o mapas de sitio multilingües
- Revisar una configuración i18n existente, claves faltantes o desviación en las traducciones
- Elegir entre next-intl, react-i18next, FormatJS, Lingui o un TMS
Las referencias relacionadas están en references/ y los scripts en scripts/. Lee solo lo que la tarea necesite. Las rutas son relativas al directorio de esta habilidad (el runtime del agente lo imprime cuando se carga la habilidad).
No negociables
- Nunca construyas una oración concatenando fragmentos traducidos. Traduce el mensaje completo con marcadores de posición:
"{count} mensajes nuevos", not('nuevo') + ' ' + count + ' ' + t('mensajes'). - Nunca uses texto en inglés como clave. Las claves son identificadores estables (
inbox.unreadCount), para que los cambios en el texto no rompan las búsquedas ni invaliden todas las traducciones. - Toda cadena visible para el usuario debe estar externalizada, incluyendo
aria-label,alt,title,placeholder, errores de validación, estados vacíos, notificaciones toast, correos electrónicos, notificaciones push y salida PDF. - Nunca hardcodees formatos. Nada de
toFixed(2) + '$', nada deMM/DD/YYYYmanual, nada de nombres de mes en inglés, nada de suposiciones de24h. UsaIntl. - Una configuración regional es una etiqueta BCP 47, no un idioma.
pt-BRypt-PT,zh-Hansyzh-Hant,es-MXyes-ESson productos diferentes. Almacena la etiqueta completa. - El estado de la configuración regional debe tener alcance de solicitud en el servidor. Nunca pongas la configuración regional actual en una variable mutable a nivel de módulo durante SSR; las solicitudes concurrentes filtrarán idiomas entre sí.
- Las traducciones son entrada no confiable si provienen de un TMS, comunidad o CMS. Nunca las pases a
innerHTML/dangerouslySetInnerHTMLsin sanitizar.
Flujo de trabajo
1. Auditar antes de modificar el código
- Detecta el stack, el enrutador y la biblioteca i18n existente antes de proponer cualquier cosa.
- Encuentra cadenas hardcodeadas:
Trata la salida como candidatos, no como verdad absoluta: omite test fixtures, constantes, URLs y registros orientados al desarrollador.node scripts/scan-hardcoded.mjs src - Inventaria cada configuración regional actualmente enviada y dónde viven los catálogos.
- Identifica las áreas de mayor riesgo primero: autenticación, pago, errores, correos electrónicos y texto legal.
2. Decidir la arquitectura (escríbela)
Responde estas preguntas antes de escribir código:
| Decisión | Valor por defecto | Notas |
|---|---|---|
| Estrategia de configuración regional | Prefijo de ruta /es/ | Mejor SEO y capacidad de caché. Subdominios/ccTLD solo con una razón. |
| Configuración regional por defecto | Idioma de origen | No redirijas automáticamente a visitantes nuevos a una configuración regional adivinada sin una opción para anularla. |
| Detección de configuración regional | URL → cookie → Accept-Language → valor por defecto | La URL gana para que los enlaces compartidos nunca cambien de idioma. |
| Formato de catálogo | JSON por configuración regional por espacio de nombres | Carga diferida de espacios de nombres; nunca envíes todas las configuraciones regionales al cliente. |
| Sintaxis de mensajes | ICU MessageFormat | Portable entre bibliotecas y TMS; soporta plural/select/ordinal. |
| Estilo de clave | feature.component.element | Estable, buscable, sin texto en inglés. |
| Fuente de verdad de traducciones | TMS o repositorio, no ambos | Elige uno; sincroniza en una dirección en CI. |
| Fallback (retroceso) | Cadena de configuración regional es-MX → es → en | Registra los fallbacks en desarrollo para detectar vacíos. |
3. Externalizar y estructurar mensajes
- Reemplaza cadenas con claves semánticas y mueve el texto al catálogo de la configuración regional base.
- Usa una clave por oración completa. Si una oración contiene un enlace o una palabra en negrita, usa interpolación de texto enriquecido en lugar de dividirla.
- Mantén los marcadores de posición con nombre (
{userName}), nunca posicionales ({0}). - No pongas HTML en los catálogos a menos que la biblioteca soporte explícitamente etiquetas tipadas.
- Agrupa por superficie de producto, no por página:
checkout.json,auth.json,errors.json.
Consulta references/icu.md para reglas sobre plural, select, ordinal y texto enriquecido.
4. Configurar enrutamiento y renderizado por configuración regional
- Deriva la configuración regional de la ruta, valídala contra la lista de soportadas y devuelve 404 para configuraciones regionales desconocidas en lugar de hacer un fallback silencioso.
- Proporciona un selector de idioma que preserve la ruta actual y la cadena de consulta.
- Establece
<html lang="...">y<html dir="...">en el documento renderizado en el servidor. - Carga solo el catálogo de la configuración regional activa más el fallback por defecto.
- Asegúrate de que el mismo catálogo esté disponible para el renderizado del servidor y del cliente; evita lógica de traducción duplicada o divergente.
Configuración específica del framework y errores comunes: references/frameworks.md.
Enrutamiento, detección, hreflang y mapas de sitio: references/routing-seo.md.
5. Manejar valores sensibles a la configuración regional
Usa Intl (o el envoltorio del framework), nunca formato manual:
new Intl.NumberFormat(locale, { style: 'currency', currency }).format(amount)
new Intl.DateTimeFormat(locale, { dateStyle: 'medium', timeZone }).format(date)
new Intl.RelativeTimeFormat(locale, { numeric: 'auto' }).format(-3, 'day')
new Intl.ListFormat(locale, { type: 'conjunction' }).format(items)
new Intl.PluralRules(locale).select(count)
new Intl.Collator(locale).compare(a, b)
- Almacena las marcas de tiempo en UTC; renderiza en la zona horaria del usuario, no la del servidor.
- La moneda no es lo mismo que la configuración regional: un usuario francés puede pagar en USD.
- Ordena/busca con
Intl.Collator, no con<olocaleCompare()sin una configuración regional. - Conversión de mayúsculas/minúsculas:
toLocaleUpperCase(locale), porque laiturca se convierte enİ.
6. Diseñar para expansión y RTL
- Asume un +30–40% de longitud de texto (alemán, finlandés, ruso) y evita botones de ancho fijo o etiquetas de una sola línea.
- Deja que los contenedores crezcan, permite el ajuste de palabras y prueba con pseudo-localización:
node scripts/pseudo-localize.mjs locales/en.json --out locales/en-XA.json - Usa propiedades CSS lógicas (
margin-inline-start,padding-inline,inset-inline) en lugar deleft/right. - Refleja iconos direccionales (flechas, chevrones, progreso), no logotipos, relojes, controles multimedia ni marcas de verificación.
- Envuelve el contenido en línea generado por el usuario en
<bdi>odir="auto".
Lista de verificación RTL completa: references/rtl.md.
7. Configurar QA de traducción en CI
Como mínimo, haz que la compilación falle cuando los catálogos sean estructuralmente inválidos:
node scripts/check-locales.mjs locales --base en
Verifica claves faltantes/extra, valores vacíos, discrepancias en marcadores de posición y sintaxis ICU rota. Agrega compilación ICU a través de la CLI de tu biblioteca cuando esté disponible:
npx formatjs compile-folder --ast locales/en locales/compiled/en
Agrega pruebas para categorías plurales, salida de fecha/número con una zona horaria fija y renderizado RTL. Consulta references/testing.md.
Decisiones clave y trampas
Selección de biblioteca
| Necesidad | Elegir |
|---|---|
| Next.js App Router, componentes del servidor | next-intl |
| React SPA, ecosistema maduro, muchos plugins | react-i18next |
| Primero ICU, independiente del framework, extracción CLI | FormatJS (react-intl) |
| Catálogos en tiempo de compilación, runtime pequeño, seguridad de tipos | Lingui |
| Vue | vue-i18n |
| Svelte | svelte-i18n o paraglide |
| Astro | enrutamiento i18n integrado + tu biblioteca de mensajes |
| Vanilla / mínimo | Intl + un cargador de catálogos pequeño |
No introduzcas una segunda biblioteca i18n en una aplicación que ya tenga una. Extiende la configuración existente.
Pluralización
Las categorías plurales son específicas del idioma. El inglés tiene one/other; el árabe tiene zero/one/two/few/many/other; el japonés tiene solo other. Nunca escribas count === 1 ? ... : ....
"inbox.unread": "{count, plural, =0 {No hay mensajes sin leer} one {# mensaje sin leer} other {# mensajes sin leer}}"
Género y concordancia gramatical
Usa select en una variable de género y mantén el valor de la variable neutral en cuanto al idioma (female, male, neutral, unknown), no palabras localizadas:
"profile.greeting": "{gender, select, female {Bienvenida, {name}} male {Bienvenido, {name}} other {Bienvenido/a, {name}}}"
Si un idioma necesita concordancia en adjetivos, sustantivos o artículos, expón el género/caso gramatical como un argumento en lugar de manipular la cadena.
Texto enriquecido
No dividas "Lee nuestros " + link + " términos" en tres claves. Usa interpolación de etiquetas:
"signup.terms": "Acepto los <link>Términos del Servicio</link>"
t.rich('signup.terms', { link: (chunks) => <a href="/terms">{chunks}</a> })
SEO
- Una URL indexable por configuración regional; nunca sirvas contenido traducido por máquina detrás de la misma URL.
hreflangauto-referenciado másx-default; URLs absolutas; incluye todas las configuraciones regionales.- Localiza
<title>, meta descripción, etiquetas OG y datos estructurados. - No combines
hreflangconnoindexen páginas localizadas.
Detalles en references/routing-seo.md.
Caché
- La configuración regional en la ruta hace que el almacenamiento en caché de CDN sea sencillo. Si negocias por encabezado, envía
Vary: Accept-Languagey espera fragmentación de caché. - No almacenes en caché la preferencia de idioma personalizada en una respuesta CDN compartida.
- Mantén
Content-Languagecoherente con la configuración regional renderizada.
Lista de verificación de revisión
Antes de declarar i18n como terminado:
- No hay cadenas hardcodeadas visibles para el usuario en componentes, correos electrónicos o errores
- No hay concatenación de oraciones; cada mensaje es una unidad traducible completa
- Las claves son semánticas y estables; no hay texto en inglés como claves
- Cada marcador de posición existe en cada configuración regional; la sintaxis ICU compila
- Los plurales usan
Intl.PluralRules/ICU, nocount === 1 - El género/concordancia se maneja mediante
select, no sufijos "/a" - Fechas, números, moneda, tiempo relativo usan
Intlcon configuración regional/zona horaria explícita -
<html lang>ydirson correctos en el primer pintado (SSR), no se establecen en un efecto - El selector de configuración regional preserva la ruta/consulta y funciona con enlaces profundos
- La configuración regional desconocida devuelve 404, no un fallback silencioso
- El diseño sobrevive a una expansión de +35% y RTL
-
hreflang, canónico, mapa del sitio y metadatos localizados son correctos - La validación del catálogo se ejecuta en CI
- El comportamiento de fallback es intencional y observable
Anti-patrones
t('hola') + ', ' + nombre— oraciones fragmentadasclave={textoEnIngles}— texto traducido como clavecount === 1 ? 'elemento' : 'elementos'— lógica plural solo en inglésnew Date().toLocaleDateString()sin configuración regional ni zona horariavalue.toFixed(2)para dinerolocale === 'ar' && { direction: 'rtl' }disperso por los componentes en lugar de undira nivel de documento- Establecer
<html lang>dentro deuseEffect— causa un destello y rompe los rastreadores - Cargar todas las configuraciones regionales en el paquete inicial
- Redirigir automáticamente por IP sin forma de anularlo
- Traducir nombres propios, nombres de productos, identificadores de código o URLs
- Traducir cadenas con máquina y enviarlas sin revisar
- Asumir que un solo archivo de catálogo escala más allá de unos pocos cientos de claves