Respuesta rápida
react-i18next es la librería i18n más popular para React con más de 3.5M de descargas semanales en npm. Instálala con npm install react-i18next i18next, crea archivos de traducción JSON, configura i18next con i18n.init(), luego usa el hook useTranslation: const { t } = useTranslation(); return <h1>{t('welcome')}</h1>. Para aplicaciones de producción, combínalo con IntlPull para la gestión de traducción impulsada por IA y actualizaciones OTA.
¿Qué es react-i18next?
react-i18next es un binding de React para i18next, el framework de internacionalización de JavaScript más probado en batalla. Proporciona:
- React hooks (
useTranslation) para componentes funcionales - HOCs y render props para componentes de clase
- Soporte de Suspense para carga de traducción asíncrona
- Compatibilidad SSR con Next.js, Remix y Gatsby
- Ecosistema de plugins para backends, detección de idioma y caché
¿Por qué elegir react-i18next?
| Característica | react-i18next | react-intl | next-intl |
|---|---|---|---|
| Descargas semanales | 3.5M+ | 1.2M | 800K |
| Tamaño del Bundle | ~15KB | ~25KB | ~12KB |
| Framework | Cualquiera React | Cualquiera React | Solo Next.js |
| Curva de aprendizaje | Baja | Media | Baja |
| Sistema de Plugin | Extenso | Limitado | N/A |
| TypeScript | Excelente | Bueno | Excelente |
Instalación y Configuración
Paso 1: Instalar Dependencias
Terminal1npm install react-i18next i18next 2# o 3yarn add react-i18next i18next 4# o 5pnpm add react-i18next i18next
Plugins opcionales:
Terminal1# Detección de idioma 2npm install i18next-browser-languagedetector 3 4# Cargar traducciones desde backend 5npm install i18next-http-backend 6 7# Para React Native 8npm install react-i18next i18next @os-team/i18next-react-native-language-detector
Paso 2: Crear Archivos de Traducción
src/
├── locales/
│ ├── en/
│ │ ├── common.json
│ │ └── home.json
│ ├── es/
│ │ ├── common.json
│ │ └── home.json
│ └── fr/
│ ├── common.json
│ └── home.json
└── i18n.ts
en/common.json:
JSON1{ 2 "welcome": "Welcome to our app", 3 "nav": { 4 "home": "Home", 5 "about": "About", 6 "contact": "Contact" 7 }, 8 "buttons": { 9 "submit": "Submit", 10 "cancel": "Cancel", 11 "save": "Save" 12 } 13}
es/common.json:
JSON1{ 2 "welcome": "Bienvenido a nuestra aplicación", 3 "nav": { 4 "home": "Inicio", 5 "about": "Acerca de", 6 "contact": "Contacto" 7 }, 8 "buttons": { 9 "submit": "Enviar", 10 "cancel": "Cancelar", 11 "save": "Guardar" 12 } 13}
Paso 3: Configurar i18next
TypeScript1// src/i18n.ts 2import i18n from 'i18next'; 3import { initReactI18next } from 'react-i18next'; 4import LanguageDetector from 'i18next-browser-languagedetector'; 5 6// Importar traducciones 7import enCommon from './locales/en/common.json'; 8import enHome from './locales/en/home.json'; 9import esCommon from './locales/es/common.json'; 10import esHome from './locales/es/home.json'; 11 12i18n 13 .use(LanguageDetector) 14 .use(initReactI18next) 15 .init({ 16 resources: { 17 en: { 18 common: enCommon, 19 home: enHome, 20 }, 21 es: { 22 common: esCommon, 23 home: esHome, 24 }, 25 }, 26 defaultNS: 'common', 27 fallbackLng: 'en', 28 supportedLngs: ['en', 'es', 'fr'], 29 30 interpolation: { 31 escapeValue: false, // React ya escapa valores 32 }, 33 34 detection: { 35 order: ['localStorage', 'navigator', 'htmlTag'], 36 caches: ['localStorage'], 37 }, 38 }); 39 40export default i18n;
Paso 4: Inicializar en tu App
TSX1// src/main.tsx o src/index.tsx 2import React from 'react'; 3import ReactDOM from 'react-dom/client'; 4import './i18n'; // Importar configuración i18n 5import App from './App'; 6 7ReactDOM.createRoot(document.getElementById('root')!).render( 8 <React.StrictMode> 9 <App /> 10 </React.StrictMode> 11);
Usando el Hook useTranslation
El hook useTranslation es la forma principal de acceder a traducciones en componentes funcionales.
Uso Básico
TSX1import { useTranslation } from 'react-i18next'; 2 3function Header() { 4 const { t } = useTranslation(); 5 6 return ( 7 <header> 8 <h1>{t('welcome')}</h1> 9 <nav> 10 <a href="/">{t('nav.home')}</a> 11 <a href="/about">{t('nav.about')}</a> 12 </nav> 13 </header> 14 ); 15}
Con Namespaces
TSX1import { useTranslation } from 'react-i18next'; 2 3function HomePage() { 4 // Cargar namespace específico 5 const { t } = useTranslation('home'); 6 7 return <h1>{t('hero.title')}</h1>; 8} 9 10function MultiNamespace() { 11 // Cargar múltiples namespaces 12 const { t } = useTranslation(['common', 'home']); 13 14 return ( 15 <div> 16 <h1>{t('home:hero.title')}</h1> 17 <button>{t('common:buttons.submit')}</button> 18 </div> 19 ); 20}
Cambiando Idioma
TSX1import { useTranslation } from 'react-i18next'; 2 3function LanguageSwitcher() { 4 const { i18n } = useTranslation(); 5 6 const changeLanguage = (lng: string) => { 7 i18n.changeLanguage(lng); 8 }; 9 10 return ( 11 <div> 12 <button onClick={() => changeLanguage('en')}>English</button> 13 <button onClick={() => changeLanguage('es')}>Español</button> 14 <button onClick={() => changeLanguage('fr')}>Français</button> 15 </div> 16 ); 17}
Interpolación de Variables
Variables Básicas
JSON1{ 2 "greeting": "Hola, {{name}}!", 3 "itemCount": "Tienes {{count}} artículos en tu carrito" 4}
TSX1function Greeting({ user }) { 2 const { t } = useTranslation(); 3 4 return ( 5 <div> 6 <h1>{t('greeting', { name: user.name })}</h1> 7 <p>{t('itemCount', { count: user.cartItems })}</p> 8 </div> 9 ); 10}
Formateando Valores
JSON1{ 2 "price": "Total: {{price, currency}}", 3 "date": "Creado el {{date, datetime}}" 4}
TypeScript1// i18n.ts - Añadir formateros 2i18n.init({ 3 // ... otra configuración 4 interpolation: { 5 escapeValue: false, 6 format: (value, format, lng) => { 7 if (format === 'currency') { 8 return new Intl.NumberFormat(lng, { 9 style: 'currency', 10 currency: 'USD', 11 }).format(value); 12 } 13 if (format === 'datetime') { 14 return new Intl.DateTimeFormat(lng).format(value); 15 } 16 return value; 17 }, 18 }, 19});
Pluralización
Plurales Básicos
JSON1{ 2 "item_one": "{{count}} artículo", 3 "item_other": "{{count}} artículos", 4 "item_zero": "Sin artículos" 5}
TSX1function CartCount({ count }) { 2 const { t } = useTranslation(); 3 4 return <span>{t('item', { count })}</span>; 5 // count=0: "Sin artículos" 6 // count=1: "1 artículo" 7 // count=5: "5 artículos" 8}
Plurales Complejos (Ruso, Árabe, etc.)
JSON1{ 2 "item_zero": "нет товаров", 3 "item_one": "{{count}} товар", 4 "item_few": "{{count}} товара", 5 "item_many": "{{count}} товаров", 6 "item_other": "{{count}} товаров" 7}
Ordinales
JSON1{ 2 "place_ordinal_one": "{{count}}er lugar", 3 "place_ordinal_two": "{{count}}do lugar", 4 "place_ordinal_few": "{{count}}er lugar", 5 "place_ordinal_other": "{{count}}to lugar" 6}
TSX1t('place', { count: 1, ordinal: true }); // "1er lugar" 2t('place', { count: 2, ordinal: true }); // "2do lugar" 3t('place', { count: 3, ordinal: true }); // "3er lugar" 4t('place', { count: 4, ordinal: true }); // "4to lugar"
Texto Rico y Componentes
Usando el Componente Trans
JSON1{ 2 "terms": "Al registrarte, aceptas nuestros <link>Términos de Servicio</link>", 3 "welcome": "Bienvenido <bold>{{name}}</bold> a nuestra plataforma!" 4}
TSX1import { Trans, useTranslation } from 'react-i18next'; 2 3function TermsText() { 4 return ( 5 <Trans 6 i18nKey="terms" 7 components={{ 8 link: <a href="/terms" class="text-blue-600" />, 9 }} 10 /> 11 ); 12} 13 14function WelcomeMessage({ name }) { 15 return ( 16 <Trans 17 i18nKey="welcome" 18 values={{ name }} 19 components={{ 20 bold: <strong class="font-bold" />, 21 }} 22 /> 23 ); 24}
HTML Anidado
JSON{ "description": "Mira nuestras <link>nuevas características</link> y <button>comienza</button> hoy!" }
TSX1<Trans 2 i18nKey="description" 3 components={{ 4 link: <a href="/features" />, 5 button: <button onClick={handleStart} />, 6 }} 7/>
Carga Perezosa de Traducciones
Para apps grandes, carga traducciones bajo demanda para reducir el tamaño del bundle inicial.
HTTP Backend
TypeScript1import i18n from 'i18next'; 2import { initReactI18next } from 'react-i18next'; 3import HttpBackend from 'i18next-http-backend'; 4import LanguageDetector from 'i18next-browser-languagedetector'; 5 6i18n 7 .use(HttpBackend) 8 .use(LanguageDetector) 9 .use(initReactI18next) 10 .init({ 11 fallbackLng: 'en', 12 ns: ['common'], 13 defaultNS: 'common', 14 15 backend: { 16 loadPath: '/locales/{{lng}}/{{ns}}.json', 17 }, 18 19 react: { 20 useSuspense: true, 21 }, 22 }); 23 24export default i18n;
Con Suspense
TSX1import { Suspense } from 'react'; 2 3function App() { 4 return ( 5 <Suspense fallback={<LoadingSpinner />}> 6 <MainContent /> 7 </Suspense> 8 ); 9}
Cargar Namespace Bajo Demanda
TSX1import { useTranslation } from 'react-i18next'; 2import { Suspense, lazy } from 'react'; 3 4// Lazy load component con su namespace 5const SettingsPage = lazy(() => import('./SettingsPage')); 6 7function App() { 8 return ( 9 <Suspense fallback={<LoadingSpinner />}> 10 <SettingsPage /> 11 </Suspense> 12 ); 13} 14 15// SettingsPage.tsx 16function SettingsPage() { 17 const { t, ready } = useTranslation('settings', { useSuspense: false }); 18 19 if (!ready) return <LoadingSpinner />; 20 21 return <h1>{t('title')}</h1>; 22}
Integración con TypeScript
Traducciones Type-Safe
TypeScript1// src/types/i18next.d.ts 2import 'i18next'; 3import common from '../locales/en/common.json'; 4import home from '../locales/en/home.json'; 5 6declare module 'i18next' { 7 interface CustomTypeOptions { 8 defaultNS: 'common'; 9 resources: { 10 common: typeof common; 11 home: typeof home; 12 }; 13 } 14}
Ahora TypeScript autocompletará las claves de traducción:
TSXconst { t } = useTranslation(); t('nav.home'); // ✅ Autocompleta t('nav.invalid'); // ❌ Error TypeScript
Con Múltiples Namespaces
TSX1const { t } = useTranslation(['common', 'home']); 2 3t('buttons.submit'); // De common (default) 4t('home:hero.title'); // De namespace home
Server-Side Rendering (SSR)
Next.js Pages Router
TypeScript1// next-i18next.config.js 2module.exports = { 3 i18n: { 4 defaultLocale: 'en', 5 locales: ['en', 'es', 'fr'], 6 }, 7};
TSX1// pages/index.tsx 2import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; 3import { useTranslation } from 'next-i18next'; 4 5export default function Home() { 6 const { t } = useTranslation('common'); 7 return <h1>{t('welcome')}</h1>; 8} 9 10export async function getStaticProps({ locale }) { 11 return { 12 props: { 13 ...(await serverSideTranslations(locale, ['common'])), 14 }, 15 }; 16}
Next.js App Router
Para Next.js App Router, recomendamos usar next-intl en su lugar. Mira nuestra guía de next-intl.
Remix
TSX1// app/entry.server.tsx 2import { createInstance } from 'i18next'; 3import { initReactI18next } from 'react-i18next'; 4 5export default async function handleRequest(request, ...) { 6 const i18n = createInstance(); 7 await i18n.use(initReactI18next).init({ 8 lng: locale, 9 resources: { [locale]: translations }, 10 }); 11 12 // Render con instancia i18n 13}
Probando con react-i18next
Configuración Mock
TypeScript1// test/setup.ts 2import i18n from 'i18next'; 3import { initReactI18next } from 'react-i18next'; 4 5i18n.use(initReactI18next).init({ 6 lng: 'en', 7 fallbackLng: 'en', 8 resources: { 9 en: { 10 common: { 11 welcome: 'Welcome', 12 'buttons.submit': 'Submit', 13 }, 14 }, 15 }, 16}); 17 18export default i18n;
Probando Componentes
TSX1import { render, screen } from '@testing-library/react'; 2import { I18nextProvider } from 'react-i18next'; 3import i18n from '../test/setup'; 4import Header from './Header'; 5 6describe('Header', () => { 7 it('renders translated welcome message', () => { 8 render( 9 <I18nextProvider i18n={i18n}> 10 <Header /> 11 </I18nextProvider> 12 ); 13 14 expect(screen.getByText('Welcome')).toBeInTheDocument(); 15 }); 16});
Mejores Prácticas de Producción
1. Organización de Namespace
locales/
├── en/
│ ├── common.json # UI compartido (botones, nav, errores)
│ ├── auth.json # Login, registro, reseteo password
│ ├── dashboard.json # Específico de Dashboard
│ ├── settings.json # Página de configuración
│ └── errors.json # Mensajes de error
2. Convención de Nombres de Claves
JSON1{ 2 "page.section.element": "Valor", 3 "auth.login.title": "Iniciar Sesión", 4 "auth.login.button": "Continuar", 5 "dashboard.stats.totalUsers": "Usuarios Totales", 6 "errors.network.timeout": "Tiempo de espera agotado" 7}
3. Manejo de Claves Faltantes
TypeScript1i18n.init({ 2 saveMissing: process.env.NODE_ENV === 'development', 3 missingKeyHandler: (lng, ns, key) => { 4 console.warn(`Falta traducción: ${lng}/${ns}/${key}`); 5 // Reportar a seguimiento de errores 6 }, 7});
4. Optimización de Rendimiento
TypeScript1// Pre-cargar namespaces críticos 2i18n.loadNamespaces(['common', 'auth']); 3 4// No re-renderizar en cambio de idioma para contenido estático 5const { t } = useTranslation('common', { useSuspense: false });
Escalando con IntlPull
A medida que tu app crece, gestionar archivos JSON manualmente se vuelve doloroso. IntlPull proporciona:
Integración CLI
Terminal1# Inicializar IntlPull en tu proyecto 2npx @intlpullhq/cli init 3 4# Extraer claves del código 5npx @intlpullhq/cli extract 6 7# Subir a IntlPull 8npx @intlpullhq/cli upload 9 10# Descargar traducciones 11npx @intlpullhq/cli download
Traducción IA
IntlPull traduce automáticamente tus claves usando GPT-4, Claude o DeepL mientras mantiene el contexto y la voz de la marca.
Actualizaciones OTA
Actualiza traducciones sin lanzamientos de app:
TypeScript1import { IntlPullBackend } from '@intlpull/i18next-backend'; 2 3i18n.use(IntlPullBackend).init({ 4 backend: { 5 projectId: 'your-project-id', 6 apiKey: process.env.INTLPULL_API_KEY, 7 }, 8});
Patrones Comunes
Traducciones Condicionales
JSON1{ 2 "greeting_morning": "¡Buenos días!", 3 "greeting_afternoon": "¡Buenas tardes!", 4 "greeting_evening": "¡Buenas noches!" 5}
TSX1function Greeting() { 2 const { t } = useTranslation(); 3 const hour = new Date().getHours(); 4 5 const getGreetingKey = () => { 6 if (hour < 12) return 'greeting_morning'; 7 if (hour < 18) return 'greeting_afternoon'; 8 return 'greeting_evening'; 9 }; 10 11 return <h1>{t(getGreetingKey())}</h1>; 12}
Claves Dinámicas
TSX1function StatusBadge({ status }: { status: 'pending' | 'approved' | 'rejected' }) { 2 const { t } = useTranslation(); 3 4 // Claves: status.pending, status.approved, status.rejected 5 return <span>{t(`status.${status}`)}</span>; 6}
Contenido de Respaldo
TSX1const { t } = useTranslation(); 2 3// Proveer fallback 4const title = t('page.title', 'Título por defecto'); 5 6// Verificar si la clave existe 7const { exists } = useTranslation(); 8if (exists('optional.feature')) { 9 // Mostrar contenido específico de la característica 10}
Solución de problemas
Advertencias de "Traducción no encontrada"
TypeScript1// Verificar si el namespace está cargado 2i18n.hasLoadedNamespace('myNamespace'); 3 4// Asegurar que la clave existe 5i18n.exists('myKey'); 6 7// Modo debug 8i18n.init({ debug: true });
Componente no se re-renderiza al cambiar idioma
TSX1// Usa el hook correctamente - se suscribe a cambios 2const { t, i18n } = useTranslation(); 3 4// Forzar re-render con key 5<MyComponent key={i18n.language} />
