测试(Vitest)

在 vitest.config.ts 中使用 aiI18nVitest(),即可测试导入 virtual:ai-i18n 的业务模块,无需 手写 alias 或 mock。测试环境不执行自动翻译,也不会修改项目中的翻译文件。

如果项目曾为 virtual:ai-i18n 添加 alias 或 vi.mock(),请先删除。手写 mock 会替换 aiI18nVitest() 提供的整个测试 Runtime;即使配置文件已经注册插件,测试实际调用的仍可能是 mock。数组被拼成字符串、对象结构丢失或数组下标只返回一个字符时,应优先检查测试 setup 中的 vi.mock('virtual:ai-i18n')。

如果 Vitest 输出“仅支持浏览器 Runtime,已跳过 SSR 转换/注入”,说明测试配置加载了生产用的 aiI18n()。不要忽略或静默过滤该提示;从 @ai-i18n/vite/vitest 改用 aiI18nVitest(),并确保同一测试配置只注册其中一个插件。

它支持 t()、useI18n()、Vue tRef()、纯 Options 的 i18nComputed() / tComputed()、消息集合宏和自动导入。测试配置的 autoImport 应与正式 Vite 配置保持一致。 Jest 或直接 Node.js 执行不会处理消息集合宏,请改用 Vitest 配置或避免执行包含宏的源码。

快速开始

// vitest.config.ts
import { aiI18nVitest } from '@ai-i18n/vite/vitest';
import { defineConfig } from 'vitest/config';

export default defineConfig({
  plugins: [
    aiI18nVitest({
      sourceLang: 'zh-CN',
      locales: [
        { value: 'zh-CN', label: '中文' },
        { value: 'en-US', label: 'English' },
      ],
      autoImport: true, // 与正式 Vite 配置保持一致
    }),
  ],
});
项目额外配置
Vanilla JS不需要框架插件。
Vue保留 @vitejs/plugin-vue。
React保留 @vitejs/plugin-react。

大多数项目不需要设置 framework;只有自定义构建环境无法识别框架时,才显式传入该选项。

与正式配置共享 options

AiI18nVitestOptions 是 AiI18nOptions 的子集,只保留 sourceLang、defaultLang、 locales、framework、persist、autoImport。把这部分抽成共享文件,vite.config.ts 与 vitest.config.ts 各自引用,语言列表和运行时策略只需要改一处:

// ai-i18n.options.ts
export const aiI18nOptions = {
  sourceLang: 'zh-CN', // 源码文案使用的语言
  defaultLang: 'zh-CN', // 没有有效持久化值时的初始语言
  // value 是语言标识,label 是组件中展示的名称。
  locales: [
    { value: 'zh-CN', label: '中文' },
    { value: 'en-US', label: 'English' },
  ],
  persist: { key: 'app-lang' }, // 使用指定 localStorage key 保存语言偏好
  autoImport: true, // 两种环境使用同一模式化自动导入契约
} as const;
// vite.config.ts
import { aiI18n } from '@ai-i18n/vite';
import { aiI18nOptions } from './ai-i18n.options';

export default defineConfig({
  plugins: [
    aiI18n(aiI18nOptions),
    react(), // 保留宿主 React 转换
  ],
});
// vitest.config.ts
import { aiI18nVitest } from '@ai-i18n/vite/vitest';
import { aiI18nOptions } from './ai-i18n.options';

export default defineConfig({
  plugins: [
    aiI18nVitest(aiI18nOptions),
    react(), // 保留宿主 React 转换
  ],
});

html、loading、cache、provider、directory、dts 等构建期字段不属于 AiI18nVitestOptions;直接把完整的 aiI18n() 配置对象传给 aiI18nVitest() 会被 TypeScript 拒绝多余字段,按需只挑测试需要的子集传入即可。

测试环境的能力范围

能力测试环境行为
t(source) / t`...`可用。字符串叶子固定返回 source 文案;数组和普通对象会递归处理并保持原结构。
setLang(value)可用,可用于测试语言切换触发的重渲染,以及 persist 写入 localStorage。
useI18n()Vue / React 模式下可用;字段契约一致,但测试 Runtime 的语言加载状态固定为 idle。
tRef()仅 Vue 模式可用;返回响应式 source fallback Ref,与生产环境保持相同 API 形状。
i18nComputed()仅 Vue 模式可用;返回与生产一致的 Options computed 配置。
tComputed()仅 Vue 模式可用;返回 source fallback 的 Options computed getter。
自动导入仅在 autoImport: true 时注入,API 集合与同一框架模式的生产插件一致。
翻译文件与自动翻译不会读取、修改或调用。
Provider / AI 自动翻译不会调用;provider 不属于 AiI18nVitestOptions。

由于没有加载任何目标语言译文,t('保存') 始终返回 "保存",即使调用过 await setLang('en-US') 之后也是如此。可以把这类测试理解为契约测试——只断言组件确实调用了 t() 并渲染出结果,不断言具体译文内容。

文案树遵循同一规则,但不会退化为字符串:

expect(t(['安静', { noun: '海豚' }])).toEqual(['安静', { noun: '海豚' }]);

loading 不属于 AiI18nVitestOptions,测试 Runtime 不创建语言 chunk loader。 getLangLoadState() 以及 useI18n() 的 loading/error 字段仍存在,但固定为 idle;真实 loading/error 变化应由应用自己的抽象单测或生产 Vite 集成测试覆盖。

单元测试不应断言某个语言一定翻译为特定字词。译文质量通过人工校对或 AI 翻译流程确认;CI 可以额外运行一次真实的 vite build。

常见问题

测试环境需要先跑过 vite build 或存在翻译记忆吗? 不需要。测试插件不依赖项目中的翻译文件,也能正常解析 virtual:ai-i18n。

能不能在同一个 vitest.config.ts 里同时注册 aiI18n() 和 aiI18nVitest()? 不要这样做。两者都会尝试解析 virtual:ai-i18n,只注册 aiI18nVitest() 即可。

为什么 t(messages) 返回了字符串,数组下标只剩一个字符? 官方测试 Runtime 会保持数组和对象结构。请搜索测试 setup、共享测试工具和 Vitest alias,删除对 virtual:ai-i18n 的手写替换。只保留 aiI18nVitest() 后再运行最小文案树用例。

端到端测试(Playwright 等)呢? aiI18nVitest() 只覆盖 Vitest 场景。端到端测试应运行真实的 vite dev 或 vite build, 以覆盖实际的语言切换和翻译结果。