From ec92afa294694004173d32e81c2fdd70fbde60a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=A8=E9=9C=96=E9=93=83?= Date: Thu, 18 Jun 2026 14:22:14 +0000 Subject: [PATCH] =?UTF-8?q?refactor(i18n):=20=E7=A7=BB=E9=99=A4=20chrome.i?= =?UTF-8?q?18n=20=E5=9B=BD=E9=99=85=E5=8C=96=EF=BC=8C=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E4=B8=AD=E6=96=87=E7=A1=AC=E7=BC=96=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除 chromeI18n 工具、_locales 翻译文件和 manifest default_locale 配置, 将所有 UI 文案改为直接硬编码中文,并更新相关测试与文档。 --- .github/I18N.md | 42 - AGENTS.md | 14 +- public/_locales/zh_CN/messages.json | 1304 ----------------- src/components/CopyButton.tsx | 12 +- src/components/ErrorBoundary.tsx | 9 +- src/components/PageErrorBoundary.tsx | 9 +- src/components/RouterContainer.tsx | 6 +- src/components/TextInputArea.tsx | 19 +- src/components/TopBar.tsx | 44 +- .../__tests__/TextInputArea.test.tsx | 25 +- src/config/__tests__/features.test.ts | 14 +- src/config/features.tsx | 44 +- src/entrypoints/content/contextMenuHandler.ts | 3 +- .../__tests__/TextMode.test.tsx | 8 +- .../components/Base64ConverterSection.tsx | 32 +- .../components/DecodeResultPaper.tsx | 11 +- .../Base64Converter/components/TextMode.tsx | 9 +- src/pages/Base64Converter/index.tsx | 8 +- .../Base64Converter/useBase64Converter.ts | 15 +- src/pages/Base64Converter/useTextMode.ts | 16 +- src/pages/Dashboard/index.tsx | 10 +- .../JsonTools/components/DiffNavigator.tsx | 11 +- src/pages/JsonTools/components/DiffResult.tsx | 7 +- .../components/JsonConvertSection.tsx | 35 +- .../components/JsonFormatSection.tsx | 17 +- src/pages/JsonTools/index.tsx | 28 +- src/pages/JsonTools/useJsonTools.ts | 6 +- src/pages/Jwt/JwtSection.tsx | 4 +- src/pages/Jwt/index.tsx | 16 +- src/pages/QrCode/components/GeneratePanel.tsx | 14 +- src/pages/QrCode/components/ImageUploader.tsx | 18 +- src/pages/QrCode/components/ParsePanel.tsx | 20 +- src/pages/QrCode/components/QrCodePreview.tsx | 9 +- src/pages/QrCode/hooks/useQrCode.ts | 188 ++- src/pages/QrCode/index.tsx | 6 +- src/pages/RightClickRestorer/index.tsx | 24 +- .../components/AutoRefreshToggle.tsx | 5 +- .../components/CleaningResult.tsx | 7 +- .../components/ErrorDisplay.tsx | 5 +- .../StorageCleaner/components/OptionItem.tsx | 19 +- .../components/StorageCleanerConfirm.tsx | 24 +- .../components/StorageOptionsGrid.tsx | 7 +- src/pages/StorageCleaner/index.tsx | 9 +- src/pages/StorageCleaner/useStorageCleaner.ts | 21 +- .../components/DataPreview.tsx | 11 +- .../components/ExportPanel.tsx | 21 +- .../components/FieldEditor.tsx | 42 +- .../components/FieldItem.tsx | 4 +- .../components/FieldList.tsx | 44 +- .../components/GenerateButton.tsx | 18 +- .../components/GenerateOptions.tsx | 11 +- .../components/GeneratorConfig.tsx | 14 +- .../components/GeneratorSelector.tsx | 4 +- .../components/ResultPanel.tsx | 27 +- .../components/RuleManager.tsx | 81 +- .../components/__tests__/RuleManager.test.tsx | 14 +- src/pages/TestDataGenerator/index.tsx | 22 +- src/pages/TextStatistics/index.tsx | 12 +- src/pages/Timestamp/components/LiveClock.tsx | 15 +- src/pages/Timestamp/components/ResultView.tsx | 9 +- src/pages/Timestamp/index.tsx | 19 +- src/pages/Timestamp/useTimestampConverter.ts | 10 +- src/utils/chromeI18n.ts | 68 - src/utils/jwt.ts | 19 +- src/utils/ruleStorage.ts | 2 +- src/utils/storageCleaner.ts | 20 +- vitest.setup.ts | 35 - wxt.config.ts | 3 +- 68 files changed, 531 insertions(+), 2148 deletions(-) delete mode 100644 .github/I18N.md delete mode 100644 public/_locales/zh_CN/messages.json delete mode 100644 src/utils/chromeI18n.ts diff --git a/.github/I18N.md b/.github/I18N.md deleted file mode 100644 index 80e4ee8..0000000 --- a/.github/I18N.md +++ /dev/null @@ -1,42 +0,0 @@ -# i18n 开发指南 - -## 概述 - -项目使用 Chrome 扩展标准的 `chrome.i18n` API 进行本地化,通过 `src/utils/chromeI18n.ts` 提供类型安全的 React Hook 包装。 - -## 配置 - -- **翻译文件**: `public/_locales/{zh_CN,en}/messages.json`(Chrome 扩展标准格式) -- **默认语言**: `zh_CN`(在 `wxt.config.ts` 的 `manifest.default_locale` 中配置) - -## 使用方式 - -```typescript -import { useI18n } from '@/utils/chromeI18n'; -``` - -## 翻译键格式 - -- **直接 key**: `t('dashboard_title')` → 查找 `dashboard_title` -- **命名空间格式(兼容旧用法)**: `t('common:buttons.search')` → 查找 `common_buttons_search` -- **带命名空间参数**: `useI18n(['common', 'features'])`,会自动尝试 `common_key`、`features_key` - -## 占位符支持 - -```typescript -t('router_notFoundDescription', { entryPointType: 'popup' }); -``` - -## Hook 返回值 - -```typescript -{ t, i18n: { language, changeLanguage }, isLoaded } -``` - -## 回退策略 - -当翻译 key 未命中时,返回 key 本身(开发模式下在控制台记录 warning) - -## 限制 - -`chrome.i18n` 无法动态切换语言,语言跟随浏览器设置,切换后需刷新页面 diff --git a/AGENTS.md b/AGENTS.md index 97606b7..d868cbe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -55,7 +55,7 @@ src/pages/FeatureName/ - 页面入口组件统一命名为 `Index`,通过 `export default function Index()` 导出 - Hook 负责所有状态管理和业务逻辑,通过返回值暴露给页面 -- 子组件可以独立调用 `useI18n` 等全局 Hook +- 子组件可以独立调用全局 Hook - 当 `index.tsx` 超过 150 行时,必须拆分为 UI + Hook 模式 - 复杂页面可增加 `contexts/`、`hooks/`、`components/` 子目录 @@ -82,27 +82,21 @@ src/pages/FeatureName/ - 全局变量: `vitest/globals` (describe, it, expect 等无需导入) - Setup 文件: `vitest.setup.ts` 自动 mock: - `chrome.*` / `browser.*` API (storage, tabs, runtime, cookies 等) - - `@/utils/chromeI18n` (从 `public/_locales/zh_CN/messages.json` 加载真实翻译) - `window.matchMedia` - 测试文件命名: `__tests__/*.test.{ts,tsx}` 或 `*.test.{ts,tsx}` - Mock 模式: 使用 `vi.mock()` 进行模块级 mock,避免在测试文件中重复 mock 代码 - 测试工具: `@testing-library/react` + `@testing-library/user-event` 进行组件测试 -## i18n (chrome.i18n) - -详见 [i18n 开发指南](./.github/I18N.md) - ## 新功能开发清单 1. 在 `src/types/storage.d.ts` 添加 `PageType` 联合类型 2. 在 `src/config/features.tsx` 的 `FEATURES` 数组添加配置(指定 key、翻译键、图标、三种渲染模式的组件) 3. 在 `src/pages/` 创建页面组件 (懒加载): - - `index.tsx` — UI 组件,使用 `useI18n` 获取翻译 + - `index.tsx` — UI 组件 - `use{FeatureName}.ts` — 业务逻辑 Hook - `constants.ts` — 常量(可选) -4. 在 `public/_locales/zh/messages.json`(及 `en/messages.json`)添加翻译 -5. 如需新权限,更新 `wxt.config.ts` 的 `manifest.permissions`;如有不使用的权限,需移除 -6. 添加对应的单元测试 +4. 如需新权限,更新 `wxt.config.ts` 的 `manifest.permissions`;如有不使用的权限,需移除 +5. 添加对应的单元测试 ## 代码规范 diff --git a/public/_locales/zh_CN/messages.json b/public/_locales/zh_CN/messages.json deleted file mode 100644 index 9717b5d..0000000 --- a/public/_locales/zh_CN/messages.json +++ /dev/null @@ -1,1304 +0,0 @@ -{ - "buttons_copy": { - "message": "复制", - "description": "Translation key: buttons_copy" - }, - "buttons_clear": { - "message": "清理", - "description": "Translation key: buttons_clear" - }, - "messages_copySuccess": { - "message": "已复制到剪贴板", - "description": "Translation key: messages_copySuccess" - }, - "messages_copyError": { - "message": "复制失败", - "description": "Translation key: messages_copyError" - }, - "messages_copyEmpty": { - "message": "无内容可复制", - "description": "Translation key: messages_copyEmpty" - }, - "errorBoundary_title": { - "message": "糟糕,出了点问题", - "description": "Translation key: errorBoundary_title" - }, - "errorBoundary_description": { - "message": "应用遇到了一些意外错误。您可以尝试刷新页面或重置应用。", - "description": "Translation key: errorBoundary_description" - }, - "errorBoundary_refresh": { - "message": "刷新应用", - "description": "Translation key: errorBoundary_refresh" - }, - "errorBoundary_retry": { - "message": "重新尝试", - "description": "Translation key: errorBoundary_retry" - }, - "pageErrorBoundary_title": { - "message": "该功能运行异常", - "description": "Translation key: pageErrorBoundary_title" - }, - "pageErrorBoundary_description": { - "message": "该页面在加载或渲染时遇到了内部脚本错误。您可以尝试重试,或者通过导航菜单切换到其他工具。", - "description": "Translation key: pageErrorBoundary_description" - }, - "router_notFound": { - "message": "页面未找到", - "description": "Translation key: router_notFound" - }, - "router_notFoundDescription": { - "message": "该功能在当前运行环境({{entryPointType}})下不可用或已被移除。", - "description": "Translation key: router_notFoundDescription" - }, - "textInputArea_clear": { - "message": "清空", - "description": "Translation key: textInputArea_clear" - }, - "textInputArea_copyContent": { - "message": "复制内容", - "description": "Translation key: textInputArea_copyContent" - }, - "textInputArea_cleared": { - "message": "已清空", - "description": "Translation key: textInputArea_cleared" - }, - "textInputArea_placeholder": { - "message": "请输入文本", - "description": "Translation key: textInputArea_placeholder" - }, - "dashboard_title": { - "message": "仪表盘", - "description": "Translation key: dashboard_title" - }, - "dashboard_searchPlaceholder": { - "message": "搜索工具...", - "description": "Translation key: dashboard_searchPlaceholder" - }, - "dashboard_recentlyUsed": { - "message": "最近使用", - "description": "Translation key: dashboard_recentlyUsed" - }, - "dashboard_allTools": { - "message": "全部工具", - "description": "Translation key: dashboard_allTools" - }, - "timestamp_title": { - "message": "时间戳", - "description": "Translation key: timestamp_title" - }, - "timestamp_description": { - "message": "Unix 毫秒数转换与格式化", - "description": "Translation key: timestamp_description" - }, - "storageCleaner_title": { - "message": "存储清理", - "description": "Translation key: storageCleaner_title" - }, - "storageCleaner_description": { - "message": "清理缓存、Cookies 及本地存储", - "description": "Translation key: storageCleaner_description" - }, - "qrCode_title": { - "message": "二维码工具", - "description": "Translation key: qrCode_title" - }, - "qrCode_description": { - "message": "生成当前选中的 URL 的二维码", - "description": "Translation key: qrCode_description" - }, - "textStatistics_title": { - "message": "文本统计", - "description": "Translation key: textStatistics_title" - }, - "textStatistics_description": { - "message": "实时分析文本字符、单词及字节", - "description": "Translation key: textStatistics_description" - }, - "jwt_title": { - "message": "JWT 解析", - "description": "Translation key: jwt_title" - }, - "jwt_description": { - "message": "JSON Web Token 解码与查看", - "description": "Translation key: jwt_description" - }, - "jsonDiff_title": { - "message": "JSON 工具", - "description": "Translation key: jsonDiff_title" - }, - "jsonDiff_description": { - "message": "差异比较、格式化、YAML/TOML 转换及压缩", - "description": "Translation key: jsonDiff_description" - }, - "base64Converter_title": { - "message": "Base64 转换器", - "description": "Translation key: base64Converter_title" - }, - "base64Converter_description": { - "message": "文本、文件与图像的 Base64 编码转换", - "description": "Translation key: base64Converter_description" - }, - "rightClickRestorer_title": { - "message": "右键恢复", - "description": "Translation key: rightClickRestorer_title" - }, - "rightClickRestorer_description": { - "message": "检测并恢复被网站禁用的浏览器右键菜单", - "description": "Translation key: rightClickRestorer_description" - }, - "base64Converter_pageTitle": { - "message": "Base64 转换器", - "description": "Translation key: base64Converter_pageTitle" - }, - "base64Converter_textMode": { - "message": "文本", - "description": "Translation key: base64Converter_textMode" - }, - "base64Converter_fileMode": { - "message": "文件", - "description": "Translation key: base64Converter_fileMode" - }, - "base64Converter_imageMode": { - "message": "图像", - "description": "Translation key: base64Converter_imageMode" - }, - "base64Converter_encode": { - "message": "编码", - "description": "Translation key: base64Converter_encode" - }, - "base64Converter_decode": { - "message": "解码", - "description": "Translation key: base64Converter_decode" - }, - "base64Converter_clear": { - "message": "清空", - "description": "Translation key: base64Converter_clear" - }, - "base64Converter_textInputPlaceholder": { - "message": "输入需要编码为 Base64 的文本...", - "description": "Translation key: base64Converter_textInputPlaceholder" - }, - "base64Converter_base64InputPlaceholder": { - "message": "输入需要解码的 Base64 字符串...", - "description": "Translation key: base64Converter_base64InputPlaceholder" - }, - "base64Converter_base64Output": { - "message": "Base64 编码结果", - "description": "Translation key: base64Converter_base64Output" - }, - "base64Converter_textOutput": { - "message": "解码文本结果", - "description": "Translation key: base64Converter_textOutput" - }, - "base64Converter_copyRaw": { - "message": "复制纯 Base64", - "description": "Translation key: base64Converter_copyRaw" - }, - "base64Converter_copyDataUri": { - "message": "复制 Data URI", - "description": "Translation key: base64Converter_copyDataUri" - }, - "base64Converter_clickOrDropToFile": { - "message": "点击或拖拽文件到此处", - "description": "Translation key: base64Converter_clickOrDropToFile" - }, - "base64Converter_clickOrDropToImage": { - "message": "点击或拖拽图像到此处", - "description": "Translation key: base64Converter_clickOrDropToImage" - }, - "base64Converter_clickOrDropToReplace": { - "message": "点击或拖拽以替换文件", - "description": "Translation key: base64Converter_clickOrDropToReplace" - }, - "base64Converter_maxFileSize": { - "message": "最大文件大小:{{max}}", - "description": "Translation key: base64Converter_maxFileSize" - }, - "base64Converter_supportedFormats": { - "message": "支持 PNG、JPG、WEBP、GIF、BMP、SVG 等格式", - "description": "Translation key: base64Converter_supportedFormats" - }, - "base64Converter_fileSizeExceeded": { - "message": "文件大小超出限制(最大 {{max}})", - "description": "Translation key: base64Converter_fileSizeExceeded" - }, - "base64Converter_unsupportedImageType": { - "message": "不支持的图像格式", - "description": "Translation key: base64Converter_unsupportedImageType" - }, - "base64Converter_conversionFailed": { - "message": "转换失败", - "description": "Translation key: base64Converter_conversionFailed" - }, - "base64Converter_originalSize": { - "message": "原始大小", - "description": "Translation key: base64Converter_originalSize" - }, - "base64Converter_encodedSize": { - "message": "编码大小", - "description": "Translation key: base64Converter_encodedSize" - }, - "base64Converter_invalidBase64": { - "message": "Base64 字符串无效", - "description": "Translation key: base64Converter_invalidBase64" - }, - "base64Converter_binaryDataDetected": { - "message": "输入似乎是二进制数据(如图片)。请切换到「图像」选项卡。", - "description": "Translation key: base64Converter_binaryDataDetected" - }, - "base64Converter_imageDataUriHint": { - "message": "检测到图片的 data URI,请使用「图像」选项卡进行解码。", - "description": "Translation key: base64Converter_imageDataUriHint" - }, - "base64Converter_switchToImageMode": { - "message": "切换到图像模式", - "description": "Translation key: base64Converter_switchToImageMode" - }, - "base64Converter_download": { - "message": "下载", - "description": "Translation key: base64Converter_download" - }, - "base64Converter_decodedFileName": { - "message": "解码后文件名", - "description": "Translation key: base64Converter_decodedFileName" - }, - "base64Converter_decodeBase64Placeholder": { - "message": "输入需要解码的 Base64 或 data URI...", - "description": "Translation key: base64Converter_decodeBase64Placeholder" - }, - "base64Converter_decodedFileOutput": { - "message": "解码文件", - "description": "Translation key: base64Converter_decodedFileOutput" - }, - "base64Converter_decodedImageOutput": { - "message": "解码图像", - "description": "Translation key: base64Converter_decodedImageOutput" - }, - "base64Converter_inferredMimeType": { - "message": "推断的 MIME 类型", - "description": "Translation key: base64Converter_inferredMimeType" - }, - "base64Converter_decodedSize": { - "message": "解码大小", - "description": "Translation key: base64Converter_decodedSize" - }, - "jsonDiff_pageTitle": { - "message": "JSON 差异比较", - "description": "Translation key: jsonDiff_pageTitle" - }, - "jsonDiff_leftPlaceholder": { - "message": "输入原始 JSON...", - "description": "Translation key: jsonDiff_leftPlaceholder" - }, - "jsonDiff_rightPlaceholder": { - "message": "输入目标 JSON...", - "description": "Translation key: jsonDiff_rightPlaceholder" - }, - "jsonDiff_leftLabel": { - "message": "原始 JSON", - "description": "Translation key: jsonDiff_leftLabel" - }, - "jsonDiff_rightLabel": { - "message": "目标 JSON", - "description": "Translation key: jsonDiff_rightLabel" - }, - "jsonDiff_sideBySideMode": { - "message": "并排", - "description": "Translation key: jsonDiff_sideBySideMode" - }, - "jsonDiff_unifiedMode": { - "message": "统一", - "description": "Translation key: jsonDiff_unifiedMode" - }, - "jsonDiff_previousDiff": { - "message": "上一个", - "description": "Translation key: jsonDiff_previousDiff" - }, - "jsonDiff_nextDiff": { - "message": "下一个", - "description": "Translation key: jsonDiff_nextDiff" - }, - "jsonDiff_noDiffs": { - "message": "无差异", - "description": "Translation key: jsonDiff_noDiffs" - }, - "jsonDiff_invalidJson": { - "message": "无效的 JSON 格式", - "description": "Translation key: jsonDiff_invalidJson" - }, - "jsonDiff_emptyHint": { - "message": "输入两侧 JSON 后点击比较", - "description": "Translation key: jsonDiff_emptyHint" - }, - "jsonDiff_fixErrorHint": { - "message": "请修正上方 JSON 的语法错误以开启实时流式比对", - "description": "Translation key: jsonDiff_fixErrorHint" - }, - "jsonDiff_added": { - "message": "新增", - "description": "Translation key: jsonDiff_added" - }, - "jsonDiff_removed": { - "message": "删除", - "description": "Translation key: jsonDiff_removed" - }, - "jsonDiff_modified": { - "message": "修改", - "description": "Translation key: jsonDiff_modified" - }, - "jsonFormat_inputPlaceholder": { - "message": "输入需要格式化的 JSON...", - "description": "Translation key: jsonFormat_inputPlaceholder" - }, - "jsonFormat_sortKeys": { - "message": "键名排序", - "description": "Translation key: jsonFormat_sortKeys" - }, - "jsonFormat_indentSize": { - "message": "缩进", - "description": "Translation key: jsonFormat_indentSize" - }, - "jsonFormat_outputLabel": { - "message": "格式化结果", - "description": "Translation key: jsonFormat_outputLabel" - }, - "jsonFormat_invalidJson": { - "message": "无效的 JSON 格式", - "description": "Translation key: jsonFormat_invalidJson" - }, - "jsonFormat_emptyHint": { - "message": "输入 JSON 后点击格式化", - "description": "Translation key: jsonFormat_emptyHint" - }, - "jsonFormat_fixErrorHint": { - "message": "请修正上方 JSON 的语法错误以开启实时流式格式化", - "description": "Translation key: jsonFormat_fixErrorHint" - }, - "jsonFormat_originalSize": { - "message": "原始大小", - "description": "Translation key: jsonFormat_originalSize" - }, - "jsonFormat_formattedSize": { - "message": "格式化后大小", - "description": "Translation key: jsonFormat_formattedSize" - }, - "jsonFormat_diffMode": { - "message": "差异比较", - "description": "Translation key: jsonFormat_diffMode" - }, - "jsonFormat_formatMode": { - "message": "格式化", - "description": "Translation key: jsonFormat_formatMode" - }, - "jsonFormat_yamlMode": { - "message": "YAML", - "description": "Translation key: jsonFormat_yamlMode" - }, - "jsonFormat_tomlMode": { - "message": "TOML", - "description": "Translation key: jsonFormat_tomlMode" - }, - "jsonFormat_minifyMode": { - "message": "压缩", - "description": "Translation key: jsonFormat_minifyMode" - }, - "jsonFormat_yamlModeInputPlaceholder": { - "message": "输入需要转换的 JSON...", - "description": "Translation key: jsonFormat_yamlModeInputPlaceholder" - }, - "jsonFormat_yamlModeOutputLabel": { - "message": "YAML 结果", - "description": "Translation key: jsonFormat_yamlModeOutputLabel" - }, - "jsonFormat_yamlModeEmptyHint": { - "message": "输入 JSON 后点击转换", - "description": "Translation key: jsonFormat_yamlModeEmptyHint" - }, - "jsonFormat_tomlModeInputPlaceholder": { - "message": "输入需要转换的 JSON...", - "description": "Translation key: jsonFormat_tomlModeInputPlaceholder" - }, - "jsonFormat_tomlModeOutputLabel": { - "message": "TOML 结果", - "description": "Translation key: jsonFormat_tomlModeOutputLabel" - }, - "jsonFormat_tomlModeEmptyHint": { - "message": "输入 JSON 后点击转换", - "description": "Translation key: jsonFormat_tomlModeEmptyHint" - }, - "jsonFormat_minifyModeInputPlaceholder": { - "message": "输入需要压缩的 JSON...", - "description": "Translation key: jsonFormat_minifyModeInputPlaceholder" - }, - "jsonFormat_minifyModeOutputLabel": { - "message": "压缩结果", - "description": "Translation key: jsonFormat_minifyModeOutputLabel" - }, - "jsonFormat_minifyModeEmptyHint": { - "message": "输入 JSON 后点击压缩", - "description": "Translation key: jsonFormat_minifyModeEmptyHint" - }, - "jwt_pageTitle": { - "message": "JWT 解析", - "description": "Translation key: jwt_pageTitle" - }, - "jwt_placeholder": { - "message": "在此粘贴 JWT 令牌 (Encoded JWT)...", - "description": "Translation key: jwt_placeholder" - }, - "jwt_headerTitle": { - "message": "HEADER: 算法 & 令牌类型", - "description": "Translation key: jwt_headerTitle" - }, - "jwt_payloadTitle": { - "message": "PAYLOAD: 数据", - "description": "Translation key: jwt_payloadTitle" - }, - "jwt_signatureTitle": { - "message": "签名", - "description": "Translation key: jwt_signatureTitle" - }, - "jwt_noSignature": { - "message": "无签名", - "description": "Translation key: jwt_noSignature" - }, - "jwt_invalidFormat": { - "message": "无法解析", - "description": "Translation key: jwt_invalidFormat" - }, - "jwt_errors_invalidBase64String": { - "message": "无效的 Base64URL 字符串", - "description": "Translation key: jwt_errors_invalidBase64String" - }, - "jwt_errors_failedToDecode": { - "message": "Base64URL 解码失败:", - "description": "Translation key: jwt_errors_failedToDecode" - }, - "jwt_errors_invalidFormat": { - "message": "JWT 格式错误:必须包含三个由 . 分隔的部分", - "description": "Translation key: jwt_errors_invalidFormat" - }, - "jwt_errors_parseHeaderFailed": { - "message": "解析 Header 失败:", - "description": "Translation key: jwt_errors_parseHeaderFailed" - }, - "jwt_errors_parsePayloadFailed": { - "message": "解析 Payload 失败:", - "description": "Translation key: jwt_errors_parsePayloadFailed" - }, - "qrCode_pageTitle": { - "message": "二维码工具", - "description": "Translation key: qrCode_pageTitle" - }, - "qrCode_urlToQr": { - "message": "文本转二维码", - "description": "Translation key: qrCode_urlToQr" - }, - "qrCode_qrToUrl": { - "message": "二维码转文本", - "description": "Translation key: qrCode_qrToUrl" - }, - "qrCode_urlInputLabel": { - "message": "输入 URL 或文本", - "description": "Translation key: qrCode_urlInputLabel" - }, - "qrCode_urlInputPlaceholder": { - "message": "请输入 URL 或文本内容,将自动生成二维码", - "description": "Translation key: qrCode_urlInputPlaceholder" - }, - "qrCode_generating": { - "message": "生成中...", - "description": "Translation key: qrCode_generating" - }, - "qrCode_qrCodeWillShow": { - "message": "二维码将显示在这里", - "description": "Translation key: qrCode_qrCodeWillShow" - }, - "qrCode_downloadButton": { - "message": "下载二维码", - "description": "Translation key: qrCode_downloadButton" - }, - "qrCode_copyQrButton": { - "message": "复制二维码", - "description": "Translation key: qrCode_copyQrButton" - }, - "qrCode_qrCodeDownloadSuccess": { - "message": "二维码下载成功", - "description": "Translation key: qrCode_qrCodeDownloadSuccess" - }, - "qrCode_qrCodeCopySuccess": { - "message": "二维码已复制到剪贴板", - "description": "Translation key: qrCode_qrCodeCopySuccess" - }, - "qrCode_parseSuccess": { - "message": "二维码解析成功", - "description": "Translation key: qrCode_parseSuccess" - }, - "qrCode_noQrDetected": { - "message": "未检测到二维码,请确保图片清晰且包含二维码", - "description": "Translation key: qrCode_noQrDetected" - }, - "qrCode_parseError": { - "message": "解析二维码失败,请重试", - "description": "Translation key: qrCode_parseError" - }, - "qrCode_copyError": { - "message": "复制失败,请重试", - "description": "Translation key: qrCode_copyError" - }, - "qrCode_imagePasted": { - "message": "图片粘贴成功,正在解析...", - "description": "Translation key: qrCode_imagePasted" - }, - "qrCode_imagePasteError": { - "message": "粘贴图片失败,请重试", - "description": "Translation key: qrCode_imagePasteError" - }, - "qrCode_imageCleared": { - "message": "图片已清除", - "description": "Translation key: qrCode_imageCleared" - }, - "qrCode_clickToUpload": { - "message": "点击、拖拽或粘贴上传二维码图片", - "description": "Translation key: qrCode_clickToUpload" - }, - "qrCode_supportFormats": { - "message": "支持 PNG、JPG、WEBP、Base64 格式", - "description": "Translation key: qrCode_supportFormats" - }, - "qrCode_resultLabel": { - "message": "解析结果", - "description": "Translation key: qrCode_resultLabel" - }, - "qrCode_clickToChange": { - "message": "点击更换图片", - "description": "Translation key: qrCode_clickToChange" - }, - "qrCode_generateButton": { - "message": "生成二维码", - "description": "Translation key: qrCode_generateButton" - }, - "qrCode_editButton": { - "message": "编辑", - "description": "Translation key: qrCode_editButton" - }, - "qrCode_textPreviewLabel": { - "message": "原始文本", - "description": "Translation key: qrCode_textPreviewLabel" - }, - "qrCode_generateFirstHint": { - "message": "输入文本后点击「生成二维码」按钮", - "description": "Translation key: qrCode_generateFirstHint" - }, - "qrCode_inputRequired": { - "message": "请输入内容", - "description": "Translation key: qrCode_inputRequired" - }, - "qrCode_generateError": { - "message": "生成二维码失败,请重试", - "description": "Translation key: qrCode_generateError" - }, - "qrCode_uploadedImage": { - "message": "已上传图片", - "description": "Translation key: qrCode_uploadedImage" - }, - "qrCode_reuploadButton": { - "message": "重新上传", - "description": "Translation key: qrCode_reuploadButton" - }, - "qrCode_parsing": { - "message": "解析中...", - "description": "Translation key: qrCode_parsing" - }, - "qrCode_resultPlaceholder": { - "message": "解析结果将显示在此处", - "description": "Translation key: qrCode_resultPlaceholder" - }, - "qrCode_imageToQr": { - "message": "解析图片二维码", - "description": "Translation key: qrCode_imageToQr" - }, - "rightClickRestorer_loading": { - "message": "正在加载...", - "description": "Translation key: rightClickRestorer_loading" - }, - "rightClickRestorer_currentDomain": { - "message": "当前域名", - "description": "Translation key: rightClickRestorer_currentDomain" - }, - "rightClickRestorer_statusLocked": { - "message": "未解锁", - "description": "Translation key: rightClickRestorer_statusLocked" - }, - "rightClickRestorer_statusUnlocked": { - "message": "已解锁", - "description": "Translation key: rightClickRestorer_statusUnlocked" - }, - "rightClickRestorer_unsupported": { - "message": "不支持", - "description": "Translation key: rightClickRestorer_unsupported" - }, - "rightClickRestorer_unsupportedDesc": { - "message": "当前页面为浏览器内部页面或扩展页面,无法解锁右键功能。请切换到普通网页后重试。", - "description": "Translation key: rightClickRestorer_unsupportedDesc" - }, - "rightClickRestorer_unlockDesc": { - "message": "点击下方按钮,为当前网站临时解锁右键菜单。刷新页面后需要重新解锁。", - "description": "Translation key: rightClickRestorer_unlockDesc" - }, - "rightClickRestorer_unlockBtn": { - "message": "解锁当前网站右键", - "description": "Translation key: rightClickRestorer_unlockBtn" - }, - "rightClickRestorer_alreadyUnlocked": { - "message": "右键已解锁", - "description": "Translation key: rightClickRestorer_alreadyUnlocked" - }, - "storageCleaner_pageTitle": { - "message": "存储清理", - "description": "Translation key: storageCleaner_pageTitle" - }, - "storageCleaner_loading": { - "message": "加载中...", - "description": "Translation key: storageCleaner_loading" - }, - "storageCleaner_initializing": { - "message": "正在读取站点数据...", - "description": "Translation key: storageCleaner_initializing" - }, - "storageCleaner_cleaning": { - "message": "正在清理...", - "description": "Translation key: storageCleaner_cleaning" - }, - "storageCleaner_cleanNow": { - "message": "立即清理", - "description": "Translation key: storageCleaner_cleanNow" - }, - "storageCleaner_autoRefresh": { - "message": "清理后自动刷新页面", - "description": "Translation key: storageCleaner_autoRefresh" - }, - "storageCleaner_selectAll": { - "message": "全选所有项", - "description": "Translation key: storageCleaner_selectAll" - }, - "storageCleaner_noData": { - "message": "无数据", - "description": "Translation key: storageCleaner_noData" - }, - "storageCleaner_errorNoTab": { - "message": "无法获取当前标签页", - "description": "Translation key: storageCleaner_errorNoTab" - }, - "storageCleaner_errorRestricted": { - "message": "存储清理功能不支持此页面", - "description": "Translation key: storageCleaner_errorRestricted" - }, - "storageCleaner_cleanSuccessReload": { - "message": "清理成功,即将刷新页面", - "description": "Translation key: storageCleaner_cleanSuccessReload" - }, - "storageCleaner_errorStandardOnly": { - "message": "存储清理功能仅适用于标准网页", - "description": "Translation key: storageCleaner_errorStandardOnly" - }, - "storageCleaner_confirmTitle": { - "message": "确认清理数据?", - "description": "Translation key: storageCleaner_confirmTitle" - }, - "storageCleaner_confirmDesc": { - "message": "您将永久删除当前页面的以下选定存储项。", - "description": "Translation key: storageCleaner_confirmDesc" - }, - "storageCleaner_irreversible": { - "message": "此操作不可撤销", - "description": "Translation key: storageCleaner_irreversible" - }, - "storageCleaner_confirmAction": { - "message": "确认清理", - "description": "Translation key: storageCleaner_confirmAction" - }, - "storageCleaner_cleanedSummary": { - "message": "清理了 {{items}}", - "description": "Translation key: storageCleaner_cleanedSummary" - }, - "storageCleaner_noDataToClean": { - "message": "该页面没有可清理的存储数据", - "description": "Translation key: storageCleaner_noDataToClean" - }, - "storageCleaner_partialFailure": { - "message": "部分清理失败", - "description": "Translation key: storageCleaner_partialFailure" - }, - "storageCleaner_options_localStorage": { - "message": "Local Storage", - "description": "Translation key: storageCleaner_options_localStorage" - }, - "storageCleaner_options_sessionStorage": { - "message": "Session Storage", - "description": "Translation key: storageCleaner_options_sessionStorage" - }, - "storageCleaner_options_indexedDB": { - "message": "站点存储", - "description": "Translation key: storageCleaner_options_indexedDB" - }, - "storageCleaner_options_cookies": { - "message": "Cookies", - "description": "Translation key: storageCleaner_options_cookies" - }, - "storageCleaner_options_cacheStorage": { - "message": "Cache Storage", - "description": "Translation key: storageCleaner_options_cacheStorage" - }, - "storageCleaner_options_serviceWorkers": { - "message": "Service Workers", - "description": "Translation key: storageCleaner_options_serviceWorkers" - }, - "textStatistics_pageTitle": { - "message": "文本统计", - "description": "Translation key: textStatistics_pageTitle" - }, - "textStatistics_placeholder": { - "message": "在此输入或粘贴文本...", - "description": "Translation key: textStatistics_placeholder" - }, - "textStatistics_characters": { - "message": "字符数", - "description": "Translation key: textStatistics_characters" - }, - "textStatistics_words": { - "message": "单词数", - "description": "Translation key: textStatistics_words" - }, - "textStatistics_lines": { - "message": "行数", - "description": "Translation key: textStatistics_lines" - }, - "textStatistics_bytes": { - "message": "字节大小", - "description": "Translation key: textStatistics_bytes" - }, - "timestamp_pageTitle": { - "message": "时间戳转换", - "description": "Translation key: timestamp_pageTitle" - }, - "timestamp_tsToDate": { - "message": "时间戳 → 日期", - "description": "Translation key: timestamp_tsToDate" - }, - "timestamp_dateToTs": { - "message": "日期 → 时间戳", - "description": "Translation key: timestamp_dateToTs" - }, - "timestamp_placeholderTs": { - "message": "输入时间戳...", - "description": "Translation key: timestamp_placeholderTs" - }, - "timestamp_placeholderDate": { - "message": "YYYY-MM-DD HH:mm:ss", - "description": "Translation key: timestamp_placeholderDate" - }, - "timestamp_unitMs": { - "message": "毫秒 (ms)", - "description": "Translation key: timestamp_unitMs" - }, - "timestamp_unitS": { - "message": "秒 (s)", - "description": "Translation key: timestamp_unitS" - }, - "timestamp_currentTs": { - "message": "当前时间戳", - "description": "Translation key: timestamp_currentTs" - }, - "timestamp_useNowTooltip": { - "message": "填充到下方", - "description": "Translation key: timestamp_useNowTooltip" - }, - "timestamp_copyTsTooltip": { - "message": "复制时间戳", - "description": "Translation key: timestamp_copyTsTooltip" - }, - "timestamp_usedSuccess": { - "message": "已使用当前时间戳", - "description": "Translation key: timestamp_usedSuccess" - }, - "timestamp_resultLabel": { - "message": "转换结果", - "description": "Translation key: timestamp_resultLabel" - }, - "timestamp_copyResultTooltip": { - "message": "复制结果", - "description": "Translation key: timestamp_copyResultTooltip" - }, - "timestamp_relativeTime": { - "message": "相对时间", - "description": "Translation key: timestamp_relativeTime" - }, - "timestamp_iso8601": { - "message": "ISO 8601", - "description": "Translation key: timestamp_iso8601" - }, - "timestamp_utcTime": { - "message": "UTC 时间", - "description": "Translation key: timestamp_utcTime" - }, - "timestamp_copyTooltip": { - "message": "复制", - "description": "Translation key: timestamp_copyTooltip" - }, - "timestamp_resultEmpty": { - "message": "请输入并点击转换", - "description": "Translation key: timestamp_resultEmpty" - }, - "common_buttons_cancel": { - "message": "取消", - "description": "Translation key: buttons_cancel" - }, - "common_buttons_copy": { - "message": "复制", - "description": "Translation key: buttons_copy" - }, - "common_buttons_themeMode_light": { - "message": "切换到深色模式", - "description": "Translation key: buttons_themeMode_light" - }, - "common_buttons_themeMode_dark": { - "message": "切换到系统模式", - "description": "Translation key: buttons_themeMode_dark" - }, - "common_buttons_themeMode_system": { - "message": "切换到浅色模式", - "description": "Translation key: buttons_themeMode_system" - }, - "common_buttons_search": { - "message": "搜索工具...", - "description": "Translation key: buttons_search" - }, - "common_buttons_back": { - "message": "返回", - "description": "Translation key: buttons_back" - }, - "common_buttons_clearSearch": { - "message": "清除搜索", - "description": "Translation key: buttons_clearSearch" - }, - "common_buttons_recentSearch": { - "message": "最近搜索", - "description": "Translation key: buttons_recentSearch" - }, - "common_buttons_noResults": { - "message": "未找到相关工具", - "description": "Translation key: buttons_noResults" - }, - "common_buttons_openInTab": { - "message": "在标签页打开", - "description": "Translation key: buttons_openInTab" - }, - "common_errorBoundary_title": { - "message": "糟糕,出了点问题", - "description": "Translation key: errorBoundary_title" - }, - "common_errorBoundary_description": { - "message": "应用遇到了一些意外错误。您可以尝试刷新页面或重置应用。", - "description": "Translation key: errorBoundary_description" - }, - "common_errorBoundary_refresh": { - "message": "刷新应用", - "description": "Translation key: errorBoundary_refresh" - }, - "common_errorBoundary_retry": { - "message": "重新尝试", - "description": "Translation key: errorBoundary_retry" - }, - "common_pageErrorBoundary_title": { - "message": "该功能运行异常", - "description": "Translation key: pageErrorBoundary_title" - }, - "common_pageErrorBoundary_description": { - "message": "该页面在加载或渲染时遇到了内部脚本错误。您可以尝试重试,或者通过导航菜单切换到其他工具。", - "description": "Translation key: pageErrorBoundary_description" - }, - "testDataGenerator_title": { - "message": "测试数据生成器", - "description": "Translation key: testDataGenerator_title" - }, - "testDataGenerator_description": { - "message": "自定义字段配置,批量生成测试数据", - "description": "Translation key: testDataGenerator_description" - }, - "testDataGenerator_fields": { - "message": "字段", - "description": "Translation key: testDataGenerator_fields" - }, - "testDataGenerator_addField": { - "message": "添加字段", - "description": "Translation key: testDataGenerator_addField" - }, - "testDataGenerator_noFields": { - "message": "暂无字段", - "description": "Translation key: testDataGenerator_noFields" - }, - "testDataGenerator_addFieldHint": { - "message": "点击上方按钮添加第一个字段", - "description": "Translation key: testDataGenerator_addFieldHint" - }, - "testDataGenerator_unique": { - "message": "唯一", - "description": "Translation key: testDataGenerator_unique" - }, - "testDataGenerator_fieldName": { - "message": "字段名称", - "description": "Translation key: testDataGenerator_fieldName" - }, - "testDataGenerator_fieldNamePlaceholder": { - "message": "请输入字段名称", - "description": "Translation key: testDataGenerator_fieldNamePlaceholder" - }, - "testDataGenerator_fieldDescription": { - "message": "字段描述", - "description": "Translation key: testDataGenerator_fieldDescription" - }, - "testDataGenerator_fieldDescriptionPlaceholder": { - "message": "可选,添加字段说明", - "description": "Translation key: testDataGenerator_fieldDescriptionPlaceholder" - }, - "testDataGenerator_required": { - "message": "必填", - "description": "Translation key: testDataGenerator_required" - }, - "testDataGenerator_nullRate": { - "message": "空值率", - "description": "Translation key: testDataGenerator_nullRate" - }, - "testDataGenerator_uniqueConstraint": { - "message": "唯一性约束", - "description": "Translation key: testDataGenerator_uniqueConstraint" - }, - "testDataGenerator_generator": { - "message": "数据生成器", - "description": "Translation key: testDataGenerator_generator" - }, - "testDataGenerator_generatorParams": { - "message": "生成器参数", - "description": "Translation key: testDataGenerator_generatorParams" - }, - "testDataGenerator_searchGenerator": { - "message": "搜索生成器...", - "description": "Translation key: testDataGenerator_searchGenerator" - }, - "testDataGenerator_searchResults": { - "message": "搜索结果", - "description": "Translation key: testDataGenerator_searchResults" - }, - "testDataGenerator_fieldConfig": { - "message": "字段配置", - "description": "Translation key: testDataGenerator_fieldConfig" - }, - "testDataGenerator_ruleManagement": { - "message": "规则管理", - "description": "Translation key: testDataGenerator_ruleManagement" - }, - "testDataGenerator_selectFieldToEdit": { - "message": "选择左侧字段进行编辑", - "description": "Translation key: testDataGenerator_selectFieldToEdit" - }, - "testDataGenerator_count": { - "message": "生成数量", - "description": "Translation key: testDataGenerator_count" - }, - "testDataGenerator_format": { - "message": "数据格式", - "description": "Translation key: testDataGenerator_format" - }, - "testDataGenerator_generate": { - "message": "生成数据", - "description": "Translation key: testDataGenerator_generate" - }, - "testDataGenerator_cancel": { - "message": "取消", - "description": "Translation key: testDataGenerator_cancel" - }, - "testDataGenerator_progress": { - "message": "已生成 {{current}} / {{total}} 条", - "description": "Translation key: testDataGenerator_progress" - }, - "testDataGenerator_estimatedTime": { - "message": "预计剩余 {{time}} 秒", - "description": "Translation key: testDataGenerator_estimatedTime" - }, - "testDataGenerator_dataPreview": { - "message": "数据预览", - "description": "Translation key: testDataGenerator_dataPreview" - }, - "testDataGenerator_noData": { - "message": "暂无数据", - "description": "Translation key: testDataGenerator_noData" - }, - "testDataGenerator_noDataHint": { - "message": "配置字段后点击「生成数据」按钮", - "description": "Translation key: testDataGenerator_noDataHint" - }, - "testDataGenerator_sampleData": { - "message": "示例数据", - "description": "Translation key: testDataGenerator_sampleData" - }, - "testDataGenerator_copied": { - "message": "已复制", - "description": "Translation key: testDataGenerator_copied" - }, - "testDataGenerator_copy": { - "message": "复制", - "description": "Translation key: testDataGenerator_copy" - }, - "testDataGenerator_pageInfo": { - "message": "第 {{current}} / {{total}} 页", - "description": "Translation key: testDataGenerator_pageInfo" - }, - "testDataGenerator_prevPage": { - "message": "上一页", - "description": "Translation key: testDataGenerator_prevPage" - }, - "testDataGenerator_nextPage": { - "message": "下一页", - "description": "Translation key: testDataGenerator_nextPage" - }, - "testDataGenerator_success": { - "message": "生成成功", - "description": "Translation key: testDataGenerator_success" - }, - "testDataGenerator_generateSuccess": { - "message": "生成完成", - "description": "Translation key: testDataGenerator_generateSuccess" - }, - "testDataGenerator_records": { - "message": "条数据", - "description": "Translation key: testDataGenerator_records" - }, - "testDataGenerator_partialSuccess": { - "message": "部分成功", - "description": "Translation key: testDataGenerator_partialSuccess" - }, - "testDataGenerator_failed": { - "message": "生成失败", - "description": "Translation key: testDataGenerator_failed" - }, - "testDataGenerator_totalCount": { - "message": "总条数", - "description": "Translation key: testDataGenerator_totalCount" - }, - "testDataGenerator_successCount": { - "message": "成功", - "description": "Translation key: testDataGenerator_successCount" - }, - "testDataGenerator_duration": { - "message": "耗时", - "description": "Translation key: testDataGenerator_duration" - }, - "testDataGenerator_warnings": { - "message": "警告", - "description": "Translation key: testDataGenerator_warnings" - }, - "testDataGenerator_moreWarnings": { - "message": "还有 {{count}} 条警告", - "description": "Translation key: testDataGenerator_moreWarnings" - }, - "testDataGenerator_export": { - "message": "导出数据", - "description": "Translation key: testDataGenerator_export" - }, - "testDataGenerator_copyJSON": { - "message": "复制 JSON", - "description": "Translation key: testDataGenerator_copyJSON" - }, - "testDataGenerator_copyCSV": { - "message": "复制 CSV", - "description": "Translation key: testDataGenerator_copyCSV" - }, - "testDataGenerator_downloadJSON": { - "message": "下载 JSON", - "description": "Translation key: testDataGenerator_downloadJSON" - }, - "testDataGenerator_downloadCSV": { - "message": "下载 CSV", - "description": "Translation key: testDataGenerator_downloadCSV" - }, - "testDataGenerator_saveRule": { - "message": "保存规则", - "description": "Translation key: testDataGenerator_saveRule" - }, - "testDataGenerator_import": { - "message": "导入", - "description": "Translation key: testDataGenerator_import" - }, - "testDataGenerator_exportRules": { - "message": "导出规则", - "description": "Translation key: testDataGenerator_exportRules" - }, - "testDataGenerator_ruleCount": { - "message": "已保存 {{count}}/{{max}} 条", - "description": "Translation key: testDataGenerator_ruleCount" - }, - "testDataGenerator_searchRules": { - "message": "搜索规则...", - "description": "Translation key: testDataGenerator_searchRules" - }, - "testDataGenerator_saveNewRule": { - "message": "保存新规则", - "description": "Translation key: testDataGenerator_saveNewRule" - }, - "testDataGenerator_ruleNamePlaceholder": { - "message": "规则名称", - "description": "Translation key: testDataGenerator_ruleNamePlaceholder" - }, - "testDataGenerator_ruleDescPlaceholder": { - "message": "规则描述(可选)", - "description": "Translation key: testDataGenerator_ruleDescPlaceholder" - }, - "testDataGenerator_confirm": { - "message": "确认", - "description": "Translation key: testDataGenerator_confirm" - }, - "testDataGenerator_done": { - "message": "完成", - "description": "Translation key: testDataGenerator_done" - }, - "testDataGenerator_noSearchResults": { - "message": "未找到匹配的规则", - "description": "Translation key: testDataGenerator_noSearchResults" - }, - "testDataGenerator_noRules": { - "message": "暂无保存的规则", - "description": "Translation key: testDataGenerator_noRules" - }, - "testDataGenerator_usedTimes": { - "message": "使用 {{count}} 次", - "description": "Translation key: testDataGenerator_usedTimes" - }, - "testDataGenerator_load": { - "message": "加载", - "description": "Translation key: testDataGenerator_load" - }, - "testDataGenerator_duplicate": { - "message": "复制", - "description": "Translation key: testDataGenerator_duplicate" - }, - "testDataGenerator_delete": { - "message": "删除", - "description": "Translation key: testDataGenerator_delete" - }, - "testDataGenerator_virtualMode": { - "message": "虚拟列表模式(共 $ 行)" - }, - "testDataGenerator_totalRows": { - "message": "共 $ 条数据" - }, - "testDataGenerator_ruleSaved": { - "message": "规则已保存", - "description": "Translation key: testDataGenerator_ruleSaved" - }, - "testDataGenerator_ruleLoaded": { - "message": "已加载规则「{{name}}」", - "description": "Translation key: testDataGenerator_ruleLoaded" - }, - "testDataGenerator_ruleDeleted": { - "message": "规则已删除", - "description": "Translation key: testDataGenerator_ruleDeleted" - }, - "testDataGenerator_ruleDuplicated": { - "message": "规则已复制", - "description": "Translation key: testDataGenerator_ruleDuplicated" - }, - "testDataGenerator_exportSuccess": { - "message": "规则已导出", - "description": "Translation key: testDataGenerator_exportSuccess" - }, - "testDataGenerator_importSuccess": { - "message": "成功导入 {{count}} 条规则", - "description": "Translation key: testDataGenerator_importSuccess" - }, - "testDataGenerator_importFailed": { - "message": "{{count}} 条规则导入失败", - "description": "Translation key: testDataGenerator_importFailed" - }, - "testDataGenerator_confirmDeleteTitle": { - "message": "确认删除规则", - "description": "Translation key: testDataGenerator_confirmDeleteTitle" - }, - "testDataGenerator_confirmDeleteDescription": { - "message": "确定要删除规则「{{name}}」吗?此操作不可撤销。", - "description": "Translation key: testDataGenerator_confirmDeleteDescription" - }, - "testDataGenerator_ruleNameDuplicate": { - "message": "已存在同名规则,是否覆盖保存?", - "description": "Translation key: testDataGenerator_ruleNameDuplicate" - }, - "testDataGenerator_overwrite": { - "message": "覆盖", - "description": "Translation key: testDataGenerator_overwrite" - }, - "testDataGenerator_edit": { - "message": "编辑", - "description": "Translation key: testDataGenerator_edit" - }, - "testDataGenerator_ruleUpdated": { - "message": "规则已更新", - "description": "Translation key: testDataGenerator_ruleUpdated" - }, - "testDataGenerator_editing": { - "message": "编辑中", - "description": "Translation key: testDataGenerator_editing" - }, - "testDataGenerator_editingRule": { - "message": "正在编辑规则「{{name}}」", - "description": "Translation key: testDataGenerator_editingRule" - }, - "testDataGenerator_updateRule": { - "message": "更新规则", - "description": "Translation key: testDataGenerator_updateRule" - }, - "testDataGenerator_saveAs": { - "message": "另存为", - "description": "Translation key: testDataGenerator_saveAs" - }, - "testDataGenerator_noGeneratorParams": { - "message": "此生成器无可配置参数", - "description": "Translation key: testDataGenerator_noGeneratorParams" - }, - "testDataGenerator_enabled": { - "message": "启用", - "description": "Translation key: testDataGenerator_enabled" - }, - "testDataGenerator_disabled": { - "message": "禁用", - "description": "Translation key: testDataGenerator_disabled" - }, - "testDataGenerator_commaSeparated": { - "message": "用逗号分隔多个值", - "description": "Translation key: testDataGenerator_commaSeparated" - }, - "testDataGenerator_copySuccess": { - "message": "已复制到剪贴板", - "description": "Translation key: testDataGenerator_copySuccess" - }, - "testDataGenerator_copyFailed": { - "message": "复制失败", - "description": "Translation key: testDataGenerator_copyFailed" - }, - "testDataGenerator_fieldNameEmpty": { - "message": "字段名称不能为空", - "description": "Translation key: testDataGenerator_fieldNameEmpty" - }, - "testDataGenerator_fieldNameDuplicate": { - "message": "字段名称已存在", - "description": "Translation key: testDataGenerator_fieldNameDuplicate" - }, - "testDataGenerator_fieldNameInvalid": { - "message": "字段名称只能包含字母、数字和下划线", - "description": "Translation key: testDataGenerator_fieldNameInvalid" - }, - "testDataGenerator_ruleCopySuffix": { - "message": "(副本)", - "description": "Translation key: testDataGenerator_ruleCopySuffix" - } -} diff --git a/src/components/CopyButton.tsx b/src/components/CopyButton.tsx index 6adf5c0..fcc57e6 100644 --- a/src/components/CopyButton.tsx +++ b/src/components/CopyButton.tsx @@ -4,7 +4,6 @@ import { copyTextToClipboard } from '@/utils/clipboard'; import { cn } from '@/lib/utils'; import { buttonVariants, type ButtonProps } from '@/components/ui/button'; import { toast } from 'sonner'; -import { useI18n } from '@/utils/chromeI18n'; interface CopyButtonProps extends Omit { text: string; @@ -19,7 +18,6 @@ export const CopyButton: React.FC = ({ className, ...props }) => { - const { t } = useI18n('common'); const [copied, setCopied] = useState(false); const timerRef = useRef | null>(null); @@ -33,18 +31,18 @@ export const CopyButton: React.FC = ({ e.stopPropagation(); if (!text) { - toast.error(t('messages.copyEmpty')); + toast.error('无内容可复制'); return; } const success = await copyTextToClipboard(text); if (success) { - toast.success(t('messages.copySuccess')); + toast.success('已复制到剪贴板'); setCopied(true); if (timerRef.current) clearTimeout(timerRef.current); timerRef.current = setTimeout(() => setCopied(false), 1500); } else { - toast.error(t('messages.copyError')); + toast.error('复制失败'); } }; @@ -52,8 +50,8 @@ export const CopyButton: React.FC = ({ diff --git a/src/components/PageErrorBoundary.tsx b/src/components/PageErrorBoundary.tsx index b483612..56a835b 100644 --- a/src/components/PageErrorBoundary.tsx +++ b/src/components/PageErrorBoundary.tsx @@ -1,7 +1,6 @@ import { Component, ErrorInfo, ReactNode } from 'react'; import { AlertCircle, RefreshCw } from 'lucide-react'; import { Button } from '@/components/ui/button'; -import { getMessage } from '@/utils/chromeI18n'; interface Props { children: ReactNode; @@ -46,11 +45,9 @@ class PageErrorBoundary extends Component { -

