🌍 Internationalization Guide (i18n)
Index
- Sistema Actual
- Agregar Nuevos Idiomas
- Agregar Traducciones
- Usar Traducciones en Componentes
- Formateo de Fechas y Números
- Mejores Prácticas
Current System
El boilerplate incluye un sistema de internacionalización basado en contexto con soporte para:
- Español (es) - Idioma por defecto
- Inglés (en)
Ubicación
contexts/
└── I18nContext.tsx # Contexto de traducciones
Uso Básico
import { useI18n } from '@/contexts/I18nContext';
function MyComponent() {
const { t, locale, setLocale } = useI18n();
return (
<View>
<Text>{t('common.save')}</Text>
<Button
title={locale === 'es' ? 'English' : 'Español'}
onPress={() => setLocale(locale === 'es' ? 'en' : 'es')}
/>
</View>
);
}
Add New Languages
1. Agregar al Tipo de Locale
// contexts/I18nContext.tsx
type Locale = 'es' | 'en' | 'fr' | 'de'; // Agregar nuevos idiomas
2. Agregar Traducciones
// En I18nContext.tsx
const translations: Record<Locale, Translations> = {
es: {
// traducciones en español
},
en: {
// traducciones en inglés
},
fr: {
// NUEVO: traducciones en francés
common: {
save: 'Enregistrer',
cancel: 'Annuler',
delete: 'Supprimer',
// ...
},
// ...
},
de: {
// NUEVO: traducciones en alemán
common: {
save: 'Speichern',
cancel: 'Abbrechen',
delete: 'Löschen',
// ...
},
// ...
},
};
3. Agregar Opción en Settings
// app/(tabs)/settings/index.tsx
const languages = [
{ code: 'es', name: 'Español', flag: '🇪🇸' },
{ code: 'en', name: 'English', flag: '🇺🇸' },
{ code: 'fr', name: 'Français', flag: '🇫🇷' },
{ code: 'de', name: 'Deutsch', flag: '🇩🇪' },
];
Add Translations
Estructura de Traducciones
interface Translations {
common: {
save: string;
cancel: string;
delete: string;
edit: string;
loading: string;
error: string;
success: string;
retry: string;
search: string;
noResults: string;
};
auth: {
login: string;
logout: string;
register: string;
email: string;
password: string;
forgotPassword: string;
};
navigation: {
home: string;
explore: string;
favorites: string;
profile: string;
settings: string;
messages: string;
activity: string;
};
screens: {
home: {
title: string;
welcome: string;
recentItems: string;
};
explore: {
title: string;
searchPlaceholder: string;
filters: string;
};
// ... más pantallas
};
errors: {
networkError: string;
unknownError: string;
requiredField: string;
invalidEmail: string;
// ... más errores
};
}
Agregar Nueva Sección
// 1. Agregar al tipo
interface Translations {
// ... secciones existentes
products: {
title: string;
addToCart: string;
outOfStock: string;
price: string;
reviews: string;
};
}
// 2. Agregar traducciones
const translations = {
es: {
// ... existentes
products: {
title: 'Productos',
addToCart: 'Agregar al carrito',
outOfStock: 'Agotado',
price: 'Precio',
reviews: 'Reseñas',
},
},
en: {
// ... existentes
products: {
title: 'Products',
addToCart: 'Add to cart',
outOfStock: 'Out of stock',
price: 'Price',
reviews: 'Reviews',
},
},
};
Use Translations in Components
Textos Simples
function ProductCard() {
const { t } = useI18n();
return (
<View>
<Text>{t('products.title')}</Text>
<Button title={t('products.addToCart')} onPress={handleAdd} />
</View>
);
}
Con Interpolación
// Definir traducción con placeholder
const translations = {
es: {
greeting: 'Hola, {{name}}!',
itemCount: '{{count}} items',
},
en: {
greeting: 'Hello, {{name}}!',
itemCount: '{{count}} items',
},
};
// Función helper para interpolación
function interpolate(text: string, params: Record<string, string | number>): string {
return text.replace(/\{\{(\w+)\}\}/g, (_, key) => String(params[key] ?? ''));
}
// Uso
const { t } = useI18n();
const greeting = interpolate(t('greeting'), { name: 'María' });
// "Hola, María!" o "Hello, María!"
Plurales
// Definir traducciones con plurales
const translations = {
es: {
items: {
zero: 'Sin items',
one: '1 item',
other: '{{count}} items',
},
},
en: {
items: {
zero: 'No items',
one: '1 item',
other: '{{count}} items',
},
},
};
// Hook para plurales
function usePlural(key: string, count: number): string {
const { t } = useI18n();
const pluralKey = count === 0 ? 'zero' : count === 1 ? 'one' : 'other';
const text = t(`${key}.${pluralKey}`);
return text.replace('{{count}}', String(count));
}
// Uso
const itemText = usePlural('items', items.length);
En Componentes de Navegación
// app/(tabs)/_layout.tsx
export default function TabLayout() {
const { t } = useI18n();
return (
<Tabs>
<Tabs.Screen
name="(home)"
options={{
title: t('navigation.home'),
tabBarIcon: ({ color, size }) => <Home size={size} color={color} />,
}}
/>
<Tabs.Screen
name="explore"
options={{
title: t('navigation.explore'),
tabBarIcon: ({ color, size }) => <Compass size={size} color={color} />,
}}
/>
{/* ... más tabs */}
</Tabs>
);
}
Date and Number Formatting
Fechas
// utils/formatters.ts
export function formatDate(date: Date | string, locale: string): string {
const d = typeof date === 'string' ? new Date(date) : date;
return new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(d);
}
export function formatTime(date: Date | string, locale: string): string {
const d = typeof date === 'string' ? new Date(date) : date;
return new Intl.DateTimeFormat(locale, {
hour: '2-digit',
minute: '2-digit',
}).format(d);
}
export function formatRelativeTime(date: Date | string, locale: string): string {
const d = typeof date === 'string' ? new Date(date) : date;
const now = new Date();
const diffInSeconds = Math.floor((now.getTime() - d.getTime()) / 1000);
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
if (diffInSeconds < 60) return rtf.format(-diffInSeconds, 'second');
if (diffInSeconds < 3600) return rtf.format(-Math.floor(diffInSeconds / 60), 'minute');
if (diffInSeconds < 86400) return rtf.format(-Math.floor(diffInSeconds / 3600), 'hour');
if (diffInSeconds < 2592000) return rtf.format(-Math.floor(diffInSeconds / 86400), 'day');
return formatDate(d, locale);
}
// Uso
const { locale } = useI18n();
<Text>{formatDate(item.createdAt, locale)}</Text>
// "15 de enero de 2024" (es) o "January 15, 2024" (en)
Números y Moneda
// utils/formatters.ts
export function formatNumber(num: number, locale: string): string {
return new Intl.NumberFormat(locale).format(num);
}
export function formatCurrency(
amount: number,
locale: string,
currency = 'EUR'
): string {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency,
}).format(amount);
}
export function formatPercent(value: number, locale: string): string {
return new Intl.NumberFormat(locale, {
style: 'percent',
minimumFractionDigits: 0,
maximumFractionDigits: 1,
}).format(value);
}
// Uso
const { locale } = useI18n();
<Text>{formatCurrency(product.price, locale, 'USD')}</Text>
// "$1,234.56" (en) o "1.234,56 US$" (es)
Best Practices
1. Organizar por Feature
const translations = {
es: {
// Agrupar por feature, no por tipo
checkout: {
title: 'Finalizar compra',
summary: 'Resumen del pedido',
total: 'Total',
pay: 'Pagar ahora',
success: '¡Pedido realizado!',
error: 'Error al procesar el pago',
},
// NO hacer esto:
// titles: { checkout: 'Finalizar compra', ... }
// buttons: { pay: 'Pagar ahora', ... }
},
};
2. Evitar Concatenación
// ❌ MAL - La concatenación rompe traducciones
<Text>{t('hello')} {userName}!</Text>
// ✅ BIEN - Usar interpolación
<Text>{interpolate(t('greeting'), { name: userName })}</Text>
// Con: greeting: 'Hola, {{name}}!'
3. Contexto en Traducciones
// ❌ MAL - Ambiguo
const translations = {
delete: 'Eliminar',
};
// ✅ BIEN - Con contexto
const translations = {
actions: {
deleteItem: 'Eliminar item',
deleteAccount: 'Eliminar cuenta',
deleteMessage: 'Eliminar mensaje',
},
};
4. Manejar Texto Largo
// Para textos largos, usar keys descriptivas
const translations = {
es: {
legal: {
privacyPolicyTitle: 'Política de Privacidad',
privacyPolicyContent: `
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
`,
termsTitle: 'Términos y Condiciones',
termsContent: `...`,
},
},
};
5. Fallback para Traducciones Faltantes
// En I18nContext.tsx
function t(key: string): string {
const keys = key.split('.');
let value: any = translations[locale];
for (const k of keys) {
value = value?.[k];
if (value === undefined) break;
}
// Fallback a inglés si falta traducción
if (value === undefined && locale !== 'en') {
value = translations.en;
for (const k of keys) {
value = value?.[k];
if (value === undefined) break;
}
}
// Fallback a la key si no hay traducción
return value ?? key;
}
6. Testing de Traducciones
// Verificar que todas las keys existen en todos los idiomas
function validateTranslations() {
const locales = Object.keys(translations) as Locale[];
const baseKeys = getAllKeys(translations.es);
for (const locale of locales) {
const localeKeys = getAllKeys(translations[locale]);
const missing = baseKeys.filter(key => !localeKeys.includes(key));
if (missing.length > 0) {
console.warn(`Missing keys in ${locale}:`, missing);
}
}
}
function getAllKeys(obj: any, prefix = ''): string[] {
return Object.entries(obj).flatMap(([key, value]) => {
const fullKey = prefix ? `${prefix}.${key}` : key;
if (typeof value === 'object' && value !== null) {
return getAllKeys(value, fullKey);
}
return [fullKey];
});
}
Checklist de i18n
- Todas las strings visibles usan
t() - Fechas formateadas con
formatDate() - Números/moneda formateados con
formatNumber()/formatCurrency() - No hay concatenación de strings traducidos
- Traducciones organizadas por feature
- Fallbacks configurados
- Selector de idioma en Settings
- Idioma persiste entre sesiones
- Probado en todos los idiomas soportados