You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

6.9 KiB

i18n

Sistema de internacionalización type-safe para TypeScript. Agnóstico de framework, sin dependencias externas.


Estructura de ficheros

i18n/
├── index.ts            # Barrel — punto de entrada público
├── i18n.ts             # Instancia por defecto (wiring de schema + factory)
├── i18n.factory.ts     # Factory: createI18n()
├── i18n.types.ts       # Tipos e interfaces
├── translations.ts     # Árbol de traducciones
└── i18n.factory.test.ts

Instalación y setup

1. Define tus traducciones

// translations.ts
import type { TranslationNode } from './i18n.types';

export const translations = {
    common: {
        ok:     { es: 'Aceptar', en: 'OK' },
        cancel: { es: 'Cancelar', en: 'Cancel' },
    },
    checkout: {
        pay: { es: 'Pagar', en: 'Pay' },

        // Con parámetros — TypeScript infiere el tipo de params automáticamente
        total: (params: { amount: number; currency: string }) => ({
            es: `Total: ${params.amount}${params.currency}`,
            en: `Total: ${params.currency}${params.amount}`,
        }),
    },
} satisfies TranslationNode;

export type TranslationSchema = typeof translations;

Regla: es es obligatorio en cada hoja. El resto de locales son opcionales y hacen fallback a es si faltan.

2. Crea la instancia

// i18n.ts
import { createI18n, translations } from './';

export const i18n = createI18n(translations, 'es');

// Desestructura para usar directamente sin prefijo
export const { t, ts, tForLocale, setLocale, getLocale, onLocaleChange } = i18n;

API

t(path, params?)

Traduce una clave del schema al locale actual.

t('common.ok')                                        // → "Aceptar"
t('checkout.pay')                                     // → "Pagar"
t('checkout.total', { amount: 99, currency: '€' })   // → "Total: 99€"
  • El path tiene autocompletado y validación en compilación — no puedes escribir una clave que no exista.
  • Los params son obligatorios si la traducción los requiere, y TypeScript los infiere automáticamente.
  • Solo acepta rutas que terminan en una traducción real. Rutas intermedias como 'checkout' dan error de tipos.

ts(value)

Translate String — resuelve un I18nString con el locale actual.

A diferencia de t(), no trabaja con el schema sino con valores dinámicos de tus datos.

ts('texto fijo')                              // → "texto fijo"  (pass-through)
ts({ es: 'una descripción', en: 'a description' }) // → "una descripción"

Útil cuando un campo de tu modelo puede estar localizado o no:

import type { I18nString } from './';

interface Product {
    id: string;
    name: I18nString;   // puede ser string simple o LocaleRecord
}

const product: Product = {
    id: '1',
    name: { es: 'Silla', en: 'Chair' },
};

ts(product.name) // → "Silla" (locale 'es')

tForLocale(path, locale, params?)

Resuelve una clave en una locale específica sin cambiar el estado global.

tForLocale('common.ok', 'en')  // → "OK"
getLocale()                    // → "es"  (no ha cambiado)

Útil para SSR, generación de emails en el idioma del usuario, o tests sin efectos secundarios.

setLocale(locale)

Cambia el locale actual y notifica a todos los listeners registrados.

setLocale('en')
t('common.ok') // → "OK"

getLocale()

Devuelve el locale actual.

getLocale() // → "es"

onLocaleChange(fn)

Registra un listener que se ejecuta cada vez que cambia el locale. Devuelve una función de unsubscribe.

const unsubscribe = onLocaleChange((locale) => {
    console.log('Nuevo locale:', locale);
});

setLocale('fr'); // → "Nuevo locale: fr"

unsubscribe();   // deja de escuchar
setLocale('en'); // el listener ya no se ejecuta

tForLocale() no dispara los listeners — solo cambia el locale internamente de forma temporal.


Locales soportadas

Código Idioma
es Español (por defecto)
en Inglés
de Alemán
fr Francés
it Italiano
pt Portugués
ca Catalán
eu Euskera
gl Gallego

Para añadir una nueva locale, solo hay que añadirla al tipo SupportedLocale en i18n.types.ts:

export type SupportedLocale =
    | DefaultLocale
    | 'en'
    | 'de'
    // añade aquí
    | 'ja';

TypeScript marcará automáticamente cualquier hoja del schema donde falte la nueva locale si decides hacerla obligatoria.


Tipos públicos

Tipo Descripción
SupportedLocale Unión de todas las locales soportadas
DefaultLocale 'es' — locale obligatoria en cada traducción
LocaleRecord Objeto { es: string, en?: string, ... }
I18nString string | LocaleRecord — para campos de datos opcionalmente localizados
TranslationNode Tipo recursivo del árbol de traducciones
TranslationFn<P> Función de traducción con parámetros tipados
TranslationSchema Tipo inferido del árbol translations

Fallback

Cuando el locale actual no tiene traducción para una clave, el sistema cae en cascada:

locale actual → defaultLocale → clave como texto

En desarrollo (NODE_ENV === 'development') se emite un console.warn indicando qué clave y locale fallan. En producción la degradación es silenciosa para no romper la UI.


Múltiples instancias

La factory permite crear instancias independientes con distintos schemas o locales por defecto:

import { createI18n } from './';
import { translations }      from './translations';
import { adminTranslations } from './admin.translations';

export const i18n      = createI18n(translations,      'es');
export const adminI18n = createI18n(adminTranslations, 'en');

Cada instancia tiene su propio estado de locale y sus propios listeners, completamente aislados entre sí.


Integración con frameworks

La factory es agnóstica. Para integrarla en cualquier framework basta con conectar setLocale y onLocaleChange al sistema reactivo correspondiente.

Vue 3

import { ref } from 'vue';
import { i18n } from './i18n';

export const locale = ref(i18n.getLocale());
i18n.onLocaleChange((l) => (locale.value = l));

Svelte

import { writable } from 'svelte/store';
import { i18n } from './i18n';

export const locale = writable(i18n.getLocale());
i18n.onLocaleChange((l) => locale.set(l));

React

import { useSyncExternalStore } from 'react';
import { i18n } from './i18n';

export function useLocale() {
    return useSyncExternalStore(i18n.onLocaleChange, i18n.getLocale);
}

Tests

vitest

Los tests usan un schema propio independiente del de producción, por lo que no hay acoplamiento entre la suite y las traducciones reales.

Powered by TurnKey Linux.