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.

390 lines
12 KiB

8 months ago
import { describe, it, expect, vi, beforeEach } from 'vitest';
8 months ago
import { createI18n } from '../index.ts';
import type { TranslationNode, I18nString } from '../index.ts';
8 months ago
8 months ago
8 months ago
// ==============================
// SCHEMA DE TEST
// ==============================
const testSchema = {
common: {
ok: {
es: 'Aceptar',
en: 'OK',
fr: 'Accepter'
},
cancel: {
es: 'Cancelar',
en: 'Cancel'
}
},
checkout: {
pay: {
es: 'Pagar',
en: 'Pay'
},
total: (params: { amount: number; currency: string }) => ({
es: `Total: ${params.amount}${params.currency}`,
en: `Total: ${params.currency}${params.amount}`
})
},
errors: {
generic: (params: { code: number }) => ({
es: `Error ${params.code}`,
en: `Error ${params.code}`
})
},
// Clave solo en español (para probar fallback)
onlySpanish: {
es: 'Solo en español'
}
} satisfies TranslationNode;
// ==============================
// TESTS
// ==============================
describe('createI18n', () => {
describe('instanciación', () => {
it('crea una instancia con el locale por defecto', () => {
const i18n = createI18n(testSchema, 'es');
expect(i18n.getLocale()).toBe('es');
});
it('crea instancias independientes con distintos locales', () => {
const i18nEs = createI18n(testSchema, 'es');
const i18nEn = createI18n(testSchema, 'en');
expect(i18nEs.getLocale()).toBe('es');
expect(i18nEn.getLocale()).toBe('en');
// Cambiar uno no afecta al otro
i18nEs.setLocale('fr');
expect(i18nEn.getLocale()).toBe('en');
});
});
describe('t() — traducciones simples', () => {
let i18n: ReturnType<typeof createI18n<typeof testSchema>>;
beforeEach(() => {
i18n = createI18n(testSchema, 'es');
});
it('devuelve la traducción en el locale actual', () => {
expect(i18n.t('common.ok')).toBe('Aceptar');
});
it('devuelve la traducción tras cambiar de locale', () => {
i18n.setLocale('en');
expect(i18n.t('common.ok')).toBe('OK');
});
it('devuelve la traducción en francés', () => {
i18n.setLocale('fr');
expect(i18n.t('common.ok')).toBe('Accepter');
});
});
describe('t() — traducciones con parámetros', () => {
let i18n: ReturnType<typeof createI18n<typeof testSchema>>;
beforeEach(() => {
i18n = createI18n(testSchema, 'es');
});
it('interpola parámetros correctamente en español', () => {
expect(i18n.t('checkout.total', { amount: 99, currency: '€' }))
.toBe('Total: 99€');
});
it('interpola parámetros correctamente en inglés', () => {
i18n.setLocale('en');
expect(i18n.t('checkout.total', { amount: 99, currency: '$' }))
.toBe('Total: $99');
});
it('interpola parámetros numéricos', () => {
expect(i18n.t('errors.generic', { code: 404 }))
.toBe('Error 404');
});
});
describe('t() — fallback', () => {
let i18n: ReturnType<typeof createI18n<typeof testSchema>>;
beforeEach(() => {
i18n = createI18n(testSchema, 'es');
});
it('hace fallback al locale por defecto si falta la traducción', () => {
i18n.setLocale('de'); // 'de' no existe en onlySpanish
expect(i18n.t('onlySpanish')).toBe('Solo en español');
});
it('devuelve la clave si no existe en ningún locale', () => {
// @ts-expect-error — clave inexistente a propósito
expect(i18n.t('this.key.does.not.exist')).toBe('this.key.does.not.exist');
});
it('loguea un warning en desarrollo cuando hace fallback', () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
const originalEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'development';
i18n.setLocale('de');
i18n.t('onlySpanish');
expect(warn).toHaveBeenCalledWith(
expect.stringContaining('Missing translation')
);
process.env.NODE_ENV = originalEnv;
warn.mockRestore();
});
it('no loguea en producción', () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
const originalEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'production';
i18n.setLocale('de');
i18n.t('onlySpanish');
expect(warn).not.toHaveBeenCalled();
process.env.NODE_ENV = originalEnv;
warn.mockRestore();
});
});
describe('tForLocale()', () => {
it('resuelve en la locale indicada sin cambiar el estado global', () => {
const i18n = createI18n(testSchema, 'es');
const result = i18n.tForLocale('common.ok', 'en');
expect(result).toBe('OK');
expect(i18n.getLocale()).toBe('es'); // no ha cambiado
});
it('funciona con parámetros', () => {
const i18n = createI18n(testSchema, 'es');
const result = i18n.tForLocale('checkout.total', 'en', { amount: 50, currency: '$' });
expect(result).toBe('Total: $50');
expect(i18n.getLocale()).toBe('es');
});
});
describe('setLocale() / getLocale()', () => {
it('actualiza el locale correctamente', () => {
const i18n = createI18n(testSchema, 'es');
i18n.setLocale('en');
expect(i18n.getLocale()).toBe('en');
});
});
describe('onLocaleChange()', () => {
it('notifica al cambiar de locale', () => {
const i18n = createI18n(testSchema, 'es');
const listener = vi.fn();
i18n.onLocaleChange(listener);
i18n.setLocale('en');
expect(listener).toHaveBeenCalledWith('en');
expect(listener).toHaveBeenCalledTimes(1);
});
it('no notifica después de hacer unsubscribe', () => {
const i18n = createI18n(testSchema, 'es');
const listener = vi.fn();
const unsubscribe = i18n.onLocaleChange(listener);
unsubscribe();
i18n.setLocale('en');
expect(listener).not.toHaveBeenCalled();
});
it('soporta múltiples listeners', () => {
const i18n = createI18n(testSchema, 'es');
const l1 = vi.fn();
const l2 = vi.fn();
i18n.onLocaleChange(l1);
i18n.onLocaleChange(l2);
i18n.setLocale('fr');
expect(l1).toHaveBeenCalledWith('fr');
expect(l2).toHaveBeenCalledWith('fr');
});
it('tForLocale no dispara los listeners', () => {
const i18n = createI18n(testSchema, 'es');
const listener = vi.fn();
i18n.onLocaleChange(listener);
i18n.tForLocale('common.ok', 'en');
expect(listener).not.toHaveBeenCalled();
});
});
});
// ==============================
8 months ago
// TESTS ts()
8 months ago
// ==============================
describe('resolve()', () => {
let i18n: ReturnType<typeof createI18n<typeof testSchema>>;
beforeEach(() => {
i18n = createI18n(testSchema, 'es');
});
it('devuelve el string tal cual si es un string simple', () => {
8 months ago
expect(i18n.ts('texto fijo')).toBe('texto fijo');
8 months ago
});
it('devuelve el string vacío sin errores', () => {
8 months ago
expect(i18n.ts('')).toBe('');
8 months ago
});
it('resuelve un LocaleRecord con el locale actual', () => {
const desc: I18nString = { es: 'una descripción', en: 'a description' };
8 months ago
expect(i18n.ts(desc)).toBe('una descripción');
8 months ago
});
it('resuelve un LocaleRecord tras cambiar de locale', () => {
const desc: I18nString = { es: 'una descripción', en: 'a description' };
i18n.setLocale('en');
8 months ago
expect(i18n.ts(desc)).toBe('a description');
8 months ago
});
it('hace fallback al defaultLocale si el locale actual no existe en el record', () => {
const desc: I18nString = { es: 'solo español' };
i18n.setLocale('de');
8 months ago
expect(i18n.ts(desc)).toBe('solo español');
8 months ago
});
it('funciona con un interface que usa I18nString', () => {
interface Producto {
id: string;
descripcion: I18nString;
}
const producto: Producto = {
id: '1',
descripcion: { es: 'Silla de madera', en: 'Wooden chair' }
};
8 months ago
expect(i18n.ts(producto.descripcion)).toBe('Silla de madera');
8 months ago
i18n.setLocale('en');
8 months ago
expect(i18n.ts(producto.descripcion)).toBe('Wooden chair');
8 months ago
});
it('funciona mezclando strings simples y LocaleRecord en el mismo array', () => {
const items: I18nString[] = [
'id-invariante',
{ es: 'nombre', en: 'name' },
'otro-invariante'
];
8 months ago
const resolved = items.map(i18n.ts);
8 months ago
expect(resolved).toEqual(['id-invariante', 'nombre', 'otro-invariante']);
});
8 months ago
});
8 months ago
// ==============================
// TESTS register()
// ==============================
describe('register()', () => {
const baseSchema = {
common: {
ok: { es: 'Aceptar', en: 'OK' }
}
} satisfies TranslationNode;
it('registra traducciones de un módulo bajo un namespace', () => {
const i18n = createI18n(baseSchema, 'es');
const extended = i18n.register('auth', {
loginFailed: { es: 'Login fallido', en: 'Login failed' }
});
expect(extended.t('auth.loginFailed')).toBe('Login fallido');
});
it('mantiene acceso a las claves del schema base', () => {
const i18n = createI18n(baseSchema, 'es');
const extended = i18n.register('auth', {
loginFailed: { es: 'Login fallido', en: 'Login failed' }
});
expect(extended.t('common.ok')).toBe('Aceptar');
});
it('hereda el locale actual en el momento del registro', () => {
const i18n = createI18n(baseSchema, 'es');
i18n.setLocale('en');
const extended = i18n.register('auth', {
loginFailed: { es: 'Login fallido', en: 'Login failed' }
});
expect(extended.t('auth.loginFailed')).toBe('Login failed');
});
it('se sincroniza cuando cambia el locale en la instancia base', () => {
const i18n = createI18n(baseSchema, 'es');
const extended = i18n.register('auth', {
loginFailed: { es: 'Login fallido', en: 'Login failed' }
});
i18n.setLocale('en');
expect(extended.t('auth.loginFailed')).toBe('Login failed');
expect(extended.t('common.ok')).toBe('OK');
});
it('múltiples módulos se acumulan correctamente', () => {
const i18n = createI18n(baseSchema, 'es');
const withAuth = i18n.register('auth', {
loginFailed: { es: 'Login fallido', en: 'Login failed' }
});
const withAll = withAuth.register('checkout', {
pay: { es: 'Pagar', en: 'Pay' }
});
expect(withAll.t('common.ok')).toBe('Aceptar');
expect(withAll.t('auth.loginFailed')).toBe('Login fallido');
expect(withAll.t('checkout.pay')).toBe('Pagar');
});
it('cambiar locale en base se propaga a todos los módulos registrados', () => {
const i18n = createI18n(baseSchema, 'es');
const withAuth = i18n.register('auth', {
loginFailed: { es: 'Login fallido', en: 'Login failed' }
});
const withAll = withAuth.register('checkout', {
pay: { es: 'Pagar', en: 'Pay' }
});
i18n.setLocale('en');
expect(withAll.t('common.ok')).toBe('OK');
expect(withAll.t('auth.loginFailed')).toBe('Login failed');
expect(withAll.t('checkout.pay')).toBe('Pay');
});
});

Powered by TurnKey Linux.