- {getMessage('pageErrorBoundary_title')} -

+

该功能运行异常

- {getMessage('pageErrorBoundary_description')} + 该页面在加载或渲染时遇到了内部脚本错误。您可以尝试重试,或者通过导航菜单切换到其他工具。

{this.state.error && ( @@ -68,7 +65,7 @@ class PageErrorBoundary extends Component { className="font-medium shadow-sm" > - {getMessage('errorBoundary_retry')} + 重新尝试 diff --git a/src/components/RouterContainer.tsx b/src/components/RouterContainer.tsx index a8de5ce..36a46ec 100644 --- a/src/components/RouterContainer.tsx +++ b/src/components/RouterContainer.tsx @@ -1,7 +1,6 @@ import { FEATURES, getEntryPointType } from '@/config/features'; import { useRouter } from '@/providers/RouterProvider'; import { Suspense } from 'react'; -import { useI18n } from '@/utils/chromeI18n'; import PageErrorBoundary from '@/components/PageErrorBoundary'; import PageSkeleton from '@/components/PageSkeleton'; import { cn } from '@/lib/utils'; @@ -11,7 +10,6 @@ const entryPointType = getEntryPointType(); export default function RouterContainer() { const { currentPage, isLoaded } = useRouter(); - const { t } = useI18n('common'); const animationClass = currentPage === 'dashboard' ? 'page-transition-dashboard' : 'page-transition-enter'; @@ -43,9 +41,9 @@ export default function RouterContainer() {
-

{t('router.notFound')}

+

{'页面未找到'}

- {t('router.notFoundDescription', { entryPointType })} + {`该功能在当前运行环境(${entryPointType})下不可用或已被移除。`}

)} diff --git a/src/components/TextInputArea.tsx b/src/components/TextInputArea.tsx index fa79fab..8031803 100644 --- a/src/components/TextInputArea.tsx +++ b/src/components/TextInputArea.tsx @@ -1,6 +1,5 @@ import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react'; import { X } from 'lucide-react'; -import { useI18n } from '@/utils/chromeI18n'; import { cn } from '@/lib/utils'; import { toast } from 'sonner'; // 推荐使用 shadcn 的默认 Toast import { CopyButton } from '@/components/CopyButton'; @@ -114,8 +113,7 @@ const TextInputArea = forwardRef((props const [internalValue, setInternalValue] = useState(defaultValue); const [error, setError] = useState(''); - const { t } = useI18n('common'); - const placeholder = placeholderProp ?? t('textInputArea.placeholder'); + const placeholder = placeholderProp ?? '请输入文本'; const isControlled = controlledValue !== undefined; const value = isControlled ? controlledValue : internalValue; @@ -159,7 +157,7 @@ const TextInputArea = forwardRef((props const handleChange = (e: React.ChangeEvent) => { const newVal = e.target.value; if (maxLength && newVal.length > maxLength) { - const msg = t('charCount', { count: maxLength }); + const msg = `内容不能超过 ${maxLength} 个字符`; setError(msg); toast.warning(msg); return; @@ -181,9 +179,9 @@ const TextInputArea = forwardRef((props onChange?.(''); setError(''); internalRef.current?.focus(); - toast.success(t('textInputArea.cleared')); + toast.success('已清空'); onClear?.(); - }, [isControlled, onChange, onClear, t]); + }, [isControlled, onChange, onClear]); const handleAction = useCallback( (action: ToolbarAction) => { @@ -272,18 +270,13 @@ const TextInputArea = forwardRef((props {/* 右侧系统按钮组 */}
{allowCopy && value && ( - + )} {showClear && value && !disabled && !readOnly && (
-

- {t(feature.labelKey)} -

+

{feature.label}

- {t(feature.descriptionKey)} + {feature.description}

)) ) : (
  • - {t('common:buttons.noResults')} + {'未找到相关工具'}
  • ) ) : ( <>
    - {t('common:buttons.recentSearch')} + {'最近搜索'}
    {displayedHistory.map((item, index) => (
  • - {item.feature && t(item.feature.labelKey)} + {item.feature?.label}

    - {item.feature && t(item.feature.descriptionKey)} + {item.feature?.description}

  • @@ -278,10 +271,19 @@ export default function TopBar() { {/* 右侧:操作区 */}
    - + - +
    diff --git a/src/components/__tests__/TextInputArea.test.tsx b/src/components/__tests__/TextInputArea.test.tsx index 1e10a8e..ceb5b41 100644 --- a/src/components/__tests__/TextInputArea.test.tsx +++ b/src/components/__tests__/TextInputArea.test.tsx @@ -18,12 +18,12 @@ describe('TextInputArea 组件', () => { it('默认显示清空按钮', () => { render( {}} />); - expect(screen.getByRole('button', { name: 'textInputArea.clear' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '清空' })).toBeInTheDocument(); }); it('无内容时清空按钮应隐藏', () => { render( {}} />); - expect(screen.queryByRole('button', { name: 'textInputArea.clear' })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: '清空' })).not.toBeInTheDocument(); }); it('disabled 时清空按钮应隐藏', () => { @@ -57,7 +57,7 @@ describe('TextInputArea 组件', () => { const handleChange = vi.fn(); render(); - fireEvent.click(screen.getByRole('button', { name: 'textInputArea.clear' })); + fireEvent.click(screen.getByRole('button', { name: '清空' })); expect(handleChange).toHaveBeenCalledWith(''); }); @@ -81,7 +81,7 @@ describe('TextInputArea 组件', () => { it('清空按钮应清空内容', () => { render(); - fireEvent.click(screen.getByRole('button', { name: 'textInputArea.clear' })); + fireEvent.click(screen.getByRole('button', { name: '清空' })); expect(screen.getByRole('textbox')).toHaveValue(''); }); @@ -90,14 +90,12 @@ describe('TextInputArea 组件', () => { describe('allowCopy 复制功能', () => { it('allowCopy 且有内容时显示复制按钮', () => { render( {}} allowCopy />); - expect(screen.getByRole('button', { name: 'textInputArea.copyContent' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '复制内容' })).toBeInTheDocument(); }); it('allowCopy 但无内容时隐藏复制按钮', () => { render( {}} allowCopy />); - expect( - screen.queryByRole('button', { name: 'textInputArea.copyContent' }), - ).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: '复制内容' })).not.toBeInTheDocument(); }); it('allowCopy=false 时不显示复制按钮', () => { @@ -113,7 +111,7 @@ describe('TextInputArea 组件', () => { render( {}} allowCopy />); - await user.click(screen.getByRole('button', { name: 'textInputArea.copyContent' })); + await user.click(screen.getByRole('button', { name: '复制内容' })); expect(writeTextSpy).toHaveBeenCalledWith('测试'); }); @@ -126,7 +124,7 @@ describe('TextInputArea 组件', () => { render( {}} allowCopy />); - await user.click(screen.getByRole('button', { name: 'textInputArea.copyContent' })); + await user.click(screen.getByRole('button', { name: '复制内容' })); expect(writeTextSpy).toHaveBeenCalledWith('测试'); }); @@ -158,6 +156,7 @@ describe('TextInputArea 组件', () => { fireEvent.change(textarea, { target: { value: '123456' } }); expect(handleChange).not.toHaveBeenCalledWith('123456'); + expect(screen.getByText('内容不能超过 5 个字符')).toBeInTheDocument(); }); it('未超出 maxLength 的输入应正常触发', () => { @@ -370,7 +369,7 @@ describe('TextInputArea 组件', () => { const writeTextSpy = vi.spyOn(navigator.clipboard, 'writeText').mockResolvedValue(undefined); render( {}} allowCopy />); - await user.click(screen.getByRole('button', { name: 'textInputArea.copyContent' })); + await user.click(screen.getByRole('button', { name: '复制内容' })); expect(writeTextSpy).toHaveBeenCalledWith('测试'); }); @@ -381,7 +380,7 @@ describe('TextInputArea 组件', () => { const handleClear = vi.fn(); render( {}} onClear={handleClear} />); - fireEvent.click(screen.getByRole('button', { name: 'textInputArea.clear' })); + fireEvent.click(screen.getByRole('button', { name: '清空' })); expect(handleClear).toHaveBeenCalledOnce(); }); @@ -389,7 +388,7 @@ describe('TextInputArea 组件', () => { it('不传 onClear 时清空按钮应正常工作', () => { render(); - fireEvent.click(screen.getByRole('button', { name: 'textInputArea.clear' })); + fireEvent.click(screen.getByRole('button', { name: '清空' })); expect(screen.getByRole('textbox')).toHaveValue(''); }); diff --git a/src/config/__tests__/features.test.ts b/src/config/__tests__/features.test.ts index 6ac2402..aff96db 100644 --- a/src/config/__tests__/features.test.ts +++ b/src/config/__tests__/features.test.ts @@ -16,13 +16,13 @@ describe('features', () => { it('should have all required properties for each feature', () => { FEATURES.forEach((feature) => { expect(feature).toHaveProperty('key'); - expect(feature).toHaveProperty('labelKey'); - expect(feature).toHaveProperty('descriptionKey'); + expect(feature).toHaveProperty('label'); + expect(feature).toHaveProperty('description'); expect(feature).toHaveProperty('defaultVisible'); expect(feature).toHaveProperty('components'); expect(typeof feature.key).toBe('string'); - expect(typeof feature.labelKey).toBe('string'); - expect(typeof feature.descriptionKey).toBe('string'); + expect(typeof feature.label).toBe('string'); + expect(typeof feature.description).toBe('string'); expect(typeof feature.defaultVisible).toBe('boolean'); expect(typeof feature.components).toBe('object'); expect(feature.components).toHaveProperty('popup'); @@ -50,14 +50,14 @@ describe('features', () => { const feature = getFeatureByKey('dashboard'); expect(feature).toBeDefined(); expect(feature?.key).toBe('dashboard'); - expect(feature?.labelKey).toBe('dashboard_title'); + expect(feature?.label).toBe('仪表盘'); }); it('should return timestamp feature', () => { const feature = getFeatureByKey('timestamp'); expect(feature).toBeDefined(); expect(feature?.key).toBe('timestamp'); - expect(feature?.labelKey).toBe('timestamp_title'); + expect(feature?.label).toBe('时间戳'); expect(feature?.themeColorKey).toBeDefined(); }); @@ -65,7 +65,7 @@ describe('features', () => { const feature = getFeatureByKey('storageCleaner'); expect(feature).toBeDefined(); expect(feature?.key).toBe('storageCleaner'); - expect(feature?.labelKey).toBe('storageCleaner_title'); + expect(feature?.label).toBe('存储清理'); }); it('should return undefined for invalid key', () => { diff --git a/src/config/features.tsx b/src/config/features.tsx index 7ef8689..f82b90d 100644 --- a/src/config/features.tsx +++ b/src/config/features.tsx @@ -29,8 +29,8 @@ const TestDataGeneratorPage = lazy(() => import('@/pages/TestDataGenerator')); export interface FeatureConfig { key: PageType; - labelKey: string; - descriptionKey: string; + label: string; + description: string; themeColorKey?: PaletteColorKey; icon?: ComponentType; defaultVisible: boolean; @@ -44,8 +44,8 @@ export interface FeatureConfig { export const FEATURES: FeatureConfig[] = [ { key: 'dashboard', - labelKey: 'dashboard_title', - descriptionKey: '', + label: '仪表盘', + description: '', defaultVisible: true, components: { popup: DashboardPage, @@ -55,8 +55,8 @@ export const FEATURES: FeatureConfig[] = [ }, { key: 'timestamp', - labelKey: 'timestamp_title', - descriptionKey: 'timestamp_description', + label: '时间戳', + description: 'Unix 毫秒数转换与格式化', themeColorKey: 'primary', icon: Clock, defaultVisible: true, @@ -68,8 +68,8 @@ export const FEATURES: FeatureConfig[] = [ }, { key: 'storageCleaner', - labelKey: 'storageCleaner_title', - descriptionKey: 'storageCleaner_description', + label: '存储清理', + description: '清理缓存、Cookies 及本地存储', themeColorKey: 'warning', icon: Database, defaultVisible: true, @@ -81,8 +81,8 @@ export const FEATURES: FeatureConfig[] = [ }, { key: 'qrCode', - labelKey: 'qrCode_title', - descriptionKey: 'qrCode_description', + label: '二维码工具', + description: '生成当前选中的 URL 的二维码', themeColorKey: 'success', icon: QrCode, defaultVisible: true, @@ -94,8 +94,8 @@ export const FEATURES: FeatureConfig[] = [ }, { key: 'textStatistics', - labelKey: 'textStatistics_title', - descriptionKey: 'textStatistics_description', + label: '文本统计', + description: '实时分析文本字符、单词及字节', themeColorKey: 'secondary', icon: FileText, defaultVisible: true, @@ -107,8 +107,8 @@ export const FEATURES: FeatureConfig[] = [ }, { key: 'jwt', - labelKey: 'jwt_title', - descriptionKey: 'jwt_description', + label: 'JWT 解析', + description: 'JSON Web Token 解码与查看', themeColorKey: 'info', icon: Key, defaultVisible: true, @@ -120,8 +120,8 @@ export const FEATURES: FeatureConfig[] = [ }, { key: 'jsonDiff', - labelKey: 'jsonDiff_title', - descriptionKey: 'jsonDiff_description', + label: 'JSON 工具', + description: '差异比较、格式化、YAML/TOML 转换及压缩', themeColorKey: 'primary', icon: GitCompareArrows, defaultVisible: true, @@ -133,8 +133,8 @@ export const FEATURES: FeatureConfig[] = [ }, { key: 'base64Converter', - labelKey: 'base64Converter_title', - descriptionKey: 'base64Converter_description', + label: 'Base64 转换器', + description: '文本、文件与图像的 Base64 编码转换', themeColorKey: 'info', icon: ArrowLeftRight, defaultVisible: true, @@ -146,8 +146,8 @@ export const FEATURES: FeatureConfig[] = [ }, { key: 'rightClickRestorer', - labelKey: 'rightClickRestorer_title', - descriptionKey: 'rightClickRestorer_description', + label: '右键恢复', + description: '检测并恢复被网站禁用的浏览器右键菜单', themeColorKey: 'success', icon: MousePointerClick, defaultVisible: true, @@ -159,8 +159,8 @@ export const FEATURES: FeatureConfig[] = [ }, { key: 'testDataGenerator', - labelKey: 'testDataGenerator_title', - descriptionKey: 'testDataGenerator_description', + label: '测试数据生成器', + description: '自定义规则批量生成测试数据', themeColorKey: 'warning', icon: FileSpreadsheet, defaultVisible: true, diff --git a/src/entrypoints/content/contextMenuHandler.ts b/src/entrypoints/content/contextMenuHandler.ts index cc9b940..4594960 100644 --- a/src/entrypoints/content/contextMenuHandler.ts +++ b/src/entrypoints/content/contextMenuHandler.ts @@ -1,11 +1,10 @@ import type { ContextMenuClickedPayload } from '@/utils/messages'; import { MessageAction, onMessage } from '@/utils/messages'; import { getTextStats } from '@/utils/textStatistics'; -import { getMessage } from '@/utils/chromeI18n'; import { hidePopover, showTextStatsResult, showTimestampResult } from './uiPopover'; function convertTimestamp(input: string): string { - const invalidText = getMessage('invalidTimestamp') || 'Invalid Timestamp'; + const invalidText = '无效时间戳'; const num = Number(input.trim()); if (isNaN(num)) { diff --git a/src/pages/Base64Converter/__tests__/TextMode.test.tsx b/src/pages/Base64Converter/__tests__/TextMode.test.tsx index 7df3180..48ea703 100644 --- a/src/pages/Base64Converter/__tests__/TextMode.test.tsx +++ b/src/pages/Base64Converter/__tests__/TextMode.test.tsx @@ -76,7 +76,7 @@ describe('TextMode', () => { }); await waitFor(() => { - expect(screen.getByText('Base64 字符串无效')).toBeInTheDocument(); + expect(screen.getByText('无效的 Base64 字符串')).toBeInTheDocument(); }); }); @@ -118,7 +118,7 @@ describe('TextMode', () => { expect(screen.getByRole('button', { name: 'SGVsbG8=' })).toBeInTheDocument(); }); - fireEvent.click(screen.getByRole('button', { name: 'textInputArea.clear' })); + fireEvent.click(screen.getByRole('button', { name: '清空' })); await waitFor(() => { expect(screen.queryByRole('button', { name: 'SGVsbG8=' })).not.toBeInTheDocument(); @@ -182,9 +182,7 @@ describe('TextMode', () => { }); await waitFor(() => { - expect( - screen.getByText('输入似乎是二进制数据(如图片)。请切换到「图像」选项卡。'), - ).toBeInTheDocument(); + expect(screen.getByText('检测到二进制数据(如图像),请使用图像选项卡')).toBeInTheDocument(); }); }); }); diff --git a/src/pages/Base64Converter/components/Base64ConverterSection.tsx b/src/pages/Base64Converter/components/Base64ConverterSection.tsx index 5a2bbc6..10c7088 100644 --- a/src/pages/Base64Converter/components/Base64ConverterSection.tsx +++ b/src/pages/Base64Converter/components/Base64ConverterSection.tsx @@ -1,6 +1,5 @@ import { Image as ImageIcon, Trash2, Upload } from 'lucide-react'; import TextInputArea from '@/components/TextInputArea'; -import { useI18n } from '@/utils/chromeI18n'; import { CopyButton } from '@/components/CopyButton'; import DecodeResultPaper from './DecodeResultPaper'; import { Button } from '@/components/ui/button'; @@ -20,8 +19,6 @@ interface Base64ConverterSectionProps { } export default function Base64ConverterSection({ mode }: Base64ConverterSectionProps) { - const { t } = useI18n('base64Converter'); - const [direction, setDirection] = useStorageState( `base64Converter/${mode}Mode/direction`, 'encode', @@ -44,7 +41,6 @@ export default function Base64ConverterSection({ mode }: Base64ConverterSectionP setCustomFileName, resetAll, safeFileSelect, - maxFileSizeStr, } = useBase64Converter({ mode }); const handleDirectionChange = (next: Base64ConvertDirection) => { @@ -63,8 +59,8 @@ export default function Base64ConverterSection({ mode }: Base64ConverterSectionP - {t('clickOrDropToReplace')} + {'点击或拖拽以替换文件'} ) : ( @@ -137,14 +133,14 @@ export default function Base64ConverterSection({ mode }: Base64ConverterSectionP )} - {mode === 'image' ? t('clickOrDropToImage') : t('clickOrDropToFile')} + {mode === 'image' ? '点击或拖拽图像到此处' : '点击或拖拽文件到此处'} - {t('maxFileSize', { max: maxFileSizeStr })} + {'最大文件大小:{{max}}'} {mode === 'image' && ( - {t('supportedFormats')} + {'支持 PNG、JPG、WEBP、GIF、BMP、SVG 等格式'} )} @@ -161,17 +157,17 @@ export default function Base64ConverterSection({ mode }: Base64ConverterSectionP
    - {t('base64Output')} + {'Base64 编码结果'}
    @@ -189,14 +185,14 @@ export default function Base64ConverterSection({ mode }: Base64ConverterSectionP
    - {t('originalSize')}:{' '} + {'原始大小'}:{' '} {formatBytes(result.originalBytes)} | - {t('encodedSize')}:{' '} + {'编码大小'}:{' '} {formatBytes(result.outputBytes)} @@ -209,7 +205,7 @@ export default function Base64ConverterSection({ mode }: Base64ConverterSectionP className="h-7 rounded-md text-muted-foreground hover:text-destructive text-[11px] gap-1 px-2" > - {t('clear')} + {'清空'}
    @@ -218,7 +214,7 @@ export default function Base64ConverterSection({ mode }: Base64ConverterSectionP ) : (
    {decoded && ( {/* 标题 */} @@ -53,17 +50,17 @@ export default function DecodeResultPaper({ {/* 文件信息 */}
    - {t('inferredMimeType')}: {mimeType} + {'推断的 MIME 类型'}: {mimeType} - {t('decodedSize')}: {formatBytes(blobSize)} + {'解码大小'}: {formatBytes(blobSize)}
    {/* 文件名输入 */}
    onFileNameChange(e.target.value)} />
    @@ -76,7 +73,7 @@ export default function DecodeResultPaper({ className="w-full rounded-lg font-bold" > - {t('download')} + {'下载'}
    ); diff --git a/src/pages/Base64Converter/components/TextMode.tsx b/src/pages/Base64Converter/components/TextMode.tsx index 59f2ab9..560ca66 100644 --- a/src/pages/Base64Converter/components/TextMode.tsx +++ b/src/pages/Base64Converter/components/TextMode.tsx @@ -20,7 +20,6 @@ export default function TextMode({ onSwitchToImageMode }: TextModeProps = {}) { error, showImageHint, handleClear, - t, } = useTextMode(); return ( @@ -29,8 +28,8 @@ export default function TextMode({ onSwitchToImageMode }: TextModeProps = {}) { - {t('imageDataUriHint')} + 检测到图片的 data URI,请使用「图像」选项卡进行解码。
    )} diff --git a/src/pages/Base64Converter/index.tsx b/src/pages/Base64Converter/index.tsx index d7b3a59..6a8a4e0 100644 --- a/src/pages/Base64Converter/index.tsx +++ b/src/pages/Base64Converter/index.tsx @@ -1,4 +1,3 @@ -import { useI18n } from '@/utils/chromeI18n'; import { useStorageState } from '@/utils/useStorageState'; import type { Base64ConverterPageMode } from '@/types/storage'; import TextMode from './components/TextMode'; @@ -12,7 +11,6 @@ const isValidPageMode = (val: unknown): val is Base64ConverterPageMode => type PageMode = Base64ConverterPageMode; export default function Index() { - const { t } = useI18n('base64Converter'); const [pageMode, setPageMode] = useStorageState( 'base64Converter/pageMode', 'text', @@ -24,9 +22,9 @@ export default function Index() { setPageMode(value)} size="small" diff --git a/src/pages/Base64Converter/useBase64Converter.ts b/src/pages/Base64Converter/useBase64Converter.ts index 9701cc8..b9f8485 100644 --- a/src/pages/Base64Converter/useBase64Converter.ts +++ b/src/pages/Base64Converter/useBase64Converter.ts @@ -1,5 +1,4 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { useI18n } from '@/utils/chromeI18n'; import type { FileToBase64Result } from '@/utils/base64Converter'; import { base64ToBlob, @@ -21,8 +20,6 @@ interface UseBase64ConverterProps { } export function useBase64Converter({ mode }: UseBase64ConverterProps) { - const { t } = useI18n('base64Converter'); - const [result, setResult] = useState(null); const [info, setInfo] = useState(null); const [isLoading, setIsLoading] = useState(false); @@ -62,7 +59,7 @@ export function useBase64Converter({ mode }: UseBase64ConverterProps) { setInfo(null); if (!isFileSizeValid(file.size)) { - setEncodeError(t('fileSizeExceeded', { max: `${MAX_FILE_SIZE / 1024 / 1024} MB` })); + setEncodeError(`文件大小超出限制(最大 ${MAX_FILE_SIZE / 1024 / 1024} MB)`); return; } @@ -71,7 +68,7 @@ export function useBase64Converter({ mode }: UseBase64ConverterProps) { !isSupportedImageType(file.type) && !isSupportedImageExtension(file.name) ) { - setEncodeError(t('unsupportedImageType')); + setEncodeError('不支持的图像格式'); return; } @@ -87,13 +84,13 @@ export function useBase64Converter({ mode }: UseBase64ConverterProps) { if (!cancelRef.current) setResult(res); } catch (e) { if (!cancelRef.current) { - setEncodeError(e instanceof Error ? e.message : t('conversionFailed')); + setEncodeError(e instanceof Error ? e.message : '转换失败'); } } finally { if (!cancelRef.current) setIsLoading(false); } }, - [mode, t], + [mode], ); const safeFileSelect = useCallback( @@ -116,10 +113,10 @@ export function useBase64Converter({ mode }: UseBase64ConverterProps) { const message = e instanceof Error ? e.message : ''; return { decoded: null, - error: message === 'Invalid Base64 string' ? t('invalidBase64') : t('conversionFailed'), + error: message === 'Invalid Base64 string' ? 'Base64 字符串无效' : '转换失败', }; } - }, [debouncedDecodeInput, t]); + }, [debouncedDecodeInput]); const decoded = decodePipeline.decoded; const decodeError = decodePipeline.error; diff --git a/src/pages/Base64Converter/useTextMode.ts b/src/pages/Base64Converter/useTextMode.ts index c911e85..a5ac180 100644 --- a/src/pages/Base64Converter/useTextMode.ts +++ b/src/pages/Base64Converter/useTextMode.ts @@ -1,19 +1,16 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; -import { useI18n } from '@/utils/chromeI18n'; import { base64ToText, textToBase64 } from '@/utils/base64Converter'; import { useContextMenuData } from '@/utils/useContextMenuData'; const IMAGE_DATA_URI_PATTERN = /^\s*data:image\//i; const ERROR_MESSAGE_TO_I18N: Record = { - 'Invalid Base64 string': 'invalidBase64', + 'Invalid Base64 string': '无效的 Base64 字符串', 'Input appears to be binary data (e.g. an image). Please use the Image tab instead.': - 'binaryDataDetected', + '检测到二进制数据(如图像),请使用图像选项卡', }; export function useTextMode() { - const { t } = useI18n('base64Converter'); - const [input, setInput] = useState(''); const [debouncedInput, setDebouncedInput] = useState(''); const [direction, setDirection] = useState<'encode' | 'decode'>('encode'); @@ -50,17 +47,17 @@ export function useTextMode() { const i18nKey = ERROR_MESSAGE_TO_I18N[message]; return { output: '', - error: i18nKey ? t(i18nKey) : message || t('conversionFailed'), + error: i18nKey || message || '转换失败', }; } - }, [debouncedInput, direction, t]); + }, [debouncedInput, direction]); const output = conversionPipeline.output; const error = conversionPipeline.error; const placeholder = - direction === 'encode' ? t('textInputPlaceholder') : t('base64InputPlaceholder'); - const outputLabel = direction === 'encode' ? t('base64Output') : t('textOutput'); + direction === 'encode' ? '输入需要编码为 Base64 的文本...' : '输入需要解码的 Base64 字符串...'; + const outputLabel = direction === 'encode' ? 'Base64 编码结果' : '解码文本结果'; const showImageHint = useMemo( () => direction === 'decode' && IMAGE_DATA_URI_PATTERN.test(input), @@ -90,6 +87,5 @@ export function useTextMode() { error, showImageHint, handleClear, - t, }; } diff --git a/src/pages/Dashboard/index.tsx b/src/pages/Dashboard/index.tsx index dff8264..7fda2a9 100644 --- a/src/pages/Dashboard/index.tsx +++ b/src/pages/Dashboard/index.tsx @@ -1,12 +1,10 @@ import { useRouter } from '@/providers/RouterProvider'; import { getFeatureByKey } from '@/config/features'; import type { PageType } from '@/types/storage'; -import { useI18n } from '@/utils/chromeI18n'; import { cn } from '@/lib/utils'; export default function Index() { const { navigateTo, visiblePages, pageOrder, recentlyUsedTools } = useRouter(); - const { t } = useI18n(['features']); const visibleSet = new Set(visiblePages); @@ -28,7 +26,7 @@ export default function Index() { {showRecent && (

    - {t('dashboard_recentlyUsed')} + {'最近使用'}

    {recentFeatures.map(({ key, feature }) => { @@ -46,7 +44,7 @@ export default function Index() { )} > - {t(feature!.labelKey)} + {feature!.label} ); })} @@ -57,7 +55,7 @@ export default function Index() { {/* 全部工具 — 紧凑 Grid */}

    - {t('dashboard_allTools')} + {'全部工具'}

    {visibleFeatures.map(({ key, feature }) => { @@ -82,7 +80,7 @@ export default function Index() { )} /> - {t(feature!.labelKey)} + {feature!.label} ); diff --git a/src/pages/JsonTools/components/DiffNavigator.tsx b/src/pages/JsonTools/components/DiffNavigator.tsx index 9298b39..cc5cb25 100644 --- a/src/pages/JsonTools/components/DiffNavigator.tsx +++ b/src/pages/JsonTools/components/DiffNavigator.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { ChevronLeft, ChevronRight } from 'lucide-react'; -import { useI18n } from '@/utils/chromeI18n'; import { cn } from '@/lib/utils'; export interface DiffNavigatorProps extends React.HTMLAttributes { @@ -19,8 +18,6 @@ export default function DiffNavigator({ className, ...props }: DiffNavigatorProps) { - const { t } = useI18n('jsonDiff'); - const isFirst = currentIndex <= 0; const isLast = currentIndex >= total - 1; @@ -33,9 +30,7 @@ export default function DiffNavigator({ )} {...props} > - - {t('jsonDiff:noDiffs')} - + {'无差异'}
    ); } @@ -52,7 +47,7 @@ export default function DiffNavigator({
    diff --git a/src/pages/JsonTools/components/JsonConvertSection.tsx b/src/pages/JsonTools/components/JsonConvertSection.tsx index b1a4ef8..3d5489b 100644 --- a/src/pages/JsonTools/components/JsonConvertSection.tsx +++ b/src/pages/JsonTools/components/JsonConvertSection.tsx @@ -1,5 +1,4 @@ import React, { useEffect, useMemo, useState } from 'react'; -import { useI18n } from '@/utils/chromeI18n'; import { formatBytes } from '@/utils/format'; import { CopyButton } from '@/components/CopyButton'; import TextInputArea from '@/components/TextInputArea'; @@ -7,6 +6,27 @@ import { validateJson } from '@/utils/jsonFormatter'; import { cn } from '@/lib/utils'; import type { ConvertFunction, ConvertResult } from '../types'; +const CONVERT_LABELS: Record< + string, + { inputPlaceholder: string; outputLabel: string; emptyHint: string } +> = { + yaml: { + inputPlaceholder: '输入需要转换的 JSON...', + outputLabel: 'YAML 结果', + emptyHint: '输入 JSON 后点击转换', + }, + toml: { + inputPlaceholder: '输入需要转换的 JSON...', + outputLabel: 'TOML 结果', + emptyHint: '输入 JSON 后点击转换', + }, + minify: { + inputPlaceholder: '输入需要压缩的 JSON...', + outputLabel: '压缩结果', + emptyHint: '输入 JSON 后点击压缩', + }, +}; + interface JsonConvertSectionProps extends React.HTMLAttributes { translationPrefix: string; convertFunction: ConvertFunction; @@ -18,12 +38,11 @@ export default function JsonConvertSection({ className, ...props }: JsonConvertSectionProps) { - const { t } = useI18n('jsonFormat'); - const [input, setInput] = useState(''); const [debouncedInput, setDebouncedInput] = useState(''); const pk = translationPrefix; + const labels = CONVERT_LABELS[pk] || CONVERT_LABELS.yaml; // Debounce input useEffect(() => { @@ -65,7 +84,7 @@ export default function JsonConvertSection({
    {/* 输入区 */}
    - {t(`jsonFormat:${pk}OutputLabel`)} + {labels.outputLabel}
    - {t('jsonFormat:originalSize')}:{' '} + {'原始大小'}:{' '} {formatBytes(result.originalBytes)} | - {t('jsonFormat:formattedSize')}:{' '} + {'格式化后大小'}:{' '} {formatBytes(result.outputBytes)} @@ -115,7 +134,7 @@ export default function JsonConvertSection({ ) : (

    - {error ? t('jsonFormat:fixErrorHint') : t(`jsonFormat:${pk}EmptyHint`)} + {error ? '请修正上方 JSON 的语法错误以开启实时流式格式化' : labels.emptyHint}

    )} diff --git a/src/pages/JsonTools/components/JsonFormatSection.tsx b/src/pages/JsonTools/components/JsonFormatSection.tsx index 116c06a..0c8fd0d 100644 --- a/src/pages/JsonTools/components/JsonFormatSection.tsx +++ b/src/pages/JsonTools/components/JsonFormatSection.tsx @@ -1,5 +1,4 @@ import { useEffect, useMemo, useState } from 'react'; -import { useI18n } from '@/utils/chromeI18n'; import { formatJson, type JsonFormatOptions, @@ -14,8 +13,6 @@ import { Checkbox } from '@/components/ui/checkbox'; import { Label } from '@/components/ui/label'; export default function JsonFormatSection() { - const { t } = useI18n('jsonFormat'); - const [input, setInput] = useState(''); const [debouncedInput, setDebouncedInput] = useState(''); const [indentSize, setIndentSize] = useState(2); @@ -66,7 +63,7 @@ export default function JsonFormatSection() { {/* 缩进配置区 */}
    - {t('jsonFormat:indentSize')} + {'缩进'} - {t('jsonFormat:sortKeys')} + {'键名排序'}
    @@ -102,7 +99,7 @@ export default function JsonFormatSection() { {/* 满血版输入终端 */}
    - {t('jsonFormat:outputLabel')} + {'格式化结果'}
    - {t('jsonFormat:originalSize')}:{' '} + {'原始大小'}:{' '} {formatBytes(result.originalBytes)} | - {t('jsonFormat:formattedSize')}:{' '} + {'格式化后大小'}:{' '} {formatBytes(result.formattedBytes)} @@ -154,7 +151,7 @@ export default function JsonFormatSection() { /* 空状态指示引导区 */

    - {error ? t('jsonFormat:fixErrorHint') : t('jsonFormat:emptyHint')} + {error ? '请修正上方 JSON 的语法错误以开启实时流式格式化' : '输入 JSON 后点击格式化'}

    )} diff --git a/src/pages/JsonTools/index.tsx b/src/pages/JsonTools/index.tsx index b0ac7e7..de68b49 100644 --- a/src/pages/JsonTools/index.tsx +++ b/src/pages/JsonTools/index.tsx @@ -1,4 +1,3 @@ -import { useI18n } from '@/utils/chromeI18n'; import JsonDiffInput from './components/JsonDiffInput'; import DiffResult from './components/DiffResult'; import DiffNavigator from './components/DiffNavigator'; @@ -12,7 +11,6 @@ import type { ViewMode } from './types'; type PageMode = JsonToolsPageMode; export default function Index() { - const { t } = useI18n(['jsonDiff', 'jsonFormat']); const { pageMode, setPageMode, @@ -41,11 +39,11 @@ export default function Index() { value={pageMode} onChange={(v: PageMode) => setPageMode(v)} options={[ - { value: 'diff', label: t('jsonFormat:diffMode') }, - { value: 'format', label: t('jsonFormat:formatMode') }, - { value: 'yaml', label: t('jsonFormat:yamlMode') }, - { value: 'toml', label: t('jsonFormat:tomlMode') }, - { value: 'minify', label: t('jsonFormat:minifyMode') }, + { value: 'diff', label: '差异比较' }, + { value: 'format', label: '格式化' }, + { value: 'yaml', label: 'YAML' }, + { value: 'toml', label: 'TOML' }, + { value: 'minify', label: '压缩' }, ]} size="small" className="w-full sm:w-auto" @@ -58,8 +56,8 @@ export default function Index() { value={viewMode} onChange={(v: ViewMode) => setViewMode(v)} options={[ - { value: 'sideBySide', label: t('jsonDiff:sideBySideMode') }, - { value: 'unified', label: t('jsonDiff:unifiedMode') }, + { value: 'sideBySide', label: '并排' }, + { value: 'unified', label: '统一' }, ]} size="small" /> @@ -67,16 +65,16 @@ export default function Index() {

    - {leftError || rightError ? t('jsonDiff:fixErrorHint') : t('jsonDiff:emptyHint')} + {leftError || rightError + ? '请修正上方 JSON 的语法错误以开启实时流式比对' + : '输入两侧 JSON 后点击比较'}

    )} diff --git a/src/pages/JsonTools/useJsonTools.ts b/src/pages/JsonTools/useJsonTools.ts index b576319..2954413 100644 --- a/src/pages/JsonTools/useJsonTools.ts +++ b/src/pages/JsonTools/useJsonTools.ts @@ -1,5 +1,4 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; -import { useI18n } from '@/utils/chromeI18n'; import { useStorageState } from '@/utils/useStorageState'; import { diffJson } from '@/utils/diffEngine'; import { jsonToYaml } from '@/utils/jsonToYaml'; @@ -34,7 +33,6 @@ export interface UseJsonToolsReturn { } export function useJsonTools(): UseJsonToolsReturn { - const { t } = useI18n(['jsonDiff', 'jsonFormat']); const [pageMode, setPageMode] = useStorageState('jsonTools/pageMode', 'diff', isValidPageMode); // Diff inputs @@ -54,12 +52,12 @@ export function useJsonTools(): UseJsonToolsReturn { // Parse debounced inputs const parseState = useMemo(() => { - const invalidMsg = t('jsonDiff:invalidJson'); + const invalidMsg = '无效的 JSON 格式'; return { left: tryParse(debouncedLeft, invalidMsg), right: tryParse(debouncedRight, invalidMsg), }; - }, [debouncedLeft, debouncedRight, t]); + }, [debouncedLeft, debouncedRight]); const leftError = parseState.left.error; const rightError = parseState.right.error; diff --git a/src/pages/Jwt/JwtSection.tsx b/src/pages/Jwt/JwtSection.tsx index 7c77fe1..8d78ecd 100644 --- a/src/pages/Jwt/JwtSection.tsx +++ b/src/pages/Jwt/JwtSection.tsx @@ -1,5 +1,4 @@ import { CopyButton } from '@/components/CopyButton'; -import { useI18n } from '@/utils/chromeI18n'; import { stringifyJson } from '@/utils/jwt'; import { cn } from '@/lib/utils'; @@ -18,7 +17,6 @@ export default function JwtSection({ bgClass, borderClass, }: JwtSectionProps) { - const { t } = useI18n('jwt'); return (
    @@ -31,7 +29,7 @@ export default function JwtSection({ />
    -        {content ? stringifyJson(content) : t('jwt:invalidFormat')}
    +        {content ? stringifyJson(content) : '无法解析'}
           
    ); diff --git a/src/pages/Jwt/index.tsx b/src/pages/Jwt/index.tsx index 31a68e6..5d77565 100644 --- a/src/pages/Jwt/index.tsx +++ b/src/pages/Jwt/index.tsx @@ -2,10 +2,8 @@ import TextInputArea from '@/components/TextInputArea'; import { CopyButton } from '@/components/CopyButton'; import JwtSection from './JwtSection'; import { useJwt } from './useJwt'; -import { useI18n } from '@/utils/chromeI18n'; export default function Index() { - const { t } = useI18n(['jwt', 'jsonFormat']); const { jwtInput, result, handleChange, handleClear } = useJwt(); return ( @@ -14,7 +12,7 @@ export default function Index() {
    - {t('jwt:signatureTitle')} + {'签名'}
    - {result.signature || t('jwt:noSignature')} + {result.signature || '无签名'}
    @@ -60,9 +58,7 @@ export default function Index() { {result?.error && (
    -

    - {t('jsonFormat:invalidJson')} -

    +

    {'无效的 JSON 格式'}

    )}
    diff --git a/src/pages/QrCode/components/GeneratePanel.tsx b/src/pages/QrCode/components/GeneratePanel.tsx index cbb114e..7db02f6 100644 --- a/src/pages/QrCode/components/GeneratePanel.tsx +++ b/src/pages/QrCode/components/GeneratePanel.tsx @@ -1,7 +1,6 @@ import { Loader2, Pencil, QrCode } from 'lucide-react'; import TextInputArea from '@/components/TextInputArea'; import QrCodePreview from './QrCodePreview'; -import { useI18n } from '@/utils/chromeI18n'; import { useQrCodeContext } from '../contexts/QrCodeContext'; import { Button } from '@/components/ui/button'; import { Label } from '@/components/ui/label'; @@ -11,7 +10,6 @@ import { cn } from '@/lib/utils'; const TEXT_PREVIEW_MAX_LENGTH = 80; export default function GeneratePanel() { - const { t } = useI18n('qrCode'); const { generatorState, setTextToEncode, @@ -42,13 +40,13 @@ export default function GeneratePanel() { >
    - {t('qrCode:generating')} + {'生成中...'} ) : ( <> - {t('qrCode:generateButton')} + {'生成二维码'} )} @@ -88,11 +86,11 @@ export default function GeneratePanel() {
    { - const { t } = useI18n('qrCode'); const fileInputRef = useRef(null); const handleFileChange = useCallback( @@ -47,8 +45,8 @@ const ImageUploader = ({ URL.revokeObjectURL(previewUrl); } onClearFile(); - toast.success(t('qrCode:imageCleared')); - }, [previewUrl, onClearFile, t]); + toast.success('图片已清除'); + }, [previewUrl, onClearFile]); const handleInputChange = (e: React.ChangeEvent) => { if (e.target.files && e.target.files.length > 0) { @@ -88,10 +86,10 @@ const ImageUploader = ({ if (file) { try { handleFileChange(file); - toast.success(t('qrCode:imagePasted')); + toast.success('图片粘贴成功,正在解析...'); } catch (error) { console.error('处理粘贴图片失败:', error); - toast.error(t('qrCode:imagePasteError')); + toast.error('粘贴图片失败,请重试'); } } break; @@ -104,7 +102,7 @@ const ImageUploader = ({ return () => { document.removeEventListener('paste', handlePaste); }; - }, [handleFileChange, t]); + }, [handleFileChange]); return (
    {selectedFile.name} - {t('qrCode:clickToChange')} + {'点击更换图片'}
    ) : ( <> @@ -160,10 +158,10 @@ const ImageUploader = ({ className="w-12 h-12 text-muted-foreground mx-auto mb-2" /> - {t('qrCode:clickToUpload')} + {'点击、拖拽或粘贴上传二维码图片'} - {t('qrCode:supportFormats')} + {'支持 PNG、JPG、WEBP、Base64 格式'} )} diff --git a/src/pages/QrCode/components/ParsePanel.tsx b/src/pages/QrCode/components/ParsePanel.tsx index 998397a..96dfb31 100644 --- a/src/pages/QrCode/components/ParsePanel.tsx +++ b/src/pages/QrCode/components/ParsePanel.tsx @@ -3,14 +3,12 @@ import { RefreshCw } from 'lucide-react'; import { toast } from 'sonner'; import TextInputArea from '@/components/TextInputArea'; import ImageUploader from './ImageUploader'; -import { useI18n } from '@/utils/chromeI18n'; import { useQrCodeContext } from '../contexts/QrCodeContext'; import { Button } from '@/components/ui/button'; import { Label } from '@/components/ui/label'; import { cn } from '@/lib/utils'; export default function ParsePanel() { - const { t } = useI18n('qrCode'); const { parserState, setParserState, handleFileChange, handleClearFile } = useQrCodeContext(); const hasFile = parserState.selectedFile !== null; @@ -27,7 +25,7 @@ export default function ParsePanel() { const file = items[i].getAsFile(); if (file) { handleFileChange(file); - toast.success(t('qrCode:imagePasted')); + toast.success('图片粘贴成功,正在解析...'); } return; } @@ -41,14 +39,14 @@ export default function ParsePanel() { const blob = await response.blob(); const file = new File([blob], 'pasted-image.png', { type: blob.type }); handleFileChange(file); - toast.success(t('qrCode:imagePasted')); + toast.success('图片粘贴成功,正在解析...'); } catch (error) { console.error('处理 Base64 图片失败:', error); - toast.error(t('qrCode:imagePasteError')); + toast.error('粘贴图片失败,请重试'); } } }, - [handleFileChange, t], + [handleFileChange], ); useEffect(() => { @@ -82,11 +80,11 @@ export default function ParsePanel() {
    @@ -98,7 +96,7 @@ export default function ParsePanel() {

    {parserState.selectedFile?.name}

    {parserState.parsing && ( -

    {t('qrCode:parsing')}

    +

    {'解析中...'}

    )}
    @@ -113,7 +111,7 @@ export default function ParsePanel() { >
    { - const { t } = useI18n('qrCode'); - // 空状态下的虚线骨架屏 if (!qrCodeDataUrl) { return ( @@ -37,7 +34,7 @@ const QrCodePreview = ({ {...props} >

    - {placeholderText || t('qrCode:qrCodeWillShow')} + {placeholderText || '二维码将显示在这里'}

    ); @@ -68,12 +65,12 @@ const QrCodePreview = ({
    diff --git a/src/pages/QrCode/hooks/useQrCode.ts b/src/pages/QrCode/hooks/useQrCode.ts index 8ec8eff..66ab3d0 100644 --- a/src/pages/QrCode/hooks/useQrCode.ts +++ b/src/pages/QrCode/hooks/useQrCode.ts @@ -3,7 +3,6 @@ import QRious from 'qrious'; import { toast } from 'sonner'; import { parseQrCodeFromFile } from '@/utils/qrCodeParser'; import { useContextMenuData } from '@/utils/useContextMenuData'; -import { useI18n } from '@/utils/chromeI18n'; import type { QrCodeContextValue } from '../contexts/QrCodeContext'; import type { QrCodeGeneratorState, QrCodeMode, QrCodeParserState } from '../types'; @@ -78,8 +77,6 @@ function generateQrCodeDataUrl(text: string): string { } export function useQrCode(): QrCodeContextValue { - const { t } = useI18n('qrCode'); - const [mode, setMode] = useState('generate'); const [generatorState, setGeneratorState] = useState({ @@ -112,40 +109,37 @@ export function useQrCode(): QrCodeContextValue { }, []); /** 自动检测URL并生成二维码 */ - const autoGenerateIfUrl = useCallback( - (text: string) => { - if (debounceTimerRef.current) { - clearTimeout(debounceTimerRef.current); + const autoGenerateIfUrl = useCallback((text: string) => { + if (debounceTimerRef.current) { + clearTimeout(debounceTimerRef.current); + } + + if (!isUrl(text)) { + return; + } + + debounceTimerRef.current = setTimeout(() => { + setGeneratorState((prev) => ({ ...prev, generating: true, inputError: '' })); + + const qrCodeDataUrl = generateQrCodeDataUrl(text); + + if (qrCodeDataUrl) { + setGeneratorState((prev) => ({ + ...prev, + step: 'preview', + savedText: text.trim(), + qrCodeDataUrl, + generating: false, + })); + } else { + setGeneratorState((prev) => ({ + ...prev, + generating: false, + inputError: '生成二维码失败,请重试', + })); } - - if (!isUrl(text)) { - return; - } - - debounceTimerRef.current = setTimeout(() => { - setGeneratorState((prev) => ({ ...prev, generating: true, inputError: '' })); - - const qrCodeDataUrl = generateQrCodeDataUrl(text); - - if (qrCodeDataUrl) { - setGeneratorState((prev) => ({ - ...prev, - step: 'preview', - savedText: text.trim(), - qrCodeDataUrl, - generating: false, - })); - } else { - setGeneratorState((prev) => ({ - ...prev, - generating: false, - inputError: t('qrCode:generateError'), - })); - } - }, DEBOUNCE_DELAY); - }, - [t], - ); + }, DEBOUNCE_DELAY); + }, []); const setTextToEncode = useCallback( (text: string) => { @@ -160,8 +154,8 @@ export function useQrCode(): QrCodeContextValue { const text = generatorState.textToEncode.trim(); if (!text) { - setGeneratorState((prev) => ({ ...prev, inputError: t('qrCode:inputRequired') })); - toast.error(t('qrCode:inputRequired')); + setGeneratorState((prev) => ({ ...prev, inputError: '请输入内容' })); + toast.error('请输入内容'); return; } @@ -176,9 +170,9 @@ export function useQrCode(): QrCodeContextValue { setGeneratorState((prev) => ({ ...prev, generating: false, - inputError: t('qrCode:generateError'), + inputError: '生成二维码失败,请重试', })); - toast.error(t('qrCode:generateError')); + toast.error('生成二维码失败,请重试'); return; } @@ -190,7 +184,7 @@ export function useQrCode(): QrCodeContextValue { generating: false, })); }, 0); - }, [generatorState.textToEncode, t]); + }, [generatorState.textToEncode]); /** 返回编辑态,保留上次输入内容 */ const backToEdit = useCallback(() => { @@ -207,46 +201,43 @@ export function useQrCode(): QrCodeContextValue { }, []); /** 从图片URL解析二维码 */ - const parseQrCodeFromUrl = useCallback( - async (imageUrl: string) => { - try { - setParserState((prev) => ({ - ...prev, - parsing: true, - parseError: '', - decodedResult: '', - previewUrl: imageUrl, - selectedFile: null, - })); + const parseQrCodeFromUrl = useCallback(async (imageUrl: string) => { + try { + setParserState((prev) => ({ + ...prev, + parsing: true, + parseError: '', + decodedResult: '', + previewUrl: imageUrl, + selectedFile: null, + })); - // 从URL获取图片并转换为File对象 - const response = await fetch(imageUrl); - const blob = await response.blob(); - const file = new File([blob], 'qrcode-image.png', { type: blob.type }); + // 从URL获取图片并转换为File对象 + const response = await fetch(imageUrl); + const blob = await response.blob(); + const file = new File([blob], 'qrcode-image.png', { type: blob.type }); - setParserState((prev) => ({ ...prev, selectedFile: file })); + setParserState((prev) => ({ ...prev, selectedFile: file })); - const result = await parseQrCodeFromFile(file); + const result = await parseQrCodeFromFile(file); - if (result.success && result.data) { - setParserState((prev) => ({ ...prev, decodedResult: result.data! })); - toast.success(t('qrCode:parseSuccess')); - } else { - const errorMsg = result.error || t('qrCode:noQrDetected'); - setParserState((prev) => ({ ...prev, parseError: errorMsg })); - toast.error(errorMsg); - } - } catch (error) { - console.error('解析图片二维码失败:', error); - const errorMsg = error instanceof Error ? error.message : t('qrCode:parseError'); + if (result.success && result.data) { + setParserState((prev) => ({ ...prev, decodedResult: result.data! })); + toast.success('二维码解析成功'); + } else { + const errorMsg = result.error || '未检测到二维码,请确保图片清晰且包含二维码'; setParserState((prev) => ({ ...prev, parseError: errorMsg })); toast.error(errorMsg); - } finally { - setParserState((prev) => ({ ...prev, parsing: false })); } - }, - [t], - ); + } catch (error) { + console.error('解析图片二维码失败:', error); + const errorMsg = error instanceof Error ? error.message : '解析二维码失败,请重试'; + setParserState((prev) => ({ ...prev, parseError: errorMsg })); + toast.error(errorMsg); + } finally { + setParserState((prev) => ({ ...prev, parsing: false })); + } + }, []); /** 右键菜单传入URL时,自动生成二维码或解析图片 */ const handleContextMenuData = useCallback( @@ -294,32 +285,29 @@ export function useQrCode(): QrCodeContextValue { useContextMenuData({ featureKey: 'qrCode', onData: handleContextMenuData }); // 反向活态解析二维码算法 - const parseQrCode = useCallback( - async (file: File) => { - try { - setParserState((prev) => ({ ...prev, parsing: true, parseError: '', decodedResult: '' })); + const parseQrCode = useCallback(async (file: File) => { + try { + setParserState((prev) => ({ ...prev, parsing: true, parseError: '', decodedResult: '' })); - const result = await parseQrCodeFromFile(file); + const result = await parseQrCodeFromFile(file); - if (result.success && result.data) { - setParserState((prev) => ({ ...prev, decodedResult: result.data! })); - toast.success(t('qrCode:parseSuccess')); - } else { - const errorMsg = result.error || t('qrCode:noQrDetected'); - setParserState((prev) => ({ ...prev, parseError: errorMsg })); - toast.error(errorMsg); - } - } catch (error) { - console.error('解析二维码失败:', error); - const errorMsg = error instanceof Error ? error.message : t('qrCode:parseError'); + if (result.success && result.data) { + setParserState((prev) => ({ ...prev, decodedResult: result.data! })); + toast.success('二维码解析成功'); + } else { + const errorMsg = result.error || '未检测到二维码,请确保图片清晰且包含二维码'; setParserState((prev) => ({ ...prev, parseError: errorMsg })); toast.error(errorMsg); - } finally { - setParserState((prev) => ({ ...prev, parsing: false })); } - }, - [t], - ); + } catch (error) { + console.error('解析二维码失败:', error); + const errorMsg = error instanceof Error ? error.message : '解析二维码失败,请重试'; + setParserState((prev) => ({ ...prev, parseError: errorMsg })); + toast.error(errorMsg); + } finally { + setParserState((prev) => ({ ...prev, parsing: false })); + } + }, []); const downloadQrCode = useCallback(() => { if (!generatorState.qrCodeDataUrl) return; @@ -328,8 +316,8 @@ export function useQrCode(): QrCodeContextValue { link.href = generatorState.qrCodeDataUrl; link.download = 'qrcode.png'; link.click(); - toast.success(t('qrCode:qrCodeDownloadSuccess')); - }, [generatorState.qrCodeDataUrl, t]); + toast.success('二维码下载成功'); + }, [generatorState.qrCodeDataUrl]); const copyQrCode = useCallback(async () => { if (!generatorState.qrCodeDataUrl) return; @@ -344,12 +332,12 @@ export function useQrCode(): QrCodeContextValue { }), ]); - toast.success(t('qrCode:qrCodeCopySuccess')); + toast.success('二维码已复制到剪贴板'); } catch (error) { console.error('复制二维码失败:', error); - toast.error(t('qrCode:copyError')); + toast.error('复制失败,请重试'); } - }, [generatorState.qrCodeDataUrl, t]); + }, [generatorState.qrCodeDataUrl]); const handleFileChange = useCallback( (file: File) => { diff --git a/src/pages/QrCode/index.tsx b/src/pages/QrCode/index.tsx index 8591e04..adb1dde 100644 --- a/src/pages/QrCode/index.tsx +++ b/src/pages/QrCode/index.tsx @@ -1,6 +1,5 @@ import SwitchButtonGroup from '@/components/SwitchButtonGroup'; -import { useI18n } from '@/utils/chromeI18n'; import { QrCodeContext } from './contexts/QrCodeContext'; import { useQrCode } from './hooks/useQrCode'; import GeneratePanel from './components/GeneratePanel'; @@ -8,13 +7,12 @@ import ParsePanel from './components/ParsePanel'; import type { QrCodeMode } from './types'; export default function Index() { - const { t } = useI18n('qrCode'); const qrCode = useQrCode(); // 模式选项驱动骨架 const modeOptions = [ - { value: 'generate' as QrCodeMode, label: t('qrCode:urlToQr') }, - { value: 'parse' as QrCodeMode, label: t('qrCode:qrToUrl') }, + { value: 'generate' as QrCodeMode, label: '文本转二维码' }, + { value: 'parse' as QrCodeMode, label: '二维码转文本' }, ]; return ( diff --git a/src/pages/RightClickRestorer/index.tsx b/src/pages/RightClickRestorer/index.tsx index 35dec5f..61a2540 100644 --- a/src/pages/RightClickRestorer/index.tsx +++ b/src/pages/RightClickRestorer/index.tsx @@ -3,17 +3,15 @@ import { Label } from '@/components/ui/label'; import { Badge } from '@/components/ui/badge'; import { Shield, ShieldCheck, MousePointerClick, AlertTriangle } from 'lucide-react'; import { useRightClickRestorer } from './useRightClickRestorer'; -import { useI18n } from '@/utils/chromeI18n'; export default function Index() { - const { t } = useI18n('rightClickRestorer'); const { domain, isLoading, isUnlocked, isUnsupported, unlock } = useRightClickRestorer(); if (isLoading) { return (
    - {t('rightClickRestorer:loading')} + {'正在加载...'}
    ); @@ -24,7 +22,7 @@ export default function Index() { {/* Current Domain */}
    - +
    {domain || '—'} @@ -32,17 +30,17 @@ export default function Index() { {isUnsupported ? ( - {t('rightClickRestorer:unsupported')} + {'不支持'} ) : isUnlocked ? ( - {t('rightClickRestorer:statusUnlocked')} + {'已解锁'} ) : ( - {t('rightClickRestorer:statusLocked')} + {'未解锁'} )}
    @@ -55,16 +53,18 @@ export default function Index() { {isUnsupported ? ( <>

    - {t('rightClickRestorer:unsupportedDesc')} + {'当前页面为浏览器内部页面或扩展页面,无法解锁右键功能。请切换到普通网页后重试。'}

    ) : ( <> -

    {t('rightClickRestorer:unlockDesc')}

    +

    + {'点击下方按钮,为当前网站临时解锁右键菜单。刷新页面后需要重新解锁。'} +

    )} diff --git a/src/pages/StorageCleaner/components/AutoRefreshToggle.tsx b/src/pages/StorageCleaner/components/AutoRefreshToggle.tsx index 6b353fb..0926cd7 100644 --- a/src/pages/StorageCleaner/components/AutoRefreshToggle.tsx +++ b/src/pages/StorageCleaner/components/AutoRefreshToggle.tsx @@ -14,7 +14,6 @@ */ import React from 'react'; -import { useI18n } from '@/utils/chromeI18n'; import { cn } from '@/lib/utils'; import { Switch } from '@/components/ui/switch'; import { Label } from '@/components/ui/label'; @@ -43,8 +42,6 @@ export default function AutoRefreshToggle({ className, ...props }: AutoRefreshToggleProps) { - const { t } = useI18n('storageCleaner'); - return (
    - {t('storageCleaner:autoRefresh')} + {'清理后自动刷新页面'} diff --git a/src/pages/StorageCleaner/components/CleaningResult.tsx b/src/pages/StorageCleaner/components/CleaningResult.tsx index 7025838..ee1f05f 100644 --- a/src/pages/StorageCleaner/components/CleaningResult.tsx +++ b/src/pages/StorageCleaner/components/CleaningResult.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { CheckCircle, XCircle } from 'lucide-react'; import type { CleaningResult as CleaningResultType } from '@/types/storage'; import { formatCleaningResult } from '@/utils/storageCleaner'; -import { useI18n } from '@/utils/chromeI18n'; import { cn } from '@/lib/utils'; interface CleaningResultProps extends React.HTMLAttributes { @@ -10,8 +9,6 @@ interface CleaningResultProps extends React.HTMLAttributes { } export default function CleaningResult({ result, className, ...props }: CleaningResultProps) { - const { t } = useI18n('storageCleaner'); - if (!result) return null; const isSuccess = result.overallSuccess; @@ -33,9 +30,7 @@ export default function CleaningResult({ result, className, ...props }: Cleaning )} - {isSuccess - ? formatCleaningResult(result, t) - : result.error || t('storageCleaner:partialFailure')} + {isSuccess ? formatCleaningResult(result) : result.error || '部分清理失败'}
    diff --git a/src/pages/StorageCleaner/components/ErrorDisplay.tsx b/src/pages/StorageCleaner/components/ErrorDisplay.tsx index 2568831..5f3728d 100644 --- a/src/pages/StorageCleaner/components/ErrorDisplay.tsx +++ b/src/pages/StorageCleaner/components/ErrorDisplay.tsx @@ -1,5 +1,4 @@ import { AlertCircle } from 'lucide-react'; -import { useI18n } from '@/utils/chromeI18n'; import { cn } from '@/lib/utils'; interface ErrorDisplayProps extends React.HTMLAttributes { @@ -7,8 +6,6 @@ interface ErrorDisplayProps extends React.HTMLAttributes { } export default function ErrorDisplay({ error, className, ...props }: ErrorDisplayProps) { - const { t } = useI18n('storageCleaner'); - return (

    - {t('storageCleaner:errorStandardOnly')} + {'存储清理功能仅适用于标准网页'}

    diff --git a/src/pages/StorageCleaner/components/OptionItem.tsx b/src/pages/StorageCleaner/components/OptionItem.tsx index 578ce54..dd4d6f3 100644 --- a/src/pages/StorageCleaner/components/OptionItem.tsx +++ b/src/pages/StorageCleaner/components/OptionItem.tsx @@ -1,10 +1,18 @@ import React from 'react'; import { formatBytes } from '@/utils/format'; -import { useI18n } from '@/utils/chromeI18n'; import { cn } from '@/lib/utils'; import type { StorageSizeInfo } from '../useStorageCleaner'; import { Checkbox } from '@/components/ui/checkbox'; +const OPTION_LABELS: Record = { + localStorage: 'Local Storage', + sessionStorage: 'Session Storage', + indexedDB: '站点存储', + cookies: 'Cookies', + cacheStorage: 'Cache Storage', + serviceWorkers: 'Service Workers', +}; + interface OptionItemProps extends React.HTMLAttributes { labelKey: string; checked: boolean; @@ -20,10 +28,9 @@ export default function OptionItem({ className, ...props }: OptionItemProps) { - const { t } = useI18n('storageCleaner'); - const sizeValue = sizeInfo?.value; const isCount = sizeInfo?.displayType === 'count'; + const label = OPTION_LABELS[labelKey] || labelKey; return (
    - {t(labelKey)} + {label} {sizeValue !== undefined && sizeValue > 0 ? ( @@ -54,11 +61,11 @@ export default function OptionItem({ checked ? 'text-primary/70' : 'text-muted-foreground/70', )} > - {isCount ? `${sizeValue} ${t('storageCleaner:countUnit')}` : formatBytes(sizeValue)} + {isCount ? `${sizeValue} 项` : formatBytes(sizeValue)} ) : ( - {t('storageCleaner:noData')} + {'无数据'} )}
    diff --git a/src/pages/StorageCleaner/components/StorageCleanerConfirm.tsx b/src/pages/StorageCleaner/components/StorageCleanerConfirm.tsx index b3397cc..5041d30 100644 --- a/src/pages/StorageCleaner/components/StorageCleanerConfirm.tsx +++ b/src/pages/StorageCleaner/components/StorageCleanerConfirm.tsx @@ -10,7 +10,6 @@ import { import { Badge } from '@/components/ui/badge'; import { AlertTriangle } from 'lucide-react'; import type { StorageCleanerOptions } from '@/types/storage'; -import { useI18n } from '@/utils/chromeI18n'; import { cn } from '@/lib/utils'; export interface StorageCleanerConfirmProps { @@ -20,17 +19,24 @@ export interface StorageCleanerConfirmProps { options: StorageCleanerOptions; } +const OPTION_LABELS: Record = { + localStorage: 'Local Storage', + sessionStorage: 'Session Storage', + indexedDB: '站点存储', + cookies: 'Cookies', + cacheStorage: 'Cache Storage', + serviceWorkers: 'Service Workers', +}; + export function StorageCleanerConfirm({ open, onClose, onConfirm, options, }: StorageCleanerConfirmProps) { - const { t } = useI18n('storageCleaner'); - const selectedOptions = Object.entries(options) .filter(([_, value]) => value) - .map(([key, _]) => t(`storageCleaner:options.${key as keyof StorageCleanerOptions}`)); + .map(([key, _]) => OPTION_LABELS[key] || key); return ( !isOpen && onClose()}> @@ -42,14 +48,14 @@ export function StorageCleanerConfirm({ {/* 头部标题区域 */} - {t('storageCleaner:confirmTitle')} + {'确认清理数据?'} {/* 内容主体:限制最大宽度,防止内部元素在大分辨率下被横向拉得太松散 */}
    - {t('storageCleaner:confirmDesc')} + {'您将永久删除当前页面的以下选定存储项。'} {/* 待清理项目徽章群 */} @@ -69,7 +75,7 @@ export function StorageCleanerConfirm({
    - {t('storageCleaner:irreversible')} + {'此操作不可撤销'}
    @@ -81,7 +87,7 @@ export function StorageCleanerConfirm({ onClick={onConfirm} className="w-full text-xs font-bold shadow-sm h-9" > - {t('storageCleaner:confirmAction')} + {'确认清理'} diff --git a/src/pages/StorageCleaner/components/StorageOptionsGrid.tsx b/src/pages/StorageCleaner/components/StorageOptionsGrid.tsx index 9873e1d..4905988 100644 --- a/src/pages/StorageCleaner/components/StorageOptionsGrid.tsx +++ b/src/pages/StorageCleaner/components/StorageOptionsGrid.tsx @@ -2,7 +2,6 @@ import React from 'react'; import type { StorageCleanerOptions } from '@/types/storage'; import type { StorageSizeInfo } from '../useStorageCleaner'; import OptionItem from './OptionItem'; -import { useI18n } from '@/utils/chromeI18n'; import { cn } from '@/lib/utils'; import { Checkbox } from '@/components/ui/checkbox'; import { Label } from '@/components/ui/label'; @@ -26,8 +25,6 @@ export default function StorageOptionsGrid({ className, ...props }: StorageOptionsGridProps) { - const { t } = useI18n('storageCleaner'); - const optionKeys: (keyof StorageCleanerOptions)[] = [ 'localStorage', 'sessionStorage', @@ -48,7 +45,7 @@ export default function StorageOptionsGrid({ {optionKeys.map((key) => ( onOptionChange(key)} @@ -62,7 +59,7 @@ export default function StorageOptionsGrid({ className="border-t border-border flex justify-between items-center pl-3.5 pr-7 py-2.5 bg-muted/20 hover:bg-muted/40 cursor-pointer select-none transition-colors" > - {t('storageCleaner:initializing')} + {'正在读取站点数据...'}
    ); @@ -75,10 +72,10 @@ export default function Index() { {loading ? ( <> - {t('storageCleaner:cleaning')} + {'正在清理...'} ) : ( - t('storageCleaner:cleanNow') + '立即清理' )}
    diff --git a/src/pages/StorageCleaner/useStorageCleaner.ts b/src/pages/StorageCleaner/useStorageCleaner.ts index 9b46579..f4ef60c 100644 --- a/src/pages/StorageCleaner/useStorageCleaner.ts +++ b/src/pages/StorageCleaner/useStorageCleaner.ts @@ -16,7 +16,6 @@ import { getSessionStorageSize, isRestrictedUrl, } from '@/utils/storageCleaner'; -import { useI18n } from '@/utils/chromeI18n'; import { toast } from 'sonner'; const DEFAULT_OPTIONS: StorageCleanerOptions = { @@ -59,7 +58,6 @@ export interface UseStorageCleanerReturn { } export function useStorageCleaner(): UseStorageCleanerReturn { - const { t } = useI18n(['storageCleaner', 'common']); const [error, setError] = useState(''); const [isInitializing, setIsInitializing] = useState(true); const [options, setOptions] = useState(DEFAULT_OPTIONS); @@ -93,11 +91,11 @@ export function useStorageCleaner(): UseStorageCleanerReturn { if (currentRequestId !== requestIdRef.current) return; if (!tab || !tab.url) { - setError(t('storageCleaner:errorNoTab')); + setError('无法获取当前标签页'); return; } if (isRestrictedUrl(tab.url)) { - setError(t('storageCleaner:errorRestricted')); + setError('存储清理功能不支持此页面'); return; } @@ -135,7 +133,7 @@ export function useStorageCleaner(): UseStorageCleanerReturn { setIsInitializing(false); } } - }, [t]); + }, []); const loadInfoRef = useRef(loadInfo); useEffect(() => { @@ -177,10 +175,7 @@ export function useStorageCleaner(): UseStorageCleanerReturn { if (storageTimerRef.current) clearTimeout(storageTimerRef.current); storageTimerRef.current = setTimeout(async () => { await storageUtil - .set('storageCleaner/preferences', { - reloadAfterClean, - selectedTypes: options, - }) + .set('storageCleaner/preferences', { reloadAfterClean, selectedTypes: options }) .catch(console.error); }, 500); }, [options, reloadAfterClean, isInitializing]); @@ -209,7 +204,7 @@ export function useStorageCleaner(): UseStorageCleanerReturn { const tab = await getCurrentTab(); if (!tab || !tab.id || !tab.url) { - toast.warning(t('storageCleaner:errorNoTab')); + toast.warning('无法获取当前标签页'); return; } @@ -219,18 +214,18 @@ export function useStorageCleaner(): UseStorageCleanerReturn { setResult(cleaningResult); if (reloadAfterClean && cleaningResult.overallSuccess) { - toast.success(t('storageCleaner:cleanSuccessReload')); + toast.success('清理成功,即将刷新页面'); await chrome.tabs.reload(tab.id); } else { await loadInfo(); } } catch (err) { - toast.error(`${t('storageCleaner:cleanError')}: ${String(err)}`); + toast.error(`清理失败: ${String(err)}`); } finally { setLoading(false); setShowConfirm(false); } - }, [options, reloadAfterClean, loadInfo, t]); + }, [options, reloadAfterClean, loadInfo]); const totalBytes = useMemo(() => { return Object.values(sizes).reduce((acc, s) => { diff --git a/src/pages/TestDataGenerator/components/DataPreview.tsx b/src/pages/TestDataGenerator/components/DataPreview.tsx index abfca39..dbb10dd 100644 --- a/src/pages/TestDataGenerator/components/DataPreview.tsx +++ b/src/pages/TestDataGenerator/components/DataPreview.tsx @@ -5,7 +5,6 @@ import { useMemo } from 'react'; import { FileJson, FileText } from 'lucide-react'; -import { useI18n } from '@/utils/chromeI18n'; import { getGeneratorById } from '@/lib/generators'; import type { FieldConfig } from '@/types/testDataGenerator'; @@ -56,8 +55,6 @@ function getValueColor(value: string): string { } export default function DataPreview({ fields }: DataPreviewProps) { - const { t } = useI18n('testDataGenerator'); - // 生成一条示例数据 const sampleData = useMemo(() => { if (fields.length === 0) return null; @@ -83,7 +80,7 @@ export default function DataPreview({ fields }: DataPreviewProps) { return (
    -

    {t('testDataGenerator_noDataHint')}

    +

    {'配置字段后点击「生成数据」按钮'}

    ); } @@ -96,12 +93,10 @@ export default function DataPreview({ fields }: DataPreviewProps) {
    - - {t('testDataGenerator_sampleData')} - + {'示例数据'}
    - {Object.keys(sampleData).length} {t('testDataGenerator_fields')} + {Object.keys(sampleData).length} {'字段'}
    diff --git a/src/pages/TestDataGenerator/components/ExportPanel.tsx b/src/pages/TestDataGenerator/components/ExportPanel.tsx index f16a1cc..29f2591 100644 --- a/src/pages/TestDataGenerator/components/ExportPanel.tsx +++ b/src/pages/TestDataGenerator/components/ExportPanel.tsx @@ -6,7 +6,6 @@ import { Copy, Download } from 'lucide-react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; -import { useI18n } from '@/utils/chromeI18n'; import { DataExporter } from '@/utils/dataExporter'; import type { GenerateResult } from '@/types/testDataGenerator'; @@ -15,8 +14,6 @@ interface ExportPanelProps { } export default function ExportPanel({ result }: ExportPanelProps) { - const { t } = useI18n('testDataGenerator'); - if (!result?.data || result.data.length === 0) { return null; } @@ -25,9 +22,9 @@ export default function ExportPanel({ result }: ExportPanelProps) { const content = DataExporter.toJSON(result.data!); const success = await DataExporter.copyToClipboard(content); if (success) { - toast.success(t('testDataGenerator_copySuccess')); + toast.success('已复制到剪贴板'); } else { - toast.error(t('testDataGenerator_copyFailed')); + toast.error('复制失败'); } }; @@ -35,9 +32,9 @@ export default function ExportPanel({ result }: ExportPanelProps) { const content = DataExporter.toCSV(result.data!); const success = await DataExporter.copyToClipboard(content); if (success) { - toast.success(t('testDataGenerator_copySuccess')); + toast.success('已复制到剪贴板'); } else { - toast.error(t('testDataGenerator_copyFailed')); + toast.error('复制失败'); } }; @@ -53,27 +50,27 @@ export default function ExportPanel({ result }: ExportPanelProps) { return (
    -

    {t('testDataGenerator_export')}

    +

    {'导出数据'}

    diff --git a/src/pages/TestDataGenerator/components/FieldEditor.tsx b/src/pages/TestDataGenerator/components/FieldEditor.tsx index afac328..1331b7f 100644 --- a/src/pages/TestDataGenerator/components/FieldEditor.tsx +++ b/src/pages/TestDataGenerator/components/FieldEditor.tsx @@ -4,7 +4,6 @@ */ import { useState, useCallback } from 'react'; -import { useI18n } from '@/utils/chromeI18n'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Switch } from '@/components/ui/switch'; @@ -22,7 +21,6 @@ interface FieldEditorProps { } export default function FieldEditor({ field, onChange, allFieldNames = [] }: FieldEditorProps) { - const { t } = useI18n('testDataGenerator'); const generator = getGeneratorById(field.generatorId); const [nameError, setNameError] = useState(null); @@ -30,20 +28,20 @@ export default function FieldEditor({ field, onChange, allFieldNames = [] }: Fie (name: string): string | null => { const trimmed = name.trim(); if (!trimmed) { - return t('testDataGenerator_fieldNameEmpty'); + return '字段名称不能为空'; } if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(trimmed)) { - return t('testDataGenerator_fieldNameInvalid'); + return '字段名称只能包含字母、数字和下划线'; } const isDuplicate = allFieldNames.some( (n, i) => n === trimmed && i !== allFieldNames.indexOf(field.name), ); if (isDuplicate) { - return t('testDataGenerator_fieldNameDuplicate'); + return '字段名称已存在'; } return null; }, - [allFieldNames, field.name, t], + [allFieldNames, field.name], ); const handleNameChange = (name: string) => { @@ -111,15 +109,13 @@ export default function FieldEditor({ field, onChange, allFieldNames = [] }: Fie
    - + {field.name.length}/20
    handleNameChange(e.target.value)} - placeholder={t('testDataGenerator_fieldNamePlaceholder')} + placeholder={'请输入字段名称'} maxLength={20} className={`h-9 ${nameError ? 'border-destructive' : ''}`} /> @@ -128,9 +124,7 @@ export default function FieldEditor({ field, onChange, allFieldNames = [] }: Fie
    - + {(field.description || '').length}/50 @@ -138,7 +132,7 @@ export default function FieldEditor({ field, onChange, allFieldNames = [] }: Fie handleDescriptionChange(e.target.value)} - placeholder={t('testDataGenerator_fieldDescriptionPlaceholder')} + placeholder={'可选,添加字段说明'} maxLength={50} className="h-9" /> @@ -148,18 +142,14 @@ export default function FieldEditor({ field, onChange, allFieldNames = [] }: Fie {/* 必填/选填配置 */}
    - +
    {!field.required && (
    - - {t('testDataGenerator_nullRate')} - + {'空值率'} {field.nullRate}% @@ -199,26 +189,20 @@ export default function FieldEditor({ field, onChange, allFieldNames = [] }: Fie {/* 唯一性约束 */}
    - +
    {/* 生成器选择 */}
    - +
    {/* 生成器参数配置 */} {generator && (
    - + - {t('testDataGenerator_unique')} + {'唯一'} )}
    diff --git a/src/pages/TestDataGenerator/components/FieldList.tsx b/src/pages/TestDataGenerator/components/FieldList.tsx index 0536780..3b5fa65 100644 --- a/src/pages/TestDataGenerator/components/FieldList.tsx +++ b/src/pages/TestDataGenerator/components/FieldList.tsx @@ -10,7 +10,6 @@ import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Dialog, DialogContent } from '@/components/ui/dialog'; -import { useI18n } from '@/utils/chromeI18n'; import * as ruleStorage from '@/utils/ruleStorage'; import { DndContext, @@ -123,7 +122,6 @@ export default function FieldList({ editingRule, onRuleSaved, }: FieldListProps) { - const { t } = useI18n('testDataGenerator'); const [scrollTop, setScrollTop] = useState(0); const [activeId, setActiveId] = useState(null); const containerRef = useRef(null); @@ -226,10 +224,10 @@ export default function FieldList({ }); if (updated) { - toast.success(t('testDataGenerator_ruleUpdated')); + toast.success('规则已更新'); onRuleSaved?.(); } - }, [editingRule, fields, t, onRuleSaved]); + }, [editingRule, fields, onRuleSaved]); // 新建规则或另存为 const handleSave = useCallback( @@ -258,18 +256,18 @@ export default function FieldList({ setShowConfirmOverwrite(false); setRuleName(''); setRuleDescription(''); - toast.success(t('testDataGenerator_ruleSaved')); + toast.success('规则已保存'); onRuleSaved?.(); } }, - [ruleName, ruleDescription, fields, t, onRuleSaved], + [ruleName, ruleDescription, fields, onRuleSaved], ); return (

    - {t('testDataGenerator_fields')} ({fields.length}/{MAX_FIELDS}) + {'字段'} ({fields.length}/{MAX_FIELDS})

    {editingRule ? ( @@ -280,10 +278,10 @@ export default function FieldList({ onClick={handleUpdateRule} disabled={fields.length === 0} className="h-8 gap-1.5 px-2.5" - title={`${t('testDataGenerator_editing')}: ${editingRule.name}`} + title={`${'编辑中'}: ${editingRule.name}`} > - {t('testDataGenerator_updateRule')} + {'更新规则'} ) : ( @@ -304,7 +302,7 @@ export default function FieldList({ className="h-8 gap-1.5 px-2.5" > - {t('testDataGenerator_saveRule')} + {'保存规则'} )}
    @@ -324,10 +322,8 @@ export default function FieldList({ {fields.length === 0 ? (
    -

    {t('testDataGenerator_noFields')}

    -

    - {t('testDataGenerator_addFieldHint')} -

    +

    {'暂无字段'}

    +

    {'点击上方按钮添加第一个字段'}

    ) : ( setRuleName(e.target.value)} - placeholder={t('testDataGenerator_ruleNamePlaceholder')} + placeholder={'规则名称'} className="h-9" /> setRuleDescription(e.target.value)} - placeholder={t('testDataGenerator_ruleDescPlaceholder')} + placeholder={'规则描述(可选)'} className="h-9" />
    @@ -447,16 +443,14 @@ export default function FieldList({ className="w-[calc(100vw-4rem)] max-w-[420px] p-0 pt-6 flex flex-col" >
    -

    - {t('testDataGenerator_ruleNameDuplicate')} -

    +

    {'已存在同名规则,是否覆盖保存?'}

    diff --git a/src/pages/TestDataGenerator/components/GenerateButton.tsx b/src/pages/TestDataGenerator/components/GenerateButton.tsx index 606866c..0b577fc 100644 --- a/src/pages/TestDataGenerator/components/GenerateButton.tsx +++ b/src/pages/TestDataGenerator/components/GenerateButton.tsx @@ -5,7 +5,6 @@ import { Play, Square } from 'lucide-react'; import { Button } from '@/components/ui/button'; -import { useI18n } from '@/utils/chromeI18n'; import type { GenerateProgress } from '@/types/testDataGenerator'; interface GenerateButtonProps { @@ -23,27 +22,20 @@ export default function GenerateButton({ progress, disabled, }: GenerateButtonProps) { - const { t } = useI18n('testDataGenerator'); - return (
    {isGenerating ? ( <> {/* 进度条 */} {progress && (
    - - {t('testDataGenerator_progress', { - current: progress.generated.toLocaleString(), - total: progress.total.toLocaleString(), - })} - + {'已生成 {{current}} / {{total}} 条'} {progress.progress}%
    @@ -54,9 +46,7 @@ export default function GenerateButton({
    {progress.estimatedTimeLeft !== undefined && (

    - {t('testDataGenerator_estimatedTime', { - time: Math.ceil(progress.estimatedTimeLeft / 1000), - })} + {'预计剩余 {{time}} 秒'}

    )}
    @@ -65,7 +55,7 @@ export default function GenerateButton({ ) : ( )}
    diff --git a/src/pages/TestDataGenerator/components/GenerateOptions.tsx b/src/pages/TestDataGenerator/components/GenerateOptions.tsx index ba5971e..fdd62dd 100644 --- a/src/pages/TestDataGenerator/components/GenerateOptions.tsx +++ b/src/pages/TestDataGenerator/components/GenerateOptions.tsx @@ -3,7 +3,6 @@ * 配置生成数量、数据格式等选项 */ -import { useI18n } from '@/utils/chromeI18n'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; @@ -27,15 +26,11 @@ export default function GenerateOptions({ format, onFormatChange, }: GenerateOptionsProps) { - const { t } = useI18n('testDataGenerator'); - return (
    {/* 生成数量 */}
    - +
    {COUNT_PRESETS.map((preset) => (
    @@ -62,9 +57,7 @@ export default function ResultPanel({ result }: ResultPanelProps) { {result.stats.success.toLocaleString()} - - {t('testDataGenerator_successCount')} - + {'成功'}
    @@ -72,7 +65,7 @@ export default function ResultPanel({ result }: ResultPanelProps) { {(result.stats.duration / 1000).toFixed(2)}s - {t('testDataGenerator_duration')} + {'耗时'}
    )} @@ -82,9 +75,7 @@ export default function ResultPanel({ result }: ResultPanelProps) {
    - - {t('testDataGenerator_warnings')} - + {'警告'}
      {result.warnings.slice(0, 5).map((warning, index) => ( @@ -93,9 +84,7 @@ export default function ResultPanel({ result }: ResultPanelProps) { ))} {result.warnings.length > 5 && ( -
    • - ... {t('testDataGenerator_moreWarnings', { count: result.warnings.length - 5 })} -
    • +
    • ... {'还有 {{count}} 条警告'}
    • )}
    diff --git a/src/pages/TestDataGenerator/components/RuleManager.tsx b/src/pages/TestDataGenerator/components/RuleManager.tsx index 6bd1d3b..b7ec20a 100644 --- a/src/pages/TestDataGenerator/components/RuleManager.tsx +++ b/src/pages/TestDataGenerator/components/RuleManager.tsx @@ -20,7 +20,6 @@ import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Dialog, DialogContent } from '@/components/ui/dialog'; -import { useI18n } from '@/utils/chromeI18n'; import * as ruleStorage from '@/utils/ruleStorage'; import type { DataRule, FieldConfig } from '@/types/testDataGenerator'; @@ -31,7 +30,6 @@ interface RuleManagerProps { } export default function RuleManager({ onLoad, onEdit, onRulesChanged }: RuleManagerProps) { - const { t, i18n } = useI18n('testDataGenerator'); const [rules, setRules] = useState(() => ruleStorage.getAll()); const [searchQuery, setSearchQuery] = useState(''); const [debouncedSearchQuery, setDebouncedSearchQuery] = useState(''); @@ -64,9 +62,9 @@ export default function RuleManager({ onLoad, onEdit, onRulesChanged }: RuleMana onLoad(rule.fields); ruleStorage.recordUse(rule.id); loadRules(); - toast.success(t('testDataGenerator_ruleLoaded', { name: rule.name })); + toast.success(`已加载规则「${rule.name}」`); }, - [onLoad, loadRules, t], + [onLoad, loadRules], ); const handleDelete = useCallback(() => { @@ -74,20 +72,20 @@ export default function RuleManager({ onLoad, onEdit, onRulesChanged }: RuleMana ruleStorage.deleteRule(ruleToDelete.id); loadRules(); setRuleToDelete(null); - toast.success(t('testDataGenerator_ruleDeleted')); + toast.success('规则已删除'); onRulesChanged?.(); - }, [ruleToDelete, loadRules, t, onRulesChanged]); + }, [ruleToDelete, loadRules, onRulesChanged]); const handleDuplicate = useCallback( (id: string) => { - const result = ruleStorage.duplicate(id, t('testDataGenerator_ruleCopySuffix')); + const result = ruleStorage.duplicate(id, '(副本)'); if (result) { loadRules(); - toast.success(t('testDataGenerator_ruleDuplicated')); + toast.success('规则已复制'); onRulesChanged?.(); } }, - [loadRules, t, onRulesChanged], + [loadRules, onRulesChanged], ); const handleEdit = useCallback( @@ -113,14 +111,14 @@ export default function RuleManager({ onLoad, onEdit, onRulesChanged }: RuleMana } finally { URL.revokeObjectURL(url); } - toast.success(t('testDataGenerator_exportSuccess')); + toast.success('规则已导出'); } catch (error) { console.error('[RuleManager] 导出失败:', error); - toast.error(t('testDataGenerator_exportFailed')); + toast.error('导出失败'); } finally { setIsExporting(false); } - }, [t]); + }, []); const handleImport = useCallback(() => { const input = document.createElement('input'); @@ -136,11 +134,11 @@ export default function RuleManager({ onLoad, onEdit, onRulesChanged }: RuleMana const result = ruleStorage.importRules(text); if (result.success > 0) { - toast.success(t('testDataGenerator_importSuccess', { count: result.success })); + toast.success(`成功导入 ${result.success} 条规则`); } if (result.failed > 0) { - toast.error(t('testDataGenerator_importFailed', { count: result.failed })); + toast.error(`${result.failed} 条规则导入失败`); console.warn('[RuleManager] 导入警告:', result.errors); } @@ -148,25 +146,22 @@ export default function RuleManager({ onLoad, onEdit, onRulesChanged }: RuleMana onRulesChanged?.(); } catch (error) { console.error('[RuleManager] 导入失败:', error); - toast.error(t('testDataGenerator_importFailed')); + toast.error('规则导入失败'); } finally { setIsImporting(false); } }; input.click(); - }, [loadRules, t, onRulesChanged]); + }, [loadRules, onRulesChanged]); - const formatDate = useCallback( - (timestamp: number) => { - return new Date(timestamp).toLocaleDateString(i18n.language || 'zh-CN', { - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - }); - }, - [i18n.language], - ); + const formatDate = useCallback((timestamp: number) => { + return new Date(timestamp).toLocaleDateString('zh-CN', { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + }, []); return (
    @@ -178,15 +173,15 @@ export default function RuleManager({ onLoad, onEdit, onRulesChanged }: RuleMana >

    - {t('testDataGenerator_confirmDeleteDescription', { name: ruleToDelete?.name ?? '' })} + {ruleToDelete && `确定要删除规则「${ruleToDelete.name}」吗?此操作不可撤销。`}

    @@ -206,7 +201,7 @@ export default function RuleManager({ onLoad, onEdit, onRulesChanged }: RuleMana ) : ( )} - {t('testDataGenerator_import')} + {'导入'}
    @@ -230,12 +225,12 @@ export default function RuleManager({ onLoad, onEdit, onRulesChanged }: RuleMana setSearchQuery(e.target.value.slice(0, 20))} - placeholder={t('testDataGenerator_searchRules')} + placeholder={'搜索规则...'} className="pl-9 pr-24 h-9" maxLength={20} /> - - {t('testDataGenerator_ruleCount', { count: rules.length, max: 20 })} + + {`已保存 ${rules.length}/${ruleStorage.MAX_RULES} 条`}
    @@ -245,9 +240,7 @@ export default function RuleManager({ onLoad, onEdit, onRulesChanged }: RuleMana

    - {debouncedSearchQuery - ? t('testDataGenerator_noSearchResults') - : t('testDataGenerator_noRules')} + {debouncedSearchQuery ? '未找到匹配的规则' : '暂无保存的规则'}

    ) : ( @@ -260,7 +253,7 @@ export default function RuleManager({ onLoad, onEdit, onRulesChanged }: RuleMana
    {rule.name} - {rule.fields.length} {t('testDataGenerator_fields')} + {rule.fields.length} {'字段'}
    {rule.description && ( @@ -273,7 +266,7 @@ export default function RuleManager({ onLoad, onEdit, onRulesChanged }: RuleMana {formatDate(rule.updatedAt)} - {t('testDataGenerator_usedTimes', { count: rule.useCount })} + {`使用 ${rule.useCount} 次`}
    @@ -283,7 +276,7 @@ export default function RuleManager({ onLoad, onEdit, onRulesChanged }: RuleMana size="icon" className="h-7 w-7" onClick={() => handleLoad(rule)} - title={t('testDataGenerator_load')} + title={'加载'} > @@ -292,7 +285,7 @@ export default function RuleManager({ onLoad, onEdit, onRulesChanged }: RuleMana size="icon" className="h-7 w-7" onClick={() => handleEdit(rule)} - title={t('testDataGenerator_edit')} + title={'编辑'} > @@ -301,7 +294,7 @@ export default function RuleManager({ onLoad, onEdit, onRulesChanged }: RuleMana size="icon" className="h-7 w-7" onClick={() => handleDuplicate(rule.id)} - title={t('testDataGenerator_duplicate')} + title={'复制'} > @@ -310,7 +303,7 @@ export default function RuleManager({ onLoad, onEdit, onRulesChanged }: RuleMana size="icon" className="h-7 w-7 text-destructive hover:text-destructive" onClick={() => setRuleToDelete(rule)} - title={t('testDataGenerator_delete')} + title={'删除'} > diff --git a/src/pages/TestDataGenerator/components/__tests__/RuleManager.test.tsx b/src/pages/TestDataGenerator/components/__tests__/RuleManager.test.tsx index 51e8cca..b84cba5 100644 --- a/src/pages/TestDataGenerator/components/__tests__/RuleManager.test.tsx +++ b/src/pages/TestDataGenerator/components/__tests__/RuleManager.test.tsx @@ -4,6 +4,7 @@ import userEvent from '@testing-library/user-event'; // Mock dependencies vi.mock('@/utils/ruleStorage', () => ({ + MAX_RULES: 20, getAll: vi.fn(() => []), save: vi.fn(), deleteRule: vi.fn(), @@ -87,8 +88,7 @@ describe('RuleManager', () => { expect(defaultProps.onLoad).toHaveBeenCalledWith(mockFields); expect(mockedRuleStorage.recordUse).toHaveBeenCalledWith('rule-1'); - // Note: t() mock doesn't handle placeholders, so we just check it was called - expect(mockedToast.success).toHaveBeenCalled(); + expect(mockedToast.success).toHaveBeenCalledWith('已加载规则「Test Rule」'); }); it('should show delete confirmation dialog', async () => { @@ -100,7 +100,7 @@ describe('RuleManager', () => { const deleteButton = screen.getByTitle('删除'); await user.click(deleteButton); - expect(screen.getByText(/确定要删除规则/)).toBeInTheDocument(); + expect(screen.getByText('确定要删除规则「Test Rule」吗?此操作不可撤销。')).toBeInTheDocument(); }); it('should delete rule after confirmation', async () => { @@ -178,6 +178,14 @@ describe('RuleManager', () => { vi.restoreAllMocks(); }); + it('should show saved rule count', () => { + mockedRuleStorage.getAll.mockReturnValue([mockRule]); + + render(); + + expect(screen.getByText('已保存 1/20 条')).toBeInTheDocument(); + }); + it('should show field count for each rule', () => { mockedRuleStorage.getAll.mockReturnValue([mockRule]); diff --git a/src/pages/TestDataGenerator/index.tsx b/src/pages/TestDataGenerator/index.tsx index d6a985f..41efd20 100644 --- a/src/pages/TestDataGenerator/index.tsx +++ b/src/pages/TestDataGenerator/index.tsx @@ -4,7 +4,6 @@ import { useState, useCallback, useRef, useEffect } from 'react'; import { Settings, Database, Tag } from 'lucide-react'; -import { useI18n } from '@/utils/chromeI18n'; import { cn } from '@/lib/utils'; import { useGenerator } from './hooks/useGenerator'; import type { FieldConfig, GenerateResult, DataRule } from '@/types/testDataGenerator'; @@ -29,7 +28,6 @@ import RuleManager from './components/RuleManager'; type TabType = 'fields' | 'rules'; export default function TestDataGeneratorPage() { - const { t } = useI18n('testDataGenerator'); const { isGenerating, progress, result, error, generate, cancel, clearResult } = useGenerator(); // 字段配置 @@ -52,11 +50,11 @@ export default function TestDataGeneratorPage() { useEffect(() => { if (result?.success && result.stats && result !== lastToastResultRef.current) { lastToastResultRef.current = result; - toast.success(t('testDataGenerator_generateSuccess'), { - description: `${result.stats.total} ${t('testDataGenerator_records')}`, + toast.success('生成完成', { + description: `${result.stats.total} ${'条数据'}`, }); } - }, [result, t]); + }, [result]); // 添加新字段 const handleAddField = useCallback(() => { @@ -132,9 +130,9 @@ export default function TestDataGeneratorPage() { setEditingRule(rule); setActiveTab('fields'); clearResult(); - toast.success(t('testDataGenerator_editingRule', { name: rule.name })); + toast.success('正在编辑规则「{{name}}」'); }, - [clearResult, t], + [clearResult], ); // 保存规则成功后清除编辑状态 @@ -176,7 +174,7 @@ export default function TestDataGeneratorPage() { )} > - {t('testDataGenerator_fieldConfig')} + {'字段配置'}
    @@ -259,7 +257,7 @@ export default function TestDataGeneratorPage() {

    - {t('testDataGenerator_dataPreview')} + {'数据预览'}

    @@ -293,10 +291,10 @@ export default function TestDataGeneratorPage() {
    diff --git a/src/pages/TextStatistics/index.tsx b/src/pages/TextStatistics/index.tsx index 3661285..25a29b3 100644 --- a/src/pages/TextStatistics/index.tsx +++ b/src/pages/TextStatistics/index.tsx @@ -1,18 +1,16 @@ import TextInputArea from '@/components/TextInputArea'; import { formatBytes } from '@/utils/format'; -import { useI18n } from '@/utils/chromeI18n'; import { useTextStatistics } from './useTextStatistics'; import { cn } from '@/lib/utils'; export default function Index() { - const { t } = useI18n('textStatistics'); const { text, stats, setText } = useTextStatistics(); const statItems = [ - { label: t('textStatistics:characters'), value: stats.characters }, - { label: t('textStatistics:words'), value: stats.words }, - { label: t('textStatistics:lines'), value: stats.lines }, - { label: t('textStatistics:bytes'), value: formatBytes(stats.bytes) }, + { label: '字符数', value: stats.characters }, + { label: '单词数', value: stats.words }, + { label: '行数', value: stats.lines }, + { label: '字节大小', value: formatBytes(stats.bytes) }, ]; return ( @@ -20,7 +18,7 @@ export default function Index() { { @@ -13,8 +12,6 @@ interface LiveClockProps extends React.HTMLAttributes { } export default function LiveClock({ unit, onUseNow, className, ...props }: LiveClockProps) { - const { t } = useI18n('timestamp'); - const [rawTime, setRawTime] = useState(() => Date.now()); useEffect(() => { @@ -36,7 +33,7 @@ export default function LiveClock({ unit, onUseNow, className, ...props }: LiveC {...props} > - {t('timestamp:currentTs')} + {'当前时间戳'} @@ -47,19 +44,15 @@ export default function LiveClock({ unit, onUseNow, className, ...props }: LiveC type="button" onClick={() => { onUseNow(rawTime); - toast.success(t('timestamp:usedSuccess')); + toast.success('已使用当前时间戳'); }} - title={t('timestamp:useNowTooltip')} + title={'填充到下方'} className="flex h-7 w-7 items-center justify-center rounded-md border border-input bg-background text-muted-foreground shadow-sm hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" > - +
    ); } diff --git a/src/pages/Timestamp/components/ResultView.tsx b/src/pages/Timestamp/components/ResultView.tsx index b4a586c..364207d 100644 --- a/src/pages/Timestamp/components/ResultView.tsx +++ b/src/pages/Timestamp/components/ResultView.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { CopyButton } from '@/components/CopyButton'; -import { useI18n } from '@/utils/chromeI18n'; import { cn } from '@/lib/utils'; interface ResultViewProps extends React.HTMLAttributes { @@ -14,8 +13,6 @@ export default function ResultView({ className, ...props }: ResultViewProps) { - const { t } = useI18n('timestamp'); - if (!result) { if (!showEmptyPlaceholder) return null; return ( @@ -26,7 +23,7 @@ export default function ResultView({ )} {...props} > - {t('timestamp:resultEmpty')} + {'请输入并点击转换'}
    ); } @@ -34,7 +31,7 @@ export default function ResultView({ return (
    - {t('timestamp:resultLabel')} + {'转换结果'}
    @@ -43,7 +40,7 @@ export default function ResultView({
    diff --git a/src/pages/Timestamp/index.tsx b/src/pages/Timestamp/index.tsx index 36ac8fd..47812a8 100644 --- a/src/pages/Timestamp/index.tsx +++ b/src/pages/Timestamp/index.tsx @@ -4,7 +4,6 @@ import type { ModeType, UnitType, ZoneType } from './constants'; import LiveClock from './components/LiveClock'; import ResultView from './components/ResultView'; import { useTimestampConverter } from './useTimestampConverter'; -import { useI18n } from '@/utils/chromeI18n'; import { cn } from '@/lib/utils'; import { Input } from '@/components/ui/input'; @@ -17,18 +16,16 @@ import { } from '@/components/ui/select'; const MODE_OPTIONS: { value: ModeType; label: string }[] = [ - { value: 'ts2dt', label: 'timestamp:tsToDate' }, - { value: 'dt2ts', label: 'timestamp:dateToTs' }, + { value: 'ts2dt', label: '时间戳转日期' }, + { value: 'dt2ts', label: '日期转时间戳' }, ]; const UNIT_OPTIONS: { value: UnitType; label: string }[] = [ - { value: 'ms', label: 'timestamp:unitMs' }, - { value: 's', label: 'timestamp:unitS' }, + { value: 'ms', label: '毫秒' }, + { value: 's', label: '秒' }, ]; export default function Index() { - const { t } = useI18n('timestamp'); - const { mode, input, @@ -53,7 +50,7 @@ export default function Index() {
    ({ ...o, label: t(o.label) }))} + options={MODE_OPTIONS} onChange={setMode} size="small" /> @@ -61,9 +58,7 @@ export default function Index() {
    setInput(e.target.value)} className={cn( @@ -78,7 +73,7 @@ export default function Index() {
    ({ ...o, label: t(o.label) }))} + options={UNIT_OPTIONS} onChange={setUnit} size="small" className="sm:w-auto shrink-0" diff --git a/src/pages/Timestamp/useTimestampConverter.ts b/src/pages/Timestamp/useTimestampConverter.ts index 7562c85..400258d 100644 --- a/src/pages/Timestamp/useTimestampConverter.ts +++ b/src/pages/Timestamp/useTimestampConverter.ts @@ -2,7 +2,6 @@ import { useMemo, useState } from 'react'; import dayjs from '@/utils/dayjs'; import type { UnitType, ZoneType, ModeType } from './constants'; import { DATE_FORMAT, msToUnit, dayjsFromTimestamp } from './constants'; -import { useI18n } from '@/utils/chromeI18n'; import { useContextMenuData } from '@/utils/useContextMenuData'; export interface UseTimestampConverterReturn { @@ -29,7 +28,6 @@ function isTimestampLike(input: string): boolean { } export function useTimestampConverter(): UseTimestampConverterReturn { - const { t } = useI18n('timestamp'); const [mode, setMode] = useState('ts2dt'); const [unit, setUnit] = useState('ms'); const [zone, setZone] = useState('Asia/Shanghai'); @@ -43,22 +41,22 @@ export function useTimestampConverter(): UseTimestampConverterReturn { if (mode === 'ts2dt') { const num = Number(rawInput); if (isNaN(num)) { - return { result: '', error: t('timestamp:errors.invalidNumber') }; + return { result: '', error: '请输入有效数字' }; } const d = dayjsFromTimestamp(num, unit); if (!d.isValid()) { - return { result: '', error: t('timestamp:errors.invalidTimestamp') }; + return { result: '', error: '无效时间戳' }; } return { result: d.tz(zone).format(DATE_FORMAT), error: '' }; } else { const d = dayjs.tz(rawInput, DATE_FORMAT, zone); if (!d.isValid()) { - return { result: '', error: t('timestamp:errors.invalidFormat') }; + return { result: '', error: '无效的日期格式' }; } const ms = d.valueOf(); return { result: String(msToUnit(ms, unit)), error: '' }; } - }, [input, mode, unit, zone, t]); + }, [input, mode, unit, zone]); const handleContextMenuData = (payload: string) => { const trimmed = payload.trim(); diff --git a/src/utils/chromeI18n.ts b/src/utils/chromeI18n.ts deleted file mode 100644 index 3442b96..0000000 --- a/src/utils/chromeI18n.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * chrome.i18n 类型安全 wrapper - * 提供与 react-i18next 兼容的接口 - */ - -/** - * 获取翻译文本 - * @param msgId 翻译 key(如 'timestamp_pageTitle') - * @param substitutions 占位符替换值(可选) - * @returns 翻译后的文本 - */ -export function getMessage(msgId: string, substitutions?: string[]): string { - try { - return chrome.i18n.getMessage(msgId, substitutions); - } catch (error) { - console.warn(`[chrome.i18n] 无法获取翻译: ${msgId}`, error); - return msgId; // 回退到 key 本身 - } -} - -/** - * react-i18next 兼容的 Hook - * 返回 t 函数和相关信息 - */ -export function useI18n(namespace?: string | string[]) { - const namespaces = Array.isArray(namespace) ? namespace : namespace ? [namespace] : []; - - const t = (key: string, options?: Record): string => { - // 统一将分隔符转换为下划线,兼容 'namespace:key.path' 和 'key.path' 两种写法 - const msgId = key.replace(':', '_').replace(/\./g, '_'); - - // 先尝试直接查找 key - let message = getMessage(msgId); - - // 如果直接查找未命中(空字符串或返回 key 本身),尝试命名空间前缀(使用转换后的 msgId) - if ((!message || message === msgId) && namespaces.length > 0) { - for (const ns of namespaces) { - const candidate = `${ns}_${msgId}`; - const result = getMessage(candidate); - if (result !== candidate) { - message = result; - break; - } - } - } - - if (options) { - for (const [placeholder, value] of Object.entries(options)) { - message = message.replace(`{{${placeholder}}}`, String(value)); - } - } - - return message; - }; - - return { - t, - i18n: { - language: 'zh', - changeLanguage: (_lng?: string) => { - // chrome.i18n 无法动态切换语言,需要刷新页面 - console.warn('[chrome.i18n] 无法动态切换语言,需要刷新页面'); - return Promise.resolve(); - }, - }, - isLoaded: true, - }; -} diff --git a/src/utils/jwt.ts b/src/utils/jwt.ts index 78d56bb..ba2ffec 100644 --- a/src/utils/jwt.ts +++ b/src/utils/jwt.ts @@ -2,8 +2,6 @@ * JWT 解析工具 */ -import { getMessage } from '@/utils/chromeI18n'; - interface JwtHeader { alg: string; typ?: string; @@ -43,7 +41,7 @@ export function decodeBase64Url(str: string): string { const pad = base64.length % 4; if (pad) { if (pad === 1) { - throw new Error(getMessage('jwt_errors_invalidBase64String')); + throw new Error('无效的 Base64URL 字符串'); } base64 += new Array(5 - pad).join('='); } @@ -58,10 +56,9 @@ export function decodeBase64Url(str: string): string { const decoder = new TextDecoder('utf-8'); return decoder.decode(bytes); } catch (e) { - throw new Error( - getMessage('jwt_errors_failedToDecode') + (e instanceof Error ? e.message : String(e)), - { cause: e }, - ); + throw new Error('Base64 解码失败: ' + (e instanceof Error ? e.message : String(e)), { + cause: e, + }); } } @@ -78,7 +75,7 @@ export function parseJwt(token: string): JwtResult { payload: null, signature: '', raw: { header: '', payload: '', signature: '' }, - error: getMessage('jwt_errors_invalidFormat'), + error: 'JWT 格式无效:应包含 3 个部分(header.payload.signature)', }; } @@ -98,8 +95,7 @@ export function parseJwt(token: string): JwtResult { const headerJson = decodeBase64Url(headerB64); result.header = JSON.parse(headerJson); } catch (e) { - result.error = - getMessage('jwt_errors_parseHeaderFailed') + (e instanceof Error ? e.message : String(e)); + result.error = 'JWT Header 解析失败: ' + (e instanceof Error ? e.message : String(e)); return result; } @@ -107,8 +103,7 @@ export function parseJwt(token: string): JwtResult { const payloadJson = decodeBase64Url(payloadB64); result.payload = JSON.parse(payloadJson); } catch (e) { - result.error = - getMessage('jwt_errors_parsePayloadFailed') + (e instanceof Error ? e.message : String(e)); + result.error = 'JWT Payload 解析失败: ' + (e instanceof Error ? e.message : String(e)); return result; } diff --git a/src/utils/ruleStorage.ts b/src/utils/ruleStorage.ts index 8253abf..d39c107 100644 --- a/src/utils/ruleStorage.ts +++ b/src/utils/ruleStorage.ts @@ -9,7 +9,7 @@ import type { DataRule, FieldConfig } from '@/types/testDataGenerator'; const STORAGE_KEY = 'testDataGenerator_rules'; /** 最大规则数量 */ -const MAX_RULES = 20; +export const MAX_RULES = 20; /** * 获取所有规则 diff --git a/src/utils/storageCleaner.ts b/src/utils/storageCleaner.ts index 65ba10f..39d1f59 100644 --- a/src/utils/storageCleaner.ts +++ b/src/utils/storageCleaner.ts @@ -20,6 +20,15 @@ const CLEAN_OPTION_KEYS: (keyof StorageCleanerOptions)[] = [ 'serviceWorkers', ]; +const OPTION_LABELS: Record = { + localStorage: 'Local Storage', + sessionStorage: 'Session Storage', + indexedDB: '站点存储', + cookies: 'Cookies', + cacheStorage: 'Cache Storage', + serviceWorkers: 'Service Workers', +}; + export async function getCurrentTab() { // For popup pages, we need to get the active tab from the browser window that triggered the popup. // We should ONLY care about the currently active tab in the last focused window. @@ -362,22 +371,19 @@ export async function clearStorage( return result; } -export function formatCleaningResult( - result: CleaningResult, - t: (key: string, options?: Record) => string, -): string { +export function formatCleaningResult(result: CleaningResult): string { const parts: string[] = []; for (const key of CLEAN_OPTION_KEYS) { const r = result[key]; if (r?.success && r.count > 0) { - parts.push(`${r.count} ${t(`storageCleaner:options.${key}`)}`); + parts.push(`${r.count} ${OPTION_LABELS[key] || key}`); } } if (parts.length === 0) { - return t('storageCleaner:noDataToClean'); + return '该页面没有可清理的存储数据'; } - return t('storageCleaner:cleanedSummary', { items: parts.join(', ') }); + return `清理了 ${parts.join(', ')}`; } diff --git a/vitest.setup.ts b/vitest.setup.ts index efd1d23..f6f381e 100644 --- a/vitest.setup.ts +++ b/vitest.setup.ts @@ -1,41 +1,6 @@ import '@testing-library/jest-dom'; import { afterEach, beforeEach, vi } from 'vitest'; import React from 'react'; -import zhMessages from './public/_locales/zh_CN/messages.json'; - -// Type assertion to allow string indexing -const zhMessagesMap = zhMessages as Record; - -vi.mock('@/utils/chromeI18n', () => ({ - useI18n: (ns?: string | string[]) => ({ - t: (key: string) => { - let msgId = key; - // Handle namespace:key format - if (key.includes(':')) { - msgId = key.replace(':', '_').replace(/\./g, '_'); - } - // Try direct key first - if (zhMessagesMap[msgId]) return zhMessagesMap[msgId].message; - // Try namespace prefix (using converted msgId) - if (ns) { - const namespaces = Array.isArray(ns) ? ns : [ns]; - for (const n of namespaces) { - const candidate = `${n}_${msgId}`; - if (zhMessagesMap[candidate]) return zhMessagesMap[candidate].message; - } - } - return msgId; - }, - i18n: { - changeLanguage: vi.fn().mockResolvedValue(undefined), - language: 'zh', - }, - isLoaded: true, - }), - getMessage: (msgId: string) => zhMessagesMap[msgId]?.message ?? msgId, - getLanguage: () => 'zh', - preloadNamespaces: vi.fn().mockResolvedValue(undefined), -})); vi.mock('@/components/CopyButton', () => ({ CopyButton: ({ diff --git a/wxt.config.ts b/wxt.config.ts index 50bf923..d17a215 100644 --- a/wxt.config.ts +++ b/wxt.config.ts @@ -45,7 +45,6 @@ export default defineConfig({ name: 'Testing Tool', description: 'A tool for testing web applications.', version_name: undefined, - default_locale: 'zh_CN', permissions: [ 'storage', 'clipboardWrite', @@ -64,7 +63,7 @@ export default defineConfig({ '128': 'icon/128.png', }, action: { - default_title: '__MSG_appName__', + default_title: 'Testing Tool', default_icon: { '16': 'icon/16.png', '32': 'icon/32.png',