服务端阅读 06月1日 09:19
i18next 国际化怎么测试?翻译缺失和语言切换如何验证?
测试 i18next 分三层:翻译函数本身、React 组件集成、边界情况(缺失 key、懒加载)。翻译函数测试最简单——初始化一个独立的 i18next 实例,注入 mock 翻译资源,断言 t('key') 的返回值,覆盖简单 key、插值({{name}})、复数形式。React 组件测试需要用 I18nextProvider 包裹被测组件,传入配置好的 i18n 实例,这样组件内的 useTranslation 和 Trans 才能正常工作。语言切换测试调用 i18n.changeLanguage() 后用 waitFor 断言 UI 文案变化。缺失 key 测试开启 saveMissing: true,配合 missingKeyHandler 断言回调被调用,或直接断言 t('nonexistent') 返回 key 本身作为 fallback。追问为什么不用真实的翻译文件跑测试,而要 mock 资源?真实翻译文件会变——翻译团队随时可能改文案,如果测试断言了具体翻译文本,每次文案调整测试就挂了。mock 资源让测试只关心"key 能正确解析"这个逻辑,不绑定具体文案内容。另外 mock 资源体积小、加载快,避免测试里引入大量 JSON 文件。最佳实践:mock 资源只放测试需要的 key,用 i18next.createInstance() 隔离,避免测试间互相污染。Trans 组件和 t() 函数测试有什么区别?t() 是纯函数,输入 key 和参数返回字符串,测试最简单。Trans 组件渲染包含 HTML 标签的翻译内容(如 Welcome <1>{{name}}</1>),需要验证组件嵌套是否正确,不能只断言文本内容——Trans 可能渲染出 <strong>John</strong> 也可能渲染出纯文本 John,取决于翻译 key 的定义。测试 Trans 时要用 container.innerHTML 或 within 查询,确认标签结构正确,而不仅是文本存在。命名空间怎么测试?懒加载命名空间呢?命名空间测试关键是每个命名空间独立初始化资源,断言 t('ns:key') 能正确解析跨命名空间 key。懒加载命名空间测试用 useSuspense: false 模式——组件先渲染 loading 状态(ready 为 false),然后通过 addResourceBundle 手动注入资源,用 waitFor 断言组件渲染出翻译内容。注意懒加载测试不要用 Suspense 包裹,否则 React Testing Library 无法捕获 loading 态。快照测试适合 i18next 吗?有什么坑?不太适合。快照会把翻译文案写死到 .snap 文件里,翻译改了快照就挂,维护成本高。而且快照只能告诉你"渲染结果和之前一样",不能告诉你翻译是否正确。如果一定要用,只对组件结构做快照(用 render 后取 asFragment()),不要断言具体翻译文本。更好的替代方案是用 t() 的 mock 验证调用参数是否正确,而不是验证返回值。// 独立实例 + mock 资源的翻译测试import i18next from 'i18next';describe('i18next translations', () => { let i18n; beforeEach(() => { i18n = i18next.createInstance(); i18n.init({ lng: 'en', resources: { en: { translation: { hello: 'Hello', greet: 'Hi {{name}}' } }, zh: { translation: { hello: '你好', greet: '你好 {{name}}' } } } }); }); test('简单 key', () => expect(i18n.t('hello')).toBe('Hello')); test('插值', () => expect(i18n.t('greet', { name: 'Li' })).toBe('Hi Li')); test('缺失 key 返回 key 本身', () => expect(i18n.t('missing')).toBe('missing'));});// React 组件测试(I18nextProvider 包裹)import { I18nextProvider } from 'react-i18next';import { render, screen, waitFor } from '@testing-library/react';function Greeting() { const { t, i18n } = useTranslation(); return <> <span>{t('hello')}</span> <button onClick={() => i18n.changeLanguage('zh')}>切换</button> </>;}test('语言切换', async () => { render(<I18nextProvider i18n={i18n}><Greeting /></I18nextProvider>); expect(screen.getByText('Hello')).toBeInTheDocument(); fireEvent.click(screen.getByText('切换')); await waitFor(() => expect(screen.getByText('你好')).toBeInTheDocument());});