Files
MetonaEditor/tests/i18n.test.js
T

413 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* i18n.js 单元测试
* 覆盖国际化系统:翻译 / 语言切换 / 插值 / 格式化 / 监听 / 持久化
*
* 注意:i18n.js 使用模块级状态(currentLocale / localeListeners / fallbackLocale),
* 测试间需要重置状态以避免相互影响。
*/
import {
i18nUtils,
t,
getCurrentLocale,
setCurrentLocale,
switchLocale,
hasTranslation,
getTranslations,
addTranslations,
getSupportedLocales,
isLocaleSupported,
getLocaleName,
getLocaleDirection,
formatNumber,
formatCurrency,
formatDate,
addLocaleListener,
removeLocaleListener,
clearLocaleListeners,
saveLocale,
loadLocale,
getDefaultLocale,
initI18n,
presetLocales,
createI18nManager,
} from '../src/i18n.js';
import { LOCALES } from '../src/constants.js';
// 确保 localStorage 存在(jsdom 兼容)
if (typeof global.localStorage === 'undefined') {
const store = {};
global.localStorage = {
getItem: (k) => (k in store ? store[k] : null),
setItem: (k, v) => { store[k] = String(v); },
removeItem: (k) => { delete store[k]; },
clear: () => { Object.keys(store).forEach((k) => delete store[k]); },
};
}
describe('当前语言', () => {
beforeEach(() => {
clearLocaleListeners();
setCurrentLocale('zh-CN');
localStorage.clear();
});
test('getCurrentLocale 默认 zh-CN', () => {
expect(getCurrentLocale()).toBe('zh-CN');
});
test('setCurrentLocale 切换语言', () => {
setCurrentLocale('en-US');
expect(getCurrentLocale()).toBe('en-US');
});
test('setCurrentLocale 未知语言回退到 fallback', () => {
const spy = jest.spyOn(console, 'warn').mockImplementation(() => {});
setCurrentLocale('fr-FR');
expect(spy).toHaveBeenCalled();
expect(getCurrentLocale()).toBe('zh-CN');
spy.mockRestore();
});
test('switchLocale 是 setCurrentLocale 的别名', () => {
switchLocale('en-US');
expect(getCurrentLocale()).toBe('en-US');
});
});
describe('翻译函数 t', () => {
beforeEach(() => {
setCurrentLocale('zh-CN');
});
test('返回当前语言的翻译', () => {
expect(t('bold')).toBe('粗体');
expect(t('italic')).toBe('斜体');
});
test('切换语言后返回新语言翻译', () => {
setCurrentLocale('en-US');
expect(t('bold')).toBe('Bold');
expect(t('italic')).toBe('Italic');
});
test('未知 key 返回 key 本身', () => {
const spy = jest.spyOn(console, 'warn').mockImplementation(() => {});
expect(t('non-existent-key')).toBe('non-existent-key');
spy.mockRestore();
});
test('插值替换占位符', () => {
// 临时添加带占位符的翻译
addTranslations('zh-CN', { greeting: '你好,{name}' });
expect(t('greeting', { name: '爸爸' })).toBe('你好,爸爸!');
});
test('插值未提供占位符值时保留原占位符', () => {
addTranslations('zh-CN', { greeting: '你好,{name}' });
expect(t('greeting', {})).toBe('你好,{name}');
});
test('fallback 语言提供翻译', () => {
// 临时切换到不存在的语言会回退;这里用 en-US 但 key 只在 zh-CN 中
setCurrentLocale('en-US');
// en-US 中也有 bold,所以构造一个只在 zh-CN 中的场景较难,
// 改为直接验证:en-US 切换后 t('bold') 来自 en-US 而非 zh-CN
expect(t('bold')).toBe('Bold');
});
});
describe('翻译查询', () => {
test('hasTranslation 已知 key 返回 true', () => {
setCurrentLocale('zh-CN');
expect(hasTranslation('bold')).toBe(true);
expect(hasTranslation('italic')).toBe(true);
});
test('hasTranslation 未知 key 返回 false', () => {
expect(hasTranslation('non-existent')).toBe(false);
});
test('getTranslations 返回指定语言的翻译对象', () => {
const tr = getTranslations('zh-CN');
expect(tr.bold).toBe('粗体');
expect(tr.italic).toBe('斜体');
});
test('getTranslations 未知语言返回空对象', () => {
expect(getTranslations('fr-FR')).toEqual({});
});
});
describe('添加翻译', () => {
beforeEach(() => {
setCurrentLocale('zh-CN');
});
test('addTranslations 添加新语言', () => {
addTranslations('ja', { bold: '太字' });
expect(LOCALES.ja).toBeDefined();
expect(LOCALES.ja.bold).toBe('太字');
setCurrentLocale('ja');
expect(t('bold')).toBe('太字');
// 清理
delete LOCALES.ja;
});
test('addTranslations 深度合并到已有语言', () => {
addTranslations('zh-CN', { customKey: '自定义值' });
expect(LOCALES['zh-CN'].customKey).toBe('自定义值');
// 原有 key 不受影响
expect(LOCALES['zh-CN'].bold).toBe('粗体');
delete LOCALES['zh-CN'].customKey;
});
});
describe('语言支持查询', () => {
test('getSupportedLocales 返回内置语言', () => {
const locales = getSupportedLocales();
expect(locales).toContain('zh-CN');
expect(locales).toContain('en-US');
});
test('isLocaleSupported 已知语言返回 true', () => {
expect(isLocaleSupported('zh-CN')).toBe(true);
expect(isLocaleSupported('en-US')).toBe(true);
});
test('isLocaleSupported 未知语言返回 false', () => {
expect(isLocaleSupported('fr-FR')).toBe(false);
expect(isLocaleSupported('')).toBe(false);
});
});
describe('语言元信息', () => {
test('getLocaleName 返回语言显示名', () => {
expect(getLocaleName('zh-CN')).toBe('简体中文');
expect(getLocaleName('en-US')).toBe('English (US)');
expect(getLocaleName('ja')).toBe('日本語');
});
test('getLocaleName 未知语言返回语言代码本身', () => {
expect(getLocaleName('unknown')).toBe('unknown');
});
test('getLocaleDirection LTR 语言', () => {
expect(getLocaleDirection('zh-CN')).toBe('ltr');
expect(getLocaleDirection('en-US')).toBe('ltr');
});
test('getLocaleDirection RTL 语言', () => {
expect(getLocaleDirection('ar')).toBe('rtl');
expect(getLocaleDirection('he')).toBe('rtl');
expect(getLocaleDirection('fa')).toBe('rtl');
});
});
describe('格式化函数', () => {
beforeEach(() => {
setCurrentLocale('en-US');
});
test('formatNumber 格式化数字', () => {
const r = formatNumber(1234567);
expect(typeof r).toBe('string');
expect(r).toContain('1');
expect(r).toContain('234');
});
test('formatNumber 接受 options', () => {
const r = formatNumber(0.5, { style: 'percent' });
expect(r).toContain('50');
});
test('formatCurrency 格式化货币', () => {
const r = formatCurrency(99.99, 'USD');
expect(typeof r).toBe('string');
expect(r).toContain('99');
});
test('formatDate 格式化日期', () => {
const r = formatDate('2024-01-15');
expect(typeof r).toBe('string');
// 至少包含年份信息
expect(r.length).toBeGreaterThan(0);
});
test('formatDate 接受 Date 对象', () => {
const r = formatDate(new Date('2024-06-15'));
expect(typeof r).toBe('string');
expect(r.length).toBeGreaterThan(0);
});
test('formatNumber 无效输入回退为字符串', () => {
const r = formatNumber('not-a-number');
expect(typeof r).toBe('string');
});
});
describe('语言监听', () => {
beforeEach(() => {
clearLocaleListeners();
setCurrentLocale('zh-CN');
});
test('addLocaleListener 注册回调', () => {
const fn = jest.fn();
addLocaleListener(fn);
setCurrentLocale('en-US');
expect(fn).toHaveBeenCalledWith('en-US');
});
test('addLocaleListener 返回取消函数', () => {
const fn = jest.fn();
const unsub = addLocaleListener(fn);
expect(typeof unsub).toBe('function');
unsub();
setCurrentLocale('en-US');
expect(fn).not.toHaveBeenCalled();
});
test('removeLocaleListener 移除回调', () => {
const fn = jest.fn();
addLocaleListener(fn);
removeLocaleListener(fn);
setCurrentLocale('en-US');
expect(fn).not.toHaveBeenCalled();
});
test('clearLocaleListeners 清空所有回调', () => {
const fn1 = jest.fn();
const fn2 = jest.fn();
addLocaleListener(fn1);
addLocaleListener(fn2);
clearLocaleListeners();
setCurrentLocale('en-US');
expect(fn1).not.toHaveBeenCalled();
expect(fn2).not.toHaveBeenCalled();
});
test('监听器抛错不影响其他监听器', () => {
const errFn = jest.fn(() => { throw new Error('boom'); });
const okFn = jest.fn();
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
addLocaleListener(errFn);
addLocaleListener(okFn);
setCurrentLocale('en-US');
expect(errFn).toHaveBeenCalled();
expect(okFn).toHaveBeenCalled();
spy.mockRestore();
});
});
describe('语言持久化', () => {
beforeEach(() => {
localStorage.clear();
setCurrentLocale('zh-CN');
});
test('saveLocale 写入 localStorage', () => {
saveLocale('en-US');
expect(localStorage.getItem('metona-editor-locale')).toBe('en-US');
});
test('loadLocale 无保存时返回默认语言', () => {
// 无保存时调用 getDefaultLocale
const r = loadLocale();
expect(typeof r).toBe('string');
expect(r.length).toBeGreaterThan(0);
});
test('loadLocale 读取已保存值', () => {
localStorage.setItem('metona-editor-locale', 'en-US');
expect(loadLocale()).toBe('en-US');
});
test('initI18n 加载已保存的语言', () => {
localStorage.setItem('metona-editor-locale', 'en-US');
initI18n();
expect(getCurrentLocale()).toBe('en-US');
});
});
describe('默认语言探测', () => {
test('getDefaultLocale 返回字符串', () => {
const r = getDefaultLocale();
expect(typeof r).toBe('string');
expect(r.length).toBeGreaterThan(0);
});
test('navigator 不支持时返回 fallback', () => {
const origNav = global.navigator;
Object.defineProperty(global, 'navigator', { value: undefined, configurable: true });
expect(getDefaultLocale()).toBe('zh-CN');
Object.defineProperty(global, 'navigator', { value: origNav, configurable: true });
});
});
describe('fallback 语言', () => {
test('getFallbackLocale 默认 zh-CN', () => {
expect(i18nUtils.getFallbackLocale()).toBe('zh-CN');
});
test('setFallbackLocale 修改 fallback', () => {
i18nUtils.setFallbackLocale('en-US');
expect(i18nUtils.getFallbackLocale()).toBe('en-US');
// 恢复
i18nUtils.setFallbackLocale('zh-CN');
});
});
describe('i18nUtils 代理层', () => {
test('暴露所有 i18n API', () => {
[
't', 'getCurrentLocale', 'setCurrentLocale', 'switchLocale',
'getFallbackLocale', 'setFallbackLocale', 'hasTranslation',
'getTranslations', 'addTranslations', 'getSupportedLocales',
'isLocaleSupported', 'getLocaleName', 'getLocaleDirection',
'formatNumber', 'formatCurrency', 'formatDate',
'addLocaleListener', 'removeLocaleListener', 'clearLocaleListeners',
'initI18n', 'saveLocale', 'loadLocale', 'getDefaultLocale',
].forEach((fn) => {
expect(typeof i18nUtils[fn]).toBe('function');
});
});
test('代理方法与具名导出一致', () => {
expect(i18nUtils.t).toBe(t);
expect(i18nUtils.getCurrentLocale).toBe(getCurrentLocale);
expect(i18nUtils.switchLocale).toBe(switchLocale);
});
});
describe('presetLocales', () => {
test('包含 zh-CN 和 en-US', () => {
expect(presetLocales['zh-CN']).toBeDefined();
expect(presetLocales['en-US']).toBeDefined();
});
test('包含 name / nativeName / direction / translations', () => {
const z = presetLocales['zh-CN'];
expect(z.name).toBeDefined();
expect(z.nativeName).toBeDefined();
expect(z.direction).toBe('ltr');
expect(z.translations).toBe(LOCALES['zh-CN']);
});
});
describe('createI18nManager', () => {
test('返回包含全部方法的管理器对象', () => {
const m = createI18nManager();
expect(typeof m.t).toBe('function');
expect(typeof m.setCurrentLocale).toBe('function');
expect(typeof m.formatNumber).toBe('function');
});
test('管理器方法可独立调用', () => {
const m = createI18nManager();
m.setCurrentLocale('en-US');
expect(m.getCurrentLocale()).toBe('en-US');
expect(m.t('bold')).toBe('Bold');
});
});