文件
OmniCard/backend/test/mpuniapp/i18n-rtl.test.ts

244 行
9.3 KiB
TypeScript

/**
* OmniCard AI (CRMmp) - 访客端多语言动态切换与 RTL 镜像契约测试 (TDD)
* 严格按照 AGENTS.md 与国际化 WCAG 标准规范
*/
import * as fs from 'fs';
import * as path from 'path';
const LOCALE_UTS = path.resolve(__dirname, '../../../mpuniapp/common/locale.uts');
const SELECTOR_UVUE = path.resolve(
__dirname,
'../../../mpuniapp/components/i18n-selector/i18n-selector.uvue'
);
const PORTAL_INDEX = path.resolve(__dirname, '../../../mpuniapp/pages/portal/index.uvue');
describe('Point 04: 访客端多语言动态切换与 RTL 镜像无感适配', () => {
let storageMap: Map<string, string>;
beforeEach(() => {
storageMap = new Map<string, string>();
(global as { uni?: unknown }).uni = {
getStorageSync: (key: string) => storageMap.get(key) || '',
setStorageSync: (key: string, val: string) => storageMap.set(key, val),
getLocale: () => '',
getSystemInfoSync: () => ({ language: '' }),
};
jest.resetModules();
});
afterEach(() => {
delete (global as { uni?: unknown }).uni;
jest.resetModules();
});
describe('1. 7 种主流语言映射与解析 (resolveAppLocale / normalizeLocale)', () => {
it('正确将各变体语言映射到 7 大主流标准语种', () => {
const { resolveAppLocale, normalizeLocale } = require(LOCALE_UTS) as {
resolveAppLocale: (raw: string, fallback?: string) => string;
normalizeLocale?: (raw: string) => string;
};
const resolve = resolveAppLocale || normalizeLocale;
expect(typeof resolve).toBe('function');
// zh 族系 -> zh-CN
expect(resolve('zh')).toBe('zh-CN');
expect(resolve('zh-CN')).toBe('zh-CN');
expect(resolve('zh-Hans')).toBe('zh-CN');
expect(resolve('zh-TW')).toBe('zh-CN');
expect(resolve('zh-HK')).toBe('zh-CN');
// en 族系 -> en-US
expect(resolve('en')).toBe('en-US');
expect(resolve('en-US')).toBe('en-US');
expect(resolve('en-GB')).toBe('en-US');
// ar 族系 -> ar-SA
expect(resolve('ar')).toBe('ar-SA');
expect(resolve('ar-SA')).toBe('ar-SA');
expect(resolve('ar-EG')).toBe('ar-SA');
expect(resolve('ar-AE')).toBe('ar-SA');
// es 族系 -> es-ES
expect(resolve('es')).toBe('es-ES');
expect(resolve('es-ES')).toBe('es-ES');
expect(resolve('es-MX')).toBe('es-ES');
// de 族系 -> de-DE
expect(resolve('de')).toBe('de-DE');
expect(resolve('de-DE')).toBe('de-DE');
expect(resolve('de-AT')).toBe('de-DE');
// fr 族系 -> fr-FR
expect(resolve('fr')).toBe('fr-FR');
expect(resolve('fr-FR')).toBe('fr-FR');
expect(resolve('fr-CA')).toBe('fr-FR');
// ja 族系 -> ja-JP
expect(resolve('ja')).toBe('ja-JP');
expect(resolve('ja-JP')).toBe('ja-JP');
// 未知语种回退 en-US
expect(resolve('ko')).toBe('en-US');
expect(resolve('ru')).toBe('en-US');
expect(resolve('')).toBe('en-US');
expect(resolve('unknown-LANG')).toBe('en-US');
});
it('7 种主流语言在词典中均完整注册', () => {
const { omniLocales } = require(LOCALE_UTS) as {
omniLocales: Record<string, { code: string; name: string; dir: string }>;
};
const expectedCodes = ['zh-CN', 'en-US', 'ar-SA', 'es-ES', 'de-DE', 'fr-FR', 'ja-JP'];
for (const code of expectedCodes) {
expect(omniLocales[code]).toBeDefined();
expect(omniLocales[code].code).toBe(code);
expect(typeof omniLocales[code].name).toBe('string');
expect(omniLocales[code].name.length).toBeGreaterThan(0);
expect(['ltr', 'rtl']).toContain(omniLocales[code].dir);
}
});
});
describe('2. RTL 判定逻辑 (isRtlLocale)', () => {
it('对 ar-SA, he-IL, fa-IR, ur-PK 等 RTL 语种准确判定为 true', () => {
const { isRtlLocale } = require(LOCALE_UTS) as {
isRtlLocale: (lang: string) => boolean;
};
expect(isRtlLocale('ar-SA')).toBe(true);
expect(isRtlLocale('ar')).toBe(true);
expect(isRtlLocale('ar-EG')).toBe(true);
expect(isRtlLocale('he-IL')).toBe(true);
expect(isRtlLocale('he')).toBe(true);
expect(isRtlLocale('fa-IR')).toBe(true);
expect(isRtlLocale('fa')).toBe(true);
expect(isRtlLocale('ur-PK')).toBe(true);
expect(isRtlLocale('ur')).toBe(true);
});
it('对 LTR 语种准确判定为 false', () => {
const { isRtlLocale } = require(LOCALE_UTS) as {
isRtlLocale: (lang: string) => boolean;
};
expect(isRtlLocale('zh-CN')).toBe(false);
expect(isRtlLocale('en-US')).toBe(false);
expect(isRtlLocale('es-ES')).toBe(false);
expect(isRtlLocale('de-DE')).toBe(false);
expect(isRtlLocale('fr-FR')).toBe(false);
expect(isRtlLocale('ja-JP')).toBe(false);
expect(isRtlLocale('')).toBe(false);
expect(isRtlLocale('ko-KR')).toBe(false);
});
});
describe('3. 动态切换与本地存储记忆 (setAppLocale / getCurrentLocale / appLocaleState)', () => {
it('setAppLocale 能正确持久化到本地存储并同步全局状态', () => {
const { setAppLocale, getCurrentLocale, appLocaleState } = require(LOCALE_UTS) as {
setAppLocale: (lang: string) => void;
getCurrentLocale: () => string;
appLocaleState: { current: string; isRtl: boolean; dir: string };
};
setAppLocale('de-DE');
expect(getCurrentLocale()).toBe('de-DE');
expect(storageMap.get('omni_locale')).toBe('de-DE');
expect(appLocaleState.current).toBe('de-DE');
expect(appLocaleState.isRtl).toBe(false);
expect(appLocaleState.dir).toBe('ltr');
// 切换至 RTL 语种
setAppLocale('ar-SA');
expect(getCurrentLocale()).toBe('ar-SA');
expect(storageMap.get('omni_locale')).toBe('ar-SA');
expect(appLocaleState.current).toBe('ar-SA');
expect(appLocaleState.isRtl).toBe(true);
expect(appLocaleState.dir).toBe('rtl');
});
it('从本地存储恢复上一次记忆的语言', () => {
storageMap.set('omni_locale', 'fr-FR');
const { getCurrentLocale, appLocaleState, initAppLocale } = require(LOCALE_UTS) as {
getCurrentLocale: () => string;
appLocaleState: { current: string; isRtl: boolean };
initAppLocale?: () => string;
};
if (typeof initAppLocale === 'function') {
initAppLocale();
}
expect(getCurrentLocale()).toBe('fr-FR');
expect(appLocaleState.current).toBe('fr-FR');
expect(appLocaleState.isRtl).toBe(false);
});
});
describe('4. 默认 GeoIP 语言回退 (resolveGeoLocale)', () => {
it('根据访客的国家码准确映射主流语言或回退 en-US', () => {
const { resolveGeoLocale } = require(LOCALE_UTS) as {
resolveGeoLocale: (countryCode?: string, fallback?: string) => string;
};
expect(resolveGeoLocale('CN')).toBe('zh-CN');
expect(resolveGeoLocale('HK')).toBe('zh-CN');
expect(resolveGeoLocale('TW')).toBe('zh-CN');
expect(resolveGeoLocale('SA')).toBe('ar-SA');
expect(resolveGeoLocale('AE')).toBe('ar-SA');
expect(resolveGeoLocale('EG')).toBe('ar-SA');
expect(resolveGeoLocale('ES')).toBe('es-ES');
expect(resolveGeoLocale('MX')).toBe('es-ES');
expect(resolveGeoLocale('DE')).toBe('de-DE');
expect(resolveGeoLocale('AT')).toBe('de-DE');
expect(resolveGeoLocale('FR')).toBe('fr-FR');
expect(resolveGeoLocale('JP')).toBe('ja-JP');
// 其余地区与空值回退 en-US
expect(resolveGeoLocale('US')).toBe('en-US');
expect(resolveGeoLocale('GB')).toBe('en-US');
expect(resolveGeoLocale('SG')).toBe('en-US');
expect(resolveGeoLocale('')).toBe('en-US');
expect(resolveGeoLocale(undefined)).toBe('en-US');
});
});
describe('5. 组件与微官网 RTL 适配契约', () => {
it('i18n-selector.uvue 存在且包含 7 种主流语言与触控无障碍尺寸', () => {
expect(fs.existsSync(SELECTOR_UVUE)).toBe(true);
const code = fs.readFileSync(SELECTOR_UVUE, 'utf8');
// 验证 7 大语种标识
expect(code.includes('zh-CN')).toBe(true);
expect(code.includes('en-US')).toBe(true);
expect(code.includes('ar-SA')).toBe(true);
expect(code.includes('es-ES')).toBe(true);
expect(code.includes('de-DE')).toBe(true);
expect(code.includes('fr-FR')).toBe(true);
expect(code.includes('ja-JP')).toBe(true);
// 验证调用或引入 setAppLocale
expect(code.includes('setAppLocale')).toBe(true);
// 约束:严禁包含任何 emoji
const emojiRegex = /[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/u;
expect(emojiRegex.test(code)).toBe(false);
// 满足触控区 >= 44x44
expect(code.includes('44px') || code.includes('min-height: 44') || code.includes('48px')).toBe(true);
});
it('index.uvue 正确引入 i18n-selector 并支持 RTL 根容器适配', () => {
const code = fs.readFileSync(PORTAL_INDEX, 'utf8');
expect(code.includes('i18n-selector') || code.includes('I18nSelector')).toBe(true);
expect(code.includes('direction: rtl') || code.includes('portal-rtl')).toBe(true);
expect(code.includes('isRtl')).toBe(true);
// 约束:严禁包含 emoji
const emojiRegex = /[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/u;
expect(emojiRegex.test(code)).toBe(false);
});
});
});