| name | i18n-react-rules |
| description | Usa esta skill cuando implementes internacionalización en un proyecto Vite SPA con React: react-i18next + i18next, ICU MessageFormat, pluralización, formateo de fechas/números/moneda con Intl API, organización de archivos de traducción, soporte RTL y detección de locale del navegador.
|
Internacionalización (i18n) — React / Vite SPA
Cross-references obligatorias
Flujo de trabajo del agente
- Instalar
react-i18next + i18next + i18next-browser-languagedetector + i18next-http-backend (sección 1).
- Configurar i18next con interpolación, detección de idioma y namespaces (sección 1).
- Usar
useTranslation en componentes (sección 2).
- ICU MessageFormat para plurales y select por género (sección 3).
- Formateo de fechas, números y moneda con
Intl API nativa (sección 4).
- Dividir traducciones por feature en namespaces (sección 5).
- Logical CSS properties (
ms-, ps-, text-start) para soporte RTL (sección 6).
- Detección de locale: navegador → cookie → default (sección 7).
1. Setup con react-i18next (Vite SPA)
pnpm add react-i18next i18next i18next-browser-languagedetector i18next-http-backend
public/
└── locales/
├── es/
│ └── translation.json
├── en/
│ └── translation.json
└── pt/
└── translation.json
{
"common": {
"save": "Guardar",
"cancel": "Cancelar",
"loading": "Cargando...",
"error": "Ha ocurrido un error"
},
"auth": {
"login": "Iniciar sesión",
"logout": "Cerrar sesión",
"welcome": "Hola, {{name}}"
},
"products": {
"title": "Productos",
"count_one": "{{count}} producto",
"count_other": "{{count}} productos",
"count_zero": "Sin productos",
"price": "Precio: {{price}}"
}
}
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import HttpBackend from 'i18next-http-backend';
i18n
.use(HttpBackend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng: 'es',
supportedLngs: ['es', 'en', 'pt'],
interpolation: {
escapeValue: false,
},
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
},
detection: {
order: ['navigator', 'cookie', 'htmlTag'],
caches: ['cookie'],
},
});
export default i18n;
import './app/i18n';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './app/App';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);
2. Uso en Componentes
import { useTranslation } from 'react-i18next';
export function ProductsPage() {
const { t } = useTranslation();
return (
<div>
<h1>{t('products.title')}</h1>
<p>{t('products.count', { count: products.length })}</p>
</div>
);
}
export function AuthHeader() {
const { t } = useTranslation('auth');
return <h2>{t('welcome', { name: user.name })}</h2>;
}
export function LanguageSwitcher() {
const { i18n } = useTranslation();
return (
<select
value={i18n.language}
onChange={(e) => i18n.changeLanguage(e.target.value)}
>
<option value="es">Español</option>
<option value="en">English</option>
<option value="pt">Português</option>
</select>
);
}
3. ICU MessageFormat — Plurales e Interpolación
i18next usa un formato propio para plurales (sufijos _one, _other, _zero):
{
"notifications": {
"unread_zero": "No tienes notificaciones",
"unread_one": "Tienes {{count}} notificación sin leer",
"unread_other": "Tienes {{count}} notificaciones sin leer"
}
}
t('notifications.unread', { count: 0 });
t('notifications.unread', { count: 1 });
t('notifications.unread', { count: 5 });
Para proyectos que necesiten ICU MessageFormat completo (select por género, etc.), agregar el plugin:
pnpm add i18next-icu intl-messageformat
import ICU from 'i18next-icu';
i18n
.use(ICU)
.use(HttpBackend)
.use(LanguageDetector)
.use(initReactI18next)
.init({ });
Con ICU habilitado, los JSON usan el formato estándar:
{
"notifications": {
"unread": "{count, plural, =0 {No tienes notificaciones} one {Tienes # notificación sin leer} other {Tienes # notificaciones sin leer}}",
"lastSeen": "{gender, select, male {Última vez visto} female {Última vez vista} other {Última conexión}}: {date, date, medium}"
}
}
4. Formateo de Fechas, Números y Moneda
En Vite SPA, usar la API Intl nativa del navegador (no depender de next-intl):
export function formatCurrency(amount: number, locale: string, currency = 'USD'): string {
return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(amount);
}
export function formatDate(date: Date, locale: string, options?: Intl.DateTimeFormatOptions): string {
return new Intl.DateTimeFormat(locale, options ?? { dateStyle: 'long' }).format(date);
}
export function formatRelativeTime(date: Date, locale: string): string {
const diff = date.getTime() - Date.now();
const seconds = Math.round(diff / 1000);
const minutes = Math.round(seconds / 60);
const hours = Math.round(minutes / 60);
const days = Math.round(hours / 24);
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
if (Math.abs(days) >= 1) return rtf.format(days, 'day');
if (Math.abs(hours) >= 1) return rtf.format(hours, 'hour');
if (Math.abs(minutes) >= 1) return rtf.format(minutes, 'minute');
return rtf.format(seconds, 'second');
}
export function formatList(items: string[], locale: string): string {
return new Intl.ListFormat(locale, { type: 'conjunction' }).format(items);
}
import { useTranslation } from 'react-i18next';
import { formatCurrency, formatDate } from '@/shared/lib/formatters';
export function ProductPrice({ price, date }: { price: number; date: Date }) {
const { i18n } = useTranslation();
const locale = i18n.language;
return (
<div>
<p>{formatCurrency(price, locale)}</p>
<p>{formatDate(date, locale, { dateStyle: 'long' })}</p>
</div>
);
}
5. Organización de Archivos de Traducción
public/
└── locales/
├── es/
│ ├── translation.json # Textos compartidos (namespace default)
│ ├── auth.json # Login, register, password
│ ├── products.json # Catálogo, precios
│ ├── checkout.json # Carrito, pago
│ └── errors.json # Mensajes de error
├── en/
│ └── ...
└── pt/
└── ...
Configurar namespaces en i18next:
i18n.init({
fallbackLng: 'es',
ns: ['translation', 'auth', 'products', 'checkout', 'errors'],
defaultNS: 'translation',
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
},
});
const { t } = useTranslation('products');
t('title');
6. Soporte RTL (Right-to-Left)
import { useTranslation } from 'react-i18next';
const RTL_LOCALES = new Set(['ar', 'he', 'fa', 'ur']);
export function App() {
const { i18n } = useTranslation();
const dir = RTL_LOCALES.has(i18n.language) ? 'rtl' : 'ltr';
useEffect(() => {
document.documentElement.lang = i18n.language;
document.documentElement.dir = dir;
}, [i18n.language, dir]);
}
<div className="ml-4 rtl:mr-4 rtl:ml-0">
<div className="text-left rtl:text-right">
// Preferir logical properties — se adaptan automáticamente a RTL
<div className="ms-4">
<div className="ps-4">
<div className="text-start">
7. Detección de Locale
i18next-browser-languagedetector detecta en este orden (configurable):
- Query string —
?lng=en
- Cookie —
i18next=en
- Navigator —
navigator.language
- HTML tag —
<html lang="en">
i18n.init({
detection: {
order: ['cookie', 'navigator', 'htmlTag'],
caches: ['cookie'],
cookieMinutes: 60 * 24 * 365,
},
});
Gotchas
- Strings hardcodeadas en componentes rompen i18n — todo texto visible debe pasar por
t().
- Ternarios para plurales (
count === 1 ? 'producto' : 'productos') fallan en idiomas con >2 formas plurales — usar sufijos de pluralización o ICU MessageFormat.
- Concatenar traducciones (
t('greeting') + ' ' + name) falla porque el orden de palabras varía por idioma — usar interpolación t('greeting', { name }).
toLocaleDateString es inconsistente entre entornos — usar helpers basados en Intl API.
- Keys crípticas (
msg_42) son imposibles de mantener — usar keys descriptivas (products.emptyState).
ml-/mr- no se adaptan a RTL — usar logical properties (ms-, me-, ps-, pe-).
i18next-http-backend carga JSONs via HTTP — los archivos deben estar en public/.
Skills Relacionadas