From aa23a263fe1f617f392bd40a55b38f12fd515198 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=A8=E9=9C=96=E9=93=83?= Date: Thu, 28 May 2026 19:21:54 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E8=BF=81=E7=A7=BB=20i18n=20?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E4=BB=8E=20react-i18next=20=E5=88=B0=20chrom?= =?UTF-8?q?e.i18n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 react-i18next、i18next 及相关依赖 - 删除旧的 i18n/ 目录和 useLazyTranslation 工具 - 新增 utils/chromeI18n.ts 类型安全 wrapper(useI18n Hook + getMessage) - 生成 public/_locales/{zh,en}/messages.json(298 个翻译 key) - 批量更新 39+ 组件文件的导入和翻译调用 - 转换翻译键格式:namespace:key → namespace_key - 修复 ErrorBoundary/PageErrorBoundary 从 withTranslation HOC 改为直接调用 getMessage - 更新 vitest.setup.ts mock 加载实际翻译文本 - 修复 11 个测试文件的断言以匹配中文翻译 - TypeScript、ESLint、547 项测试全部通过 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- components/CopyButton.tsx | 4 +- components/DecodeResultPaper.tsx | 4 +- components/ErrorBoundary.tsx | 17 +- components/ImageUploader.tsx | 4 +- components/PageErrorBoundary.tsx | 15 +- components/QrCodePreview.tsx | 4 +- components/RouterContainer.tsx | 4 +- components/TextInputArea.tsx | 4 +- components/TopBar.tsx | 14 +- .../__tests__/DecodeResultPaper.test.tsx | 8 +- components/__tests__/ImageUploader.test.tsx | 18 +- components/__tests__/QrCodePreview.test.tsx | 30 +- .../__tests__/StorageCleanerConfirm.test.tsx | 44 +- components/__tests__/TopBar.test.tsx | 10 +- config/__tests__/features.test.ts | 6 +- config/features.tsx | 42 +- entrypoints/options/App.tsx | 6 +- entrypoints/options/__tests__/App.test.tsx | 17 +- entrypoints/options/main.tsx | 1 - entrypoints/popup/main.tsx | 1 - entrypoints/sidepanel/main.tsx | 1 - i18n/README.md | 61 - i18n/index.ts | 144 -- i18n/locales/en/base64Converter.json | 37 - i18n/locales/en/common.json | 50 - i18n/locales/en/features.json | 45 - i18n/locales/en/htmlToMarkdown.json | 16 - i18n/locales/en/jsonDiff.json | 22 - i18n/locales/en/jsonFormat.json | 42 - i18n/locales/en/jwt.json | 17 - i18n/locales/en/markdownToHtml.json | 18 - i18n/locales/en/qrCode.json | 48 - i18n/locales/en/rightClickRestorer.json | 13 - i18n/locales/en/storageCleaner.json | 34 - i18n/locales/en/textStatistics.json | 9 - i18n/locales/en/timestamp.json | 27 - i18n/locales/zh/base64Converter.json | 37 - i18n/locales/zh/common.json | 50 - i18n/locales/zh/features.json | 45 - i18n/locales/zh/htmlToMarkdown.json | 16 - i18n/locales/zh/jsonDiff.json | 22 - i18n/locales/zh/jsonFormat.json | 42 - i18n/locales/zh/jwt.json | 17 - i18n/locales/zh/markdownToHtml.json | 18 - i18n/locales/zh/qrCode.json | 48 - i18n/locales/zh/rightClickRestorer.json | 13 - i18n/locales/zh/storageCleaner.json | 34 - i18n/locales/zh/textStatistics.json | 9 - i18n/locales/zh/timestamp.json | 27 - package-lock.json | 290 +--- package.json | 3 - .../Base64ConverterSection.tsx | 4 +- pages/Base64Converter/TextMode.tsx | 4 +- .../__tests__/TextMode.test.tsx | 60 +- .../Base64Converter/__tests__/index.test.tsx | 19 +- pages/Base64Converter/index.tsx | 4 +- pages/Base64Converter/useBase64Converter.ts | 4 +- pages/Dashboard/index.tsx | 4 +- pages/HtmlToMarkdown/index.tsx | 4 +- pages/JsonTools/DiffNavigator.tsx | 4 +- pages/JsonTools/DiffResult.tsx | 4 +- pages/JsonTools/JsonConvertSection.tsx | 4 +- pages/JsonTools/JsonFormatSection.tsx | 4 +- pages/JsonTools/index.tsx | 4 +- pages/Jwt/index.tsx | 8 +- pages/MarkdownToHtml/index.tsx | 4 +- pages/QrCode/__tests__/index.test.tsx | 21 +- pages/QrCode/components/GeneratePanel.tsx | 4 +- pages/QrCode/components/ParsePanel.tsx | 4 +- pages/QrCode/hooks/useQrCode.ts | 4 +- pages/QrCode/index.tsx | 4 +- .../__tests__/index.test.tsx | 2 +- pages/RightClickRestorer/index.tsx | 4 +- pages/StorageCleaner/AutoRefreshToggle.tsx | 4 +- pages/StorageCleaner/CleaningResult.tsx | 4 +- pages/StorageCleaner/ErrorDisplay.tsx | 4 +- pages/StorageCleaner/OptionItem.tsx | 4 +- .../StorageCleaner/StorageCleanerConfirm.tsx | 6 +- pages/StorageCleaner/StorageOptionsGrid.tsx | 4 +- pages/StorageCleaner/index.tsx | 4 +- pages/StorageCleaner/useStorageCleaner.ts | 4 +- pages/TextStatistics/index.tsx | 4 +- pages/Timestamp/LiveClock.tsx | 4 +- pages/Timestamp/ResultView.tsx | 4 +- pages/Timestamp/index.tsx | 4 +- pages/Timestamp/useTimestampConverter.ts | 4 +- public/_locales/en/messages.json | 1290 +++++++++++++++++ public/_locales/zh/messages.json | 1290 +++++++++++++++++ utils/__tests__/useLazyTranslation.test.ts | 193 --- utils/chromeI18n.ts | 102 ++ utils/jwt.ts | 12 +- utils/useLazyTranslation.ts | 109 -- vitest.setup.ts | 85 +- wxt.config.ts | 10 +- 94 files changed, 2987 insertions(+), 1844 deletions(-) delete mode 100644 i18n/README.md delete mode 100644 i18n/index.ts delete mode 100644 i18n/locales/en/base64Converter.json delete mode 100644 i18n/locales/en/common.json delete mode 100644 i18n/locales/en/features.json delete mode 100644 i18n/locales/en/htmlToMarkdown.json delete mode 100644 i18n/locales/en/jsonDiff.json delete mode 100644 i18n/locales/en/jsonFormat.json delete mode 100644 i18n/locales/en/jwt.json delete mode 100644 i18n/locales/en/markdownToHtml.json delete mode 100644 i18n/locales/en/qrCode.json delete mode 100644 i18n/locales/en/rightClickRestorer.json delete mode 100644 i18n/locales/en/storageCleaner.json delete mode 100644 i18n/locales/en/textStatistics.json delete mode 100644 i18n/locales/en/timestamp.json delete mode 100644 i18n/locales/zh/base64Converter.json delete mode 100644 i18n/locales/zh/common.json delete mode 100644 i18n/locales/zh/features.json delete mode 100644 i18n/locales/zh/htmlToMarkdown.json delete mode 100644 i18n/locales/zh/jsonDiff.json delete mode 100644 i18n/locales/zh/jsonFormat.json delete mode 100644 i18n/locales/zh/jwt.json delete mode 100644 i18n/locales/zh/markdownToHtml.json delete mode 100644 i18n/locales/zh/qrCode.json delete mode 100644 i18n/locales/zh/rightClickRestorer.json delete mode 100644 i18n/locales/zh/storageCleaner.json delete mode 100644 i18n/locales/zh/textStatistics.json delete mode 100644 i18n/locales/zh/timestamp.json create mode 100644 public/_locales/en/messages.json create mode 100644 public/_locales/zh/messages.json delete mode 100644 utils/__tests__/useLazyTranslation.test.ts create mode 100644 utils/chromeI18n.ts delete mode 100644 utils/useLazyTranslation.ts diff --git a/components/CopyButton.tsx b/components/CopyButton.tsx index 608edf7..23f9bb3 100644 --- a/components/CopyButton.tsx +++ b/components/CopyButton.tsx @@ -4,7 +4,7 @@ import { copyTextToClipboard } from '@/utils/clipboard'; import { cn } from '@/lib/utils'; import { buttonVariants, type ButtonProps } from '@/components/ui/button'; import { toast } from 'sonner'; -import { useTranslation } from 'react-i18next'; +import { useI18n } from '@/utils/chromeI18n'; interface CopyButtonProps extends Omit { text: string; @@ -19,7 +19,7 @@ export const CopyButton: React.FC = ({ className, ...props }) => { - const { t } = useTranslation('common'); + const { t } = useI18n('common'); const [copied, setCopied] = useState(false); const timerRef = useRef | null>(null); diff --git a/components/DecodeResultPaper.tsx b/components/DecodeResultPaper.tsx index 5c20dca..3984b0f 100644 --- a/components/DecodeResultPaper.tsx +++ b/components/DecodeResultPaper.tsx @@ -10,7 +10,7 @@ import { Download } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { formatFileSize } from '@/utils/base64Converter'; -import { useLazyTranslation } from '@/utils/useLazyTranslation'; +import { useI18n } from '@/utils/chromeI18n'; interface DecodeResultPaperProps { /** 标题文案,由调用方传入 i18n key 对应的值(如 decodedFileOutput / decodedImageOutput) */ @@ -38,7 +38,7 @@ export default function DecodeResultPaper({ onDownload, children, }: DecodeResultPaperProps) { - const { t } = useLazyTranslation('base64Converter'); + const { t } = useI18n('base64Converter'); return (
diff --git a/components/ErrorBoundary.tsx b/components/ErrorBoundary.tsx index c74e7a6..1c62b15 100644 --- a/components/ErrorBoundary.tsx +++ b/components/ErrorBoundary.tsx @@ -1,9 +1,9 @@ import { Component, ErrorInfo, ReactNode } from 'react'; -import { withTranslation, type WithTranslation } from 'react-i18next'; import { AlertCircle, RefreshCw } from 'lucide-react'; import { Button } from '@/components/ui/button'; +import { getMessage } from '@/utils/chromeI18n'; -interface Props extends WithTranslation { +interface Props { children: ReactNode; } @@ -12,7 +12,7 @@ interface State { error: Error | null; } -class ErrorBoundaryBase extends Component { +class ErrorBoundary extends Component { state: State = { hasError: false, error: null, @@ -37,7 +37,6 @@ class ErrorBoundaryBase extends Component { }; render() { - const { t } = this.props; if (this.state.hasError) { return (
@@ -46,9 +45,11 @@ class ErrorBoundaryBase extends Component {

- {t('errorBoundary.title')} + {getMessage('errorBoundary_title')}

-

{t('errorBoundary.description')}

+

+ {getMessage('errorBoundary_description')} +

{this.state.error && (
@@ -62,7 +63,7 @@ class ErrorBoundaryBase extends Component {
               className="rounded-lg font-bold shadow-sm"
             >
               
-              {t('errorBoundary.refresh')}
+              {getMessage('errorBoundary_refresh')}
             
           
@@ -73,5 +74,5 @@ class ErrorBoundaryBase extends Component { } } -export const ErrorBoundary = withTranslation('common')(ErrorBoundaryBase); +export { ErrorBoundary }; export default ErrorBoundary; diff --git a/components/ImageUploader.tsx b/components/ImageUploader.tsx index 4744764..0173353 100644 --- a/components/ImageUploader.tsx +++ b/components/ImageUploader.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useRef } from 'react'; import { Image, X } from 'lucide-react'; import { useSnackbar } from '@/components/GlobalSnackbar'; -import { useLazyTranslation } from '@/utils/useLazyTranslation'; +import { useI18n } from '@/utils/chromeI18n'; interface ImageUploaderProps { /** 选中的文件 */ @@ -29,7 +29,7 @@ const ImageUploader = ({ dragging, onDraggingChange, }: ImageUploaderProps) => { - const { t } = useLazyTranslation('qrCode'); + const { t } = useI18n('qrCode'); const { showMessage } = useSnackbar(); const fileInputRef = useRef(null); diff --git a/components/PageErrorBoundary.tsx b/components/PageErrorBoundary.tsx index bb1cc12..b483612 100644 --- a/components/PageErrorBoundary.tsx +++ b/components/PageErrorBoundary.tsx @@ -1,9 +1,9 @@ import { Component, ErrorInfo, ReactNode } from 'react'; -import { withTranslation, type WithTranslation } from 'react-i18next'; import { AlertCircle, RefreshCw } from 'lucide-react'; import { Button } from '@/components/ui/button'; +import { getMessage } from '@/utils/chromeI18n'; -interface Props extends WithTranslation { +interface Props { children: ReactNode; resetKey?: string | number; } @@ -13,7 +13,7 @@ interface State { error: Error | null; } -class PageErrorBoundaryBase extends Component { +class PageErrorBoundary extends Component { state: State = { hasError: false, error: null, @@ -38,7 +38,6 @@ class PageErrorBoundaryBase extends Component { }; render() { - const { t } = this.props; if (this.state.hasError) { return (
@@ -48,10 +47,10 @@ class PageErrorBoundaryBase extends Component {

- {t('pageErrorBoundary.title')} + {getMessage('pageErrorBoundary_title')}

- {t('pageErrorBoundary.description')} + {getMessage('pageErrorBoundary_description')}

{this.state.error && ( @@ -69,7 +68,7 @@ class PageErrorBoundaryBase extends Component { className="font-medium shadow-sm" > - {t('errorBoundary.retry')} + {getMessage('errorBoundary_retry')} @@ -80,5 +79,5 @@ class PageErrorBoundaryBase extends Component { } } -export const PageErrorBoundary = withTranslation('common')(PageErrorBoundaryBase); +export { PageErrorBoundary }; export default PageErrorBoundary; diff --git a/components/QrCodePreview.tsx b/components/QrCodePreview.tsx index 82864f1..35c43cb 100644 --- a/components/QrCodePreview.tsx +++ b/components/QrCodePreview.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Copy, Download } from 'lucide-react'; -import { useLazyTranslation } from '@/utils/useLazyTranslation'; +import { useI18n } from '@/utils/chromeI18n'; import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/button'; @@ -21,7 +21,7 @@ const QrCodePreview = ({ className, ...props }: QrCodePreviewProps) => { - const { t } = useLazyTranslation('qrCode'); + const { t } = useI18n('qrCode'); // 空状态下的虚线骨架屏 if (!qrCodeDataUrl) { diff --git a/components/RouterContainer.tsx b/components/RouterContainer.tsx index 2689f41..41bd145 100644 --- a/components/RouterContainer.tsx +++ b/components/RouterContainer.tsx @@ -1,7 +1,7 @@ import { FEATURES, getEntryPointType } from '@/config/features'; import { useRouter } from '@/providers/RouterProvider'; import { Suspense, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; +import { useI18n } from '@/utils/chromeI18n'; import PageErrorBoundary from '@/components/PageErrorBoundary'; import PageSkeleton from '@/components/PageSkeleton'; import { cn } from '@/lib/utils'; // 1. 引入标准的 shadcn 工具函数 @@ -9,7 +9,7 @@ import { AlertTriangle } from 'lucide-react'; // 用于标准的 404 异常展 export default function RouterContainer() { const { currentPage, isLoaded } = useRouter(); - const { t } = useTranslation('common'); + const { t } = useI18n('common'); // 2. 稳定的动态动画类名映射 const animationClass = useMemo(() => { diff --git a/components/TextInputArea.tsx b/components/TextInputArea.tsx index a41c188..06666c7 100644 --- a/components/TextInputArea.tsx +++ b/components/TextInputArea.tsx @@ -1,6 +1,6 @@ import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react'; import { X } from 'lucide-react'; -import { useTranslation } from 'react-i18next'; +import { useI18n } from '@/utils/chromeI18n'; import { cn } from '@/lib/utils'; import { toast } from 'sonner'; // 推荐使用 shadcn 的默认 Toast import { CopyButton } from '@/components/CopyButton'; @@ -114,7 +114,7 @@ const TextInputArea = forwardRef((props const [internalValue, setInternalValue] = useState(defaultValue); const [error, setError] = useState(''); - const { t } = useTranslation('common'); + const { t } = useI18n('common'); const placeholder = placeholderProp ?? t('textInputArea.placeholder'); const isControlled = controlledValue !== undefined; diff --git a/components/TopBar.tsx b/components/TopBar.tsx index bad30ac..bd02c25 100644 --- a/components/TopBar.tsx +++ b/components/TopBar.tsx @@ -16,8 +16,8 @@ import { useThemeMode } from '@/providers/ThemeModeProvider'; import { FeatureConfig, FEATURES } from '@/config/features'; import { storageUtil } from '@/utils/chromeStorage'; import { openExtensionPage } from '@/utils/chromeTabs'; -import { useTranslation } from 'react-i18next'; -import { normalizeLanguage, SUPPORTED_LANGUAGES } from '@/i18n'; +import { useI18n } from '@/utils/chromeI18n'; +import { normalizeLanguage, SUPPORTED_LANGUAGES } from '@/utils/chromeI18n'; import { cn } from '@/lib/utils'; // 1. 引入 shadcn 核心工具函数 // 常量配置抽取(无需写在全局变量或 styles 对象里) @@ -27,7 +27,7 @@ const SEARCH_HISTORY_DISPLAY = 5; export default function TopBar({ onOpenOptions }: { onOpenOptions: () => void }) { const { currentPage, goBack, navigateTo } = useRouter(); const { mode, setMode } = useThemeMode(); - const { t, i18n } = useTranslation(['common', 'features']); + const { t, i18n } = useI18n(['common', 'features']); const [searchQuery, setSearchQuery] = useState(''); const [showResults, setShowResults] = useState(false); @@ -159,7 +159,7 @@ export default function TopBar({ onOpenOptions }: { onOpenOptions: () => void }) diff --git a/pages/StorageCleaner/StorageOptionsGrid.tsx b/pages/StorageCleaner/StorageOptionsGrid.tsx index 699913d..338e664 100644 --- a/pages/StorageCleaner/StorageOptionsGrid.tsx +++ b/pages/StorageCleaner/StorageOptionsGrid.tsx @@ -1,7 +1,7 @@ import React from 'react'; import type { StorageCleanerOptions } from '@/types/storage'; import OptionItem from './OptionItem'; -import { useLazyTranslation } from '@/utils/useLazyTranslation'; +import { useI18n } from '@/utils/chromeI18n'; import { cn } from '@/lib/utils'; // 1. 引入官方标准的 Checkbox 原子组件 import { Checkbox } from '@/components/ui/checkbox'; @@ -26,7 +26,7 @@ export default function StorageOptionsGrid({ className, ...props }: StorageOptionsGridProps) { - const { t } = useLazyTranslation('storageCleaner'); + const { t } = useI18n('storageCleaner'); const optionKeys: { key: keyof StorageCleanerOptions; isCount?: boolean }[] = [ { key: 'localStorage' }, diff --git a/pages/StorageCleaner/index.tsx b/pages/StorageCleaner/index.tsx index 02ddd2d..7456dfb 100644 --- a/pages/StorageCleaner/index.tsx +++ b/pages/StorageCleaner/index.tsx @@ -6,10 +6,10 @@ import StorageOptionsGrid from './StorageOptionsGrid'; import AutoRefreshToggle from './AutoRefreshToggle'; import ErrorDisplay from './ErrorDisplay'; import CleaningResult from './CleaningResult'; -import { useLazyTranslation } from '@/utils/useLazyTranslation'; +import { useI18n } from '@/utils/chromeI18n'; export default function Index() { - const { t } = useLazyTranslation('storageCleaner'); + const { t } = useI18n('storageCleaner'); const { error, diff --git a/pages/StorageCleaner/useStorageCleaner.ts b/pages/StorageCleaner/useStorageCleaner.ts index 41a54aa..28dc5e1 100644 --- a/pages/StorageCleaner/useStorageCleaner.ts +++ b/pages/StorageCleaner/useStorageCleaner.ts @@ -17,7 +17,7 @@ import { isRestrictedUrl, } from '@/utils/storageCleaner'; import { MessageAction, sendMessage } from '@/utils/messages'; -import { useLazyTranslation } from '@/utils/useLazyTranslation'; +import { useI18n } from '@/utils/chromeI18n'; import { toast } from 'sonner'; // 1. 直接引用 shadcn 推荐的 Sonner 单例通知,踢出回调依赖 const DEFAULT_OPTIONS: StorageCleanerOptions = { @@ -56,7 +56,7 @@ export interface UseStorageCleanerReturn { } export function useStorageCleaner(): UseStorageCleanerReturn { - const { t } = useLazyTranslation(['storageCleaner', 'common']); + const { t } = useI18n(['storageCleaner', 'common']); const [domain, setDomain] = useState(''); const [error, setError] = useState(''); const [isInitializing, setIsInitializing] = useState(true); diff --git a/pages/TextStatistics/index.tsx b/pages/TextStatistics/index.tsx index 7d063c6..a78c55a 100644 --- a/pages/TextStatistics/index.tsx +++ b/pages/TextStatistics/index.tsx @@ -1,12 +1,12 @@ import { useCallback, useMemo, useState } from 'react'; import TextInputArea from '@/components/TextInputArea'; import { formatByteSize, getTextStats } from '@/utils/textStatistics'; -import { useLazyTranslation } from '@/utils/useLazyTranslation'; +import { useI18n } from '@/utils/chromeI18n'; import { useContextMenuData } from '@/utils/useContextMenuData'; import { cn } from '@/lib/utils'; export default function Index() { - const { t } = useLazyTranslation('textStatistics'); + const { t } = useI18n('textStatistics'); const [text, setText] = useState(''); const handleContextMenuData = useCallback((payload: string) => { diff --git a/pages/Timestamp/LiveClock.tsx b/pages/Timestamp/LiveClock.tsx index a503e87..a82ae4b 100644 --- a/pages/Timestamp/LiveClock.tsx +++ b/pages/Timestamp/LiveClock.tsx @@ -3,7 +3,7 @@ import { Clock } from 'lucide-react'; import CopyButton from '@/components/CopyButton'; import { useSnackbar } from '@/components/GlobalSnackbar'; import type { UnitType } from './constants'; -import { useLazyTranslation } from '@/utils/useLazyTranslation'; +import { useI18n } from '@/utils/chromeI18n'; import { cn } from '@/lib/utils'; // 引入标准的 shadcn 工具函数 interface LiveClockProps extends React.HTMLAttributes { @@ -12,7 +12,7 @@ interface LiveClockProps extends React.HTMLAttributes { } const LiveClock = React.memo(({ unit, onUseNow, className, ...props }: LiveClockProps) => { - const { t } = useLazyTranslation('timestamp'); + const { t } = useI18n('timestamp'); const { showMessage } = useSnackbar(); const onUseNowRef = useRef(onUseNow); diff --git a/pages/Timestamp/ResultView.tsx b/pages/Timestamp/ResultView.tsx index 536d138..00981fa 100644 --- a/pages/Timestamp/ResultView.tsx +++ b/pages/Timestamp/ResultView.tsx @@ -3,7 +3,7 @@ import dayjs from '@/utils/dayjs'; import CopyButton from '@/components/CopyButton'; import type { UnitType } from './constants'; import { DATE_FORMAT } from './constants'; -import { useLazyTranslation } from '@/utils/useLazyTranslation'; +import { useI18n } from '@/utils/chromeI18n'; import { cn } from '@/lib/utils'; // shadcn 核心类名合并工具 interface ResultViewProps extends React.HTMLAttributes { @@ -25,7 +25,7 @@ const ResultView = React.memo( className, ...props }: ResultViewProps) => { - const { t } = useLazyTranslation('timestamp'); + const { t } = useI18n('timestamp'); // 严谨计算时间衍生的附加时区/相对时间状态 const extraInfo = useMemo(() => { diff --git a/pages/Timestamp/index.tsx b/pages/Timestamp/index.tsx index d362db2..eb215a3 100644 --- a/pages/Timestamp/index.tsx +++ b/pages/Timestamp/index.tsx @@ -3,7 +3,7 @@ import { ZONES } from './constants'; import LiveClock from './LiveClock'; import ResultView from './ResultView'; import { useTimestampConverter } from './useTimestampConverter'; -import { useLazyTranslation } from '@/utils/useLazyTranslation'; +import { useI18n } from '@/utils/chromeI18n'; import { cn } from '@/lib/utils'; // 1. 引入标准的 shadcn/ui 原子表单组件(代替原生的原生 Input 和 Select) @@ -17,7 +17,7 @@ import { } from '@/components/ui/select'; export default function Index() { - const { t } = useLazyTranslation('timestamp'); + const { t } = useI18n('timestamp'); // 2. 完美对接全新重构后的统一单源响应式 Hook const { diff --git a/pages/Timestamp/useTimestampConverter.ts b/pages/Timestamp/useTimestampConverter.ts index da3c771..4a67f56 100644 --- a/pages/Timestamp/useTimestampConverter.ts +++ b/pages/Timestamp/useTimestampConverter.ts @@ -2,7 +2,7 @@ import { useCallback, useMemo, useState } from 'react'; import dayjs from '@/utils/dayjs'; import type { UnitType, ZoneType } from './constants'; import { DATE_FORMAT } from './constants'; -import { useLazyTranslation } from '@/utils/useLazyTranslation'; +import { useI18n } from '@/utils/chromeI18n'; import { useContextMenuData } from '@/utils/useContextMenuData'; export interface UseTimestampConverterReturn { @@ -26,7 +26,7 @@ function isTimestampLike(input: string): boolean { } export function useTimestampConverter(): UseTimestampConverterReturn { - const { t } = useLazyTranslation('timestamp'); + const { t } = useI18n('timestamp'); const [mode, setMode] = useState<'ts2dt' | 'dt2ts'>('ts2dt'); const [unit, setUnit] = useState('ms'); const [zone, setZone] = useState('Asia/Shanghai'); diff --git a/public/_locales/en/messages.json b/public/_locales/en/messages.json new file mode 100644 index 0000000..b45c9fe --- /dev/null +++ b/public/_locales/en/messages.json @@ -0,0 +1,1290 @@ +{ + "appName": { + "message": "Testing Tools", + "description": "Translation key: appName" + }, + "buttons_save": { + "message": "Save", + "description": "Translation key: buttons_save" + }, + "buttons_cancel": { + "message": "Cancel", + "description": "Translation key: buttons_cancel" + }, + "buttons_confirm": { + "message": "Confirm", + "description": "Translation key: buttons_confirm" + }, + "buttons_copy": { + "message": "Copy", + "description": "Translation key: buttons_copy" + }, + "buttons_clear": { + "message": "Clear", + "description": "Translation key: buttons_clear" + }, + "buttons_refresh": { + "message": "Refresh", + "description": "Translation key: buttons_refresh" + }, + "buttons_toggleLanguage": { + "message": "Switch Language", + "description": "Translation key: buttons_toggleLanguage" + }, + "buttons_toggleTheme": { + "message": "Toggle theme", + "description": "Translation key: buttons_toggleTheme" + }, + "buttons_themeMode_light": { + "message": "Switch to dark mode", + "description": "Translation key: buttons_themeMode_light" + }, + "buttons_themeMode_dark": { + "message": "Switch to system mode", + "description": "Translation key: buttons_themeMode_dark" + }, + "buttons_themeMode_system": { + "message": "Switch to light mode", + "description": "Translation key: buttons_themeMode_system" + }, + "buttons_search": { + "message": "Search tools...", + "description": "Translation key: buttons_search" + }, + "buttons_back": { + "message": "Back", + "description": "Translation key: buttons_back" + }, + "buttons_clearSearch": { + "message": "Clear search", + "description": "Translation key: buttons_clearSearch" + }, + "buttons_recentSearch": { + "message": "Recent search", + "description": "Translation key: buttons_recentSearch" + }, + "buttons_noResults": { + "message": "No tools found", + "description": "Translation key: buttons_noResults" + }, + "buttons_openInTab": { + "message": "Open in tab", + "description": "Translation key: buttons_openInTab" + }, + "buttons_settings": { + "message": "Settings", + "description": "Translation key: buttons_settings" + }, + "messages_copySuccess": { + "message": "Copied to clipboard", + "description": "Translation key: messages_copySuccess" + }, + "messages_copyError": { + "message": "Copy failed", + "description": "Translation key: messages_copyError" + }, + "messages_copyEmpty": { + "message": "Nothing to copy", + "description": "Translation key: messages_copyEmpty" + }, + "errorBoundary_title": { + "message": "Oops, something went wrong", + "description": "Translation key: errorBoundary_title" + }, + "errorBoundary_description": { + "message": "The app encountered an unexpected error. You can try refreshing the page or resetting the app.", + "description": "Translation key: errorBoundary_description" + }, + "errorBoundary_refresh": { + "message": "Refresh App", + "description": "Translation key: errorBoundary_refresh" + }, + "errorBoundary_retry": { + "message": "Retry", + "description": "Translation key: errorBoundary_retry" + }, + "pageErrorBoundary_title": { + "message": "This feature has encountered an error", + "description": "Translation key: pageErrorBoundary_title" + }, + "pageErrorBoundary_description": { + "message": "An internal script error occurred while loading or rendering this page. You can try again or switch to another tool from the navigation menu.", + "description": "Translation key: pageErrorBoundary_description" + }, + "router_notFound": { + "message": "Page Not Found", + "description": "Translation key: router_notFound" + }, + "router_notFoundDescription": { + "message": "This feature is not available or has been removed in the current runtime ({{entryPointType}}).", + "description": "Translation key: router_notFoundDescription" + }, + "textInputArea_clear": { + "message": "Clear", + "description": "Translation key: textInputArea_clear" + }, + "textInputArea_copyContent": { + "message": "Copy content", + "description": "Translation key: textInputArea_copyContent" + }, + "textInputArea_cleared": { + "message": "Cleared", + "description": "Translation key: textInputArea_cleared" + }, + "textInputArea_placeholder": { + "message": "placeholder text", + "description": "Translation key: textInputArea_placeholder" + }, + "dashboard_title": { + "message": "Dashboard", + "description": "Translation key: dashboard_title" + }, + "timestamp_title": { + "message": "Timestamp", + "description": "Translation key: timestamp_title" + }, + "timestamp_description": { + "message": "Unix millisecond conversion and formatting", + "description": "Translation key: timestamp_description" + }, + "storageCleaner_title": { + "message": "Storage Cleaner", + "description": "Translation key: storageCleaner_title" + }, + "storageCleaner_description": { + "message": "Clean cache, cookies, and local storage", + "description": "Translation key: storageCleaner_description" + }, + "qrCode_title": { + "message": "QR Code Tools", + "description": "Translation key: qrCode_title" + }, + "qrCode_description": { + "message": "Generate QR code for the selected URL", + "description": "Translation key: qrCode_description" + }, + "textStatistics_title": { + "message": "Text Statistics", + "description": "Translation key: textStatistics_title" + }, + "textStatistics_description": { + "message": "Real-time analysis of text characters, words, and bytes", + "description": "Translation key: textStatistics_description" + }, + "jwt_title": { + "message": "JWT Parser", + "description": "Translation key: jwt_title" + }, + "jwt_description": { + "message": "JSON Web Token decoding and viewing", + "description": "Translation key: jwt_description" + }, + "jsonDiff_title": { + "message": "JSON Tools", + "description": "Translation key: jsonDiff_title" + }, + "jsonDiff_description": { + "message": "Diff, format, YAML/TOML conversion, and minify", + "description": "Translation key: jsonDiff_description" + }, + "base64Converter_title": { + "message": "Base64 Converter", + "description": "Translation key: base64Converter_title" + }, + "base64Converter_description": { + "message": "Encode text, files, and images to Base64", + "description": "Translation key: base64Converter_description" + }, + "markdownToHtml_title": { + "message": "Markdown to HTML", + "description": "Translation key: markdownToHtml_title" + }, + "markdownToHtml_description": { + "message": "Real-time Markdown conversion and HTML preview", + "description": "Translation key: markdownToHtml_description" + }, + "htmlToMarkdown_title": { + "message": "HTML to Markdown", + "description": "Translation key: htmlToMarkdown_title" + }, + "htmlToMarkdown_description": { + "message": "Real-time HTML conversion and Markdown preview", + "description": "Translation key: htmlToMarkdown_description" + }, + "rightClickRestorer_title": { + "message": "Right Click Restorer", + "description": "Translation key: rightClickRestorer_title" + }, + "rightClickRestorer_description": { + "message": "Detect and restore disabled browser right-click menus", + "description": "Translation key: rightClickRestorer_description" + }, + "base64Converter_pageTitle": { + "message": "Base64 Converter", + "description": "Translation key: base64Converter_pageTitle" + }, + "base64Converter_pageSubtitle": { + "message": "Encode and decode text, files, and images with Base64", + "description": "Translation key: base64Converter_pageSubtitle" + }, + "base64Converter_textMode": { + "message": "Text", + "description": "Translation key: base64Converter_textMode" + }, + "base64Converter_fileMode": { + "message": "File", + "description": "Translation key: base64Converter_fileMode" + }, + "base64Converter_imageMode": { + "message": "Image", + "description": "Translation key: base64Converter_imageMode" + }, + "base64Converter_encode": { + "message": "Encode", + "description": "Translation key: base64Converter_encode" + }, + "base64Converter_decode": { + "message": "Decode", + "description": "Translation key: base64Converter_decode" + }, + "base64Converter_clear": { + "message": "Clear", + "description": "Translation key: base64Converter_clear" + }, + "base64Converter_textInputPlaceholder": { + "message": "Enter text to encode to Base64...", + "description": "Translation key: base64Converter_textInputPlaceholder" + }, + "base64Converter_base64InputPlaceholder": { + "message": "Enter Base64 string to decode...", + "description": "Translation key: base64Converter_base64InputPlaceholder" + }, + "base64Converter_base64Output": { + "message": "Base64 Output", + "description": "Translation key: base64Converter_base64Output" + }, + "base64Converter_textOutput": { + "message": "Decoded Text Output", + "description": "Translation key: base64Converter_textOutput" + }, + "base64Converter_copyRaw": { + "message": "Copy Raw Base64", + "description": "Translation key: base64Converter_copyRaw" + }, + "base64Converter_copyDataUri": { + "message": "Copy Data URI", + "description": "Translation key: base64Converter_copyDataUri" + }, + "base64Converter_clickOrDropToFile": { + "message": "Click or drop a file here", + "description": "Translation key: base64Converter_clickOrDropToFile" + }, + "base64Converter_clickOrDropToImage": { + "message": "Click or drop an image here", + "description": "Translation key: base64Converter_clickOrDropToImage" + }, + "base64Converter_clickOrDropToReplace": { + "message": "Click or drop to replace the file", + "description": "Translation key: base64Converter_clickOrDropToReplace" + }, + "base64Converter_maxFileSize": { + "message": "Maximum file size: {{max}}", + "description": "Translation key: base64Converter_maxFileSize" + }, + "base64Converter_supportedFormats": { + "message": "Supports PNG, JPG, WEBP, GIF, BMP, SVG, etc.", + "description": "Translation key: base64Converter_supportedFormats" + }, + "base64Converter_fileSizeExceeded": { + "message": "File size exceeds the limit (max {{max}})", + "description": "Translation key: base64Converter_fileSizeExceeded" + }, + "base64Converter_unsupportedImageType": { + "message": "Unsupported image format", + "description": "Translation key: base64Converter_unsupportedImageType" + }, + "base64Converter_conversionFailed": { + "message": "Conversion failed", + "description": "Translation key: base64Converter_conversionFailed" + }, + "base64Converter_originalSize": { + "message": "Original Size", + "description": "Translation key: base64Converter_originalSize" + }, + "base64Converter_encodedSize": { + "message": "Encoded Size", + "description": "Translation key: base64Converter_encodedSize" + }, + "base64Converter_invalidBase64": { + "message": "Invalid Base64 string", + "description": "Translation key: base64Converter_invalidBase64" + }, + "base64Converter_binaryDataDetected": { + "message": "Input appears to be binary data (e.g. an image). Please switch to the Image tab.", + "description": "Translation key: base64Converter_binaryDataDetected" + }, + "base64Converter_imageDataUriHint": { + "message": "Detected an image data URI — please use the Image tab to decode it.", + "description": "Translation key: base64Converter_imageDataUriHint" + }, + "base64Converter_switchToImageMode": { + "message": "Switch to Image mode", + "description": "Translation key: base64Converter_switchToImageMode" + }, + "base64Converter_download": { + "message": "Download", + "description": "Translation key: base64Converter_download" + }, + "base64Converter_decodedFileName": { + "message": "Decoded file name", + "description": "Translation key: base64Converter_decodedFileName" + }, + "base64Converter_decodeBase64Placeholder": { + "message": "Enter Base64 or data URI to decode...", + "description": "Translation key: base64Converter_decodeBase64Placeholder" + }, + "base64Converter_decodedFileOutput": { + "message": "Decoded File", + "description": "Translation key: base64Converter_decodedFileOutput" + }, + "base64Converter_decodedImageOutput": { + "message": "Decoded Image", + "description": "Translation key: base64Converter_decodedImageOutput" + }, + "base64Converter_inferredMimeType": { + "message": "Inferred MIME type", + "description": "Translation key: base64Converter_inferredMimeType" + }, + "base64Converter_decodedSize": { + "message": "Decoded size", + "description": "Translation key: base64Converter_decodedSize" + }, + "htmlToMarkdown_pageTitle": { + "message": "HTML to Markdown", + "description": "Translation key: htmlToMarkdown_pageTitle" + }, + "htmlToMarkdown_pageSubtitle": { + "message": "Convert HTML to Markdown in real-time", + "description": "Translation key: htmlToMarkdown_pageSubtitle" + }, + "htmlToMarkdown_splitMode": { + "message": "Split", + "description": "Translation key: htmlToMarkdown_splitMode" + }, + "htmlToMarkdown_previewMode": { + "message": "Preview", + "description": "Translation key: htmlToMarkdown_previewMode" + }, + "htmlToMarkdown_markdownMode": { + "message": "Markdown", + "description": "Translation key: htmlToMarkdown_markdownMode" + }, + "htmlToMarkdown_clear": { + "message": "Clear", + "description": "Translation key: htmlToMarkdown_clear" + }, + "htmlToMarkdown_inputLabel": { + "message": "HTML Input", + "description": "Translation key: htmlToMarkdown_inputLabel" + }, + "htmlToMarkdown_previewLabel": { + "message": "Markdown Preview", + "description": "Translation key: htmlToMarkdown_previewLabel" + }, + "htmlToMarkdown_markdownOutputLabel": { + "message": "Markdown Output", + "description": "Translation key: htmlToMarkdown_markdownOutputLabel" + }, + "htmlToMarkdown_inputPlaceholder": { + "message": "Enter your HTML content here...", + "description": "Translation key: htmlToMarkdown_inputPlaceholder" + }, + "htmlToMarkdown_charCount": { + "message": "{{count}} chars", + "description": "Translation key: htmlToMarkdown_charCount" + }, + "htmlToMarkdown_copyMarkdown": { + "message": "Copy Markdown", + "description": "Translation key: htmlToMarkdown_copyMarkdown" + }, + "htmlToMarkdown_download": { + "message": "Download", + "description": "Translation key: htmlToMarkdown_download" + }, + "htmlToMarkdown_emptyHint": { + "message": "Enter HTML content to see the conversion result", + "description": "Translation key: htmlToMarkdown_emptyHint" + }, + "jsonDiff_pageTitle": { + "message": "JSON Diff", + "description": "Translation key: jsonDiff_pageTitle" + }, + "jsonDiff_pageSubtitle": { + "message": "Compare differences between two JSON values", + "description": "Translation key: jsonDiff_pageSubtitle" + }, + "jsonDiff_leftPlaceholder": { + "message": "Enter the original JSON...", + "description": "Translation key: jsonDiff_leftPlaceholder" + }, + "jsonDiff_rightPlaceholder": { + "message": "Enter the target JSON...", + "description": "Translation key: jsonDiff_rightPlaceholder" + }, + "jsonDiff_leftLabel": { + "message": "Original JSON", + "description": "Translation key: jsonDiff_leftLabel" + }, + "jsonDiff_rightLabel": { + "message": "Target JSON", + "description": "Translation key: jsonDiff_rightLabel" + }, + "jsonDiff_compareButton": { + "message": "Compare", + "description": "Translation key: jsonDiff_compareButton" + }, + "jsonDiff_clearButton": { + "message": "Clear", + "description": "Translation key: jsonDiff_clearButton" + }, + "jsonDiff_sideBySideMode": { + "message": "Side by side", + "description": "Translation key: jsonDiff_sideBySideMode" + }, + "jsonDiff_unifiedMode": { + "message": "Unified", + "description": "Translation key: jsonDiff_unifiedMode" + }, + "jsonDiff_previousDiff": { + "message": "Previous", + "description": "Translation key: jsonDiff_previousDiff" + }, + "jsonDiff_nextDiff": { + "message": "Next", + "description": "Translation key: jsonDiff_nextDiff" + }, + "jsonDiff_noDiffs": { + "message": "No differences", + "description": "Translation key: jsonDiff_noDiffs" + }, + "jsonDiff_diffCount": { + "message": "{{count}} differences", + "description": "Translation key: jsonDiff_diffCount" + }, + "jsonDiff_invalidJson": { + "message": "Invalid JSON format", + "description": "Translation key: jsonDiff_invalidJson" + }, + "jsonDiff_emptyHint": { + "message": "Enter JSON on both sides and click Compare", + "description": "Translation key: jsonDiff_emptyHint" + }, + "jsonDiff_fixErrorHint": { + "message": "Fix the JSON syntax errors above to enable live comparison", + "description": "Translation key: jsonDiff_fixErrorHint" + }, + "jsonDiff_added": { + "message": "Added", + "description": "Translation key: jsonDiff_added" + }, + "jsonDiff_removed": { + "message": "Removed", + "description": "Translation key: jsonDiff_removed" + }, + "jsonDiff_modified": { + "message": "Modified", + "description": "Translation key: jsonDiff_modified" + }, + "jsonFormat_formatTitle": { + "message": "JSON Formatter", + "description": "Translation key: jsonFormat_formatTitle" + }, + "jsonFormat_formatSubtitle": { + "message": "Beautify and format JSON data", + "description": "Translation key: jsonFormat_formatSubtitle" + }, + "jsonFormat_inputPlaceholder": { + "message": "Enter JSON to format...", + "description": "Translation key: jsonFormat_inputPlaceholder" + }, + "jsonFormat_formatButton": { + "message": "Format", + "description": "Translation key: jsonFormat_formatButton" + }, + "jsonFormat_clearButton": { + "message": "Clear", + "description": "Translation key: jsonFormat_clearButton" + }, + "jsonFormat_sortKeys": { + "message": "Sort Keys", + "description": "Translation key: jsonFormat_sortKeys" + }, + "jsonFormat_sortKeysTooltip": { + "message": "Sort JSON object keys in alphabetical order", + "description": "Translation key: jsonFormat_sortKeysTooltip" + }, + "jsonFormat_indentSize": { + "message": "Indent", + "description": "Translation key: jsonFormat_indentSize" + }, + "jsonFormat_indentSpaces": { + "message": "{{count}} spaces", + "description": "Translation key: jsonFormat_indentSpaces" + }, + "jsonFormat_outputLabel": { + "message": "Formatted Result", + "description": "Translation key: jsonFormat_outputLabel" + }, + "jsonFormat_copySuccess": { + "message": "Copied", + "description": "Translation key: jsonFormat_copySuccess" + }, + "jsonFormat_copyFail": { + "message": "Copy failed", + "description": "Translation key: jsonFormat_copyFail" + }, + "jsonFormat_noContent": { + "message": "Nothing to copy", + "description": "Translation key: jsonFormat_noContent" + }, + "jsonFormat_invalidJson": { + "message": "Invalid JSON format", + "description": "Translation key: jsonFormat_invalidJson" + }, + "jsonFormat_emptyHint": { + "message": "Enter JSON and click Format", + "description": "Translation key: jsonFormat_emptyHint" + }, + "jsonFormat_fixErrorHint": { + "message": "Fix the JSON syntax errors above to enable live formatting", + "description": "Translation key: jsonFormat_fixErrorHint" + }, + "jsonFormat_originalSize": { + "message": "Original size", + "description": "Translation key: jsonFormat_originalSize" + }, + "jsonFormat_formattedSize": { + "message": "Formatted size", + "description": "Translation key: jsonFormat_formattedSize" + }, + "jsonFormat_diffMode": { + "message": "Diff", + "description": "Translation key: jsonFormat_diffMode" + }, + "jsonFormat_formatMode": { + "message": "Format", + "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": "Minify", + "description": "Translation key: jsonFormat_minifyMode" + }, + "jsonFormat_yamlTitle": { + "message": "JSON to YAML", + "description": "Translation key: jsonFormat_yamlTitle" + }, + "jsonFormat_yamlSubtitle": { + "message": "Convert JSON data to YAML format", + "description": "Translation key: jsonFormat_yamlSubtitle" + }, + "jsonFormat_yamlModeInputPlaceholder": { + "message": "Enter JSON to convert...", + "description": "Translation key: jsonFormat_yamlModeInputPlaceholder" + }, + "jsonFormat_yamlModeOutputLabel": { + "message": "YAML Result", + "description": "Translation key: jsonFormat_yamlModeOutputLabel" + }, + "jsonFormat_yamlModeEmptyHint": { + "message": "Enter JSON and click Convert", + "description": "Translation key: jsonFormat_yamlModeEmptyHint" + }, + "jsonFormat_convertButton": { + "message": "Convert", + "description": "Translation key: jsonFormat_convertButton" + }, + "jsonFormat_tomlTitle": { + "message": "JSON to TOML", + "description": "Translation key: jsonFormat_tomlTitle" + }, + "jsonFormat_tomlSubtitle": { + "message": "Convert JSON data to TOML format", + "description": "Translation key: jsonFormat_tomlSubtitle" + }, + "jsonFormat_tomlModeInputPlaceholder": { + "message": "Enter JSON to convert...", + "description": "Translation key: jsonFormat_tomlModeInputPlaceholder" + }, + "jsonFormat_tomlModeOutputLabel": { + "message": "TOML Result", + "description": "Translation key: jsonFormat_tomlModeOutputLabel" + }, + "jsonFormat_tomlModeEmptyHint": { + "message": "Enter JSON and click Convert", + "description": "Translation key: jsonFormat_tomlModeEmptyHint" + }, + "jsonFormat_minifyTitle": { + "message": "JSON Minifier", + "description": "Translation key: jsonFormat_minifyTitle" + }, + "jsonFormat_minifySubtitle": { + "message": "Compress JSON into a compact single-line format", + "description": "Translation key: jsonFormat_minifySubtitle" + }, + "jsonFormat_minifyModeInputPlaceholder": { + "message": "Enter JSON to minify...", + "description": "Translation key: jsonFormat_minifyModeInputPlaceholder" + }, + "jsonFormat_minifyModeOutputLabel": { + "message": "Minified Result", + "description": "Translation key: jsonFormat_minifyModeOutputLabel" + }, + "jsonFormat_minifyModeEmptyHint": { + "message": "Enter JSON and click Minify", + "description": "Translation key: jsonFormat_minifyModeEmptyHint" + }, + "jsonFormat_minifyButton": { + "message": "Minify", + "description": "Translation key: jsonFormat_minifyButton" + }, + "jwt_pageTitle": { + "message": "JWT Parser", + "description": "Translation key: jwt_pageTitle" + }, + "jwt_pageSubtitle": { + "message": "JSON Web Token decoding and viewing", + "description": "Translation key: jwt_pageSubtitle" + }, + "jwt_placeholder": { + "message": "Paste Encoded JWT here...", + "description": "Translation key: jwt_placeholder" + }, + "jwt_headerTitle": { + "message": "HEADER: Algorithm & Token Type", + "description": "Translation key: jwt_headerTitle" + }, + "jwt_payloadTitle": { + "message": "PAYLOAD: Data", + "description": "Translation key: jwt_payloadTitle" + }, + "jwt_signatureTitle": { + "message": "Signature", + "description": "Translation key: jwt_signatureTitle" + }, + "jwt_noSignature": { + "message": "No Signature", + "description": "Translation key: jwt_noSignature" + }, + "jwt_invalidFormat": { + "message": "Unable to parse", + "description": "Translation key: jwt_invalidFormat" + }, + "jwt_errors_invalidBase64String": { + "message": "Invalid base64url string", + "description": "Translation key: jwt_errors_invalidBase64String" + }, + "jwt_errors_failedToDecode": { + "message": "Failed to decode base64url: ", + "description": "Translation key: jwt_errors_failedToDecode" + }, + "jwt_errors_invalidFormat": { + "message": "Invalid JWT format: expected 3 parts separated by .", + "description": "Translation key: jwt_errors_invalidFormat" + }, + "jwt_errors_parseHeaderFailed": { + "message": "Failed to parse Header: ", + "description": "Translation key: jwt_errors_parseHeaderFailed" + }, + "jwt_errors_parsePayloadFailed": { + "message": "Failed to parse Payload: ", + "description": "Translation key: jwt_errors_parsePayloadFailed" + }, + "markdownToHtml_pageTitle": { + "message": "Markdown to HTML", + "description": "Translation key: markdownToHtml_pageTitle" + }, + "markdownToHtml_pageSubtitle": { + "message": "Convert Markdown to HTML in real-time with preview", + "description": "Translation key: markdownToHtml_pageSubtitle" + }, + "markdownToHtml_splitMode": { + "message": "Split", + "description": "Translation key: markdownToHtml_splitMode" + }, + "markdownToHtml_previewMode": { + "message": "Preview", + "description": "Translation key: markdownToHtml_previewMode" + }, + "markdownToHtml_htmlMode": { + "message": "HTML", + "description": "Translation key: markdownToHtml_htmlMode" + }, + "markdownToHtml_clear": { + "message": "Clear", + "description": "Translation key: markdownToHtml_clear" + }, + "markdownToHtml_print": { + "message": "Print", + "description": "Translation key: markdownToHtml_print" + }, + "markdownToHtml_download": { + "message": "Download", + "description": "Translation key: markdownToHtml_download" + }, + "markdownToHtml_inputLabel": { + "message": "Markdown Input", + "description": "Translation key: markdownToHtml_inputLabel" + }, + "markdownToHtml_previewLabel": { + "message": "Live Preview", + "description": "Translation key: markdownToHtml_previewLabel" + }, + "markdownToHtml_htmlOutputLabel": { + "message": "HTML Output", + "description": "Translation key: markdownToHtml_htmlOutputLabel" + }, + "markdownToHtml_inputPlaceholder": { + "message": "Enter your Markdown content here...", + "description": "Translation key: markdownToHtml_inputPlaceholder" + }, + "markdownToHtml_charCount": { + "message": "{{count}} chars", + "description": "Translation key: markdownToHtml_charCount" + }, + "markdownToHtml_copyHtml": { + "message": "Copy HTML", + "description": "Translation key: markdownToHtml_copyHtml" + }, + "markdownToHtml_downloadSuccess": { + "message": "File downloaded successfully", + "description": "Translation key: markdownToHtml_downloadSuccess" + }, + "markdownToHtml_printSuccess": { + "message": "Print window opened", + "description": "Translation key: markdownToHtml_printSuccess" + }, + "qrCode_pageTitle": { + "message": "QR Code Tools", + "description": "Translation key: qrCode_pageTitle" + }, + "qrCode_pageSubtitle": { + "message": "Generate and parse QR codes", + "description": "Translation key: qrCode_pageSubtitle" + }, + "qrCode_generateMode": { + "message": "Generate QR Code", + "description": "Translation key: qrCode_generateMode" + }, + "qrCode_parseMode": { + "message": "Parse QR Code", + "description": "Translation key: qrCode_parseMode" + }, + "qrCode_urlToQr": { + "message": "Text to QR Code", + "description": "Translation key: qrCode_urlToQr" + }, + "qrCode_qrToUrl": { + "message": "QR Code to Text", + "description": "Translation key: qrCode_qrToUrl" + }, + "qrCode_urlInputLabel": { + "message": "Enter URL or Text", + "description": "Translation key: qrCode_urlInputLabel" + }, + "qrCode_urlInputPlaceholder": { + "message": "Enter URL or text content, QR code will be generated automatically", + "description": "Translation key: qrCode_urlInputPlaceholder" + }, + "qrCode_generateButton": { + "message": "Generate QR Code", + "description": "Translation key: qrCode_generateButton" + }, + "qrCode_generating": { + "message": "Generating...", + "description": "Translation key: qrCode_generating" + }, + "qrCode_qrCodeWillShow": { + "message": "QR code will be shown here", + "description": "Translation key: qrCode_qrCodeWillShow" + }, + "qrCode_downloadButton": { + "message": "Download QR Code", + "description": "Translation key: qrCode_downloadButton" + }, + "qrCode_copyQrButton": { + "message": "Copy QR Code Image", + "description": "Translation key: qrCode_copyQrButton" + }, + "qrCode_qrCodeSuccess": { + "message": "QR code generated successfully", + "description": "Translation key: qrCode_qrCodeSuccess" + }, + "qrCode_qrCodeDownloadSuccess": { + "message": "QR code downloaded successfully", + "description": "Translation key: qrCode_qrCodeDownloadSuccess" + }, + "qrCode_qrCodeCopySuccess": { + "message": "QR code copied to clipboard", + "description": "Translation key: qrCode_qrCodeCopySuccess" + }, + "qrCode_selectImage": { + "message": "Please select a QR code image", + "description": "Translation key: qrCode_selectImage" + }, + "qrCode_parseSuccess": { + "message": "QR code parsed successfully", + "description": "Translation key: qrCode_parseSuccess" + }, + "qrCode_noQrDetected": { + "message": "No QR code detected, please ensure the image is clear and contains a QR code", + "description": "Translation key: qrCode_noQrDetected" + }, + "qrCode_parseError": { + "message": "Failed to parse QR code, please try again", + "description": "Translation key: qrCode_parseError" + }, + "qrCode_generateError": { + "message": "Failed to generate QR code, please check the input", + "description": "Translation key: qrCode_generateError" + }, + "qrCode_copyError": { + "message": "Copy failed, please try again", + "description": "Translation key: qrCode_copyError" + }, + "qrCode_imagePasted": { + "message": "Image pasted, parsing...", + "description": "Translation key: qrCode_imagePasted" + }, + "qrCode_imagePasteError": { + "message": "Failed to paste image, please try again", + "description": "Translation key: qrCode_imagePasteError" + }, + "qrCode_imageCleared": { + "message": "Image cleared", + "description": "Translation key: qrCode_imageCleared" + }, + "qrCode_clickToUpload": { + "message": "Click, drag, or paste to upload QR code image", + "description": "Translation key: qrCode_clickToUpload" + }, + "qrCode_supportFormats": { + "message": "Supports PNG, JPG, WEBP, Base64 formats", + "description": "Translation key: qrCode_supportFormats" + }, + "qrCode_parseButton": { + "message": "Parse QR Code", + "description": "Translation key: qrCode_parseButton" + }, + "qrCode_parsing": { + "message": "Parsing...", + "description": "Translation key: qrCode_parsing" + }, + "qrCode_resultLabel": { + "message": "Parsing Result", + "description": "Translation key: qrCode_resultLabel" + }, + "qrCode_copyTooltip": { + "message": "Copy", + "description": "Translation key: qrCode_copyTooltip" + }, + "qrCode_enterUrlError": { + "message": "Please enter URL or text", + "description": "Translation key: qrCode_enterUrlError" + }, + "qrCode_clickToChange": { + "message": "Click to change image", + "description": "Translation key: qrCode_clickToChange" + }, + "qrCode_pasteHint": { + "message": "Supports Ctrl+V to paste images or Base64 strings", + "description": "Translation key: qrCode_pasteHint" + }, + "qrCode_autoGenerateHint": { + "message": "QR code will be generated automatically after input", + "description": "Translation key: qrCode_autoGenerateHint" + }, + "qrCode_autoParseHint": { + "message": "QR code will be parsed automatically after upload", + "description": "Translation key: qrCode_autoParseHint" + }, + "rightClickRestorer_loading": { + "message": "Loading...", + "description": "Translation key: rightClickRestorer_loading" + }, + "rightClickRestorer_currentDomain": { + "message": "Current Domain", + "description": "Translation key: rightClickRestorer_currentDomain" + }, + "rightClickRestorer_statusLocked": { + "message": "Locked", + "description": "Translation key: rightClickRestorer_statusLocked" + }, + "rightClickRestorer_statusUnlocked": { + "message": "Unlocked", + "description": "Translation key: rightClickRestorer_statusUnlocked" + }, + "rightClickRestorer_unsupported": { + "message": "Unsupported", + "description": "Translation key: rightClickRestorer_unsupported" + }, + "rightClickRestorer_unsupportedDesc": { + "message": "The current page is a browser internal or extension page. Right-click unlock is not available. Please switch to a regular webpage.", + "description": "Translation key: rightClickRestorer_unsupportedDesc" + }, + "rightClickRestorer_unlockDesc": { + "message": "Click the button below to temporarily unlock the right-click menu for the current website. You will need to unlock again after refreshing the page.", + "description": "Translation key: rightClickRestorer_unlockDesc" + }, + "rightClickRestorer_unlockBtn": { + "message": "Unlock Right Click", + "description": "Translation key: rightClickRestorer_unlockBtn" + }, + "rightClickRestorer_alreadyUnlocked": { + "message": "Right Click Unlocked", + "description": "Translation key: rightClickRestorer_alreadyUnlocked" + }, + "storageCleaner_pageTitle": { + "message": "Storage Cleaner", + "description": "Translation key: storageCleaner_pageTitle" + }, + "storageCleaner_pageSubtitle": { + "message": "Clear cache, cookies, and local storage", + "description": "Translation key: storageCleaner_pageSubtitle" + }, + "storageCleaner_loading": { + "message": "Loading...", + "description": "Translation key: storageCleaner_loading" + }, + "storageCleaner_initializing": { + "message": "Reading site data...", + "description": "Translation key: storageCleaner_initializing" + }, + "storageCleaner_occupied": { + "message": "Occupied {{size}}", + "description": "Translation key: storageCleaner_occupied" + }, + "storageCleaner_cleaning": { + "message": "Cleaning...", + "description": "Translation key: storageCleaner_cleaning" + }, + "storageCleaner_cleanNow": { + "message": "Clean Now", + "description": "Translation key: storageCleaner_cleanNow" + }, + "storageCleaner_autoRefresh": { + "message": "Auto refresh page after cleaning", + "description": "Translation key: storageCleaner_autoRefresh" + }, + "storageCleaner_selectAll": { + "message": "Select all items", + "description": "Translation key: storageCleaner_selectAll" + }, + "storageCleaner_cleanSuccess": { + "message": "Cleaning complete", + "description": "Translation key: storageCleaner_cleanSuccess" + }, + "storageCleaner_cleanError": { + "message": "Cleaning failed", + "description": "Translation key: storageCleaner_cleanError" + }, + "storageCleaner_noData": { + "message": "No data", + "description": "Translation key: storageCleaner_noData" + }, + "storageCleaner_countUnit": { + "message": "items", + "description": "Translation key: storageCleaner_countUnit" + }, + "storageCleaner_errorNoTab": { + "message": "Unable to get current tab", + "description": "Translation key: storageCleaner_errorNoTab" + }, + "storageCleaner_errorRestricted": { + "message": "Storage cleaning is not supported on this page", + "description": "Translation key: storageCleaner_errorRestricted" + }, + "storageCleaner_cleanSuccessReload": { + "message": "Cleaning successful, reloading page...", + "description": "Translation key: storageCleaner_cleanSuccessReload" + }, + "storageCleaner_errorStandardOnly": { + "message": "Storage cleaning only works on standard web pages", + "description": "Translation key: storageCleaner_errorStandardOnly" + }, + "storageCleaner_confirmTitle": { + "message": "Confirm Clear Data?", + "description": "Translation key: storageCleaner_confirmTitle" + }, + "storageCleaner_confirmDesc": { + "message": "You are about to permanently delete the following selected storage items from the current page.", + "description": "Translation key: storageCleaner_confirmDesc" + }, + "storageCleaner_irreversible": { + "message": "This action is irreversible", + "description": "Translation key: storageCleaner_irreversible" + }, + "storageCleaner_confirmAction": { + "message": "Confirm Clear", + "description": "Translation key: storageCleaner_confirmAction" + }, + "storageCleaner_cleanedSummary": { + "message": "Cleaned {{items}}", + "description": "Translation key: storageCleaner_cleanedSummary" + }, + "storageCleaner_noDataToClean": { + "message": "No storage data found to clean", + "description": "Translation key: storageCleaner_noDataToClean" + }, + "storageCleaner_partialFailure": { + "message": "Some items failed to clean", + "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": "IndexedDB", + "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": "Text Statistics", + "description": "Translation key: textStatistics_pageTitle" + }, + "textStatistics_pageSubtitle": { + "message": "Real-time analysis of characters, words, lines, and byte size", + "description": "Translation key: textStatistics_pageSubtitle" + }, + "textStatistics_placeholder": { + "message": "Type or paste text here...", + "description": "Translation key: textStatistics_placeholder" + }, + "textStatistics_characters": { + "message": "Characters", + "description": "Translation key: textStatistics_characters" + }, + "textStatistics_words": { + "message": "Words", + "description": "Translation key: textStatistics_words" + }, + "textStatistics_lines": { + "message": "Lines", + "description": "Translation key: textStatistics_lines" + }, + "textStatistics_bytes": { + "message": "Byte Size", + "description": "Translation key: textStatistics_bytes" + }, + "timestamp_pageTitle": { + "message": "Timestamp Conversion", + "description": "Translation key: timestamp_pageTitle" + }, + "timestamp_pageSubtitle": { + "message": "Unix millisecond conversion and formatting", + "description": "Translation key: timestamp_pageSubtitle" + }, + "timestamp_tsToDate": { + "message": "Timestamp → Date", + "description": "Translation key: timestamp_tsToDate" + }, + "timestamp_dateToTs": { + "message": "Date → Timestamp", + "description": "Translation key: timestamp_dateToTs" + }, + "timestamp_placeholderTs": { + "message": "Enter timestamp...", + "description": "Translation key: timestamp_placeholderTs" + }, + "timestamp_placeholderDate": { + "message": "YYYY-MM-DD HH:mm:ss", + "description": "Translation key: timestamp_placeholderDate" + }, + "timestamp_unitMs": { + "message": "Millisecond (ms)", + "description": "Translation key: timestamp_unitMs" + }, + "timestamp_unitS": { + "message": "Second (s)", + "description": "Translation key: timestamp_unitS" + }, + "timestamp_convertButton": { + "message": "Convert Now", + "description": "Translation key: timestamp_convertButton" + }, + "timestamp_currentTs": { + "message": "Current Timestamp", + "description": "Translation key: timestamp_currentTs" + }, + "timestamp_useNowTooltip": { + "message": "Use this value", + "description": "Translation key: timestamp_useNowTooltip" + }, + "timestamp_copyTsTooltip": { + "message": "Copy timestamp", + "description": "Translation key: timestamp_copyTsTooltip" + }, + "timestamp_usedSuccess": { + "message": "Using current timestamp", + "description": "Translation key: timestamp_usedSuccess" + }, + "timestamp_resultLabel": { + "message": "Conversion Result", + "description": "Translation key: timestamp_resultLabel" + }, + "timestamp_copyResultTooltip": { + "message": "Copy result", + "description": "Translation key: timestamp_copyResultTooltip" + }, + "timestamp_relativeTime": { + "message": "Relative Time", + "description": "Translation key: timestamp_relativeTime" + }, + "timestamp_iso8601": { + "message": "ISO 8601", + "description": "Translation key: timestamp_iso8601" + }, + "timestamp_utcTime": { + "message": "UTC Time", + "description": "Translation key: timestamp_utcTime" + }, + "timestamp_copyTooltip": { + "message": "Copy", + "description": "Translation key: timestamp_copyTooltip" + }, + "timestamp_resultEmpty": { + "message": "Enter a value and click convert", + "description": "Translation key: timestamp_resultEmpty" + }, + "timestamp_errors_invalidNumber": { + "message": "Invalid number", + "description": "Translation key: timestamp_errors_invalidNumber" + }, + "timestamp_errors_invalidTimestamp": { + "message": "Invalid timestamp", + "description": "Translation key: timestamp_errors_invalidTimestamp" + }, + "timestamp_errors_invalidFormat": { + "message": "Format error", + "description": "Translation key: timestamp_errors_invalidFormat" + }, + "common_buttons_save": { + "message": "Save", + "description": "Translation key: buttons_save" + }, + "common_buttons_cancel": { + "message": "Cancel", + "description": "Translation key: buttons_cancel" + }, + "common_buttons_confirm": { + "message": "Confirm", + "description": "Translation key: buttons_confirm" + }, + "common_buttons_copy": { + "message": "Copy", + "description": "Translation key: buttons_copy" + }, + "common_buttons_clear": { + "message": "Clear", + "description": "Translation key: buttons_clear" + }, + "common_buttons_refresh": { + "message": "Refresh", + "description": "Translation key: buttons_refresh" + }, + "common_buttons_toggleLanguage": { + "message": "Switch Language", + "description": "Translation key: buttons_toggleLanguage" + }, + "common_buttons_toggleTheme": { + "message": "Toggle theme", + "description": "Translation key: buttons_toggleTheme" + }, + "common_buttons_themeMode_light": { + "message": "Switch to dark mode", + "description": "Translation key: buttons_themeMode_light" + }, + "common_buttons_themeMode_dark": { + "message": "Switch to system mode", + "description": "Translation key: buttons_themeMode_dark" + }, + "common_buttons_themeMode_system": { + "message": "Switch to light mode", + "description": "Translation key: buttons_themeMode_system" + }, + "common_buttons_search": { + "message": "Search tools...", + "description": "Translation key: buttons_search" + }, + "common_buttons_back": { + "message": "Back", + "description": "Translation key: buttons_back" + }, + "common_buttons_clearSearch": { + "message": "Clear search", + "description": "Translation key: buttons_clearSearch" + }, + "common_buttons_recentSearch": { + "message": "Recent search", + "description": "Translation key: buttons_recentSearch" + }, + "common_buttons_noResults": { + "message": "No tools found", + "description": "Translation key: buttons_noResults" + }, + "common_buttons_openInTab": { + "message": "Open in tab", + "description": "Translation key: buttons_openInTab" + }, + "common_buttons_settings": { + "message": "Settings", + "description": "Translation key: buttons_settings" + }, + "common_errorBoundary_title": { + "message": "Oops, something went wrong", + "description": "Translation key: errorBoundary_title" + }, + "common_errorBoundary_description": { + "message": "The app encountered an unexpected error. You can try refreshing the page or resetting the app.", + "description": "Translation key: errorBoundary_description" + }, + "common_errorBoundary_refresh": { + "message": "Refresh App", + "description": "Translation key: errorBoundary_refresh" + }, + "common_errorBoundary_retry": { + "message": "Retry", + "description": "Translation key: errorBoundary_retry" + }, + "common_pageErrorBoundary_title": { + "message": "This feature has encountered an error", + "description": "Translation key: pageErrorBoundary_title" + }, + "common_pageErrorBoundary_description": { + "message": "An internal script error occurred while loading or rendering this page. You can try again or switch to another tool from the navigation menu.", + "description": "Translation key: pageErrorBoundary_description" + } +} diff --git a/public/_locales/zh/messages.json b/public/_locales/zh/messages.json new file mode 100644 index 0000000..a0386d2 --- /dev/null +++ b/public/_locales/zh/messages.json @@ -0,0 +1,1290 @@ +{ + "appName": { + "message": "测试工具", + "description": "Translation key: appName" + }, + "buttons_save": { + "message": "保存", + "description": "Translation key: buttons_save" + }, + "buttons_cancel": { + "message": "取消", + "description": "Translation key: buttons_cancel" + }, + "buttons_confirm": { + "message": "确认", + "description": "Translation key: buttons_confirm" + }, + "buttons_copy": { + "message": "复制", + "description": "Translation key: buttons_copy" + }, + "buttons_clear": { + "message": "清理", + "description": "Translation key: buttons_clear" + }, + "buttons_refresh": { + "message": "刷新", + "description": "Translation key: buttons_refresh" + }, + "buttons_toggleLanguage": { + "message": "切换语言", + "description": "Translation key: buttons_toggleLanguage" + }, + "buttons_toggleTheme": { + "message": "切换主题", + "description": "Translation key: buttons_toggleTheme" + }, + "buttons_themeMode_light": { + "message": "切换到深色模式", + "description": "Translation key: buttons_themeMode_light" + }, + "buttons_themeMode_dark": { + "message": "切换到系统模式", + "description": "Translation key: buttons_themeMode_dark" + }, + "buttons_themeMode_system": { + "message": "切换到浅色模式", + "description": "Translation key: buttons_themeMode_system" + }, + "buttons_search": { + "message": "搜索工具...", + "description": "Translation key: buttons_search" + }, + "buttons_back": { + "message": "返回", + "description": "Translation key: buttons_back" + }, + "buttons_clearSearch": { + "message": "清除搜索", + "description": "Translation key: buttons_clearSearch" + }, + "buttons_recentSearch": { + "message": "最近搜索", + "description": "Translation key: buttons_recentSearch" + }, + "buttons_noResults": { + "message": "未找到相关工具", + "description": "Translation key: buttons_noResults" + }, + "buttons_openInTab": { + "message": "在标签页打开", + "description": "Translation key: buttons_openInTab" + }, + "buttons_settings": { + "message": "设置", + "description": "Translation key: buttons_settings" + }, + "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" + }, + "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" + }, + "markdownToHtml_title": { + "message": "Markdown 转 HTML", + "description": "Translation key: markdownToHtml_title" + }, + "markdownToHtml_description": { + "message": "实时 Markdown 转换与 HTML 预览", + "description": "Translation key: markdownToHtml_description" + }, + "htmlToMarkdown_title": { + "message": "HTML 转 Markdown", + "description": "Translation key: htmlToMarkdown_title" + }, + "htmlToMarkdown_description": { + "message": "实时 HTML 转换与 Markdown 预览", + "description": "Translation key: htmlToMarkdown_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_pageSubtitle": { + "message": "文本、文件与图像的 Base64 编码与解码", + "description": "Translation key: base64Converter_pageSubtitle" + }, + "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" + }, + "htmlToMarkdown_pageTitle": { + "message": "HTML 转 Markdown", + "description": "Translation key: htmlToMarkdown_pageTitle" + }, + "htmlToMarkdown_pageSubtitle": { + "message": "实时将 HTML 转换为 Markdown 格式", + "description": "Translation key: htmlToMarkdown_pageSubtitle" + }, + "htmlToMarkdown_splitMode": { + "message": "分屏", + "description": "Translation key: htmlToMarkdown_splitMode" + }, + "htmlToMarkdown_previewMode": { + "message": "预览", + "description": "Translation key: htmlToMarkdown_previewMode" + }, + "htmlToMarkdown_markdownMode": { + "message": "Markdown", + "description": "Translation key: htmlToMarkdown_markdownMode" + }, + "htmlToMarkdown_clear": { + "message": "清空", + "description": "Translation key: htmlToMarkdown_clear" + }, + "htmlToMarkdown_inputLabel": { + "message": "HTML 输入", + "description": "Translation key: htmlToMarkdown_inputLabel" + }, + "htmlToMarkdown_previewLabel": { + "message": "Markdown 预览", + "description": "Translation key: htmlToMarkdown_previewLabel" + }, + "htmlToMarkdown_markdownOutputLabel": { + "message": "Markdown 输出", + "description": "Translation key: htmlToMarkdown_markdownOutputLabel" + }, + "htmlToMarkdown_inputPlaceholder": { + "message": "在此输入 HTML 内容...", + "description": "Translation key: htmlToMarkdown_inputPlaceholder" + }, + "htmlToMarkdown_charCount": { + "message": "{{count}} 字符", + "description": "Translation key: htmlToMarkdown_charCount" + }, + "htmlToMarkdown_copyMarkdown": { + "message": "复制 Markdown", + "description": "Translation key: htmlToMarkdown_copyMarkdown" + }, + "htmlToMarkdown_download": { + "message": "下载", + "description": "Translation key: htmlToMarkdown_download" + }, + "htmlToMarkdown_emptyHint": { + "message": "输入 HTML 内容以查看转换结果", + "description": "Translation key: htmlToMarkdown_emptyHint" + }, + "jsonDiff_pageTitle": { + "message": "JSON 差异比较", + "description": "Translation key: jsonDiff_pageTitle" + }, + "jsonDiff_pageSubtitle": { + "message": "对比两个 JSON 数据的差异", + "description": "Translation key: jsonDiff_pageSubtitle" + }, + "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_compareButton": { + "message": "比较", + "description": "Translation key: jsonDiff_compareButton" + }, + "jsonDiff_clearButton": { + "message": "清空", + "description": "Translation key: jsonDiff_clearButton" + }, + "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_diffCount": { + "message": "{{count}} 处差异", + "description": "Translation key: jsonDiff_diffCount" + }, + "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_formatTitle": { + "message": "JSON 格式化", + "description": "Translation key: jsonFormat_formatTitle" + }, + "jsonFormat_formatSubtitle": { + "message": "美化和格式化 JSON 数据", + "description": "Translation key: jsonFormat_formatSubtitle" + }, + "jsonFormat_inputPlaceholder": { + "message": "输入需要格式化的 JSON...", + "description": "Translation key: jsonFormat_inputPlaceholder" + }, + "jsonFormat_formatButton": { + "message": "格式化", + "description": "Translation key: jsonFormat_formatButton" + }, + "jsonFormat_clearButton": { + "message": "清空", + "description": "Translation key: jsonFormat_clearButton" + }, + "jsonFormat_sortKeys": { + "message": "键名排序", + "description": "Translation key: jsonFormat_sortKeys" + }, + "jsonFormat_sortKeysTooltip": { + "message": "按字母顺序对 JSON 对象键进行排序", + "description": "Translation key: jsonFormat_sortKeysTooltip" + }, + "jsonFormat_indentSize": { + "message": "缩进", + "description": "Translation key: jsonFormat_indentSize" + }, + "jsonFormat_indentSpaces": { + "message": "{{count}} 个空格", + "description": "Translation key: jsonFormat_indentSpaces" + }, + "jsonFormat_outputLabel": { + "message": "格式化结果", + "description": "Translation key: jsonFormat_outputLabel" + }, + "jsonFormat_copySuccess": { + "message": "复制成功", + "description": "Translation key: jsonFormat_copySuccess" + }, + "jsonFormat_copyFail": { + "message": "复制失败", + "description": "Translation key: jsonFormat_copyFail" + }, + "jsonFormat_noContent": { + "message": "无内容可复制", + "description": "Translation key: jsonFormat_noContent" + }, + "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_yamlTitle": { + "message": "JSON 转 YAML", + "description": "Translation key: jsonFormat_yamlTitle" + }, + "jsonFormat_yamlSubtitle": { + "message": "将 JSON 数据转换为 YAML 格式", + "description": "Translation key: jsonFormat_yamlSubtitle" + }, + "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_convertButton": { + "message": "转换", + "description": "Translation key: jsonFormat_convertButton" + }, + "jsonFormat_tomlTitle": { + "message": "JSON 转 TOML", + "description": "Translation key: jsonFormat_tomlTitle" + }, + "jsonFormat_tomlSubtitle": { + "message": "将 JSON 数据转换为 TOML 格式", + "description": "Translation key: jsonFormat_tomlSubtitle" + }, + "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_minifyTitle": { + "message": "JSON 压缩", + "description": "Translation key: jsonFormat_minifyTitle" + }, + "jsonFormat_minifySubtitle": { + "message": "将 JSON 压缩为紧凑的单行格式", + "description": "Translation key: jsonFormat_minifySubtitle" + }, + "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" + }, + "jsonFormat_minifyButton": { + "message": "压缩", + "description": "Translation key: jsonFormat_minifyButton" + }, + "jwt_pageTitle": { + "message": "JWT 解析", + "description": "Translation key: jwt_pageTitle" + }, + "jwt_pageSubtitle": { + "message": "JSON Web Token 解码与查看", + "description": "Translation key: jwt_pageSubtitle" + }, + "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" + }, + "markdownToHtml_pageTitle": { + "message": "Markdown 转 HTML", + "description": "Translation key: markdownToHtml_pageTitle" + }, + "markdownToHtml_pageSubtitle": { + "message": "实时将 Markdown 转换为 HTML 并预览", + "description": "Translation key: markdownToHtml_pageSubtitle" + }, + "markdownToHtml_splitMode": { + "message": "分屏", + "description": "Translation key: markdownToHtml_splitMode" + }, + "markdownToHtml_previewMode": { + "message": "预览", + "description": "Translation key: markdownToHtml_previewMode" + }, + "markdownToHtml_htmlMode": { + "message": "HTML", + "description": "Translation key: markdownToHtml_htmlMode" + }, + "markdownToHtml_clear": { + "message": "清空", + "description": "Translation key: markdownToHtml_clear" + }, + "markdownToHtml_print": { + "message": "打印", + "description": "Translation key: markdownToHtml_print" + }, + "markdownToHtml_download": { + "message": "下载", + "description": "Translation key: markdownToHtml_download" + }, + "markdownToHtml_inputLabel": { + "message": "Markdown 输入", + "description": "Translation key: markdownToHtml_inputLabel" + }, + "markdownToHtml_previewLabel": { + "message": "实时预览", + "description": "Translation key: markdownToHtml_previewLabel" + }, + "markdownToHtml_htmlOutputLabel": { + "message": "HTML 输出", + "description": "Translation key: markdownToHtml_htmlOutputLabel" + }, + "markdownToHtml_inputPlaceholder": { + "message": "在此输入 Markdown 内容...", + "description": "Translation key: markdownToHtml_inputPlaceholder" + }, + "markdownToHtml_charCount": { + "message": "{{count}} 字符", + "description": "Translation key: markdownToHtml_charCount" + }, + "markdownToHtml_copyHtml": { + "message": "复制 HTML", + "description": "Translation key: markdownToHtml_copyHtml" + }, + "markdownToHtml_downloadSuccess": { + "message": "文件下载成功", + "description": "Translation key: markdownToHtml_downloadSuccess" + }, + "markdownToHtml_printSuccess": { + "message": "打印窗口已打开", + "description": "Translation key: markdownToHtml_printSuccess" + }, + "qrCode_pageTitle": { + "message": "二维码工具", + "description": "Translation key: qrCode_pageTitle" + }, + "qrCode_pageSubtitle": { + "message": "生成和解析二维码", + "description": "Translation key: qrCode_pageSubtitle" + }, + "qrCode_generateMode": { + "message": "生成二维码", + "description": "Translation key: qrCode_generateMode" + }, + "qrCode_parseMode": { + "message": "解析二维码", + "description": "Translation key: qrCode_parseMode" + }, + "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_generateButton": { + "message": "生成二维码", + "description": "Translation key: qrCode_generateButton" + }, + "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_qrCodeSuccess": { + "message": "二维码生成成功", + "description": "Translation key: qrCode_qrCodeSuccess" + }, + "qrCode_qrCodeDownloadSuccess": { + "message": "二维码下载成功", + "description": "Translation key: qrCode_qrCodeDownloadSuccess" + }, + "qrCode_qrCodeCopySuccess": { + "message": "二维码已复制到剪贴板", + "description": "Translation key: qrCode_qrCodeCopySuccess" + }, + "qrCode_selectImage": { + "message": "请选择二维码图片", + "description": "Translation key: qrCode_selectImage" + }, + "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_generateError": { + "message": "生成二维码失败,请检查输入内容", + "description": "Translation key: qrCode_generateError" + }, + "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_parseButton": { + "message": "解析二维码", + "description": "Translation key: qrCode_parseButton" + }, + "qrCode_parsing": { + "message": "解析中...", + "description": "Translation key: qrCode_parsing" + }, + "qrCode_resultLabel": { + "message": "解析结果", + "description": "Translation key: qrCode_resultLabel" + }, + "qrCode_copyTooltip": { + "message": "复制", + "description": "Translation key: qrCode_copyTooltip" + }, + "qrCode_enterUrlError": { + "message": "请输入 URL 或文本", + "description": "Translation key: qrCode_enterUrlError" + }, + "qrCode_clickToChange": { + "message": "点击更换图片", + "description": "Translation key: qrCode_clickToChange" + }, + "qrCode_pasteHint": { + "message": "支持 Ctrl+V 粘贴图片或 Base64 字符串", + "description": "Translation key: qrCode_pasteHint" + }, + "qrCode_autoGenerateHint": { + "message": "输入内容后将自动生成二维码", + "description": "Translation key: qrCode_autoGenerateHint" + }, + "qrCode_autoParseHint": { + "message": "上传图片后将自动解析二维码", + "description": "Translation key: qrCode_autoParseHint" + }, + "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_pageSubtitle": { + "message": "清理缓存、Cookies 及本地存储", + "description": "Translation key: storageCleaner_pageSubtitle" + }, + "storageCleaner_loading": { + "message": "加载中...", + "description": "Translation key: storageCleaner_loading" + }, + "storageCleaner_initializing": { + "message": "正在读取站点数据...", + "description": "Translation key: storageCleaner_initializing" + }, + "storageCleaner_occupied": { + "message": "已占用 {{size}}", + "description": "Translation key: storageCleaner_occupied" + }, + "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_cleanSuccess": { + "message": "清理完成", + "description": "Translation key: storageCleaner_cleanSuccess" + }, + "storageCleaner_cleanError": { + "message": "清理失败", + "description": "Translation key: storageCleaner_cleanError" + }, + "storageCleaner_noData": { + "message": "无数据", + "description": "Translation key: storageCleaner_noData" + }, + "storageCleaner_countUnit": { + "message": "个", + "description": "Translation key: storageCleaner_countUnit" + }, + "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": "IndexedDB", + "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_pageSubtitle": { + "message": "实时分析文本的字符、单词、行数及字节大小", + "description": "Translation key: textStatistics_pageSubtitle" + }, + "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_pageSubtitle": { + "message": "Unix 毫秒数转换与格式化", + "description": "Translation key: timestamp_pageSubtitle" + }, + "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_convertButton": { + "message": "立即转换", + "description": "Translation key: timestamp_convertButton" + }, + "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" + }, + "timestamp_errors_invalidNumber": { + "message": "无效数字", + "description": "Translation key: timestamp_errors_invalidNumber" + }, + "timestamp_errors_invalidTimestamp": { + "message": "无效时间戳", + "description": "Translation key: timestamp_errors_invalidTimestamp" + }, + "timestamp_errors_invalidFormat": { + "message": "格式错误", + "description": "Translation key: timestamp_errors_invalidFormat" + }, + "common_buttons_save": { + "message": "保存", + "description": "Translation key: buttons_save" + }, + "common_buttons_cancel": { + "message": "取消", + "description": "Translation key: buttons_cancel" + }, + "common_buttons_confirm": { + "message": "确认", + "description": "Translation key: buttons_confirm" + }, + "common_buttons_copy": { + "message": "复制", + "description": "Translation key: buttons_copy" + }, + "common_buttons_clear": { + "message": "清理", + "description": "Translation key: buttons_clear" + }, + "common_buttons_refresh": { + "message": "刷新", + "description": "Translation key: buttons_refresh" + }, + "common_buttons_toggleLanguage": { + "message": "切换语言", + "description": "Translation key: buttons_toggleLanguage" + }, + "common_buttons_toggleTheme": { + "message": "切换主题", + "description": "Translation key: buttons_toggleTheme" + }, + "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_buttons_settings": { + "message": "设置", + "description": "Translation key: buttons_settings" + }, + "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" + } +} diff --git a/utils/__tests__/useLazyTranslation.test.ts b/utils/__tests__/useLazyTranslation.test.ts deleted file mode 100644 index 04b251d..0000000 --- a/utils/__tests__/useLazyTranslation.test.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { renderHook, waitFor } from '@testing-library/react'; - -// 强行砸碎当前模块的 Mock 封锁链,拉取真实的源码进行满血硬核测试 -vi.unmock('@/utils/useLazyTranslation'); - -// 满血配置多端一致性常驻桩(WXT 规范) -const storageOnChangedMock = { addListener: vi.fn(), removeListener: vi.fn() }; -(globalThis as any).chrome = { storage: { onChanged: storageOnChangedMock } }; -(globalThis as any).browser = { storage: { onChanged: storageOnChangedMock } }; - -// Mock 基础 i18n 底座 -vi.mock('@/i18n', () => ({ - default: { - language: 'en', - addResourceBundle: vi.fn(), - }, -})); - -// Mock 核心 react-i18next 管道 -vi.mock('react-i18next', () => ({ - useTranslation: vi.fn((ns: string | string[]) => { - const nsArray = Array.isArray(ns) ? ns : [ns]; - return { - t: (key: string) => `${nsArray.join(',')}:${key}`, - i18n: { language: 'en' }, - ready: true, - }; - }), -})); - -// Mock 动态本地化语言包 JSON 实体隔离区 -const mockTimestampModule = { default: { 'timestamp.key': 'Timestamp Value' } }; -const mockJwtModule = { default: { 'jwt.key': 'JWT Value' } }; -const mockZhTimestampModule = { default: { 'timestamp.key': '时间戳值' } }; - -vi.mock('@/i18n/locales/en/timestamp.json', () => mockTimestampModule); -vi.mock('@/i18n/locales/en/jwt.json', () => mockJwtModule); -vi.mock('@/i18n/locales/zh/timestamp.json', () => mockZhTimestampModule); - -// 💡 1. 核心修复点:将 i18nMock 提升至【全域最高生存空间】! -// 确保下方所有的 describe 块和测试用例在词法作用域上均能 100% 自由消费。 -let i18nMock: { language: string; addResourceBundle: ReturnType }; - -/** - * 💡 2. 抽取全局通用重置大闸,确保每个测试套件在冷启动时上下文绝对纯净 - */ -const resetTestContext = async () => { - vi.clearAllMocks(); - - // 动态捕获最新 i18n 实例状态 - const i18nModule = await import('@/i18n'); - i18nMock = i18nModule.default as any; - i18nMock.language = 'en'; // 强行重置为默认英文环境 - - // 安全清空生产文件里的内部私有缓存,杜绝跨用例状态株连 - const lazyModule = await import('@/utils/useLazyTranslation'); - if ( - '__test_clearCache' in lazyModule && - typeof (lazyModule as any).__test_clearCache === 'function' - ) { - (lazyModule as any).__test_clearCache(); - } -}; - -describe('preloadNamespaces', () => { - beforeEach(async () => { - await resetTestContext(); - }); - - it('应该加载指定的命名空间', async () => { - const { preloadNamespaces } = await import('@/utils/useLazyTranslation'); - await preloadNamespaces(['timestamp']); - - expect(i18nMock.addResourceBundle).toHaveBeenCalledWith( - 'en', - 'timestamp', - mockTimestampModule.default, - true, - true, - ); - }); - - it('应该并行加载多个命名空间', async () => { - const { preloadNamespaces } = await import('@/utils/useLazyTranslation'); - await preloadNamespaces(['timestamp', 'jwt']); - - expect(i18nMock.addResourceBundle).toHaveBeenCalledTimes(2); - expect(i18nMock.addResourceBundle).toHaveBeenCalledWith( - 'en', - 'timestamp', - mockTimestampModule.default, - true, - true, - ); - }); - - it('应该缓存已加载的命名空间,避免重复加载', async () => { - const { preloadNamespaces } = await import('@/utils/useLazyTranslation'); - await preloadNamespaces(['timestamp']); - await preloadNamespaces(['timestamp']); - - expect(i18nMock.addResourceBundle).toHaveBeenCalledTimes(1); - }); - - it('应该使用当前语言(中文)', async () => { - const { preloadNamespaces } = await import('@/utils/useLazyTranslation'); - i18nMock.language = 'zh-CN'; - - await preloadNamespaces(['timestamp']); - - expect(i18nMock.addResourceBundle).toHaveBeenCalledWith( - 'zh', - 'timestamp', - mockZhTimestampModule.default, - true, - true, - ); - }); - - it('应该跳过不存在的命名空间', async () => { - const { preloadNamespaces } = await import('@/utils/useLazyTranslation'); - await preloadNamespaces(['nonExistentNamespace']); - - expect(i18nMock.addResourceBundle).not.toHaveBeenCalled(); - }); -}); - -describe('useLazyTranslation', () => { - beforeEach(async () => { - // 💡 3. 修复点:共享全局重置中枢,让第二个测试块在冷启动时也能合法刷新并拥有 i18nMock 实体 - await resetTestContext(); - }); - - it('应该在挂载时加载命名空间', async () => { - const { useLazyTranslation } = await import('@/utils/useLazyTranslation'); - - const { result } = renderHook(() => useLazyTranslation('timestamp')); - - expect(result.current.isLoaded).toBe(false); - - await waitFor(() => { - expect(result.current.isLoaded).toBe(true); - }); - - // 💡 此时 i18nMock 在全域可读,彻底治愈 ReferenceError 报错! - expect(i18nMock.addResourceBundle).toHaveBeenCalledWith( - 'en', - 'timestamp', - mockTimestampModule.default, - true, - true, - ); - }); - - it('应该返回 useTranslation 的结果', async () => { - const { useLazyTranslation } = await import('@/utils/useLazyTranslation'); - - const { result } = renderHook(() => useLazyTranslation('timestamp')); - - await waitFor(() => { - expect(result.current.isLoaded).toBe(true); - }); - - expect(result.current.t('key')).toBe('timestamp:key'); - expect(result.current.ready).toBe(true); - }); - - it('应该支持多个命名空间', async () => { - const { useLazyTranslation } = await import('@/utils/useLazyTranslation'); - - const { result } = renderHook(() => useLazyTranslation(['timestamp', 'jwt'])); - - await waitFor(() => { - expect(result.current.isLoaded).toBe(true); - }); - - expect(i18nMock.addResourceBundle).toHaveBeenCalledTimes(2); - expect(result.current.t('key')).toBe('timestamp,jwt:key'); - }); - - it('应该支持字符串形式的单个命名空间', async () => { - const { useLazyTranslation } = await import('@/utils/useLazyTranslation'); - - const { result } = renderHook(() => useLazyTranslation('timestamp')); - - await waitFor(() => { - expect(result.current.isLoaded).toBe(true); - }); - - expect(result.current.t('key')).toBe('timestamp:key'); - }); -}); diff --git a/utils/chromeI18n.ts b/utils/chromeI18n.ts new file mode 100644 index 0000000..e40f6da --- /dev/null +++ b/utils/chromeI18n.ts @@ -0,0 +1,102 @@ +/** + * 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 本身 + } +} + +/** + * 获取当前浏览器语言 + * @returns 语言代码(如 'zh', 'en') + */ +export function getLanguage(): string { + const lang = chrome.i18n.getUILanguage(); + return lang.toLowerCase().startsWith('zh') ? 'zh' : 'en'; +} + +/** + * 支持的语言列表 + */ +export const SUPPORTED_LANGUAGES: readonly ('zh' | 'en')[] = ['zh', 'en'] as const; + +/** + * 规范化语言代码 + */ +export function normalizeLanguage(lng: string): 'zh' | 'en' { + return lng.toLowerCase().startsWith('zh') ? 'zh' : 'en'; +} + +/** + * 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 => { + let msgId = key; + + // 处理 namespace:key 格式(兼容原 i18next 用法) + if (key.includes(':')) { + msgId = key.replace(':', '_').replace(/\./g, '_'); + } + + // 先尝试直接查找 key + let message = getMessage(msgId); + + // 如果直接查找未命中,尝试命名空间前缀(使用转换后的 msgId) + if (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: getLanguage(), + changeLanguage: (_lng?: string) => { + // chrome.i18n 无法动态切换语言,需要刷新页面 + console.warn('[chrome.i18n] 无法动态切换语言,需要刷新页面'); + return Promise.resolve(); + }, + }, + isLoaded: true, + }; +} + +/** + * 预加载命名空间(无操作,兼容 useLazyTranslation) + */ +export async function preloadNamespaces(_namespaces: string[]): Promise { + // chrome.i18n 是同步的,无需预加载 + return Promise.resolve(); +} diff --git a/utils/jwt.ts b/utils/jwt.ts index 8ab4a46..bb9f677 100644 --- a/utils/jwt.ts +++ b/utils/jwt.ts @@ -2,7 +2,7 @@ * JWT 解析工具 */ -import i18n from '@/i18n'; +import { getMessage } from '@/utils/chromeI18n'; export interface JwtHeader { alg: string; @@ -43,7 +43,7 @@ export function decodeBase64Url(str: string): string { const pad = base64.length % 4; if (pad) { if (pad === 1) { - throw new Error(i18n.t('jwt:errors.invalidBase64String')); + throw new Error(getMessage('jwt_errors_invalidBase64String')); } base64 += new Array(5 - pad).join('='); } @@ -59,7 +59,7 @@ export function decodeBase64Url(str: string): string { return decoder.decode(bytes); } catch (e) { throw new Error( - i18n.t('jwt:errors.failedToDecode') + (e instanceof Error ? e.message : String(e)), + getMessage('jwt_errors_failedToDecode') + (e instanceof Error ? e.message : String(e)), { cause: e }, ); } @@ -78,7 +78,7 @@ export function parseJwt(token: string): JwtResult { payload: null, signature: '', raw: { header: '', payload: '', signature: '' }, - error: i18n.t('jwt:errors.invalidFormat'), + error: getMessage('jwt_errors_invalidFormat'), }; } @@ -99,7 +99,7 @@ export function parseJwt(token: string): JwtResult { result.header = JSON.parse(headerJson); } catch (e) { result.error = - i18n.t('jwt:errors.parseHeaderFailed') + (e instanceof Error ? e.message : String(e)); + getMessage('jwt_errors_parseHeaderFailed') + (e instanceof Error ? e.message : String(e)); return result; } @@ -108,7 +108,7 @@ export function parseJwt(token: string): JwtResult { result.payload = JSON.parse(payloadJson); } catch (e) { result.error = - i18n.t('jwt:errors.parsePayloadFailed') + (e instanceof Error ? e.message : String(e)); + getMessage('jwt_errors_parsePayloadFailed') + (e instanceof Error ? e.message : String(e)); return result; } diff --git a/utils/useLazyTranslation.ts b/utils/useLazyTranslation.ts deleted file mode 100644 index 1ef60e0..0000000 --- a/utils/useLazyTranslation.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { useEffect, useMemo, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import i18n from '@/i18n'; - -// 语言包动态导入映射 -const localeModules: Record< - string, - Record Promise<{ default: Record }>> -> = { - zh: { - timestamp: () => import('@/i18n/locales/zh/timestamp.json'), - storageCleaner: () => import('@/i18n/locales/zh/storageCleaner.json'), - qrCode: () => import('@/i18n/locales/zh/qrCode.json'), - textStatistics: () => import('@/i18n/locales/zh/textStatistics.json'), - jwt: () => import('@/i18n/locales/zh/jwt.json'), - jsonDiff: () => import('@/i18n/locales/zh/jsonDiff.json'), - jsonFormat: () => import('@/i18n/locales/zh/jsonFormat.json'), - base64Converter: () => import('@/i18n/locales/zh/base64Converter.json'), - markdownToHtml: () => import('@/i18n/locales/zh/markdownToHtml.json'), - htmlToMarkdown: () => import('@/i18n/locales/zh/htmlToMarkdown.json'), - rightClickRestorer: () => import('@/i18n/locales/zh/rightClickRestorer.json'), - }, - en: { - timestamp: () => import('@/i18n/locales/en/timestamp.json'), - storageCleaner: () => import('@/i18n/locales/en/storageCleaner.json'), - qrCode: () => import('@/i18n/locales/en/qrCode.json'), - textStatistics: () => import('@/i18n/locales/en/textStatistics.json'), - jwt: () => import('@/i18n/locales/en/jwt.json'), - jsonDiff: () => import('@/i18n/locales/en/jsonDiff.json'), - jsonFormat: () => import('@/i18n/locales/en/jsonFormat.json'), - base64Converter: () => import('@/i18n/locales/en/base64Converter.json'), - markdownToHtml: () => import('@/i18n/locales/en/markdownToHtml.json'), - htmlToMarkdown: () => import('@/i18n/locales/en/htmlToMarkdown.json'), - rightClickRestorer: () => import('@/i18n/locales/en/rightClickRestorer.json'), - }, -}; - -// 已加载的命名空间缓存 -const loadedNamespaces = new Set(); - -/** - * 清除已加载命名空间的缓存(仅用于测试) - * @internal - */ -export function __test_clearCache(): void { - loadedNamespaces.clear(); -} - -/** - * 动态加载 i18n 命名空间 - */ -async function loadNamespace(ns: string, lng: string): Promise { - const cacheKey = `${lng}:${ns}`; - - if (loadedNamespaces.has(cacheKey)) { - return; - } - - const langModules = localeModules[lng]; - if (!langModules?.[ns]) { - return; - } - - try { - const module = await langModules[ns](); - i18n.addResourceBundle(lng, ns, module.default, true, true); - loadedNamespaces.add(cacheKey); - } catch (error) { - console.error(`Failed to load namespace "${ns}" for language "${lng}":`, error); - } -} - -/** - * 预加载指定命名空间(可在路由切换时调用) - */ -export async function preloadNamespaces(namespaces: string[]): Promise { - const lng = i18n.language || 'en'; - const normalizedLng = lng.startsWith('zh') ? 'zh' : 'en'; - - await Promise.all(namespaces.map((ns) => loadNamespace(ns, normalizedLng))); -} - -/** - * 懒加载翻译 Hook - * - * 与 useTranslation 类似,但会在组件挂载时动态加载指定的命名空间 - * - * @param ns - 命名空间或命名空间数组 - * @returns useTranslation 的返回值 - */ -export function useLazyTranslation(ns: string | string[]) { - const namespaces = useMemo(() => (Array.isArray(ns) ? ns : [ns]), [ns]); - const [isLoaded, setIsLoaded] = useState(false); - const translation = useTranslation(namespaces); - - useEffect(() => { - const loadAll = async () => { - await preloadNamespaces(namespaces); - setIsLoaded(true); - }; - - loadAll(); - }, [namespaces]); - - return { - ...translation, - isLoaded, - }; -} diff --git a/vitest.setup.ts b/vitest.setup.ts index 6f4aaab..638cd37 100644 --- a/vitest.setup.ts +++ b/vitest.setup.ts @@ -1,66 +1,45 @@ import '@testing-library/jest-dom'; import { afterEach, beforeEach, vi } from 'vitest'; -import { readFileSync } from 'fs'; -import { resolve } from 'path'; import React from 'react'; -// 读取中文翻译文件用于 withTranslation mock -const zhCommon = JSON.parse( - readFileSync(resolve(__dirname, 'i18n/locales/zh/common.json'), 'utf-8'), -); +// Load actual zh translations for getMessage mock +// eslint-disable-next-line @typescript-eslint/no-require-imports +const zhMessages: Record< + string, + { message: string } +> = require('./public/_locales/zh/messages.json'); -// 支持嵌套 key 查找,如 "errorBoundary.title" → zhCommon.errorBoundary.title -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function nestedLookup(obj: Record, key: string): string | undefined { - const parts = key.split('.'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let current: any = obj; - for (const part of parts) { - if (current == null || typeof current !== 'object') return undefined; - current = current[part]; - } - return typeof current === 'string' ? current : undefined; -} - -vi.mock('@/utils/useLazyTranslation', () => ({ - useLazyTranslation: (ns?: string) => ({ - // 自动承接命名空间前缀过滤,100% 模拟真实多语种直出 - t: (key: string) => (ns ? `${ns}:${key}` : key), +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 (zhMessages[msgId]) return zhMessages[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 (zhMessages[candidate]) return zhMessages[candidate].message; + } + } + return msgId; + }, i18n: { changeLanguage: vi.fn().mockResolvedValue(undefined), - language: 'zh-CN', + language: 'zh', }, isLoaded: true, }), -})); - -vi.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - i18n: { - changeLanguage: vi.fn().mockResolvedValue(undefined), - language: 'zh-CN', - }, - }), - withTranslation: (ns?: string) => { - const translations = ns === 'common' ? zhCommon : {}; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (Component: React.ComponentType) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const Wrapped = (props: any) => - React.createElement(Component, { - ...props, - t: (key: string) => nestedLookup(translations, key) || key, - i18n: { language: 'zh-CN', changeLanguage: vi.fn() }, - }); - Wrapped.displayName = `withTranslation(${Component.displayName || Component.name || 'Component'})`; - return Wrapped; - }; - }, - initReactI18next: { - type: '3rdParty', - init: vi.fn().mockResolvedValue(undefined), - }, + getMessage: (msgId: string) => zhMessages[msgId]?.message ?? msgId, + getLanguage: () => 'zh', + normalizeLanguage: (lng: string) => (lng.startsWith('zh') ? 'zh' : 'en'), + SUPPORTED_LANGUAGES: ['zh', 'en'], + preloadNamespaces: vi.fn().mockResolvedValue(undefined), })); vi.mock('@/components/CopyButton', () => ({ diff --git a/wxt.config.ts b/wxt.config.ts index 407ddf0..a3bf45f 100644 --- a/wxt.config.ts +++ b/wxt.config.ts @@ -22,13 +22,6 @@ function manualChunksForHtmlOnly(): Plugin { ) { return 'vendor-react'; } - if ( - id.includes('i18next') || - id.includes('react-i18next') || - id.includes('intl-messageformat') - ) { - return 'vendor-i18n'; - } // 二维码活态感知依赖分流 if (id.includes('qr-scanner') || id.includes('qrious')) { return 'vendor-qr'; @@ -55,6 +48,7 @@ export default defineConfig({ name: 'Testing Tool', description: 'A tool for testing web applications.', version_name: undefined, + default_locale: 'zh', permissions: [ 'storage', 'unlimitedStorage', @@ -69,7 +63,7 @@ export default defineConfig({ ], host_permissions: [''], action: { - default_title: '__MSG_extName__', + default_title: '__MSG_appName__', }, side_panel: { default_path: 'entrypoints/sidepanel/index.html',