import { describe, it, expect } from 'vitest' import zhCN from '../../locales/zh-CN' import zhTW from '../../locales/zh-TW' import en from '../../locales/en' import ja from '../../locales/ja' import ko from '../../locales/ko' import de from '../../locales/de' import fr from '../../locales/fr' import es from '../../locales/es' import pt from '../../locales/pt' import { i18n, I18N_OPTIONS } from '../../locales' // 递归收集所有键路径 function collectKeys(obj: Record, prefix = ''): string[] { const keys: string[] = [] for (const [key, value] of Object.entries(obj)) { const fullKey = prefix ? `${prefix}.${key}` : key if (typeof value === 'object' && value !== null && !Array.isArray(value)) { keys.push(...collectKeys(value, fullKey)) } else { keys.push(fullKey) } } return keys.sort() } // 递归收集键路径及其值,用于比较值是否为空 function collectKeysWithValues(obj: Record, prefix = ''): Map { const result = new Map() for (const [key, value] of Object.entries(obj)) { const fullKey = prefix ? `${prefix}.${key}` : key if (typeof value === 'object' && value !== null && !Array.isArray(value)) { const nested = collectKeysWithValues(value, fullKey) nested.forEach((v, k) => result.set(k, v)) } else { result.set(fullKey, String(value)) } } return result } const zhCNKeys = collectKeys(zhCN) const localeModules = { 'zh-TW': zhTW, 'en': en, 'ja': ja, 'ko': ko, 'de': de, 'fr': fr, 'es': es, 'pt': pt, } as const describe('TC-I18N-001~009: 国际化完整性验证', () => { for (const [locale, module] of Object.entries(localeModules)) { const localeName = { 'zh-TW': '繁体中文', 'en': '英文', 'ja': '日文', 'ko': '韩文', 'de': '德文', 'fr': '法文', 'es': '西班牙文', 'pt': '葡萄牙文', }[locale] describe(`${locale} (${localeName})`, () => { it(`应包含所有 zh-CN 中的键`, () => { const localeKeys = collectKeys(module) const missingKeys = zhCNKeys.filter(k => !localeKeys.includes(k)) if (missingKeys.length > 0) { console.warn(`[${locale}] 缺少以下键:`, missingKeys) } expect(missingKeys).toEqual([]) }) it(`不应包含多余的键`, () => { const localeKeys = collectKeys(module) const extraKeys = localeKeys.filter(k => !zhCNKeys.includes(k)) if (extraKeys.length > 0) { console.warn(`[${locale}] 存在额外键:`, extraKeys) } expect(extraKeys).toEqual([]) }) it(`所有键的值不应为空字符串`, () => { const values = collectKeysWithValues(module) const emptyValues: string[] = [] values.forEach((value, key) => { if (value.trim() === '') emptyValues.push(key) }) expect(emptyValues).toEqual([]) }) it(`应包含正确数量的键 (${zhCNKeys.length} 个)`, () => { const localeKeys = collectKeys(module) expect(localeKeys.length).toBe(zhCNKeys.length) }) }) } }) describe('TC-I18N-010: i18n 配置验证(防 P0 回归)', () => { it('createI18n 应启用 globalInjection,确保小程序端模板 $t 可用', () => { // legacy:false 模式下 globalInjection 必须为 true, // 否则小程序端模板渲染报 "TypeError: a.$t is not a function",全部页面异常 expect(I18N_OPTIONS.globalInjection).toBe(true) }) it('createI18n 应使用 Composition API(legacy: false)', () => { expect(I18N_OPTIONS.legacy).toBe(false) expect((i18n as any).mode).toBe('composition') }) it('app.use(i18n) 后应在 globalProperties 注入 $t(小程序端模板可用)', async () => { const { createSSRApp } = await import('vue') const app = createSSRApp({ render: () => null }) app.use(i18n) // globalInjection:true 会将 $t/$rt 注入 Vue 全局属性,供模板/render 使用 const globalProperties = (app as any).config.globalProperties expect(globalProperties).toBeDefined() expect(typeof globalProperties.$t).toBe('function') expect(typeof globalProperties.$rt).toBe('function') }) it('Messages Functions 应正确插值(uni-app 小程序端不支持 {var} 字符串插值,必须用函数)', () => { i18n.global.locale.value = 'zh-CN' // 小程序端 {count} 字符串插值失效,函数形式是官方兼容方案 expect(i18n.global.t('search.daysUnit', { count: 30 })).toBe('30天') expect(i18n.global.t('search.resultCount', { count: 20 })).toBe('共20条结果') expect(i18n.global.t('fortune.overallScore', { score: 55 })).toBe('综合运势:55分') expect(i18n.global.t('search.dateFormat', { year: 2026, month: 8, day: 18 })).toBe('2026年8月18日') expect(i18n.global.t('search.historyConditionDays', { count: 2, days: 30 })).toBe('2个条件,30天范围') }) })