refactor: 迁移 i18n 系统从 react-i18next 到 chrome.i18n

- 移除 react-i18next、i18next 及相关依赖
- 删除旧的 i18n/ 目录和 useLazyTranslation 工具
- 新增 utils/chromeI18n.ts 类型安全 wrapper(useI18n Hook + getMessage)
- 生成 public/_locales/{zh,en}/messages.json(298 个翻译 key)
- 批量更新 39+ 组件文件的导入和翻译调用
- 转换翻译键格式:namespace:key → namespace_key
- 修复 ErrorBoundary/PageErrorBoundary 从 withTranslation HOC 改为直接调用 getMessage
- 更新 vitest.setup.ts mock 加载实际翻译文本
- 修复 11 个测试文件的断言以匹配中文翻译
- TypeScript、ESLint、547 项测试全部通过

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
雨霖铃
2026-05-28 19:21:54 +08:00
parent 64e23994a6
commit aa23a263fe
94 changed files with 2987 additions and 1844 deletions
@@ -40,7 +40,7 @@ describe('DecodeResultPaper 组件', () => {
it('应渲染下载按钮', () => {
render(<DecodeResultPaper {...defaultProps} />);
expect(screen.getByRole('button', { name: 'base64Converter:download' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '下载' })).toBeInTheDocument();
});
it('应渲染 children 内容', () => {
@@ -64,7 +64,7 @@ describe('DecodeResultPaper 组件', () => {
it('点击下载按钮时应调用 onDownload', () => {
render(<DecodeResultPaper {...defaultProps} />);
fireEvent.click(screen.getByRole('button', { name: 'base64Converter:download' }));
fireEvent.click(screen.getByRole('button', { name: '下载' }));
expect(defaultProps.onDownload).toHaveBeenCalledTimes(1);
});
});
@@ -72,12 +72,12 @@ describe('DecodeResultPaper 组件', () => {
describe('按钮状态', () => {
it('文件名为空时下载按钮应禁用', () => {
render(<DecodeResultPaper {...{ ...defaultProps, fileName: '' }} />);
expect(screen.getByRole('button', { name: 'base64Converter:download' })).toBeDisabled();
expect(screen.getByRole('button', { name: '下载' })).toBeDisabled();
});
it('文件名不为空时下载按钮应启用', () => {
render(<DecodeResultPaper {...defaultProps} />);
expect(screen.getByRole('button', { name: 'base64Converter:download' })).toBeEnabled();
expect(screen.getByRole('button', { name: '下载' })).toBeEnabled();
});
});
});
+3 -15
View File
@@ -7,18 +7,6 @@ const storageOnChangedMock = { addListener: vi.fn(), removeListener: vi.fn() };
(globalThis as any).chrome = { storage: { onChanged: storageOnChangedMock } };
(globalThis as any).browser = { storage: { onChanged: storageOnChangedMock } };
// 💡 1. 规范对齐:挂载标准的 react-i18next 统一桩函数,防止多进程前缀破产
vi.mock('react-i18next', () => ({
useTranslation: vi.fn((ns: string | string[]) => {
const nsArray = Array.isArray(ns) ? ns : [ns];
return {
t: (key: string) => `${nsArray.join(',')}:${key}`,
i18n: { language: 'en' },
ready: true,
};
}),
}));
// 模拟 URL API
const mockCreateObjectURL = vi.fn();
const mockRevokeObjectURL = vi.fn();
@@ -62,8 +50,8 @@ describe('ImageUploader 组件', () => {
it('当没有选中文件时应显示上传提示', () => {
render(<ImageUploader {...defaultProps} />);
// 💡 修复点 2:全面切换为高弹性正则,斩断双重命名空间死锁!
expect(screen.getByText(/clickToUpload/)).toBeInTheDocument();
expect(screen.getByText(/supportFormats/)).toBeInTheDocument();
expect(screen.getByText(/点击.*拖拽/)).toBeInTheDocument();
expect(screen.getByText(/格式/)).toBeInTheDocument();
});
it('当没有选中文件时应显示 ImageIcon', () => {
@@ -78,7 +66,7 @@ describe('ImageUploader 组件', () => {
);
expect(screen.getByText('test.png')).toBeInTheDocument();
// 💡 修复点 3(自愈第 62 行崩溃位置):利用正则模糊命中,彻底通过!
expect(screen.getByText(/clickToChange/)).toBeInTheDocument();
expect(screen.getByText(/点击更换/)).toBeInTheDocument();
});
it('当选中文件时应显示预览图片', () => {
+7 -23
View File
@@ -2,19 +2,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { fireEvent, render, screen } from '@testing-library/react';
import QrCodePreview from '@/components/QrCodePreview';
// 💡 1. 规范对齐:在这个测试文件的头部同样挂载统一的 react-i18next 桩函数,
// 与你整个工程的国际化解耦架构完美闭环。
vi.mock('react-i18next', () => ({
useTranslation: vi.fn((ns: string | string[]) => {
const nsArray = Array.isArray(ns) ? ns : [ns];
return {
t: (key: string) => `${nsArray.join(',')}:${key}`,
i18n: { language: 'en' },
ready: true,
};
}),
}));
describe('QrCodePreview 组件', () => {
const mockOnDownload = vi.fn();
const mockOnCopy = vi.fn();
@@ -32,7 +19,7 @@ describe('QrCodePreview 组件', () => {
it('当 qrCodeDataUrl 为空时应显示占位文本', () => {
render(<QrCodePreview qrCodeDataUrl="" onDownload={mockOnDownload} onCopy={mockOnCopy} />);
// 💡 修复点 2:全面拥抱柔性正则匹配,直接终结多层 'qrCode:qrCode:' 前缀踩踏!
expect(screen.getByText(/qrCodeWillShow/)).toBeInTheDocument();
expect(screen.getByText(/二维码将显示/)).toBeInTheDocument();
});
it('当 qrCodeDataUrl 有值时应显示二维码图片', () => {
@@ -58,7 +45,7 @@ describe('QrCodePreview 组件', () => {
/>,
);
// 💡 修复点 3:切换为正则,无缝过检
expect(screen.getByText(/downloadButton/)).toBeInTheDocument();
expect(screen.getByText(/下载二维码/)).toBeInTheDocument();
});
it('当 qrCodeDataUrl 有值时应显示复制按钮', () => {
@@ -69,14 +56,13 @@ describe('QrCodePreview 组件', () => {
onCopy={mockOnCopy}
/>,
);
// 💡 修复点 4:切换为正则,无缝过检
expect(screen.getByText(/copyQrButton/)).toBeInTheDocument();
expect(screen.getByText(/复制二维码/)).toBeInTheDocument();
});
it('当 qrCodeDataUrl 为空时不应显示操作按钮', () => {
render(<QrCodePreview qrCodeDataUrl="" onDownload={mockOnDownload} onCopy={mockOnCopy} />);
expect(screen.queryByText(/downloadButton/)).not.toBeInTheDocument();
expect(screen.queryByText(/copyQrButton/)).not.toBeInTheDocument();
expect(screen.queryByText(/下载二维码/)).not.toBeInTheDocument();
expect(screen.queryByText(/复制二维码/)).not.toBeInTheDocument();
});
});
@@ -89,8 +75,7 @@ describe('QrCodePreview 组件', () => {
onCopy={mockOnCopy}
/>,
);
// 💡 修复点 5:点击行为同步更改为正则匹配定位,保障状态修改流一帧直达
fireEvent.click(screen.getByText(/downloadButton/));
fireEvent.click(screen.getByText(/下载二维码/));
expect(mockOnDownload).toHaveBeenCalledTimes(1);
});
@@ -102,8 +87,7 @@ describe('QrCodePreview 组件', () => {
onCopy={mockOnCopy}
/>,
);
// 💡 修复点 6:彻底修复第 88 行报错位置,改用正则解开死锁!
fireEvent.click(screen.getByText(/copyQrButton/));
fireEvent.click(screen.getByText(/复制二维码/));
expect(mockOnCopy).toHaveBeenCalledTimes(1);
});
});
@@ -9,18 +9,6 @@ const storageOnChangedMock = { addListener: vi.fn(), removeListener: vi.fn() };
(globalThis as any).chrome = { storage: { onChanged: storageOnChangedMock } };
(globalThis as any).browser = { storage: { onChanged: storageOnChangedMock } };
// 💡 2. 对齐 react-i18next 的分布式国际化桩
vi.mock('react-i18next', () => ({
useTranslation: vi.fn((ns: string | string[]) => {
const nsArray = Array.isArray(ns) ? ns : [ns];
return {
t: (key: string) => `${nsArray.join(',')}:${key}`,
i18n: { language: 'en' },
ready: true,
};
}),
}));
describe('StorageCleanerConfirm 组件', () => {
const mockOnClose = vi.fn();
const mockOnConfirm = vi.fn();
@@ -55,26 +43,26 @@ describe('StorageCleanerConfirm 组件', () => {
renderComponent();
// 💡 修复点 3:拥抱模糊正则断言。
// 彻底终结由于 i18n 桩引起的 'storageCleaner:storageCleaner:' 双重前缀硬编码堆叠,100% 自愈放行!
expect(screen.getByText(/confirmTitle/)).toBeInTheDocument();
expect(screen.getByRole('heading', { name: /确认清理/ })).toBeInTheDocument();
});
it('应显示警告信息', () => {
renderComponent();
expect(screen.getByText(/irreversible/i)).toBeInTheDocument();
expect(screen.getByText(/不可撤销/)).toBeInTheDocument();
});
it('应将选中的选项显示为标签', () => {
renderComponent();
expect(screen.getByText(/options\.localStorage/)).toBeInTheDocument();
expect(screen.getByText(/options\.sessionStorage/)).toBeInTheDocument();
expect(screen.getByText(/options\.cookies/)).toBeInTheDocument();
expect(screen.getByText(/Local Storage/)).toBeInTheDocument();
expect(screen.getByText(/Session Storage/)).toBeInTheDocument();
expect(screen.getByText(/Cookies/)).toBeInTheDocument();
});
it('应显示取消和确认按钮', () => {
renderComponent();
// 💡 修复点 4:按钮的 Accessible Name 匹配同步切回高弹性正则模式,抵抗一切国际化双前缀污染
expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /confirmAction/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /取消/ })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /确认清理/ })).toBeInTheDocument();
});
});
@@ -82,7 +70,7 @@ describe('StorageCleanerConfirm 组件', () => {
it('点击取消时应调用 onClose', () => {
renderComponent();
fireEvent.click(screen.getByRole('button', { name: /cancel/i }));
fireEvent.click(screen.getByRole('button', { name: /取消/ }));
expect(mockOnClose).toHaveBeenCalledTimes(1);
expect(mockOnConfirm).not.toHaveBeenCalled();
});
@@ -90,7 +78,7 @@ describe('StorageCleanerConfirm 组件', () => {
it('点击确认时应调用 onConfirm', () => {
renderComponent();
fireEvent.click(screen.getByRole('button', { name: /confirmAction/i }));
fireEvent.click(screen.getByRole('button', { name: /确认清理/ }));
expect(mockOnConfirm).toHaveBeenCalledTimes(1);
expect(mockOnClose).not.toHaveBeenCalled();
});
@@ -109,10 +97,10 @@ describe('StorageCleanerConfirm 组件', () => {
renderComponent({ options: partialOptions });
expect(screen.getByText(/options\.localStorage/)).toBeInTheDocument();
expect(screen.getByText(/options\.indexedDB/)).toBeInTheDocument();
expect(screen.queryByText(/options\.sessionStorage/)).not.toBeInTheDocument();
expect(screen.queryByText(/options\.cookies/)).not.toBeInTheDocument();
expect(screen.getByText(/Local Storage/)).toBeInTheDocument();
expect(screen.getByText(/IndexedDB/)).toBeInTheDocument();
expect(screen.queryByText(/Session Storage/)).not.toBeInTheDocument();
expect(screen.queryByText(/Cookies$/)).not.toBeInTheDocument();
});
it('应处理空选项', () => {
@@ -135,7 +123,7 @@ describe('StorageCleanerConfirm 组件', () => {
describe('对话框行为测试', () => {
it('open 为 false 时不应渲染', () => {
renderComponent({ open: false });
expect(screen.queryByText(/confirmTitle/)).not.toBeInTheDocument();
expect(screen.queryByRole('heading', { name: /确认清理/ })).not.toBeInTheDocument();
});
it('应使用不同选项渲染', () => {
@@ -150,8 +138,8 @@ describe('StorageCleanerConfirm 组件', () => {
renderComponent({ options: customOptions });
expect(screen.getByText(/options\.sessionStorage/)).toBeInTheDocument();
expect(screen.getByText(/options\.cookies/)).toBeInTheDocument();
expect(screen.getByText(/Session Storage/)).toBeInTheDocument();
expect(screen.getByText(/Cookies/)).toBeInTheDocument();
});
});
});
+5 -5
View File
@@ -55,18 +55,18 @@ describe('TopBar 组件', () => {
it('不在 dashboard 时应渲染返回按钮', () => {
mockRouterValue.currentPage = 'timestamp';
renderWithProvider(<TopBar onOpenOptions={vi.fn()} />);
expect(screen.getByLabelText('common:buttons.back')).toBeInTheDocument();
expect(screen.getByLabelText('返回')).toBeInTheDocument();
});
it('在 dashboard 上不应渲染返回按钮', () => {
mockRouterValue.currentPage = 'dashboard';
renderWithProvider(<TopBar onOpenOptions={vi.fn()} />);
expect(screen.queryByLabelText('common:buttons.back')).not.toBeInTheDocument();
expect(screen.queryByLabelText('返回')).not.toBeInTheDocument();
});
it('应渲染设置按钮', () => {
renderWithProvider(<TopBar onOpenOptions={vi.fn()} />);
expect(screen.getByLabelText('common:buttons.settings')).toBeInTheDocument();
expect(screen.getByLabelText('设置')).toBeInTheDocument();
});
});
@@ -75,7 +75,7 @@ describe('TopBar 组件', () => {
const handleOpenOptions = vi.fn();
renderWithProvider(<TopBar onOpenOptions={handleOpenOptions} />);
fireEvent.click(screen.getByLabelText('common:buttons.settings'));
fireEvent.click(screen.getByLabelText('设置'));
expect(handleOpenOptions).toHaveBeenCalledTimes(1);
});
@@ -83,7 +83,7 @@ describe('TopBar 组件', () => {
mockRouterValue.currentPage = 'timestamp';
renderWithProvider(<TopBar onOpenOptions={vi.fn()} />);
fireEvent.click(screen.getByLabelText('common:buttons.back'));
fireEvent.click(screen.getByLabelText('返回'));
expect(mockRouterValue.goBack).toHaveBeenCalledTimes(1);
});
});