🌍 Internationalization Guide (i18n)

Index

  1. Sistema Actual
  2. Agregar Nuevos Idiomas
  3. Agregar Traducciones
  4. Usar Traducciones en Componentes
  5. Formateo de Fechas y Números
  6. 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