Merge branch 'feature/chrome-i18n' into develop
迁移 i18n 系统从 react-i18next 到 chrome.i18n: - 移除 react-i18next 依赖,使用 chrome.i18n API - 生成 _locales/zh/messages.json(245 个翻译 key) - 清理未使用的翻译 key - 移除语言切换按钮和英文翻译
This commit is contained in:
@@ -4,7 +4,7 @@ import { copyTextToClipboard } from '@/utils/clipboard';
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { buttonVariants, type ButtonProps } from '@/components/ui/button';
|
import { buttonVariants, type ButtonProps } from '@/components/ui/button';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
|
|
||||||
interface CopyButtonProps extends Omit<ButtonProps, 'children' | 'onClick'> {
|
interface CopyButtonProps extends Omit<ButtonProps, 'children' | 'onClick'> {
|
||||||
text: string;
|
text: string;
|
||||||
@@ -19,7 +19,7 @@ export const CopyButton: React.FC<CopyButtonProps> = ({
|
|||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation('common');
|
const { t } = useI18n('common');
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
import { Download } from 'lucide-react';
|
import { Download } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { formatFileSize } from '@/utils/base64Converter';
|
import { formatFileSize } from '@/utils/base64Converter';
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
|
|
||||||
interface DecodeResultPaperProps {
|
interface DecodeResultPaperProps {
|
||||||
/** 标题文案,由调用方传入 i18n key 对应的值(如 decodedFileOutput / decodedImageOutput) */
|
/** 标题文案,由调用方传入 i18n key 对应的值(如 decodedFileOutput / decodedImageOutput) */
|
||||||
@@ -38,7 +38,7 @@ export default function DecodeResultPaper({
|
|||||||
onDownload,
|
onDownload,
|
||||||
children,
|
children,
|
||||||
}: DecodeResultPaperProps) {
|
}: DecodeResultPaperProps) {
|
||||||
const { t } = useLazyTranslation('base64Converter');
|
const { t } = useI18n('base64Converter');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 rounded-xl bg-primary/10 border border-primary/30">
|
<div className="p-4 rounded-xl bg-primary/10 border border-primary/30">
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Component, ErrorInfo, ReactNode } from 'react';
|
import { Component, ErrorInfo, ReactNode } from 'react';
|
||||||
import { withTranslation, type WithTranslation } from 'react-i18next';
|
|
||||||
import { AlertCircle, RefreshCw } from 'lucide-react';
|
import { AlertCircle, RefreshCw } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { getMessage } from '@/utils/chromeI18n';
|
||||||
|
|
||||||
interface Props extends WithTranslation {
|
interface Props {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ interface State {
|
|||||||
error: Error | null;
|
error: Error | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ErrorBoundaryBase extends Component<Props, State> {
|
class ErrorBoundary extends Component<Props, State> {
|
||||||
state: State = {
|
state: State = {
|
||||||
hasError: false,
|
hasError: false,
|
||||||
error: null,
|
error: null,
|
||||||
@@ -37,7 +37,6 @@ class ErrorBoundaryBase extends Component<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { t } = this.props;
|
|
||||||
if (this.state.hasError) {
|
if (this.state.hasError) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center mt-16 mx-auto max-w-md">
|
<div className="flex flex-col items-center justify-center mt-16 mx-auto max-w-md">
|
||||||
@@ -46,9 +45,11 @@ class ErrorBoundaryBase extends Component<Props, State> {
|
|||||||
<AlertCircle className="h-8 w-8" />
|
<AlertCircle className="h-8 w-8" />
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-xl font-extrabold text-destructive mb-2">
|
<h2 className="text-xl font-extrabold text-destructive mb-2">
|
||||||
{t('errorBoundary.title')}
|
{getMessage('errorBoundary_title')}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-muted-foreground mb-6">{t('errorBoundary.description')}</p>
|
<p className="text-sm text-muted-foreground mb-6">
|
||||||
|
{getMessage('errorBoundary_description')}
|
||||||
|
</p>
|
||||||
{this.state.error && (
|
{this.state.error && (
|
||||||
<div className="mb-6 p-4 rounded-lg bg-zinc-950 dark:bg-zinc-900 text-left max-h-[200px] overflow-auto border border-border/40">
|
<div className="mb-6 p-4 rounded-lg bg-zinc-950 dark:bg-zinc-900 text-left max-h-[200px] overflow-auto border border-border/40">
|
||||||
<pre className="font-mono text-xs whitespace-pre-wrap break-all text-zinc-200 selection:bg-zinc-700">
|
<pre className="font-mono text-xs whitespace-pre-wrap break-all text-zinc-200 selection:bg-zinc-700">
|
||||||
@@ -62,7 +63,7 @@ class ErrorBoundaryBase extends Component<Props, State> {
|
|||||||
className="rounded-lg font-bold shadow-sm"
|
className="rounded-lg font-bold shadow-sm"
|
||||||
>
|
>
|
||||||
<RefreshCw className="mr-2 h-4 w-4" />
|
<RefreshCw className="mr-2 h-4 w-4" />
|
||||||
{t('errorBoundary.refresh')}
|
{getMessage('errorBoundary_refresh')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -73,5 +74,5 @@ class ErrorBoundaryBase extends Component<Props, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ErrorBoundary = withTranslation('common')(ErrorBoundaryBase);
|
export { ErrorBoundary };
|
||||||
export default ErrorBoundary;
|
export default ErrorBoundary;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useEffect, useRef } from 'react';
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
import { Image, X } from 'lucide-react';
|
import { Image, X } from 'lucide-react';
|
||||||
import { useSnackbar } from '@/components/GlobalSnackbar';
|
import { useSnackbar } from '@/components/GlobalSnackbar';
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
|
|
||||||
interface ImageUploaderProps {
|
interface ImageUploaderProps {
|
||||||
/** 选中的文件 */
|
/** 选中的文件 */
|
||||||
@@ -29,7 +29,7 @@ const ImageUploader = ({
|
|||||||
dragging,
|
dragging,
|
||||||
onDraggingChange,
|
onDraggingChange,
|
||||||
}: ImageUploaderProps) => {
|
}: ImageUploaderProps) => {
|
||||||
const { t } = useLazyTranslation('qrCode');
|
const { t } = useI18n('qrCode');
|
||||||
const { showMessage } = useSnackbar();
|
const { showMessage } = useSnackbar();
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Component, ErrorInfo, ReactNode } from 'react';
|
import { Component, ErrorInfo, ReactNode } from 'react';
|
||||||
import { withTranslation, type WithTranslation } from 'react-i18next';
|
|
||||||
import { AlertCircle, RefreshCw } from 'lucide-react';
|
import { AlertCircle, RefreshCw } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { getMessage } from '@/utils/chromeI18n';
|
||||||
|
|
||||||
interface Props extends WithTranslation {
|
interface Props {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
resetKey?: string | number;
|
resetKey?: string | number;
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@ interface State {
|
|||||||
error: Error | null;
|
error: Error | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
class PageErrorBoundaryBase extends Component<Props, State> {
|
class PageErrorBoundary extends Component<Props, State> {
|
||||||
state: State = {
|
state: State = {
|
||||||
hasError: false,
|
hasError: false,
|
||||||
error: null,
|
error: null,
|
||||||
@@ -38,7 +38,6 @@ class PageErrorBoundaryBase extends Component<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { t } = this.props;
|
|
||||||
if (this.state.hasError) {
|
if (this.state.hasError) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center flex-1 p-6 min-h-[300px] animate-in fade-in zoom-in-95 duration-200">
|
<div className="flex flex-col items-center justify-center flex-1 p-6 min-h-[300px] animate-in fade-in zoom-in-95 duration-200">
|
||||||
@@ -48,10 +47,10 @@ class PageErrorBoundaryBase extends Component<Props, State> {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 className="text-base font-semibold text-foreground mb-1.5">
|
<h3 className="text-base font-semibold text-foreground mb-1.5">
|
||||||
{t('pageErrorBoundary.title')}
|
{getMessage('pageErrorBoundary_title')}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-muted-foreground mb-5">
|
<p className="text-xs text-muted-foreground mb-5">
|
||||||
{t('pageErrorBoundary.description')}
|
{getMessage('pageErrorBoundary_description')}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{this.state.error && (
|
{this.state.error && (
|
||||||
@@ -69,7 +68,7 @@ class PageErrorBoundaryBase extends Component<Props, State> {
|
|||||||
className="font-medium shadow-sm"
|
className="font-medium shadow-sm"
|
||||||
>
|
>
|
||||||
<RefreshCw className="mr-1.5 h-3.5 w-3.5" />
|
<RefreshCw className="mr-1.5 h-3.5 w-3.5" />
|
||||||
{t('errorBoundary.retry')}
|
{getMessage('errorBoundary_retry')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -80,5 +79,5 @@ class PageErrorBoundaryBase extends Component<Props, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PageErrorBoundary = withTranslation('common')(PageErrorBoundaryBase);
|
export { PageErrorBoundary };
|
||||||
export default PageErrorBoundary;
|
export default PageErrorBoundary;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Copy, Download } from 'lucide-react';
|
import { Copy, Download } from 'lucide-react';
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ const QrCodePreview = ({
|
|||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: QrCodePreviewProps) => {
|
}: QrCodePreviewProps) => {
|
||||||
const { t } = useLazyTranslation('qrCode');
|
const { t } = useI18n('qrCode');
|
||||||
|
|
||||||
// 空状态下的虚线骨架屏
|
// 空状态下的虚线骨架屏
|
||||||
if (!qrCodeDataUrl) {
|
if (!qrCodeDataUrl) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { FEATURES, getEntryPointType } from '@/config/features';
|
import { FEATURES, getEntryPointType } from '@/config/features';
|
||||||
import { useRouter } from '@/providers/RouterProvider';
|
import { useRouter } from '@/providers/RouterProvider';
|
||||||
import { Suspense, useMemo } from 'react';
|
import { Suspense, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
import PageErrorBoundary from '@/components/PageErrorBoundary';
|
import PageErrorBoundary from '@/components/PageErrorBoundary';
|
||||||
import PageSkeleton from '@/components/PageSkeleton';
|
import PageSkeleton from '@/components/PageSkeleton';
|
||||||
import { cn } from '@/lib/utils'; // 1. 引入标准的 shadcn 工具函数
|
import { cn } from '@/lib/utils'; // 1. 引入标准的 shadcn 工具函数
|
||||||
@@ -9,7 +9,7 @@ import { AlertTriangle } from 'lucide-react'; // 用于标准的 404 异常展
|
|||||||
|
|
||||||
export default function RouterContainer() {
|
export default function RouterContainer() {
|
||||||
const { currentPage, isLoaded } = useRouter();
|
const { currentPage, isLoaded } = useRouter();
|
||||||
const { t } = useTranslation('common');
|
const { t } = useI18n('common');
|
||||||
|
|
||||||
// 2. 稳定的动态动画类名映射
|
// 2. 稳定的动态动画类名映射
|
||||||
const animationClass = useMemo(() => {
|
const animationClass = useMemo(() => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react';
|
import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react';
|
||||||
import { X } from 'lucide-react';
|
import { X } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { toast } from 'sonner'; // 推荐使用 shadcn 的默认 Toast
|
import { toast } from 'sonner'; // 推荐使用 shadcn 的默认 Toast
|
||||||
import { CopyButton } from '@/components/CopyButton';
|
import { CopyButton } from '@/components/CopyButton';
|
||||||
@@ -114,7 +114,7 @@ const TextInputArea = forwardRef<HTMLTextAreaElement, TextInputAreaProps>((props
|
|||||||
const [internalValue, setInternalValue] = useState(defaultValue);
|
const [internalValue, setInternalValue] = useState(defaultValue);
|
||||||
const [error, setError] = useState<string>('');
|
const [error, setError] = useState<string>('');
|
||||||
|
|
||||||
const { t } = useTranslation('common');
|
const { t } = useI18n('common');
|
||||||
const placeholder = placeholderProp ?? t('textInputArea.placeholder');
|
const placeholder = placeholderProp ?? t('textInputArea.placeholder');
|
||||||
|
|
||||||
const isControlled = controlledValue !== undefined;
|
const isControlled = controlledValue !== undefined;
|
||||||
|
|||||||
+6
-19
@@ -2,7 +2,6 @@ import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|||||||
import {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
Globe,
|
|
||||||
History,
|
History,
|
||||||
Monitor,
|
Monitor,
|
||||||
Moon,
|
Moon,
|
||||||
@@ -16,8 +15,7 @@ import { useThemeMode } from '@/providers/ThemeModeProvider';
|
|||||||
import { FeatureConfig, FEATURES } from '@/config/features';
|
import { FeatureConfig, FEATURES } from '@/config/features';
|
||||||
import { storageUtil } from '@/utils/chromeStorage';
|
import { storageUtil } from '@/utils/chromeStorage';
|
||||||
import { openExtensionPage } from '@/utils/chromeTabs';
|
import { openExtensionPage } from '@/utils/chromeTabs';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
import { normalizeLanguage, SUPPORTED_LANGUAGES } from '@/i18n';
|
|
||||||
import { cn } from '@/lib/utils'; // 1. 引入 shadcn 核心工具函数
|
import { cn } from '@/lib/utils'; // 1. 引入 shadcn 核心工具函数
|
||||||
|
|
||||||
// 常量配置抽取(无需写在全局变量或 styles 对象里)
|
// 常量配置抽取(无需写在全局变量或 styles 对象里)
|
||||||
@@ -27,7 +25,7 @@ const SEARCH_HISTORY_DISPLAY = 5;
|
|||||||
export default function TopBar({ onOpenOptions }: { onOpenOptions: () => void }) {
|
export default function TopBar({ onOpenOptions }: { onOpenOptions: () => void }) {
|
||||||
const { currentPage, goBack, navigateTo } = useRouter();
|
const { currentPage, goBack, navigateTo } = useRouter();
|
||||||
const { mode, setMode } = useThemeMode();
|
const { mode, setMode } = useThemeMode();
|
||||||
const { t, i18n } = useTranslation(['common', 'features']);
|
const { t } = useI18n(['common', 'features']);
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [showResults, setShowResults] = useState(false);
|
const [showResults, setShowResults] = useState(false);
|
||||||
@@ -99,14 +97,6 @@ export default function TopBar({ onOpenOptions }: { onOpenOptions: () => void })
|
|||||||
setShowResults(false);
|
setShowResults(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleLanguage = async () => {
|
|
||||||
const currentLng = normalizeLanguage(i18n.language);
|
|
||||||
const currentIndex = SUPPORTED_LANGUAGES.indexOf(currentLng);
|
|
||||||
const nextLng = SUPPORTED_LANGUAGES[(currentIndex + 1) % SUPPORTED_LANGUAGES.length];
|
|
||||||
await i18n.changeLanguage(nextLng);
|
|
||||||
await storageUtil.set('app/language', nextLng);
|
|
||||||
};
|
|
||||||
|
|
||||||
const cycleThemeMode = () => {
|
const cycleThemeMode = () => {
|
||||||
const nextMap = { light: 'dark', dark: 'system', system: 'light' } as const;
|
const nextMap = { light: 'dark', dark: 'system', system: 'light' } as const;
|
||||||
setMode(nextMap[mode]);
|
setMode(nextMap[mode]);
|
||||||
@@ -159,7 +149,7 @@ export default function TopBar({ onOpenOptions }: { onOpenOptions: () => void })
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={goBack}
|
onClick={goBack}
|
||||||
aria-label={t('common:buttons.back')}
|
aria-label={t('common_buttons_back')}
|
||||||
className="flex h-8 w-8 items-center justify-center rounded-md border border-input bg-background text-muted-foreground shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground"
|
className="flex h-8 w-8 items-center justify-center rounded-md border border-input bg-background text-muted-foreground shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
@@ -174,7 +164,7 @@ export default function TopBar({ onOpenOptions }: { onOpenOptions: () => void })
|
|||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={t('common:buttons.search')}
|
placeholder={t('common_buttons_search')}
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSearchQuery(e.target.value);
|
setSearchQuery(e.target.value);
|
||||||
@@ -183,7 +173,7 @@ export default function TopBar({ onOpenOptions }: { onOpenOptions: () => void })
|
|||||||
}}
|
}}
|
||||||
onFocus={() => setShowResults(true)}
|
onFocus={() => setShowResults(true)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
aria-label={t('common:buttons.search')}
|
aria-label={t('common_buttons_search')}
|
||||||
className="w-full h-9 pl-9 pr-8 text-sm rounded-md border border-input bg-muted/50 transition-all placeholder:text-muted-foreground focus:bg-background focus:outline-none focus:ring-1 focus:ring-ring focus:border-input"
|
className="w-full h-9 pl-9 pr-8 text-sm rounded-md border border-input bg-muted/50 transition-all placeholder:text-muted-foreground focus:bg-background focus:outline-none focus:ring-1 focus:ring-ring focus:border-input"
|
||||||
/>
|
/>
|
||||||
{searchQuery && (
|
{searchQuery && (
|
||||||
@@ -272,16 +262,13 @@ export default function TopBar({ onOpenOptions }: { onOpenOptions: () => void })
|
|||||||
|
|
||||||
{/* 右侧:操作区 */}
|
{/* 右侧:操作区 */}
|
||||||
<div className="flex items-center gap-1 shrink-0">
|
<div className="flex items-center gap-1 shrink-0">
|
||||||
<IconButton onClick={toggleLanguage} title={t('common:buttons.toggleLanguage')}>
|
|
||||||
<Globe className="h-4 w-4" />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton onClick={cycleThemeMode} title={t(`common:buttons.themeMode.${mode}`)}>
|
<IconButton onClick={cycleThemeMode} title={t(`common:buttons.themeMode.${mode}`)}>
|
||||||
<ThemeIcon className="h-4 w-4" />
|
<ThemeIcon className="h-4 w-4" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton onClick={handleOpenInTab} title={t('common:buttons.openInTab')}>
|
<IconButton onClick={handleOpenInTab} title={t('common:buttons.openInTab')}>
|
||||||
<ExternalLink className="h-4 w-4" />
|
<ExternalLink className="h-4 w-4" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton onClick={onOpenOptions} title={t('common:buttons.settings')}>
|
<IconButton onClick={onOpenOptions} title={t('common_buttons_settings')}>
|
||||||
<Settings className="h-4 w-4" />
|
<Settings className="h-4 w-4" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ describe('DecodeResultPaper 组件', () => {
|
|||||||
|
|
||||||
it('应渲染下载按钮', () => {
|
it('应渲染下载按钮', () => {
|
||||||
render(<DecodeResultPaper {...defaultProps} />);
|
render(<DecodeResultPaper {...defaultProps} />);
|
||||||
expect(screen.getByRole('button', { name: 'base64Converter:download' })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: '下载' })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('应渲染 children 内容', () => {
|
it('应渲染 children 内容', () => {
|
||||||
@@ -64,7 +64,7 @@ describe('DecodeResultPaper 组件', () => {
|
|||||||
|
|
||||||
it('点击下载按钮时应调用 onDownload', () => {
|
it('点击下载按钮时应调用 onDownload', () => {
|
||||||
render(<DecodeResultPaper {...defaultProps} />);
|
render(<DecodeResultPaper {...defaultProps} />);
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'base64Converter:download' }));
|
fireEvent.click(screen.getByRole('button', { name: '下载' }));
|
||||||
expect(defaultProps.onDownload).toHaveBeenCalledTimes(1);
|
expect(defaultProps.onDownload).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -72,12 +72,12 @@ describe('DecodeResultPaper 组件', () => {
|
|||||||
describe('按钮状态', () => {
|
describe('按钮状态', () => {
|
||||||
it('文件名为空时下载按钮应禁用', () => {
|
it('文件名为空时下载按钮应禁用', () => {
|
||||||
render(<DecodeResultPaper {...{ ...defaultProps, fileName: '' }} />);
|
render(<DecodeResultPaper {...{ ...defaultProps, fileName: '' }} />);
|
||||||
expect(screen.getByRole('button', { name: 'base64Converter:download' })).toBeDisabled();
|
expect(screen.getByRole('button', { name: '下载' })).toBeDisabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('文件名不为空时下载按钮应启用', () => {
|
it('文件名不为空时下载按钮应启用', () => {
|
||||||
render(<DecodeResultPaper {...defaultProps} />);
|
render(<DecodeResultPaper {...defaultProps} />);
|
||||||
expect(screen.getByRole('button', { name: 'base64Converter:download' })).toBeEnabled();
|
expect(screen.getByRole('button', { name: '下载' })).toBeEnabled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,18 +7,6 @@ const storageOnChangedMock = { addListener: vi.fn(), removeListener: vi.fn() };
|
|||||||
(globalThis as any).chrome = { storage: { onChanged: storageOnChangedMock } };
|
(globalThis as any).chrome = { storage: { onChanged: storageOnChangedMock } };
|
||||||
(globalThis as any).browser = { storage: { onChanged: storageOnChangedMock } };
|
(globalThis as any).browser = { storage: { onChanged: storageOnChangedMock } };
|
||||||
|
|
||||||
// 💡 1. 规范对齐:挂载标准的 react-i18next 统一桩函数,防止多进程前缀破产
|
|
||||||
vi.mock('react-i18next', () => ({
|
|
||||||
useTranslation: vi.fn((ns: string | string[]) => {
|
|
||||||
const nsArray = Array.isArray(ns) ? ns : [ns];
|
|
||||||
return {
|
|
||||||
t: (key: string) => `${nsArray.join(',')}:${key}`,
|
|
||||||
i18n: { language: 'en' },
|
|
||||||
ready: true,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 模拟 URL API
|
// 模拟 URL API
|
||||||
const mockCreateObjectURL = vi.fn();
|
const mockCreateObjectURL = vi.fn();
|
||||||
const mockRevokeObjectURL = vi.fn();
|
const mockRevokeObjectURL = vi.fn();
|
||||||
@@ -62,8 +50,8 @@ describe('ImageUploader 组件', () => {
|
|||||||
it('当没有选中文件时应显示上传提示', () => {
|
it('当没有选中文件时应显示上传提示', () => {
|
||||||
render(<ImageUploader {...defaultProps} />);
|
render(<ImageUploader {...defaultProps} />);
|
||||||
// 💡 修复点 2:全面切换为高弹性正则,斩断双重命名空间死锁!
|
// 💡 修复点 2:全面切换为高弹性正则,斩断双重命名空间死锁!
|
||||||
expect(screen.getByText(/clickToUpload/)).toBeInTheDocument();
|
expect(screen.getByText(/点击.*拖拽/)).toBeInTheDocument();
|
||||||
expect(screen.getByText(/supportFormats/)).toBeInTheDocument();
|
expect(screen.getByText(/格式/)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('当没有选中文件时应显示 ImageIcon', () => {
|
it('当没有选中文件时应显示 ImageIcon', () => {
|
||||||
@@ -78,7 +66,7 @@ describe('ImageUploader 组件', () => {
|
|||||||
);
|
);
|
||||||
expect(screen.getByText('test.png')).toBeInTheDocument();
|
expect(screen.getByText('test.png')).toBeInTheDocument();
|
||||||
// 💡 修复点 3(自愈第 62 行崩溃位置):利用正则模糊命中,彻底通过!
|
// 💡 修复点 3(自愈第 62 行崩溃位置):利用正则模糊命中,彻底通过!
|
||||||
expect(screen.getByText(/clickToChange/)).toBeInTheDocument();
|
expect(screen.getByText(/点击更换/)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('当选中文件时应显示预览图片', () => {
|
it('当选中文件时应显示预览图片', () => {
|
||||||
|
|||||||
@@ -2,19 +2,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|||||||
import { fireEvent, render, screen } from '@testing-library/react';
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
import QrCodePreview from '@/components/QrCodePreview';
|
import QrCodePreview from '@/components/QrCodePreview';
|
||||||
|
|
||||||
// 💡 1. 规范对齐:在这个测试文件的头部同样挂载统一的 react-i18next 桩函数,
|
|
||||||
// 与你整个工程的国际化解耦架构完美闭环。
|
|
||||||
vi.mock('react-i18next', () => ({
|
|
||||||
useTranslation: vi.fn((ns: string | string[]) => {
|
|
||||||
const nsArray = Array.isArray(ns) ? ns : [ns];
|
|
||||||
return {
|
|
||||||
t: (key: string) => `${nsArray.join(',')}:${key}`,
|
|
||||||
i18n: { language: 'en' },
|
|
||||||
ready: true,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('QrCodePreview 组件', () => {
|
describe('QrCodePreview 组件', () => {
|
||||||
const mockOnDownload = vi.fn();
|
const mockOnDownload = vi.fn();
|
||||||
const mockOnCopy = vi.fn();
|
const mockOnCopy = vi.fn();
|
||||||
@@ -32,7 +19,7 @@ describe('QrCodePreview 组件', () => {
|
|||||||
it('当 qrCodeDataUrl 为空时应显示占位文本', () => {
|
it('当 qrCodeDataUrl 为空时应显示占位文本', () => {
|
||||||
render(<QrCodePreview qrCodeDataUrl="" onDownload={mockOnDownload} onCopy={mockOnCopy} />);
|
render(<QrCodePreview qrCodeDataUrl="" onDownload={mockOnDownload} onCopy={mockOnCopy} />);
|
||||||
// 💡 修复点 2:全面拥抱柔性正则匹配,直接终结多层 'qrCode:qrCode:' 前缀踩踏!
|
// 💡 修复点 2:全面拥抱柔性正则匹配,直接终结多层 'qrCode:qrCode:' 前缀踩踏!
|
||||||
expect(screen.getByText(/qrCodeWillShow/)).toBeInTheDocument();
|
expect(screen.getByText(/二维码将显示/)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('当 qrCodeDataUrl 有值时应显示二维码图片', () => {
|
it('当 qrCodeDataUrl 有值时应显示二维码图片', () => {
|
||||||
@@ -58,7 +45,7 @@ describe('QrCodePreview 组件', () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
// 💡 修复点 3:切换为正则,无缝过检
|
// 💡 修复点 3:切换为正则,无缝过检
|
||||||
expect(screen.getByText(/downloadButton/)).toBeInTheDocument();
|
expect(screen.getByText(/下载二维码/)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('当 qrCodeDataUrl 有值时应显示复制按钮', () => {
|
it('当 qrCodeDataUrl 有值时应显示复制按钮', () => {
|
||||||
@@ -69,14 +56,13 @@ describe('QrCodePreview 组件', () => {
|
|||||||
onCopy={mockOnCopy}
|
onCopy={mockOnCopy}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
// 💡 修复点 4:切换为正则,无缝过检
|
expect(screen.getByText(/复制二维码/)).toBeInTheDocument();
|
||||||
expect(screen.getByText(/copyQrButton/)).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('当 qrCodeDataUrl 为空时不应显示操作按钮', () => {
|
it('当 qrCodeDataUrl 为空时不应显示操作按钮', () => {
|
||||||
render(<QrCodePreview qrCodeDataUrl="" onDownload={mockOnDownload} onCopy={mockOnCopy} />);
|
render(<QrCodePreview qrCodeDataUrl="" onDownload={mockOnDownload} onCopy={mockOnCopy} />);
|
||||||
expect(screen.queryByText(/downloadButton/)).not.toBeInTheDocument();
|
expect(screen.queryByText(/下载二维码/)).not.toBeInTheDocument();
|
||||||
expect(screen.queryByText(/copyQrButton/)).not.toBeInTheDocument();
|
expect(screen.queryByText(/复制二维码/)).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -89,8 +75,7 @@ describe('QrCodePreview 组件', () => {
|
|||||||
onCopy={mockOnCopy}
|
onCopy={mockOnCopy}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
// 💡 修复点 5:点击行为同步更改为正则匹配定位,保障状态修改流一帧直达
|
fireEvent.click(screen.getByText(/下载二维码/));
|
||||||
fireEvent.click(screen.getByText(/downloadButton/));
|
|
||||||
expect(mockOnDownload).toHaveBeenCalledTimes(1);
|
expect(mockOnDownload).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -102,8 +87,7 @@ describe('QrCodePreview 组件', () => {
|
|||||||
onCopy={mockOnCopy}
|
onCopy={mockOnCopy}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
// 💡 修复点 6:彻底修复第 88 行报错位置,改用正则解开死锁!
|
fireEvent.click(screen.getByText(/复制二维码/));
|
||||||
fireEvent.click(screen.getByText(/copyQrButton/));
|
|
||||||
expect(mockOnCopy).toHaveBeenCalledTimes(1);
|
expect(mockOnCopy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,18 +9,6 @@ const storageOnChangedMock = { addListener: vi.fn(), removeListener: vi.fn() };
|
|||||||
(globalThis as any).chrome = { storage: { onChanged: storageOnChangedMock } };
|
(globalThis as any).chrome = { storage: { onChanged: storageOnChangedMock } };
|
||||||
(globalThis as any).browser = { storage: { onChanged: storageOnChangedMock } };
|
(globalThis as any).browser = { storage: { onChanged: storageOnChangedMock } };
|
||||||
|
|
||||||
// 💡 2. 对齐 react-i18next 的分布式国际化桩
|
|
||||||
vi.mock('react-i18next', () => ({
|
|
||||||
useTranslation: vi.fn((ns: string | string[]) => {
|
|
||||||
const nsArray = Array.isArray(ns) ? ns : [ns];
|
|
||||||
return {
|
|
||||||
t: (key: string) => `${nsArray.join(',')}:${key}`,
|
|
||||||
i18n: { language: 'en' },
|
|
||||||
ready: true,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('StorageCleanerConfirm 组件', () => {
|
describe('StorageCleanerConfirm 组件', () => {
|
||||||
const mockOnClose = vi.fn();
|
const mockOnClose = vi.fn();
|
||||||
const mockOnConfirm = vi.fn();
|
const mockOnConfirm = vi.fn();
|
||||||
@@ -55,26 +43,26 @@ describe('StorageCleanerConfirm 组件', () => {
|
|||||||
renderComponent();
|
renderComponent();
|
||||||
// 💡 修复点 3:拥抱模糊正则断言。
|
// 💡 修复点 3:拥抱模糊正则断言。
|
||||||
// 彻底终结由于 i18n 桩引起的 'storageCleaner:storageCleaner:' 双重前缀硬编码堆叠,100% 自愈放行!
|
// 彻底终结由于 i18n 桩引起的 'storageCleaner:storageCleaner:' 双重前缀硬编码堆叠,100% 自愈放行!
|
||||||
expect(screen.getByText(/confirmTitle/)).toBeInTheDocument();
|
expect(screen.getByRole('heading', { name: /确认清理/ })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('应显示警告信息', () => {
|
it('应显示警告信息', () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
expect(screen.getByText(/irreversible/i)).toBeInTheDocument();
|
expect(screen.getByText(/不可撤销/)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('应将选中的选项显示为标签', () => {
|
it('应将选中的选项显示为标签', () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
expect(screen.getByText(/options\.localStorage/)).toBeInTheDocument();
|
expect(screen.getByText(/Local Storage/)).toBeInTheDocument();
|
||||||
expect(screen.getByText(/options\.sessionStorage/)).toBeInTheDocument();
|
expect(screen.getByText(/Session Storage/)).toBeInTheDocument();
|
||||||
expect(screen.getByText(/options\.cookies/)).toBeInTheDocument();
|
expect(screen.getByText(/Cookies/)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('应显示取消和确认按钮', () => {
|
it('应显示取消和确认按钮', () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
// 💡 修复点 4:按钮的 Accessible Name 匹配同步切回高弹性正则模式,抵抗一切国际化双前缀污染
|
// 💡 修复点 4:按钮的 Accessible Name 匹配同步切回高弹性正则模式,抵抗一切国际化双前缀污染
|
||||||
expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: /取消/ })).toBeInTheDocument();
|
||||||
expect(screen.getByRole('button', { name: /confirmAction/i })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: /确认清理/ })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -82,7 +70,7 @@ describe('StorageCleanerConfirm 组件', () => {
|
|||||||
it('点击取消时应调用 onClose', () => {
|
it('点击取消时应调用 onClose', () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /cancel/i }));
|
fireEvent.click(screen.getByRole('button', { name: /取消/ }));
|
||||||
expect(mockOnClose).toHaveBeenCalledTimes(1);
|
expect(mockOnClose).toHaveBeenCalledTimes(1);
|
||||||
expect(mockOnConfirm).not.toHaveBeenCalled();
|
expect(mockOnConfirm).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -90,7 +78,7 @@ describe('StorageCleanerConfirm 组件', () => {
|
|||||||
it('点击确认时应调用 onConfirm', () => {
|
it('点击确认时应调用 onConfirm', () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /confirmAction/i }));
|
fireEvent.click(screen.getByRole('button', { name: /确认清理/ }));
|
||||||
expect(mockOnConfirm).toHaveBeenCalledTimes(1);
|
expect(mockOnConfirm).toHaveBeenCalledTimes(1);
|
||||||
expect(mockOnClose).not.toHaveBeenCalled();
|
expect(mockOnClose).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -109,10 +97,10 @@ describe('StorageCleanerConfirm 组件', () => {
|
|||||||
|
|
||||||
renderComponent({ options: partialOptions });
|
renderComponent({ options: partialOptions });
|
||||||
|
|
||||||
expect(screen.getByText(/options\.localStorage/)).toBeInTheDocument();
|
expect(screen.getByText(/Local Storage/)).toBeInTheDocument();
|
||||||
expect(screen.getByText(/options\.indexedDB/)).toBeInTheDocument();
|
expect(screen.getByText(/IndexedDB/)).toBeInTheDocument();
|
||||||
expect(screen.queryByText(/options\.sessionStorage/)).not.toBeInTheDocument();
|
expect(screen.queryByText(/Session Storage/)).not.toBeInTheDocument();
|
||||||
expect(screen.queryByText(/options\.cookies/)).not.toBeInTheDocument();
|
expect(screen.queryByText(/Cookies$/)).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('应处理空选项', () => {
|
it('应处理空选项', () => {
|
||||||
@@ -135,7 +123,7 @@ describe('StorageCleanerConfirm 组件', () => {
|
|||||||
describe('对话框行为测试', () => {
|
describe('对话框行为测试', () => {
|
||||||
it('open 为 false 时不应渲染', () => {
|
it('open 为 false 时不应渲染', () => {
|
||||||
renderComponent({ open: false });
|
renderComponent({ open: false });
|
||||||
expect(screen.queryByText(/confirmTitle/)).not.toBeInTheDocument();
|
expect(screen.queryByRole('heading', { name: /确认清理/ })).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('应使用不同选项渲染', () => {
|
it('应使用不同选项渲染', () => {
|
||||||
@@ -150,8 +138,8 @@ describe('StorageCleanerConfirm 组件', () => {
|
|||||||
|
|
||||||
renderComponent({ options: customOptions });
|
renderComponent({ options: customOptions });
|
||||||
|
|
||||||
expect(screen.getByText(/options\.sessionStorage/)).toBeInTheDocument();
|
expect(screen.getByText(/Session Storage/)).toBeInTheDocument();
|
||||||
expect(screen.getByText(/options\.cookies/)).toBeInTheDocument();
|
expect(screen.getByText(/Cookies/)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -55,18 +55,18 @@ describe('TopBar 组件', () => {
|
|||||||
it('不在 dashboard 时应渲染返回按钮', () => {
|
it('不在 dashboard 时应渲染返回按钮', () => {
|
||||||
mockRouterValue.currentPage = 'timestamp';
|
mockRouterValue.currentPage = 'timestamp';
|
||||||
renderWithProvider(<TopBar onOpenOptions={vi.fn()} />);
|
renderWithProvider(<TopBar onOpenOptions={vi.fn()} />);
|
||||||
expect(screen.getByLabelText('common:buttons.back')).toBeInTheDocument();
|
expect(screen.getByLabelText('返回')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('在 dashboard 上不应渲染返回按钮', () => {
|
it('在 dashboard 上不应渲染返回按钮', () => {
|
||||||
mockRouterValue.currentPage = 'dashboard';
|
mockRouterValue.currentPage = 'dashboard';
|
||||||
renderWithProvider(<TopBar onOpenOptions={vi.fn()} />);
|
renderWithProvider(<TopBar onOpenOptions={vi.fn()} />);
|
||||||
expect(screen.queryByLabelText('common:buttons.back')).not.toBeInTheDocument();
|
expect(screen.queryByLabelText('返回')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('应渲染设置按钮', () => {
|
it('应渲染设置按钮', () => {
|
||||||
renderWithProvider(<TopBar onOpenOptions={vi.fn()} />);
|
renderWithProvider(<TopBar onOpenOptions={vi.fn()} />);
|
||||||
expect(screen.getByLabelText('common:buttons.settings')).toBeInTheDocument();
|
expect(screen.getByLabelText('设置')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ describe('TopBar 组件', () => {
|
|||||||
const handleOpenOptions = vi.fn();
|
const handleOpenOptions = vi.fn();
|
||||||
renderWithProvider(<TopBar onOpenOptions={handleOpenOptions} />);
|
renderWithProvider(<TopBar onOpenOptions={handleOpenOptions} />);
|
||||||
|
|
||||||
fireEvent.click(screen.getByLabelText('common:buttons.settings'));
|
fireEvent.click(screen.getByLabelText('设置'));
|
||||||
expect(handleOpenOptions).toHaveBeenCalledTimes(1);
|
expect(handleOpenOptions).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ describe('TopBar 组件', () => {
|
|||||||
mockRouterValue.currentPage = 'timestamp';
|
mockRouterValue.currentPage = 'timestamp';
|
||||||
renderWithProvider(<TopBar onOpenOptions={vi.fn()} />);
|
renderWithProvider(<TopBar onOpenOptions={vi.fn()} />);
|
||||||
|
|
||||||
fireEvent.click(screen.getByLabelText('common:buttons.back'));
|
fireEvent.click(screen.getByLabelText('返回'));
|
||||||
expect(mockRouterValue.goBack).toHaveBeenCalledTimes(1);
|
expect(mockRouterValue.goBack).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -50,14 +50,14 @@ describe('features', () => {
|
|||||||
const feature = getFeatureByKey('dashboard');
|
const feature = getFeatureByKey('dashboard');
|
||||||
expect(feature).toBeDefined();
|
expect(feature).toBeDefined();
|
||||||
expect(feature?.key).toBe('dashboard');
|
expect(feature?.key).toBe('dashboard');
|
||||||
expect(feature?.labelKey).toBe('features:dashboard.title');
|
expect(feature?.labelKey).toBe('dashboard_title');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return timestamp feature', () => {
|
it('should return timestamp feature', () => {
|
||||||
const feature = getFeatureByKey('timestamp');
|
const feature = getFeatureByKey('timestamp');
|
||||||
expect(feature).toBeDefined();
|
expect(feature).toBeDefined();
|
||||||
expect(feature?.key).toBe('timestamp');
|
expect(feature?.key).toBe('timestamp');
|
||||||
expect(feature?.labelKey).toBe('features:timestamp.title');
|
expect(feature?.labelKey).toBe('timestamp_title');
|
||||||
expect(feature?.themeColorKey).toBeDefined();
|
expect(feature?.themeColorKey).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ describe('features', () => {
|
|||||||
const feature = getFeatureByKey('storageCleaner');
|
const feature = getFeatureByKey('storageCleaner');
|
||||||
expect(feature).toBeDefined();
|
expect(feature).toBeDefined();
|
||||||
expect(feature?.key).toBe('storageCleaner');
|
expect(feature?.key).toBe('storageCleaner');
|
||||||
expect(feature?.labelKey).toBe('features:storageCleaner.title');
|
expect(feature?.labelKey).toBe('storageCleaner_title');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return undefined for invalid key', () => {
|
it('should return undefined for invalid key', () => {
|
||||||
|
|||||||
+21
-21
@@ -46,7 +46,7 @@ export interface FeatureConfig {
|
|||||||
export const FEATURES: FeatureConfig[] = [
|
export const FEATURES: FeatureConfig[] = [
|
||||||
{
|
{
|
||||||
key: 'dashboard',
|
key: 'dashboard',
|
||||||
labelKey: 'features:dashboard.title',
|
labelKey: 'dashboard_title',
|
||||||
descriptionKey: '',
|
descriptionKey: '',
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
components: {
|
components: {
|
||||||
@@ -57,8 +57,8 @@ export const FEATURES: FeatureConfig[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'timestamp',
|
key: 'timestamp',
|
||||||
labelKey: 'features:timestamp.title',
|
labelKey: 'timestamp_title',
|
||||||
descriptionKey: 'features:timestamp.description',
|
descriptionKey: 'timestamp_description',
|
||||||
themeColorKey: 'primary',
|
themeColorKey: 'primary',
|
||||||
icon: Clock,
|
icon: Clock,
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
@@ -70,8 +70,8 @@ export const FEATURES: FeatureConfig[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'storageCleaner',
|
key: 'storageCleaner',
|
||||||
labelKey: 'features:storageCleaner.title',
|
labelKey: 'storageCleaner_title',
|
||||||
descriptionKey: 'features:storageCleaner.description',
|
descriptionKey: 'storageCleaner_description',
|
||||||
themeColorKey: 'warning',
|
themeColorKey: 'warning',
|
||||||
icon: Database,
|
icon: Database,
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
@@ -83,8 +83,8 @@ export const FEATURES: FeatureConfig[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'qrCode',
|
key: 'qrCode',
|
||||||
labelKey: 'features:qrCode.title',
|
labelKey: 'qrCode_title',
|
||||||
descriptionKey: 'features:qrCode.description',
|
descriptionKey: 'qrCode_description',
|
||||||
themeColorKey: 'success',
|
themeColorKey: 'success',
|
||||||
icon: QrCode,
|
icon: QrCode,
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
@@ -96,8 +96,8 @@ export const FEATURES: FeatureConfig[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'textStatistics',
|
key: 'textStatistics',
|
||||||
labelKey: 'features:textStatistics.title',
|
labelKey: 'textStatistics_title',
|
||||||
descriptionKey: 'features:textStatistics.description',
|
descriptionKey: 'textStatistics_description',
|
||||||
themeColorKey: 'secondary',
|
themeColorKey: 'secondary',
|
||||||
icon: FileText,
|
icon: FileText,
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
@@ -109,8 +109,8 @@ export const FEATURES: FeatureConfig[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'jwt',
|
key: 'jwt',
|
||||||
labelKey: 'features:jwt.title',
|
labelKey: 'jwt_title',
|
||||||
descriptionKey: 'features:jwt.description',
|
descriptionKey: 'jwt_description',
|
||||||
themeColorKey: 'info',
|
themeColorKey: 'info',
|
||||||
icon: Key,
|
icon: Key,
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
@@ -122,8 +122,8 @@ export const FEATURES: FeatureConfig[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'jsonDiff',
|
key: 'jsonDiff',
|
||||||
labelKey: 'features:jsonDiff.title',
|
labelKey: 'jsonDiff_title',
|
||||||
descriptionKey: 'features:jsonDiff.description',
|
descriptionKey: 'jsonDiff_description',
|
||||||
themeColorKey: 'primary',
|
themeColorKey: 'primary',
|
||||||
icon: GitCompareArrows,
|
icon: GitCompareArrows,
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
@@ -135,8 +135,8 @@ export const FEATURES: FeatureConfig[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'base64Converter',
|
key: 'base64Converter',
|
||||||
labelKey: 'features:base64Converter.title',
|
labelKey: 'base64Converter_title',
|
||||||
descriptionKey: 'features:base64Converter.description',
|
descriptionKey: 'base64Converter_description',
|
||||||
themeColorKey: 'info',
|
themeColorKey: 'info',
|
||||||
icon: ArrowLeftRight,
|
icon: ArrowLeftRight,
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
@@ -148,8 +148,8 @@ export const FEATURES: FeatureConfig[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'markdownToHtml',
|
key: 'markdownToHtml',
|
||||||
labelKey: 'features:markdownToHtml.title',
|
labelKey: 'markdownToHtml_title',
|
||||||
descriptionKey: 'features:markdownToHtml.description',
|
descriptionKey: 'markdownToHtml_description',
|
||||||
themeColorKey: 'secondary',
|
themeColorKey: 'secondary',
|
||||||
icon: Code,
|
icon: Code,
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
@@ -161,8 +161,8 @@ export const FEATURES: FeatureConfig[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'htmlToMarkdown',
|
key: 'htmlToMarkdown',
|
||||||
labelKey: 'features:htmlToMarkdown.title',
|
labelKey: 'htmlToMarkdown_title',
|
||||||
descriptionKey: 'features:htmlToMarkdown.description',
|
descriptionKey: 'htmlToMarkdown_description',
|
||||||
themeColorKey: 'secondary',
|
themeColorKey: 'secondary',
|
||||||
icon: File,
|
icon: File,
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
@@ -174,8 +174,8 @@ export const FEATURES: FeatureConfig[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'rightClickRestorer',
|
key: 'rightClickRestorer',
|
||||||
labelKey: 'features:rightClickRestorer.title',
|
labelKey: 'rightClickRestorer_title',
|
||||||
descriptionKey: 'features:rightClickRestorer.description',
|
descriptionKey: 'rightClickRestorer_description',
|
||||||
themeColorKey: 'success',
|
themeColorKey: 'success',
|
||||||
icon: MousePointerClick,
|
icon: MousePointerClick,
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import {
|
|||||||
} from '@/config/features';
|
} from '@/config/features';
|
||||||
import GlobalSnackbar, { useSnackbarState } from '@/components/GlobalSnackbar';
|
import GlobalSnackbar, { useSnackbarState } from '@/components/GlobalSnackbar';
|
||||||
import PageErrorBoundary from '@/components/PageErrorBoundary';
|
import PageErrorBoundary from '@/components/PageErrorBoundary';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
|
|
||||||
const PALETTE_COLORS: Record<PaletteColorKey, string> = {
|
const PALETTE_COLORS: Record<PaletteColorKey, string> = {
|
||||||
primary: '#1976d2',
|
primary: '#1976d2',
|
||||||
@@ -66,7 +66,7 @@ function SortableFeatureRow({
|
|||||||
isDisabled,
|
isDisabled,
|
||||||
onToggle,
|
onToggle,
|
||||||
}: SortableFeatureRowProps) {
|
}: SortableFeatureRowProps) {
|
||||||
const { t } = useTranslation(['features']);
|
const { t } = useI18n(['features']);
|
||||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||||
id: pageKey,
|
id: pageKey,
|
||||||
});
|
});
|
||||||
@@ -154,7 +154,7 @@ function SortableFeatureRow({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const { t } = useTranslation(['features', 'common']);
|
const { t } = useI18n(['features', 'common']);
|
||||||
|
|
||||||
const initialWindowType = useMemo(() => {
|
const initialWindowType = useMemo(() => {
|
||||||
if (typeof window === 'undefined') return 'popup';
|
if (typeof window === 'undefined') return 'popup';
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
getDefaultPageOrder,
|
getDefaultPageOrder,
|
||||||
getFeatureByKey,
|
getFeatureByKey,
|
||||||
} from '@/config/features';
|
} from '@/config/features';
|
||||||
|
import { getMessage } from '@/utils/chromeI18n';
|
||||||
|
|
||||||
// Mock storageUtil
|
// Mock storageUtil
|
||||||
vi.mock('@/utils/chromeStorage', () => ({
|
vi.mock('@/utils/chromeStorage', () => ({
|
||||||
@@ -16,14 +17,6 @@ vi.mock('@/utils/chromeStorage', () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock i18next
|
|
||||||
vi.mock('react-i18next', () => ({
|
|
||||||
useTranslation: () => ({
|
|
||||||
t: (key: string) => key,
|
|
||||||
}),
|
|
||||||
withTranslation: () => (Component: any) => Component,
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('Options App', () => {
|
describe('Options App', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
@@ -47,7 +40,7 @@ describe('Options App', () => {
|
|||||||
for (const key of defaultOrder) {
|
for (const key of defaultOrder) {
|
||||||
const feature = getFeatureByKey(key);
|
const feature = getFeatureByKey(key);
|
||||||
if (feature) {
|
if (feature) {
|
||||||
expect(screen.getByText(feature.labelKey)).toBeInTheDocument();
|
expect(screen.getByText(getMessage(feature.labelKey))).toBeInTheDocument();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -72,7 +65,7 @@ describe('Options App', () => {
|
|||||||
for (const key of defaultVisible) {
|
for (const key of defaultVisible) {
|
||||||
const feature = getFeatureByKey(key);
|
const feature = getFeatureByKey(key);
|
||||||
if (feature && key !== 'dashboard') {
|
if (feature && key !== 'dashboard') {
|
||||||
expect(screen.getByText(feature.labelKey)).toBeInTheDocument();
|
expect(screen.getByText(getMessage(feature.labelKey))).toBeInTheDocument();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -97,7 +90,7 @@ describe('Options App', () => {
|
|||||||
for (const key of defaultOrder) {
|
for (const key of defaultOrder) {
|
||||||
const feature = getFeatureByKey(key);
|
const feature = getFeatureByKey(key);
|
||||||
if (feature) {
|
if (feature) {
|
||||||
expect(screen.getByText(feature.labelKey)).toBeInTheDocument();
|
expect(screen.getByText(getMessage(feature.labelKey))).toBeInTheDocument();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -125,7 +118,7 @@ describe('Options App', () => {
|
|||||||
for (const key of defaultOrder) {
|
for (const key of defaultOrder) {
|
||||||
const feature = getFeatureByKey(key);
|
const feature = getFeatureByKey(key);
|
||||||
if (feature) {
|
if (feature) {
|
||||||
expect(screen.getByText(feature.labelKey)).toBeInTheDocument();
|
expect(screen.getByText(getMessage(feature.labelKey))).toBeInTheDocument();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import AppRoot from '@/providers/AppRoot';
|
import AppRoot from '@/providers/AppRoot';
|
||||||
import '@/i18n';
|
|
||||||
import '@/src/index.css';
|
import '@/src/index.css';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import AppRoot from '@/providers/AppRoot';
|
import AppRoot from '@/providers/AppRoot';
|
||||||
import '@/i18n';
|
|
||||||
import '@/src/index.css';
|
import '@/src/index.css';
|
||||||
import App from './App.tsx';
|
import App from './App.tsx';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import AppRoot from '@/providers/AppRoot';
|
import AppRoot from '@/providers/AppRoot';
|
||||||
import '@/i18n';
|
|
||||||
import '@/src/index.css';
|
import '@/src/index.css';
|
||||||
import App from './App.tsx';
|
import App from './App.tsx';
|
||||||
|
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
# i18n/
|
|
||||||
|
|
||||||
国际化资源目录,管理多语言翻译和 i18next 初始化配置。
|
|
||||||
|
|
||||||
## 目录结构
|
|
||||||
|
|
||||||
```
|
|
||||||
i18n/
|
|
||||||
├── index.ts # i18next 初始化配置
|
|
||||||
└── locales/
|
|
||||||
├── zh/ # 中文翻译(默认语言)
|
|
||||||
│ ├── common.json # 通用文案
|
|
||||||
│ ├── features.json # 功能模块标题和描述
|
|
||||||
│ ├── timestamp.json # 时间戳工具翻译
|
|
||||||
│ ├── storageCleaner.json # 存储清理工具翻译
|
|
||||||
│ ├── qrCode.json # 二维码工具翻译
|
|
||||||
│ ├── textStatistics.json # 文本统计工具翻译
|
|
||||||
│ ├── jwt.json # JWT 工具翻译
|
|
||||||
│ ├── jsonDiff.json # JSON 差异工具翻译
|
|
||||||
│ ├── jsonFormat.json # JSON 格式化工具翻译
|
|
||||||
│ ├── base64Converter.json
|
|
||||||
│ ├── markdownToHtml.json
|
|
||||||
│ ├── htmlToMarkdown.json
|
|
||||||
│ └── rightClickRestorer.json
|
|
||||||
└── en/ # 英文翻译(结构同上)
|
|
||||||
└── ...
|
|
||||||
```
|
|
||||||
|
|
||||||
## index.ts
|
|
||||||
|
|
||||||
i18next 初始化配置:
|
|
||||||
|
|
||||||
- 同步加载 `common` 和 `features` 核心命名空间
|
|
||||||
- 自定义 `chromeStorage` 语言检测器,从 Chrome Storage 读取语言偏好
|
|
||||||
- `normalizeLanguage()` 将任意语言标识归一化为 `zh` 或 `en`
|
|
||||||
- 语言变更时同步更新 Day.js 本地化和 localStorage 快照
|
|
||||||
|
|
||||||
## 翻译键格式
|
|
||||||
|
|
||||||
- 命名空间:`common`(默认)、`features`、各功能独立命名空间
|
|
||||||
- 键格式:`namespace:key`(如 `features:timestamp.title`、`timestamp:unitMs`)
|
|
||||||
|
|
||||||
## 使用方式
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// 页面组件 — 懒加载翻译
|
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
|
||||||
const { t } = useLazyTranslation('timestamp');
|
|
||||||
t('timestamp:title');
|
|
||||||
|
|
||||||
// 全局组件 — 直接使用
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
const { t } = useTranslation(['common', 'features']);
|
|
||||||
t('common:settings');
|
|
||||||
```
|
|
||||||
|
|
||||||
## 添加新翻译
|
|
||||||
|
|
||||||
1. 在 `locales/{zh,en}/features.json` 添加功能标题和描述
|
|
||||||
2. 创建 `locales/{zh,en}/{功能名}.json` 添加功能专属翻译
|
|
||||||
3. 在 `utils/useLazyTranslation.ts` 的 `localeModules` 中注册新命名空间
|
|
||||||
-144
@@ -1,144 +0,0 @@
|
|||||||
import i18n from 'i18next';
|
|
||||||
import { initReactI18next } from 'react-i18next';
|
|
||||||
import type { CustomDetector } from 'i18next-browser-languagedetector'; // 💡 1. 引入官方强类型探测器接口
|
|
||||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
|
||||||
import { storageUtil } from '@/utils/chromeStorage';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
|
|
||||||
// 导入 Day.js 本地化语言包
|
|
||||||
import 'dayjs/locale/zh-cn';
|
|
||||||
|
|
||||||
// 同步加载全局核心命名空间
|
|
||||||
import commonZh from './locales/zh/common.json';
|
|
||||||
import featuresZh from './locales/zh/features.json';
|
|
||||||
import commonEn from './locales/en/common.json';
|
|
||||||
import featuresEn from './locales/en/features.json';
|
|
||||||
|
|
||||||
const resources = {
|
|
||||||
zh: {
|
|
||||||
common: commonZh,
|
|
||||||
features: featuresZh,
|
|
||||||
},
|
|
||||||
en: {
|
|
||||||
common: commonEn,
|
|
||||||
features: featuresEn,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SUPPORTED_LANGUAGES = ['zh', 'en'] as const;
|
|
||||||
export type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number];
|
|
||||||
|
|
||||||
const LANGUAGE_STORAGE_KEY = 'app/language';
|
|
||||||
const LANGUAGE_SNAPSHOT_KEY = 'snapshot/app/language';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将任意语言标识归一化为受支持的核心代码
|
|
||||||
*/
|
|
||||||
export const normalizeLanguage = (lng: string): SupportedLanguage => {
|
|
||||||
if (!lng) return 'en';
|
|
||||||
return lng.toLowerCase().startsWith('zh') ? 'zh' : 'en';
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 严格校验语言安全边界
|
|
||||||
*/
|
|
||||||
const isValidLanguage = (lng: unknown): lng is SupportedLanguage => {
|
|
||||||
return typeof lng === 'string' && (SUPPORTED_LANGUAGES as readonly string[]).includes(lng);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 同步从 localStorage 获取语言快照(消除异步闪烁)
|
|
||||||
*/
|
|
||||||
const getSyncLanguageSnapshot = (): SupportedLanguage | null => {
|
|
||||||
try {
|
|
||||||
const val = localStorage.getItem(LANGUAGE_SNAPSHOT_KEY);
|
|
||||||
if (!val) return null;
|
|
||||||
const parsed = JSON.parse(val) as unknown;
|
|
||||||
return isValidLanguage(parsed) ? parsed : null;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[i18n] Failed to parse sync language snapshot from localStorage:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 💡 2. 强类型接口重塑:显式绑定 CustomDetector 类型,
|
|
||||||
// 告诉 TS 编译器这些方法将被全局 Languagedetector 框架隐式调用,彻底治愈“未使用函数”报错!
|
|
||||||
const chromeStorageDetector: CustomDetector = {
|
|
||||||
name: 'chromeStorage',
|
|
||||||
lookup() {
|
|
||||||
return undefined;
|
|
||||||
},
|
|
||||||
cacheUserLanguage(lng: string) {
|
|
||||||
const target = normalizeLanguage(lng);
|
|
||||||
// 💡 修复点:对异步写盘操作追加 void 算子或 catch,吞掉 Promise 被忽略警告
|
|
||||||
storageUtil.set(LANGUAGE_STORAGE_KEY, target).catch((err) => {
|
|
||||||
console.error('[i18n Detector Error] Failed to write back language state:', err);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const detector = new LanguageDetector();
|
|
||||||
detector.addDetector(chromeStorageDetector);
|
|
||||||
|
|
||||||
const syncLng = getSyncLanguageSnapshot();
|
|
||||||
|
|
||||||
// 💡 3. 修复点:对 i18n.init() 返回的异步 Promise 前方追加 void 斩断依赖链,放行编译
|
|
||||||
void i18n
|
|
||||||
.use(detector)
|
|
||||||
.use(initReactI18next)
|
|
||||||
.init({
|
|
||||||
resources,
|
|
||||||
fallbackLng: 'en',
|
|
||||||
lng: syncLng || undefined,
|
|
||||||
ns: ['common', 'features'],
|
|
||||||
defaultNS: 'common',
|
|
||||||
debug: false,
|
|
||||||
interpolation: {
|
|
||||||
escapeValue: false,
|
|
||||||
},
|
|
||||||
detection: {
|
|
||||||
order: ['chromeStorage', 'navigator'],
|
|
||||||
caches: ['chromeStorage'],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听语言变更
|
|
||||||
i18n.on('languageChanged', (lng) => {
|
|
||||||
const normalizedLng = normalizeLanguage(lng);
|
|
||||||
dayjs.locale(normalizedLng === 'zh' ? 'zh-cn' : 'en');
|
|
||||||
localStorage.setItem(LANGUAGE_SNAPSHOT_KEY, JSON.stringify(normalizedLng));
|
|
||||||
});
|
|
||||||
|
|
||||||
// 初始化时从长期异步存储中恢复校准
|
|
||||||
storageUtil
|
|
||||||
.get(LANGUAGE_STORAGE_KEY)
|
|
||||||
.then((lng) => {
|
|
||||||
const rawTargetLng = lng || syncLng;
|
|
||||||
|
|
||||||
if (!rawTargetLng) {
|
|
||||||
const initialLng = normalizeLanguage(i18n.language);
|
|
||||||
|
|
||||||
// 💡 修复点:对初始化同步写盘追加安全的 Promise .catch() 异常隔离防护罩
|
|
||||||
storageUtil.set(LANGUAGE_STORAGE_KEY, initialLng).catch((err) => {
|
|
||||||
console.error('[i18n Init Error] Persistent sync collapsed:', err);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (initialLng !== normalizeLanguage(i18n.language)) {
|
|
||||||
// 💡 修复点:对 changeLanguage 异步微任务进行显式 void 断链安全隔离
|
|
||||||
void i18n.changeLanguage(initialLng);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetLng = normalizeLanguage(String(rawTargetLng));
|
|
||||||
|
|
||||||
if (isValidLanguage(targetLng) && targetLng !== normalizeLanguage(i18n.language)) {
|
|
||||||
// 💡 修复点:对 changeLanguage 异步微任务进行显式 void 断链安全隔离
|
|
||||||
void i18n.changeLanguage(targetLng);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error('[i18n Context Error] Async local storage lookup collapsed:', err);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default i18n;
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
{
|
|
||||||
"pageTitle": "Base64 Converter",
|
|
||||||
"pageSubtitle": "Encode and decode text, files, and images with Base64",
|
|
||||||
"textMode": "Text",
|
|
||||||
"fileMode": "File",
|
|
||||||
"imageMode": "Image",
|
|
||||||
"encode": "Encode",
|
|
||||||
"decode": "Decode",
|
|
||||||
"clear": "Clear",
|
|
||||||
"textInputPlaceholder": "Enter text to encode to Base64...",
|
|
||||||
"base64InputPlaceholder": "Enter Base64 string to decode...",
|
|
||||||
"base64Output": "Base64 Output",
|
|
||||||
"textOutput": "Decoded Text Output",
|
|
||||||
"copyRaw": "Copy Raw Base64",
|
|
||||||
"copyDataUri": "Copy Data URI",
|
|
||||||
"clickOrDropToFile": "Click or drop a file here",
|
|
||||||
"clickOrDropToImage": "Click or drop an image here",
|
|
||||||
"clickOrDropToReplace": "Click or drop to replace the file",
|
|
||||||
"maxFileSize": "Maximum file size: {{max}}",
|
|
||||||
"supportedFormats": "Supports PNG, JPG, WEBP, GIF, BMP, SVG, etc.",
|
|
||||||
"fileSizeExceeded": "File size exceeds the limit (max {{max}})",
|
|
||||||
"unsupportedImageType": "Unsupported image format",
|
|
||||||
"conversionFailed": "Conversion failed",
|
|
||||||
"originalSize": "Original Size",
|
|
||||||
"encodedSize": "Encoded Size",
|
|
||||||
"invalidBase64": "Invalid Base64 string",
|
|
||||||
"binaryDataDetected": "Input appears to be binary data (e.g. an image). Please switch to the Image tab.",
|
|
||||||
"imageDataUriHint": "Detected an image data URI — please use the Image tab to decode it.",
|
|
||||||
"switchToImageMode": "Switch to Image mode",
|
|
||||||
"download": "Download",
|
|
||||||
"decodedFileName": "Decoded file name",
|
|
||||||
"decodeBase64Placeholder": "Enter Base64 or data URI to decode...",
|
|
||||||
"decodedFileOutput": "Decoded File",
|
|
||||||
"decodedImageOutput": "Decoded Image",
|
|
||||||
"inferredMimeType": "Inferred MIME type",
|
|
||||||
"decodedSize": "Decoded size"
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
{
|
|
||||||
"appName": "Testing Tools",
|
|
||||||
"buttons": {
|
|
||||||
"save": "Save",
|
|
||||||
"cancel": "Cancel",
|
|
||||||
"confirm": "Confirm",
|
|
||||||
"copy": "Copy",
|
|
||||||
"clear": "Clear",
|
|
||||||
"refresh": "Refresh",
|
|
||||||
"toggleLanguage": "Switch Language",
|
|
||||||
"toggleTheme": "Toggle theme",
|
|
||||||
"themeMode": {
|
|
||||||
"light": "Switch to dark mode",
|
|
||||||
"dark": "Switch to system mode",
|
|
||||||
"system": "Switch to light mode"
|
|
||||||
},
|
|
||||||
"search": "Search tools...",
|
|
||||||
"back": "Back",
|
|
||||||
"clearSearch": "Clear search",
|
|
||||||
"recentSearch": "Recent search",
|
|
||||||
"noResults": "No tools found",
|
|
||||||
"openInTab": "Open in tab",
|
|
||||||
"settings": "Settings"
|
|
||||||
},
|
|
||||||
"messages": {
|
|
||||||
"copySuccess": "Copied to clipboard",
|
|
||||||
"copyError": "Copy failed",
|
|
||||||
"copyEmpty": "Nothing to copy"
|
|
||||||
},
|
|
||||||
"errorBoundary": {
|
|
||||||
"title": "Oops, something went wrong",
|
|
||||||
"description": "The app encountered an unexpected error. You can try refreshing the page or resetting the app.",
|
|
||||||
"refresh": "Refresh App",
|
|
||||||
"retry": "Retry"
|
|
||||||
},
|
|
||||||
"pageErrorBoundary": {
|
|
||||||
"title": "This feature has encountered an error",
|
|
||||||
"description": "An internal script error occurred while loading or rendering this page. You can try again or switch to another tool from the navigation menu."
|
|
||||||
},
|
|
||||||
"router": {
|
|
||||||
"notFound": "Page Not Found",
|
|
||||||
"notFoundDescription": "This feature is not available or has been removed in the current runtime ({{entryPointType}})."
|
|
||||||
},
|
|
||||||
"textInputArea": {
|
|
||||||
"clear": "Clear",
|
|
||||||
"copyContent": "Copy content",
|
|
||||||
"cleared": "Cleared",
|
|
||||||
"placeholder": "placeholder text"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
{
|
|
||||||
"dashboard": {
|
|
||||||
"title": "Dashboard"
|
|
||||||
},
|
|
||||||
"timestamp": {
|
|
||||||
"title": "Timestamp",
|
|
||||||
"description": "Unix millisecond conversion and formatting"
|
|
||||||
},
|
|
||||||
"storageCleaner": {
|
|
||||||
"title": "Storage Cleaner",
|
|
||||||
"description": "Clean cache, cookies, and local storage"
|
|
||||||
},
|
|
||||||
"qrCode": {
|
|
||||||
"title": "QR Code Tools",
|
|
||||||
"description": "Generate QR code for the selected URL"
|
|
||||||
},
|
|
||||||
"textStatistics": {
|
|
||||||
"title": "Text Statistics",
|
|
||||||
"description": "Real-time analysis of text characters, words, and bytes"
|
|
||||||
},
|
|
||||||
"jwt": {
|
|
||||||
"title": "JWT Parser",
|
|
||||||
"description": "JSON Web Token decoding and viewing"
|
|
||||||
},
|
|
||||||
"jsonDiff": {
|
|
||||||
"title": "JSON Tools",
|
|
||||||
"description": "Diff, format, YAML/TOML conversion, and minify"
|
|
||||||
},
|
|
||||||
"base64Converter": {
|
|
||||||
"title": "Base64 Converter",
|
|
||||||
"description": "Encode text, files, and images to Base64"
|
|
||||||
},
|
|
||||||
"markdownToHtml": {
|
|
||||||
"title": "Markdown to HTML",
|
|
||||||
"description": "Real-time Markdown conversion and HTML preview"
|
|
||||||
},
|
|
||||||
"htmlToMarkdown": {
|
|
||||||
"title": "HTML to Markdown",
|
|
||||||
"description": "Real-time HTML conversion and Markdown preview"
|
|
||||||
},
|
|
||||||
"rightClickRestorer": {
|
|
||||||
"title": "Right Click Restorer",
|
|
||||||
"description": "Detect and restore disabled browser right-click menus"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"pageTitle": "HTML to Markdown",
|
|
||||||
"pageSubtitle": "Convert HTML to Markdown in real-time",
|
|
||||||
"splitMode": "Split",
|
|
||||||
"previewMode": "Preview",
|
|
||||||
"markdownMode": "Markdown",
|
|
||||||
"clear": "Clear",
|
|
||||||
"inputLabel": "HTML Input",
|
|
||||||
"previewLabel": "Markdown Preview",
|
|
||||||
"markdownOutputLabel": "Markdown Output",
|
|
||||||
"inputPlaceholder": "Enter your HTML content here...",
|
|
||||||
"charCount": "{{count}} chars",
|
|
||||||
"copyMarkdown": "Copy Markdown",
|
|
||||||
"download": "Download",
|
|
||||||
"emptyHint": "Enter HTML content to see the conversion result"
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"pageTitle": "JSON Diff",
|
|
||||||
"pageSubtitle": "Compare differences between two JSON values",
|
|
||||||
"leftPlaceholder": "Enter the original JSON...",
|
|
||||||
"rightPlaceholder": "Enter the target JSON...",
|
|
||||||
"leftLabel": "Original JSON",
|
|
||||||
"rightLabel": "Target JSON",
|
|
||||||
"compareButton": "Compare",
|
|
||||||
"clearButton": "Clear",
|
|
||||||
"sideBySideMode": "Side by side",
|
|
||||||
"unifiedMode": "Unified",
|
|
||||||
"previousDiff": "Previous",
|
|
||||||
"nextDiff": "Next",
|
|
||||||
"noDiffs": "No differences",
|
|
||||||
"diffCount": "{{count}} differences",
|
|
||||||
"invalidJson": "Invalid JSON format",
|
|
||||||
"emptyHint": "Enter JSON on both sides and click Compare",
|
|
||||||
"fixErrorHint": "Fix the JSON syntax errors above to enable live comparison",
|
|
||||||
"added": "Added",
|
|
||||||
"removed": "Removed",
|
|
||||||
"modified": "Modified"
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
{
|
|
||||||
"formatTitle": "JSON Formatter",
|
|
||||||
"formatSubtitle": "Beautify and format JSON data",
|
|
||||||
"inputPlaceholder": "Enter JSON to format...",
|
|
||||||
"formatButton": "Format",
|
|
||||||
"clearButton": "Clear",
|
|
||||||
"sortKeys": "Sort Keys",
|
|
||||||
"sortKeysTooltip": "Sort JSON object keys in alphabetical order",
|
|
||||||
"indentSize": "Indent",
|
|
||||||
"indentSpaces": "{{count}} spaces",
|
|
||||||
"outputLabel": "Formatted Result",
|
|
||||||
"copySuccess": "Copied",
|
|
||||||
"copyFail": "Copy failed",
|
|
||||||
"noContent": "Nothing to copy",
|
|
||||||
"invalidJson": "Invalid JSON format",
|
|
||||||
"emptyHint": "Enter JSON and click Format",
|
|
||||||
"fixErrorHint": "Fix the JSON syntax errors above to enable live formatting",
|
|
||||||
"originalSize": "Original size",
|
|
||||||
"formattedSize": "Formatted size",
|
|
||||||
"diffMode": "Diff",
|
|
||||||
"formatMode": "Format",
|
|
||||||
"yamlMode": "YAML",
|
|
||||||
"tomlMode": "TOML",
|
|
||||||
"minifyMode": "Minify",
|
|
||||||
"yamlTitle": "JSON to YAML",
|
|
||||||
"yamlSubtitle": "Convert JSON data to YAML format",
|
|
||||||
"yamlModeInputPlaceholder": "Enter JSON to convert...",
|
|
||||||
"yamlModeOutputLabel": "YAML Result",
|
|
||||||
"yamlModeEmptyHint": "Enter JSON and click Convert",
|
|
||||||
"convertButton": "Convert",
|
|
||||||
"tomlTitle": "JSON to TOML",
|
|
||||||
"tomlSubtitle": "Convert JSON data to TOML format",
|
|
||||||
"tomlModeInputPlaceholder": "Enter JSON to convert...",
|
|
||||||
"tomlModeOutputLabel": "TOML Result",
|
|
||||||
"tomlModeEmptyHint": "Enter JSON and click Convert",
|
|
||||||
"minifyTitle": "JSON Minifier",
|
|
||||||
"minifySubtitle": "Compress JSON into a compact single-line format",
|
|
||||||
"minifyModeInputPlaceholder": "Enter JSON to minify...",
|
|
||||||
"minifyModeOutputLabel": "Minified Result",
|
|
||||||
"minifyModeEmptyHint": "Enter JSON and click Minify",
|
|
||||||
"minifyButton": "Minify"
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"pageTitle": "JWT Parser",
|
|
||||||
"pageSubtitle": "JSON Web Token decoding and viewing",
|
|
||||||
"placeholder": "Paste Encoded JWT here...",
|
|
||||||
"headerTitle": "HEADER: Algorithm & Token Type",
|
|
||||||
"payloadTitle": "PAYLOAD: Data",
|
|
||||||
"signatureTitle": "Signature",
|
|
||||||
"noSignature": "No Signature",
|
|
||||||
"invalidFormat": "Unable to parse",
|
|
||||||
"errors": {
|
|
||||||
"invalidBase64String": "Invalid base64url string",
|
|
||||||
"failedToDecode": "Failed to decode base64url: ",
|
|
||||||
"invalidFormat": "Invalid JWT format: expected 3 parts separated by .",
|
|
||||||
"parseHeaderFailed": "Failed to parse Header: ",
|
|
||||||
"parsePayloadFailed": "Failed to parse Payload: "
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"pageTitle": "Markdown to HTML",
|
|
||||||
"pageSubtitle": "Convert Markdown to HTML in real-time with preview",
|
|
||||||
"splitMode": "Split",
|
|
||||||
"previewMode": "Preview",
|
|
||||||
"htmlMode": "HTML",
|
|
||||||
"clear": "Clear",
|
|
||||||
"print": "Print",
|
|
||||||
"download": "Download",
|
|
||||||
"inputLabel": "Markdown Input",
|
|
||||||
"previewLabel": "Live Preview",
|
|
||||||
"htmlOutputLabel": "HTML Output",
|
|
||||||
"inputPlaceholder": "Enter your Markdown content here...",
|
|
||||||
"charCount": "{{count}} chars",
|
|
||||||
"copyHtml": "Copy HTML",
|
|
||||||
"downloadSuccess": "File downloaded successfully",
|
|
||||||
"printSuccess": "Print window opened"
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
{
|
|
||||||
"pageTitle": "QR Code Tools",
|
|
||||||
"pageSubtitle": "Generate and parse QR codes",
|
|
||||||
|
|
||||||
"generateMode": "Generate QR Code",
|
|
||||||
"parseMode": "Parse QR Code",
|
|
||||||
|
|
||||||
"urlToQr": "Text to QR Code",
|
|
||||||
"qrToUrl": "QR Code to Text",
|
|
||||||
|
|
||||||
"urlInputLabel": "Enter URL or Text",
|
|
||||||
"urlInputPlaceholder": "Enter URL or text content, QR code will be generated automatically",
|
|
||||||
"generateButton": "Generate QR Code",
|
|
||||||
"generating": "Generating...",
|
|
||||||
|
|
||||||
"qrCodeWillShow": "QR code will be shown here",
|
|
||||||
"downloadButton": "Download QR Code",
|
|
||||||
"copyQrButton": "Copy QR Code Image",
|
|
||||||
|
|
||||||
"qrCodeSuccess": "QR code generated successfully",
|
|
||||||
"qrCodeDownloadSuccess": "QR code downloaded successfully",
|
|
||||||
"qrCodeCopySuccess": "QR code copied to clipboard",
|
|
||||||
|
|
||||||
"selectImage": "Please select a QR code image",
|
|
||||||
"parseSuccess": "QR code parsed successfully",
|
|
||||||
"noQrDetected": "No QR code detected, please ensure the image is clear and contains a QR code",
|
|
||||||
"parseError": "Failed to parse QR code, please try again",
|
|
||||||
"generateError": "Failed to generate QR code, please check the input",
|
|
||||||
"copyError": "Copy failed, please try again",
|
|
||||||
|
|
||||||
"imagePasted": "Image pasted, parsing...",
|
|
||||||
"imagePasteError": "Failed to paste image, please try again",
|
|
||||||
"imageCleared": "Image cleared",
|
|
||||||
"clickToUpload": "Click, drag, or paste to upload QR code image",
|
|
||||||
"supportFormats": "Supports PNG, JPG, WEBP, Base64 formats",
|
|
||||||
|
|
||||||
"parseButton": "Parse QR Code",
|
|
||||||
"parsing": "Parsing...",
|
|
||||||
"resultLabel": "Parsing Result",
|
|
||||||
"copyTooltip": "Copy",
|
|
||||||
|
|
||||||
"enterUrlError": "Please enter URL or text",
|
|
||||||
"clickToChange": "Click to change image",
|
|
||||||
|
|
||||||
"pasteHint": "Supports Ctrl+V to paste images or Base64 strings",
|
|
||||||
"autoGenerateHint": "QR code will be generated automatically after input",
|
|
||||||
"autoParseHint": "QR code will be parsed automatically after upload"
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"title": "Right Click Restorer",
|
|
||||||
"description": "Detect and restore disabled browser right-click menus",
|
|
||||||
"loading": "Loading...",
|
|
||||||
"currentDomain": "Current Domain",
|
|
||||||
"statusLocked": "Locked",
|
|
||||||
"statusUnlocked": "Unlocked",
|
|
||||||
"unsupported": "Unsupported",
|
|
||||||
"unsupportedDesc": "The current page is a browser internal or extension page. Right-click unlock is not available. Please switch to a regular webpage.",
|
|
||||||
"unlockDesc": "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.",
|
|
||||||
"unlockBtn": "Unlock Right Click",
|
|
||||||
"alreadyUnlocked": "Right Click Unlocked"
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
{
|
|
||||||
"pageTitle": "Storage Cleaner",
|
|
||||||
"pageSubtitle": "Clear cache, cookies, and local storage",
|
|
||||||
"loading": "Loading...",
|
|
||||||
"initializing": "Reading site data...",
|
|
||||||
"occupied": "Occupied {{size}}",
|
|
||||||
"cleaning": "Cleaning...",
|
|
||||||
"cleanNow": "Clean Now",
|
|
||||||
"autoRefresh": "Auto refresh page after cleaning",
|
|
||||||
"selectAll": "Select all items",
|
|
||||||
"cleanSuccess": "Cleaning complete",
|
|
||||||
"cleanError": "Cleaning failed",
|
|
||||||
"noData": "No data",
|
|
||||||
"countUnit": "items",
|
|
||||||
"errorNoTab": "Unable to get current tab",
|
|
||||||
"errorRestricted": "Storage cleaning is not supported on this page",
|
|
||||||
"cleanSuccessReload": "Cleaning successful, reloading page...",
|
|
||||||
"errorStandardOnly": "Storage cleaning only works on standard web pages",
|
|
||||||
"confirmTitle": "Confirm Clear Data?",
|
|
||||||
"confirmDesc": "You are about to permanently delete the following selected storage items from the current page.",
|
|
||||||
"irreversible": "This action is irreversible",
|
|
||||||
"confirmAction": "Confirm Clear",
|
|
||||||
"cleanedSummary": "Cleaned {{items}}",
|
|
||||||
"noDataToClean": "No storage data found to clean",
|
|
||||||
"partialFailure": "Some items failed to clean",
|
|
||||||
"options": {
|
|
||||||
"localStorage": "Local Storage",
|
|
||||||
"sessionStorage": "Session Storage",
|
|
||||||
"indexedDB": "IndexedDB",
|
|
||||||
"cookies": "Cookies",
|
|
||||||
"cacheStorage": "Cache Storage",
|
|
||||||
"serviceWorkers": "Service Workers"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"pageTitle": "Text Statistics",
|
|
||||||
"pageSubtitle": "Real-time analysis of characters, words, lines, and byte size",
|
|
||||||
"placeholder": "Type or paste text here...",
|
|
||||||
"characters": "Characters",
|
|
||||||
"words": "Words",
|
|
||||||
"lines": "Lines",
|
|
||||||
"bytes": "Byte Size"
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
{
|
|
||||||
"pageTitle": "Timestamp Conversion",
|
|
||||||
"pageSubtitle": "Unix millisecond conversion and formatting",
|
|
||||||
"tsToDate": "Timestamp → Date",
|
|
||||||
"dateToTs": "Date → Timestamp",
|
|
||||||
"placeholderTs": "Enter timestamp...",
|
|
||||||
"placeholderDate": "YYYY-MM-DD HH:mm:ss",
|
|
||||||
"unitMs": "Millisecond (ms)",
|
|
||||||
"unitS": "Second (s)",
|
|
||||||
"convertButton": "Convert Now",
|
|
||||||
"currentTs": "Current Timestamp",
|
|
||||||
"useNowTooltip": "Use this value",
|
|
||||||
"copyTsTooltip": "Copy timestamp",
|
|
||||||
"usedSuccess": "Using current timestamp",
|
|
||||||
"resultLabel": "Conversion Result",
|
|
||||||
"copyResultTooltip": "Copy result",
|
|
||||||
"relativeTime": "Relative Time",
|
|
||||||
"iso8601": "ISO 8601",
|
|
||||||
"utcTime": "UTC Time",
|
|
||||||
"copyTooltip": "Copy",
|
|
||||||
"resultEmpty": "Enter a value and click convert",
|
|
||||||
"errors": {
|
|
||||||
"invalidNumber": "Invalid number",
|
|
||||||
"invalidTimestamp": "Invalid timestamp",
|
|
||||||
"invalidFormat": "Format error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
{
|
|
||||||
"pageTitle": "Base64 转换器",
|
|
||||||
"pageSubtitle": "文本、文件与图像的 Base64 编码与解码",
|
|
||||||
"textMode": "文本",
|
|
||||||
"fileMode": "文件",
|
|
||||||
"imageMode": "图像",
|
|
||||||
"encode": "编码",
|
|
||||||
"decode": "解码",
|
|
||||||
"clear": "清空",
|
|
||||||
"textInputPlaceholder": "输入需要编码为 Base64 的文本...",
|
|
||||||
"base64InputPlaceholder": "输入需要解码的 Base64 字符串...",
|
|
||||||
"base64Output": "Base64 编码结果",
|
|
||||||
"textOutput": "解码文本结果",
|
|
||||||
"copyRaw": "复制纯 Base64",
|
|
||||||
"copyDataUri": "复制 Data URI",
|
|
||||||
"clickOrDropToFile": "点击或拖拽文件到此处",
|
|
||||||
"clickOrDropToImage": "点击或拖拽图像到此处",
|
|
||||||
"clickOrDropToReplace": "点击或拖拽以替换文件",
|
|
||||||
"maxFileSize": "最大文件大小:{{max}}",
|
|
||||||
"supportedFormats": "支持 PNG、JPG、WEBP、GIF、BMP、SVG 等格式",
|
|
||||||
"fileSizeExceeded": "文件大小超出限制(最大 {{max}})",
|
|
||||||
"unsupportedImageType": "不支持的图像格式",
|
|
||||||
"conversionFailed": "转换失败",
|
|
||||||
"originalSize": "原始大小",
|
|
||||||
"encodedSize": "编码大小",
|
|
||||||
"invalidBase64": "Base64 字符串无效",
|
|
||||||
"binaryDataDetected": "输入似乎是二进制数据(如图片)。请切换到「图像」选项卡。",
|
|
||||||
"imageDataUriHint": "检测到图片的 data URI,请使用「图像」选项卡进行解码。",
|
|
||||||
"switchToImageMode": "切换到图像模式",
|
|
||||||
"download": "下载",
|
|
||||||
"decodedFileName": "解码后文件名",
|
|
||||||
"decodeBase64Placeholder": "输入需要解码的 Base64 或 data URI...",
|
|
||||||
"decodedFileOutput": "解码文件",
|
|
||||||
"decodedImageOutput": "解码图像",
|
|
||||||
"inferredMimeType": "推断的 MIME 类型",
|
|
||||||
"decodedSize": "解码大小"
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
{
|
|
||||||
"appName": "测试工具",
|
|
||||||
"buttons": {
|
|
||||||
"save": "保存",
|
|
||||||
"cancel": "取消",
|
|
||||||
"confirm": "确认",
|
|
||||||
"copy": "复制",
|
|
||||||
"clear": "清理",
|
|
||||||
"refresh": "刷新",
|
|
||||||
"toggleLanguage": "切换语言",
|
|
||||||
"toggleTheme": "切换主题",
|
|
||||||
"themeMode": {
|
|
||||||
"light": "切换到深色模式",
|
|
||||||
"dark": "切换到系统模式",
|
|
||||||
"system": "切换到浅色模式"
|
|
||||||
},
|
|
||||||
"search": "搜索工具...",
|
|
||||||
"back": "返回",
|
|
||||||
"clearSearch": "清除搜索",
|
|
||||||
"recentSearch": "最近搜索",
|
|
||||||
"noResults": "未找到相关工具",
|
|
||||||
"openInTab": "在标签页打开",
|
|
||||||
"settings": "设置"
|
|
||||||
},
|
|
||||||
"messages": {
|
|
||||||
"copySuccess": "已复制到剪贴板",
|
|
||||||
"copyError": "复制失败",
|
|
||||||
"copyEmpty": "无内容可复制"
|
|
||||||
},
|
|
||||||
"errorBoundary": {
|
|
||||||
"title": "糟糕,出了点问题",
|
|
||||||
"description": "应用遇到了一些意外错误。您可以尝试刷新页面或重置应用。",
|
|
||||||
"refresh": "刷新应用",
|
|
||||||
"retry": "重新尝试"
|
|
||||||
},
|
|
||||||
"pageErrorBoundary": {
|
|
||||||
"title": "该功能运行异常",
|
|
||||||
"description": "该页面在加载或渲染时遇到了内部脚本错误。您可以尝试重试,或者通过导航菜单切换到其他工具。"
|
|
||||||
},
|
|
||||||
"router": {
|
|
||||||
"notFound": "页面未找到",
|
|
||||||
"notFoundDescription": "该功能在当前运行环境({{entryPointType}})下不可用或已被移除。"
|
|
||||||
},
|
|
||||||
"textInputArea": {
|
|
||||||
"clear": "清空",
|
|
||||||
"copyContent": "复制内容",
|
|
||||||
"cleared": "已清空",
|
|
||||||
"placeholder": "请输入文本"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
{
|
|
||||||
"dashboard": {
|
|
||||||
"title": "仪表盘"
|
|
||||||
},
|
|
||||||
"timestamp": {
|
|
||||||
"title": "时间戳",
|
|
||||||
"description": "Unix 毫秒数转换与格式化"
|
|
||||||
},
|
|
||||||
"storageCleaner": {
|
|
||||||
"title": "存储清理",
|
|
||||||
"description": "清理缓存、Cookies 及本地存储"
|
|
||||||
},
|
|
||||||
"qrCode": {
|
|
||||||
"title": "二维码工具",
|
|
||||||
"description": "生成当前选中的 URL 的二维码"
|
|
||||||
},
|
|
||||||
"textStatistics": {
|
|
||||||
"title": "文本统计",
|
|
||||||
"description": "实时分析文本字符、单词及字节"
|
|
||||||
},
|
|
||||||
"jwt": {
|
|
||||||
"title": "JWT 解析",
|
|
||||||
"description": "JSON Web Token 解码与查看"
|
|
||||||
},
|
|
||||||
"jsonDiff": {
|
|
||||||
"title": "JSON 工具",
|
|
||||||
"description": "差异比较、格式化、YAML/TOML 转换及压缩"
|
|
||||||
},
|
|
||||||
"base64Converter": {
|
|
||||||
"title": "Base64 转换器",
|
|
||||||
"description": "文本、文件与图像的 Base64 编码转换"
|
|
||||||
},
|
|
||||||
"markdownToHtml": {
|
|
||||||
"title": "Markdown 转 HTML",
|
|
||||||
"description": "实时 Markdown 转换与 HTML 预览"
|
|
||||||
},
|
|
||||||
"htmlToMarkdown": {
|
|
||||||
"title": "HTML 转 Markdown",
|
|
||||||
"description": "实时 HTML 转换与 Markdown 预览"
|
|
||||||
},
|
|
||||||
"rightClickRestorer": {
|
|
||||||
"title": "右键恢复",
|
|
||||||
"description": "检测并恢复被网站禁用的浏览器右键菜单"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"pageTitle": "HTML 转 Markdown",
|
|
||||||
"pageSubtitle": "实时将 HTML 转换为 Markdown 格式",
|
|
||||||
"splitMode": "分屏",
|
|
||||||
"previewMode": "预览",
|
|
||||||
"markdownMode": "Markdown",
|
|
||||||
"clear": "清空",
|
|
||||||
"inputLabel": "HTML 输入",
|
|
||||||
"previewLabel": "Markdown 预览",
|
|
||||||
"markdownOutputLabel": "Markdown 输出",
|
|
||||||
"inputPlaceholder": "在此输入 HTML 内容...",
|
|
||||||
"charCount": "{{count}} 字符",
|
|
||||||
"copyMarkdown": "复制 Markdown",
|
|
||||||
"download": "下载",
|
|
||||||
"emptyHint": "输入 HTML 内容以查看转换结果"
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"pageTitle": "JSON 差异比较",
|
|
||||||
"pageSubtitle": "对比两个 JSON 数据的差异",
|
|
||||||
"leftPlaceholder": "输入原始 JSON...",
|
|
||||||
"rightPlaceholder": "输入目标 JSON...",
|
|
||||||
"leftLabel": "原始 JSON",
|
|
||||||
"rightLabel": "目标 JSON",
|
|
||||||
"compareButton": "比较",
|
|
||||||
"clearButton": "清空",
|
|
||||||
"sideBySideMode": "并排",
|
|
||||||
"unifiedMode": "统一",
|
|
||||||
"previousDiff": "上一个",
|
|
||||||
"nextDiff": "下一个",
|
|
||||||
"noDiffs": "无差异",
|
|
||||||
"diffCount": "{{count}} 处差异",
|
|
||||||
"invalidJson": "无效的 JSON 格式",
|
|
||||||
"emptyHint": "输入两侧 JSON 后点击比较",
|
|
||||||
"fixErrorHint": "请修正上方 JSON 的语法错误以开启实时流式比对",
|
|
||||||
"added": "新增",
|
|
||||||
"removed": "删除",
|
|
||||||
"modified": "修改"
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
{
|
|
||||||
"formatTitle": "JSON 格式化",
|
|
||||||
"formatSubtitle": "美化和格式化 JSON 数据",
|
|
||||||
"inputPlaceholder": "输入需要格式化的 JSON...",
|
|
||||||
"formatButton": "格式化",
|
|
||||||
"clearButton": "清空",
|
|
||||||
"sortKeys": "键名排序",
|
|
||||||
"sortKeysTooltip": "按字母顺序对 JSON 对象键进行排序",
|
|
||||||
"indentSize": "缩进",
|
|
||||||
"indentSpaces": "{{count}} 个空格",
|
|
||||||
"outputLabel": "格式化结果",
|
|
||||||
"copySuccess": "复制成功",
|
|
||||||
"copyFail": "复制失败",
|
|
||||||
"noContent": "无内容可复制",
|
|
||||||
"invalidJson": "无效的 JSON 格式",
|
|
||||||
"emptyHint": "输入 JSON 后点击格式化",
|
|
||||||
"fixErrorHint": "请修正上方 JSON 的语法错误以开启实时流式格式化",
|
|
||||||
"originalSize": "原始大小",
|
|
||||||
"formattedSize": "格式化后大小",
|
|
||||||
"diffMode": "差异比较",
|
|
||||||
"formatMode": "格式化",
|
|
||||||
"yamlMode": "YAML",
|
|
||||||
"tomlMode": "TOML",
|
|
||||||
"minifyMode": "压缩",
|
|
||||||
"yamlTitle": "JSON 转 YAML",
|
|
||||||
"yamlSubtitle": "将 JSON 数据转换为 YAML 格式",
|
|
||||||
"yamlModeInputPlaceholder": "输入需要转换的 JSON...",
|
|
||||||
"yamlModeOutputLabel": "YAML 结果",
|
|
||||||
"yamlModeEmptyHint": "输入 JSON 后点击转换",
|
|
||||||
"convertButton": "转换",
|
|
||||||
"tomlTitle": "JSON 转 TOML",
|
|
||||||
"tomlSubtitle": "将 JSON 数据转换为 TOML 格式",
|
|
||||||
"tomlModeInputPlaceholder": "输入需要转换的 JSON...",
|
|
||||||
"tomlModeOutputLabel": "TOML 结果",
|
|
||||||
"tomlModeEmptyHint": "输入 JSON 后点击转换",
|
|
||||||
"minifyTitle": "JSON 压缩",
|
|
||||||
"minifySubtitle": "将 JSON 压缩为紧凑的单行格式",
|
|
||||||
"minifyModeInputPlaceholder": "输入需要压缩的 JSON...",
|
|
||||||
"minifyModeOutputLabel": "压缩结果",
|
|
||||||
"minifyModeEmptyHint": "输入 JSON 后点击压缩",
|
|
||||||
"minifyButton": "压缩"
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"pageTitle": "JWT 解析",
|
|
||||||
"pageSubtitle": "JSON Web Token 解码与查看",
|
|
||||||
"placeholder": "在此粘贴 JWT 令牌 (Encoded JWT)...",
|
|
||||||
"headerTitle": "HEADER: 算法 & 令牌类型",
|
|
||||||
"payloadTitle": "PAYLOAD: 数据",
|
|
||||||
"signatureTitle": "签名",
|
|
||||||
"noSignature": "无签名",
|
|
||||||
"invalidFormat": "无法解析",
|
|
||||||
"errors": {
|
|
||||||
"invalidBase64String": "无效的 Base64URL 字符串",
|
|
||||||
"failedToDecode": "Base64URL 解码失败:",
|
|
||||||
"invalidFormat": "JWT 格式错误:必须包含三个由 . 分隔的部分",
|
|
||||||
"parseHeaderFailed": "解析 Header 失败:",
|
|
||||||
"parsePayloadFailed": "解析 Payload 失败:"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"pageTitle": "Markdown 转 HTML",
|
|
||||||
"pageSubtitle": "实时将 Markdown 转换为 HTML 并预览",
|
|
||||||
"splitMode": "分屏",
|
|
||||||
"previewMode": "预览",
|
|
||||||
"htmlMode": "HTML",
|
|
||||||
"clear": "清空",
|
|
||||||
"print": "打印",
|
|
||||||
"download": "下载",
|
|
||||||
"inputLabel": "Markdown 输入",
|
|
||||||
"previewLabel": "实时预览",
|
|
||||||
"htmlOutputLabel": "HTML 输出",
|
|
||||||
"inputPlaceholder": "在此输入 Markdown 内容...",
|
|
||||||
"charCount": "{{count}} 字符",
|
|
||||||
"copyHtml": "复制 HTML",
|
|
||||||
"downloadSuccess": "文件下载成功",
|
|
||||||
"printSuccess": "打印窗口已打开"
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
{
|
|
||||||
"pageTitle": "二维码工具",
|
|
||||||
"pageSubtitle": "生成和解析二维码",
|
|
||||||
|
|
||||||
"generateMode": "生成二维码",
|
|
||||||
"parseMode": "解析二维码",
|
|
||||||
|
|
||||||
"urlToQr": "文本转二维码",
|
|
||||||
"qrToUrl": "二维码转文本",
|
|
||||||
|
|
||||||
"urlInputLabel": "输入 URL 或文本",
|
|
||||||
"urlInputPlaceholder": "请输入 URL 或文本内容,将自动生成二维码",
|
|
||||||
"generateButton": "生成二维码",
|
|
||||||
"generating": "生成中...",
|
|
||||||
|
|
||||||
"qrCodeWillShow": "二维码将显示在这里",
|
|
||||||
"downloadButton": "下载二维码",
|
|
||||||
"copyQrButton": "复制二维码",
|
|
||||||
|
|
||||||
"qrCodeSuccess": "二维码生成成功",
|
|
||||||
"qrCodeDownloadSuccess": "二维码下载成功",
|
|
||||||
"qrCodeCopySuccess": "二维码已复制到剪贴板",
|
|
||||||
|
|
||||||
"selectImage": "请选择二维码图片",
|
|
||||||
"parseSuccess": "二维码解析成功",
|
|
||||||
"noQrDetected": "未检测到二维码,请确保图片清晰且包含二维码",
|
|
||||||
"parseError": "解析二维码失败,请重试",
|
|
||||||
"generateError": "生成二维码失败,请检查输入内容",
|
|
||||||
"copyError": "复制失败,请重试",
|
|
||||||
|
|
||||||
"imagePasted": "图片粘贴成功,正在解析...",
|
|
||||||
"imagePasteError": "粘贴图片失败,请重试",
|
|
||||||
"imageCleared": "图片已清除",
|
|
||||||
"clickToUpload": "点击、拖拽或粘贴上传二维码图片",
|
|
||||||
"supportFormats": "支持 PNG、JPG、WEBP、Base64 格式",
|
|
||||||
|
|
||||||
"parseButton": "解析二维码",
|
|
||||||
"parsing": "解析中...",
|
|
||||||
"resultLabel": "解析结果",
|
|
||||||
"copyTooltip": "复制",
|
|
||||||
|
|
||||||
"enterUrlError": "请输入 URL 或文本",
|
|
||||||
"clickToChange": "点击更换图片",
|
|
||||||
|
|
||||||
"pasteHint": "支持 Ctrl+V 粘贴图片或 Base64 字符串",
|
|
||||||
"autoGenerateHint": "输入内容后将自动生成二维码",
|
|
||||||
"autoParseHint": "上传图片后将自动解析二维码"
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"title": "右键恢复",
|
|
||||||
"description": "检测并恢复被网站禁用的浏览器右键菜单",
|
|
||||||
"loading": "正在加载...",
|
|
||||||
"currentDomain": "当前域名",
|
|
||||||
"statusLocked": "未解锁",
|
|
||||||
"statusUnlocked": "已解锁",
|
|
||||||
"unsupported": "不支持",
|
|
||||||
"unsupportedDesc": "当前页面为浏览器内部页面或扩展页面,无法解锁右键功能。请切换到普通网页后重试。",
|
|
||||||
"unlockDesc": "点击下方按钮,为当前网站临时解锁右键菜单。刷新页面后需要重新解锁。",
|
|
||||||
"unlockBtn": "解锁当前网站右键",
|
|
||||||
"alreadyUnlocked": "右键已解锁"
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
{
|
|
||||||
"pageTitle": "存储清理",
|
|
||||||
"pageSubtitle": "清理缓存、Cookies 及本地存储",
|
|
||||||
"loading": "加载中...",
|
|
||||||
"initializing": "正在读取站点数据...",
|
|
||||||
"occupied": "已占用 {{size}}",
|
|
||||||
"cleaning": "正在清理...",
|
|
||||||
"cleanNow": "立即清理",
|
|
||||||
"autoRefresh": "清理后自动刷新页面",
|
|
||||||
"selectAll": "全选所有项",
|
|
||||||
"cleanSuccess": "清理完成",
|
|
||||||
"cleanError": "清理失败",
|
|
||||||
"noData": "无数据",
|
|
||||||
"countUnit": "个",
|
|
||||||
"errorNoTab": "无法获取当前标签页",
|
|
||||||
"errorRestricted": "存储清理功能不支持此页面",
|
|
||||||
"cleanSuccessReload": "清理成功,即将刷新页面",
|
|
||||||
"errorStandardOnly": "存储清理功能仅适用于标准网页",
|
|
||||||
"confirmTitle": "确认清理数据?",
|
|
||||||
"confirmDesc": "您将永久删除当前页面的以下选定存储项。",
|
|
||||||
"irreversible": "此操作不可撤销",
|
|
||||||
"confirmAction": "确认清理",
|
|
||||||
"cleanedSummary": "清理了 {{items}}",
|
|
||||||
"noDataToClean": "该页面没有可清理的存储数据",
|
|
||||||
"partialFailure": "部分清理失败",
|
|
||||||
"options": {
|
|
||||||
"localStorage": "Local Storage",
|
|
||||||
"sessionStorage": "Session Storage",
|
|
||||||
"indexedDB": "IndexedDB",
|
|
||||||
"cookies": "Cookies",
|
|
||||||
"cacheStorage": "Cache Storage",
|
|
||||||
"serviceWorkers": "Service Workers"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"pageTitle": "文本统计",
|
|
||||||
"pageSubtitle": "实时分析文本的字符、单词、行数及字节大小",
|
|
||||||
"placeholder": "在此输入或粘贴文本...",
|
|
||||||
"characters": "字符数",
|
|
||||||
"words": "单词数",
|
|
||||||
"lines": "行数",
|
|
||||||
"bytes": "字节大小"
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
{
|
|
||||||
"pageTitle": "时间戳转换",
|
|
||||||
"pageSubtitle": "Unix 毫秒数转换与格式化",
|
|
||||||
"tsToDate": "时间戳 → 日期",
|
|
||||||
"dateToTs": "日期 → 时间戳",
|
|
||||||
"placeholderTs": "输入时间戳...",
|
|
||||||
"placeholderDate": "YYYY-MM-DD HH:mm:ss",
|
|
||||||
"unitMs": "毫秒 (ms)",
|
|
||||||
"unitS": "秒 (s)",
|
|
||||||
"convertButton": "立即转换",
|
|
||||||
"currentTs": "当前时间戳",
|
|
||||||
"useNowTooltip": "填充到下方",
|
|
||||||
"copyTsTooltip": "复制时间戳",
|
|
||||||
"usedSuccess": "已使用当前时间戳",
|
|
||||||
"resultLabel": "转换结果",
|
|
||||||
"copyResultTooltip": "复制结果",
|
|
||||||
"relativeTime": "相对时间",
|
|
||||||
"iso8601": "ISO 8601",
|
|
||||||
"utcTime": "UTC 时间",
|
|
||||||
"copyTooltip": "复制",
|
|
||||||
"resultEmpty": "请输入并点击转换",
|
|
||||||
"errors": {
|
|
||||||
"invalidNumber": "无效数字",
|
|
||||||
"invalidTimestamp": "无效时间戳",
|
|
||||||
"invalidFormat": "格式错误"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Generated
+53
-237
File diff suppressed because it is too large
Load Diff
@@ -36,15 +36,12 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dayjs": "^1.11.20",
|
"dayjs": "^1.11.20",
|
||||||
"i18next": "^26.2.0",
|
|
||||||
"i18next-browser-languagedetector": "^8.2.1",
|
|
||||||
"lucide-react": "^1.16.0",
|
"lucide-react": "^1.16.0",
|
||||||
"marked": "^18.0.4",
|
"marked": "^18.0.4",
|
||||||
"qr-scanner": "^1.4.2",
|
"qr-scanner": "^1.4.2",
|
||||||
"qrious": "^4.0.2",
|
"qrious": "^4.0.2",
|
||||||
"react": "^19.2.6",
|
"react": "^19.2.6",
|
||||||
"react-dom": "^19.2.6",
|
"react-dom": "^19.2.6",
|
||||||
"react-i18next": "^17.0.8",
|
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.6.0"
|
"tailwind-merge": "^3.6.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Image as ImageIcon, Trash2, Upload } from 'lucide-react';
|
import { Image as ImageIcon, Trash2, Upload } from 'lucide-react';
|
||||||
import TextInputArea from '@/components/TextInputArea';
|
import TextInputArea from '@/components/TextInputArea';
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
import CopyButton from '@/components/CopyButton';
|
import CopyButton from '@/components/CopyButton';
|
||||||
import DecodeResultPaper from '@/components/DecodeResultPaper';
|
import DecodeResultPaper from '@/components/DecodeResultPaper';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -19,7 +19,7 @@ interface Base64ConverterSectionProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Base64ConverterSection({ mode }: Base64ConverterSectionProps) {
|
export default function Base64ConverterSection({ mode }: Base64ConverterSectionProps) {
|
||||||
const { t } = useLazyTranslation('base64Converter');
|
const { t } = useI18n('base64Converter');
|
||||||
|
|
||||||
const [direction, setDirection] = useStorageState(
|
const [direction, setDirection] = useStorageState(
|
||||||
`base64Converter/${mode}Mode/direction`,
|
`base64Converter/${mode}Mode/direction`,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import TextInputArea from '@/components/TextInputArea';
|
import TextInputArea from '@/components/TextInputArea';
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
import CopyButton from '@/components/CopyButton';
|
import CopyButton from '@/components/CopyButton';
|
||||||
import { base64ToText, textToBase64 } from '@/utils/base64Converter';
|
import { base64ToText, textToBase64 } from '@/utils/base64Converter';
|
||||||
import SwitchButtonGroup from '@/components/SwitchButtonGroup';
|
import SwitchButtonGroup from '@/components/SwitchButtonGroup';
|
||||||
@@ -20,7 +20,7 @@ interface TextModeProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function TextMode({ onSwitchToImageMode }: TextModeProps = {}) {
|
export default function TextMode({ onSwitchToImageMode }: TextModeProps = {}) {
|
||||||
const { t } = useLazyTranslation('base64Converter');
|
const { t } = useI18n('base64Converter');
|
||||||
|
|
||||||
// 1. 纯净的核心源状态机:只保留输入源和转换方向
|
// 1. 纯净的核心源状态机:只保留输入源和转换方向
|
||||||
const [input, setInput] = useState('');
|
const [input, setInput] = useState('');
|
||||||
|
|||||||
@@ -19,18 +19,18 @@ describe('TextMode', () => {
|
|||||||
|
|
||||||
it('应该渲染编码/解码切换按钮', () => {
|
it('应该渲染编码/解码切换按钮', () => {
|
||||||
render(<TextMode />);
|
render(<TextMode />);
|
||||||
expect(screen.getByText('base64Converter:encode')).toBeInTheDocument();
|
expect(screen.getByText('编码')).toBeInTheDocument();
|
||||||
expect(screen.getByText('base64Converter:decode')).toBeInTheDocument();
|
expect(screen.getByText('解码')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('应该渲染输入框和转换按钮', () => {
|
it('应该渲染输入框和转换按钮', () => {
|
||||||
render(<TextMode />);
|
render(<TextMode />);
|
||||||
expect(screen.getByPlaceholderText('base64Converter:textInputPlaceholder')).toBeInTheDocument();
|
expect(screen.getByPlaceholderText('输入需要编码为 Base64 的文本...')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('应该将文本编码为 Base64', async () => {
|
it('应该将文本编码为 Base64', async () => {
|
||||||
render(<TextMode />);
|
render(<TextMode />);
|
||||||
const input = screen.getByPlaceholderText('base64Converter:textInputPlaceholder');
|
const input = screen.getByPlaceholderText('输入需要编码为 Base64 的文本...');
|
||||||
fireEvent.change(input, { target: { value: 'Hello' } });
|
fireEvent.change(input, { target: { value: 'Hello' } });
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@@ -38,7 +38,7 @@ describe('TextMode', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText('base64Converter:base64Output')).toBeInTheDocument();
|
expect(screen.getByText('Base64 编码结果')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
expect(screen.getByRole('button', { name: 'SGVsbG8=' })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: 'SGVsbG8=' })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@@ -47,9 +47,9 @@ describe('TextMode', () => {
|
|||||||
render(<TextMode />);
|
render(<TextMode />);
|
||||||
|
|
||||||
// 切换到解码模式
|
// 切换到解码模式
|
||||||
fireEvent.click(screen.getByText('base64Converter:decode'));
|
fireEvent.click(screen.getByText('解码'));
|
||||||
|
|
||||||
const input = screen.getByPlaceholderText('base64Converter:base64InputPlaceholder');
|
const input = screen.getByPlaceholderText('输入需要解码的 Base64 字符串...');
|
||||||
fireEvent.change(input, { target: { value: 'SGVsbG8=' } });
|
fireEvent.change(input, { target: { value: 'SGVsbG8=' } });
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@@ -57,7 +57,7 @@ describe('TextMode', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText('base64Converter:textOutput')).toBeInTheDocument();
|
expect(screen.getByText('解码文本结果')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
expect(screen.getByRole('button', { name: 'Hello' })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: 'Hello' })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@@ -66,9 +66,9 @@ describe('TextMode', () => {
|
|||||||
render(<TextMode />);
|
render(<TextMode />);
|
||||||
|
|
||||||
// 切换到解码模式
|
// 切换到解码模式
|
||||||
fireEvent.click(screen.getByText('base64Converter:decode'));
|
fireEvent.click(screen.getByText('解码'));
|
||||||
|
|
||||||
const input = screen.getByPlaceholderText('base64Converter:base64InputPlaceholder');
|
const input = screen.getByPlaceholderText('输入需要解码的 Base64 字符串...');
|
||||||
fireEvent.change(input, { target: { value: 'invalid!!!' } });
|
fireEvent.change(input, { target: { value: 'invalid!!!' } });
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@@ -76,7 +76,7 @@ describe('TextMode', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText('base64Converter:invalidBase64')).toBeInTheDocument();
|
expect(screen.getByText('Base64 字符串无效')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ describe('TextMode', () => {
|
|||||||
render(<TextMode />);
|
render(<TextMode />);
|
||||||
|
|
||||||
// 先编码
|
// 先编码
|
||||||
const input = screen.getByPlaceholderText('base64Converter:textInputPlaceholder');
|
const input = screen.getByPlaceholderText('输入需要编码为 Base64 的文本...');
|
||||||
fireEvent.change(input, { target: { value: 'Hello' } });
|
fireEvent.change(input, { target: { value: 'Hello' } });
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@@ -96,7 +96,7 @@ describe('TextMode', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 切换方向
|
// 切换方向
|
||||||
fireEvent.click(screen.getByText('base64Converter:decode'));
|
fireEvent.click(screen.getByText('解码'));
|
||||||
|
|
||||||
// 输出应该被清除
|
// 输出应该被清除
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -107,7 +107,7 @@ describe('TextMode', () => {
|
|||||||
it('点击清除按钮应该清空所有内容', async () => {
|
it('点击清除按钮应该清空所有内容', async () => {
|
||||||
render(<TextMode />);
|
render(<TextMode />);
|
||||||
|
|
||||||
const input = screen.getByPlaceholderText('base64Converter:textInputPlaceholder');
|
const input = screen.getByPlaceholderText('输入需要编码为 Base64 的文本...');
|
||||||
fireEvent.change(input, { target: { value: 'Hello' } });
|
fireEvent.change(input, { target: { value: 'Hello' } });
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@@ -129,48 +129,52 @@ describe('TextMode', () => {
|
|||||||
it('解码模式下粘贴图片 data URI 时应该显示切换图像模式的提示', () => {
|
it('解码模式下粘贴图片 data URI 时应该显示切换图像模式的提示', () => {
|
||||||
render(<TextMode />);
|
render(<TextMode />);
|
||||||
|
|
||||||
fireEvent.click(screen.getByText('base64Converter:decode'));
|
fireEvent.click(screen.getByText('解码'));
|
||||||
|
|
||||||
const input = screen.getByPlaceholderText('base64Converter:base64InputPlaceholder');
|
const input = screen.getByPlaceholderText('输入需要解码的 Base64 字符串...');
|
||||||
fireEvent.change(input, {
|
fireEvent.change(input, {
|
||||||
target: { value: 'data:image/png;base64,iVBORw0KGgo=' },
|
target: { value: 'data:image/png;base64,iVBORw0KGgo=' },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(screen.getByText('base64Converter:imageDataUriHint')).toBeInTheDocument();
|
expect(
|
||||||
expect(screen.getByText('base64Converter:switchToImageMode')).toBeInTheDocument();
|
screen.getByText('检测到图片的 data URI,请使用「图像」选项卡进行解码。'),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('切换到图像模式')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('粘贴非图片 data URI 时不应该显示图像模式提示', () => {
|
it('粘贴非图片 data URI 时不应该显示图像模式提示', () => {
|
||||||
render(<TextMode />);
|
render(<TextMode />);
|
||||||
|
|
||||||
fireEvent.click(screen.getByText('base64Converter:decode'));
|
fireEvent.click(screen.getByText('解码'));
|
||||||
|
|
||||||
const input = screen.getByPlaceholderText('base64Converter:base64InputPlaceholder');
|
const input = screen.getByPlaceholderText('输入需要解码的 Base64 字符串...');
|
||||||
fireEvent.change(input, { target: { value: 'SGVsbG8=' } });
|
fireEvent.change(input, { target: { value: 'SGVsbG8=' } });
|
||||||
|
|
||||||
expect(screen.queryByText('base64Converter:imageDataUriHint')).not.toBeInTheDocument();
|
expect(
|
||||||
|
screen.queryByText('检测到图片的 data URI,请使用「图像」选项卡进行解码。'),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('点击切换图像模式按钮应该调用 onSwitchToImageMode 回调', () => {
|
it('点击切换图像模式按钮应该调用 onSwitchToImageMode 回调', () => {
|
||||||
const onSwitch = vi.fn();
|
const onSwitch = vi.fn();
|
||||||
render(<TextMode onSwitchToImageMode={onSwitch} />);
|
render(<TextMode onSwitchToImageMode={onSwitch} />);
|
||||||
|
|
||||||
fireEvent.click(screen.getByText('base64Converter:decode'));
|
fireEvent.click(screen.getByText('解码'));
|
||||||
const input = screen.getByPlaceholderText('base64Converter:base64InputPlaceholder');
|
const input = screen.getByPlaceholderText('输入需要解码的 Base64 字符串...');
|
||||||
fireEvent.change(input, {
|
fireEvent.change(input, {
|
||||||
target: { value: 'data:image/png;base64,iVBORw0KGgo=' },
|
target: { value: 'data:image/png;base64,iVBORw0KGgo=' },
|
||||||
});
|
});
|
||||||
|
|
||||||
fireEvent.click(screen.getByText('base64Converter:switchToImageMode'));
|
fireEvent.click(screen.getByText('切换到图像模式'));
|
||||||
expect(onSwitch).toHaveBeenCalledTimes(1);
|
expect(onSwitch).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('解码模式下对二进制数据应该显示更清晰的错误', async () => {
|
it('解码模式下对二进制数据应该显示更清晰的错误', async () => {
|
||||||
render(<TextMode />);
|
render(<TextMode />);
|
||||||
|
|
||||||
fireEvent.click(screen.getByText('base64Converter:decode'));
|
fireEvent.click(screen.getByText('解码'));
|
||||||
|
|
||||||
const input = screen.getByPlaceholderText('base64Converter:base64InputPlaceholder');
|
const input = screen.getByPlaceholderText('输入需要解码的 Base64 字符串...');
|
||||||
fireEvent.change(input, { target: { value: 'iVBORw0KGgo=' } });
|
fireEvent.change(input, { target: { value: 'iVBORw0KGgo=' } });
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@@ -178,7 +182,9 @@ describe('TextMode', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText('base64Converter:binaryDataDetected')).toBeInTheDocument();
|
expect(
|
||||||
|
screen.getByText('输入似乎是二进制数据(如图片)。请切换到「图像」选项卡。'),
|
||||||
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,15 +2,6 @@ import { describe, expect, it, vi } from 'vitest';
|
|||||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||||
import Base64ConverterPage from '../index';
|
import Base64ConverterPage from '../index';
|
||||||
|
|
||||||
// Mock useLazyTranslation
|
|
||||||
vi.mock('@/utils/useLazyTranslation', () => ({
|
|
||||||
useLazyTranslation: () => ({
|
|
||||||
t: (key: string) => key,
|
|
||||||
i18n: { changeLanguage: vi.fn(), language: 'zh-CN' },
|
|
||||||
isLoaded: true,
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Mock getEntryPointType
|
// Mock getEntryPointType
|
||||||
vi.mock('@/config/features', async (importOriginal) => {
|
vi.mock('@/config/features', async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import('@/config/features')>();
|
const actual = await importOriginal<typeof import('@/config/features')>();
|
||||||
@@ -49,15 +40,15 @@ describe('Base64ConverterPage', () => {
|
|||||||
it('应该渲染模式切换按钮', async () => {
|
it('应该渲染模式切换按钮', async () => {
|
||||||
render(<Base64ConverterPage />);
|
render(<Base64ConverterPage />);
|
||||||
await waitForStorageInit();
|
await waitForStorageInit();
|
||||||
expect(screen.getByText('base64Converter:textMode')).toBeInTheDocument();
|
expect(screen.getByText('文本')).toBeInTheDocument();
|
||||||
expect(screen.getByText('base64Converter:fileMode')).toBeInTheDocument();
|
expect(screen.getByText('文件')).toBeInTheDocument();
|
||||||
expect(screen.getByText('base64Converter:imageMode')).toBeInTheDocument();
|
expect(screen.getByText('图像')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('切换到文件模式应该渲染 FileMode', async () => {
|
it('切换到文件模式应该渲染 FileMode', async () => {
|
||||||
render(<Base64ConverterPage />);
|
render(<Base64ConverterPage />);
|
||||||
await waitForStorageInit();
|
await waitForStorageInit();
|
||||||
fireEvent.click(screen.getByText('base64Converter:fileMode'));
|
fireEvent.click(screen.getByText('文件'));
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByTestId('file-mode')).toBeInTheDocument();
|
expect(screen.getByTestId('file-mode')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@@ -67,7 +58,7 @@ describe('Base64ConverterPage', () => {
|
|||||||
it('切换到图像模式应该渲染 ImageMode', async () => {
|
it('切换到图像模式应该渲染 ImageMode', async () => {
|
||||||
render(<Base64ConverterPage />);
|
render(<Base64ConverterPage />);
|
||||||
await waitForStorageInit();
|
await waitForStorageInit();
|
||||||
fireEvent.click(screen.getByText('base64Converter:imageMode'));
|
fireEvent.click(screen.getByText('图像'));
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByTestId('image-mode')).toBeInTheDocument();
|
expect(screen.getByTestId('image-mode')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
import { useStorageState } from '@/utils/useStorageState';
|
import { useStorageState } from '@/utils/useStorageState';
|
||||||
import type { Base64ConverterPageMode } from '@/types/storage';
|
import type { Base64ConverterPageMode } from '@/types/storage';
|
||||||
import TextMode from './TextMode';
|
import TextMode from './TextMode';
|
||||||
@@ -12,7 +12,7 @@ const isValidPageMode = (val: unknown): val is Base64ConverterPageMode =>
|
|||||||
type PageMode = Base64ConverterPageMode;
|
type PageMode = Base64ConverterPageMode;
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
const { t } = useLazyTranslation('base64Converter');
|
const { t } = useI18n('base64Converter');
|
||||||
const [pageMode, setPageMode] = useStorageState(
|
const [pageMode, setPageMode] = useStorageState(
|
||||||
'base64Converter/pageMode',
|
'base64Converter/pageMode',
|
||||||
'text',
|
'text',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
import type { FileToBase64Result } from '@/utils/base64Converter';
|
import type { FileToBase64Result } from '@/utils/base64Converter';
|
||||||
import {
|
import {
|
||||||
base64ToBlob,
|
base64ToBlob,
|
||||||
@@ -21,7 +21,7 @@ interface UseBase64ConverterProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useBase64Converter({ mode }: UseBase64ConverterProps) {
|
export function useBase64Converter({ mode }: UseBase64ConverterProps) {
|
||||||
const { t } = useLazyTranslation('base64Converter');
|
const { t } = useI18n('base64Converter');
|
||||||
|
|
||||||
const [result, setResult] = useState<FileToBase64Result | null>(null);
|
const [result, setResult] = useState<FileToBase64Result | null>(null);
|
||||||
const [info, setInfo] = useState<FileInfo | null>(null);
|
const [info, setInfo] = useState<FileInfo | null>(null);
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import ToolCard from '@/pages/Dashboard/ToolCard';
|
|||||||
import { getFeatureByKey } from '@/config/features';
|
import { getFeatureByKey } from '@/config/features';
|
||||||
import type { PageType } from '@/types/storage';
|
import type { PageType } from '@/types/storage';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
const { navigateTo, visiblePages, pageOrder } = useRouter();
|
const { navigateTo, visiblePages, pageOrder } = useRouter();
|
||||||
const { t } = useTranslation(['features']);
|
const { t } = useI18n(['features']);
|
||||||
|
|
||||||
const visibleSet = useMemo(() => new Set<string>(visiblePages), [visiblePages]);
|
const visibleSet = useMemo(() => new Set<string>(visiblePages), [visiblePages]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import { Download, Trash2 } from 'lucide-react';
|
import { Download, Trash2 } from 'lucide-react';
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
import CopyButton from '@/components/CopyButton';
|
import CopyButton from '@/components/CopyButton';
|
||||||
import SwitchButtonGroup from '@/components/SwitchButtonGroup';
|
import SwitchButtonGroup from '@/components/SwitchButtonGroup';
|
||||||
import { Button } from '@/components/ui/button'; // 💡 1. 全面回归规范:引入原生的 shadcn 原子 Button
|
import { Button } from '@/components/ui/button'; // 💡 1. 全面回归规范:引入原生的 shadcn 原子 Button
|
||||||
@@ -13,7 +13,7 @@ const isValidPreviewMode = (val: unknown): val is HtmlToMarkdownPreviewMode =>
|
|||||||
typeof val === 'string' && ['split', 'preview', 'markdown'].includes(val);
|
typeof val === 'string' && ['split', 'preview', 'markdown'].includes(val);
|
||||||
|
|
||||||
export default function HtmlToMarkdownPage() {
|
export default function HtmlToMarkdownPage() {
|
||||||
const { t } = useLazyTranslation('htmlToMarkdown');
|
const { t } = useI18n('htmlToMarkdown');
|
||||||
const [previewMode, setPreviewMode] = useStorageState(
|
const [previewMode, setPreviewMode] = useStorageState(
|
||||||
'htmlToMarkdown/previewMode',
|
'htmlToMarkdown/previewMode',
|
||||||
'split' as HtmlToMarkdownPreviewMode,
|
'split' as HtmlToMarkdownPreviewMode,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
import { cn } from '@/lib/utils'; // 1. 引入标准的 shadcn 工具函数
|
import { cn } from '@/lib/utils'; // 1. 引入标准的 shadcn 工具函数
|
||||||
|
|
||||||
export interface DiffNavigatorProps extends React.HTMLAttributes<HTMLDivElement> {
|
export interface DiffNavigatorProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
@@ -19,7 +19,7 @@ export default function DiffNavigator({
|
|||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: DiffNavigatorProps) {
|
}: DiffNavigatorProps) {
|
||||||
const { t } = useLazyTranslation('jsonDiff');
|
const { t } = useI18n('jsonDiff');
|
||||||
|
|
||||||
// 计算当前的边界禁用状态守卫
|
// 计算当前的边界禁用状态守卫
|
||||||
const isFirst = currentIndex <= 0;
|
const isFirst = currentIndex <= 0;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import JsonTree from './JsonTree';
|
import JsonTree from './JsonTree';
|
||||||
import type { DiffNode, DiffResult as DiffResultType, DiffType, ViewMode } from './types';
|
import type { DiffNode, DiffResult as DiffResultType, DiffType, ViewMode } from './types';
|
||||||
@@ -18,7 +18,7 @@ export default function DiffResult({
|
|||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: DiffResultProps) {
|
}: DiffResultProps) {
|
||||||
const { t } = useLazyTranslation('jsonDiff');
|
const { t } = useI18n('jsonDiff');
|
||||||
|
|
||||||
if (viewMode === 'sideBySide') {
|
if (viewMode === 'sideBySide') {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
import { formatByteSize } from '@/utils/textStatistics';
|
import { formatByteSize } from '@/utils/textStatistics';
|
||||||
import CopyButton from '@/components/CopyButton';
|
import CopyButton from '@/components/CopyButton';
|
||||||
import TextInputArea from '@/components/TextInputArea';
|
import TextInputArea from '@/components/TextInputArea';
|
||||||
@@ -25,7 +25,7 @@ export default function JsonConvertSection({
|
|||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: JsonConvertSectionProps) {
|
}: JsonConvertSectionProps) {
|
||||||
const { t } = useLazyTranslation('jsonFormat');
|
const { t } = useI18n('jsonFormat');
|
||||||
|
|
||||||
const [input, setInput] = useState('');
|
const [input, setInput] = useState('');
|
||||||
const [debouncedInput, setDebouncedInput] = useState('');
|
const [debouncedInput, setDebouncedInput] = useState('');
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
import {
|
import {
|
||||||
formatJson,
|
formatJson,
|
||||||
type JsonFormatOptions,
|
type JsonFormatOptions,
|
||||||
@@ -14,7 +14,7 @@ import { Checkbox } from '@/components/ui/checkbox';
|
|||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
|
|
||||||
export default function JsonFormatSection() {
|
export default function JsonFormatSection() {
|
||||||
const { t } = useLazyTranslation('jsonFormat');
|
const { t } = useI18n('jsonFormat');
|
||||||
|
|
||||||
const [input, setInput] = useState('');
|
const [input, setInput] = useState('');
|
||||||
const [debouncedInput, setDebouncedInput] = useState('');
|
const [debouncedInput, setDebouncedInput] = useState('');
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
import JsonDiffInput from './JsonDiffInput';
|
import JsonDiffInput from './JsonDiffInput';
|
||||||
import DiffResult from './DiffResult';
|
import DiffResult from './DiffResult';
|
||||||
import DiffNavigator from './DiffNavigator';
|
import DiffNavigator from './DiffNavigator';
|
||||||
@@ -37,7 +37,7 @@ const isValidPageMode = (val: unknown): val is JsonToolsPageMode =>
|
|||||||
type PageMode = JsonToolsPageMode;
|
type PageMode = JsonToolsPageMode;
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
const { t } = useLazyTranslation(['jsonDiff', 'jsonFormat']);
|
const { t } = useI18n(['jsonDiff', 'jsonFormat']);
|
||||||
const [pageMode, setPageMode] = useStorageState('jsonTools/pageMode', 'diff', isValidPageMode);
|
const [pageMode, setPageMode] = useStorageState('jsonTools/pageMode', 'diff', isValidPageMode);
|
||||||
|
|
||||||
// 1. 受控原始输入源
|
// 1. 受控原始输入源
|
||||||
|
|||||||
+4
-4
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
|||||||
import { parseJwt, stringifyJson } from '@/utils/jwt';
|
import { parseJwt, stringifyJson } from '@/utils/jwt';
|
||||||
import CopyButton from '@/components/CopyButton';
|
import CopyButton from '@/components/CopyButton';
|
||||||
import TextInputArea from '@/components/TextInputArea';
|
import TextInputArea from '@/components/TextInputArea';
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
import { useContextMenuData } from '@/utils/useContextMenuData';
|
import { useContextMenuData } from '@/utils/useContextMenuData';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ interface SectionProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Section = ({ title, content, colorClass, bgClass, borderClass }: SectionProps) => {
|
const Section = ({ title, content, colorClass, bgClass, borderClass }: SectionProps) => {
|
||||||
const { t } = useLazyTranslation('jwt');
|
const { t } = useI18n('jwt');
|
||||||
return (
|
return (
|
||||||
<div className={cn('p-4 rounded-xl border border-solid', bgClass, borderClass)}>
|
<div className={cn('p-4 rounded-xl border border-solid', bgClass, borderClass)}>
|
||||||
<div className="flex justify-between items-center mb-2 select-none">
|
<div className="flex justify-between items-center mb-2 select-none">
|
||||||
@@ -39,7 +39,7 @@ const Section = ({ title, content, colorClass, bgClass, borderClass }: SectionPr
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
const { t } = useLazyTranslation(['jwt', 'jsonFormat']);
|
const { t } = useI18n(['jwt', 'jsonFormat']);
|
||||||
const [jwtInput, setJwtInput] = useState('');
|
const [jwtInput, setJwtInput] = useState('');
|
||||||
|
|
||||||
// 2. 防抖中转管道:切断高频键盘敲击时的红色语法闪烁
|
// 2. 防抖中转管道:切断高频键盘敲击时的红色语法闪烁
|
||||||
@@ -74,7 +74,7 @@ export default function Index() {
|
|||||||
<TextInputArea
|
<TextInputArea
|
||||||
minRows={5}
|
minRows={5}
|
||||||
maxRows={10}
|
maxRows={10}
|
||||||
placeholder={t('jwt:placeholder')}
|
placeholder={t('jwt_placeholder')}
|
||||||
value={jwtInput}
|
value={jwtInput}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
const cleaned = val.replace(/^Bearer\s*/i, '').trim();
|
const cleaned = val.replace(/^Bearer\s*/i, '').trim();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { Download, Printer, Trash2 } from 'lucide-react';
|
import { Download, Printer, Trash2 } from 'lucide-react';
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
import CopyButton from '@/components/CopyButton';
|
import CopyButton from '@/components/CopyButton';
|
||||||
import SwitchButtonGroup from '@/components/SwitchButtonGroup';
|
import SwitchButtonGroup from '@/components/SwitchButtonGroup';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -82,7 +82,7 @@ const PREVIEW_STYLES = `
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export default function MarkdownToHtmlPage() {
|
export default function MarkdownToHtmlPage() {
|
||||||
const { t } = useLazyTranslation('markdownToHtml');
|
const { t } = useI18n('markdownToHtml');
|
||||||
const [previewMode, setPreviewMode] = useStorageState(
|
const [previewMode, setPreviewMode] = useStorageState(
|
||||||
'markdownToHtml/previewMode',
|
'markdownToHtml/previewMode',
|
||||||
'split' as MarkdownToHtmlPreviewMode,
|
'split' as MarkdownToHtmlPreviewMode,
|
||||||
|
|||||||
@@ -11,15 +11,6 @@ vi.mock('lucide-react', async (importOriginal) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mock useLazyTranslation
|
|
||||||
vi.mock('@/utils/useLazyTranslation', () => ({
|
|
||||||
useLazyTranslation: () => ({
|
|
||||||
t: (key: string) => key,
|
|
||||||
i18n: { changeLanguage: vi.fn(), language: 'zh-CN' },
|
|
||||||
isLoaded: true,
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Mock useSnackbar
|
// Mock useSnackbar
|
||||||
vi.mock('@/components/GlobalSnackbar', () => ({
|
vi.mock('@/components/GlobalSnackbar', () => ({
|
||||||
useSnackbar: () => ({
|
useSnackbar: () => ({
|
||||||
@@ -64,13 +55,13 @@ describe('QrCodePage', () => {
|
|||||||
|
|
||||||
it('应该渲染模式切换按钮', () => {
|
it('应该渲染模式切换按钮', () => {
|
||||||
render(<QrCodePage />);
|
render(<QrCodePage />);
|
||||||
expect(screen.getByText('qrCode:urlToQr')).toBeInTheDocument();
|
expect(screen.getByText('文本转二维码')).toBeInTheDocument();
|
||||||
expect(screen.getByText('qrCode:qrToUrl')).toBeInTheDocument();
|
expect(screen.getByText('二维码转文本')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('切换到解析模式应该渲染 ImageUploader', () => {
|
it('切换到解析模式应该渲染 ImageUploader', () => {
|
||||||
render(<QrCodePage />);
|
render(<QrCodePage />);
|
||||||
fireEvent.click(screen.getByText('qrCode:qrToUrl'));
|
fireEvent.click(screen.getByText('二维码转文本'));
|
||||||
expect(screen.getByTestId('image-uploader')).toBeInTheDocument();
|
expect(screen.getByTestId('image-uploader')).toBeInTheDocument();
|
||||||
expect(screen.queryByTestId('qr-code-preview')).not.toBeInTheDocument();
|
expect(screen.queryByTestId('qr-code-preview')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@@ -78,16 +69,16 @@ describe('QrCodePage', () => {
|
|||||||
it('切换回生成模式应该渲染 QrCodePreview', () => {
|
it('切换回生成模式应该渲染 QrCodePreview', () => {
|
||||||
render(<QrCodePage />);
|
render(<QrCodePage />);
|
||||||
// 先切换到解析模式
|
// 先切换到解析模式
|
||||||
fireEvent.click(screen.getByText('qrCode:qrToUrl'));
|
fireEvent.click(screen.getByText('二维码转文本'));
|
||||||
expect(screen.getByTestId('image-uploader')).toBeInTheDocument();
|
expect(screen.getByTestId('image-uploader')).toBeInTheDocument();
|
||||||
// 再切换回生成模式
|
// 再切换回生成模式
|
||||||
fireEvent.click(screen.getByText('qrCode:urlToQr'));
|
fireEvent.click(screen.getByText('文本转二维码'));
|
||||||
expect(screen.getByTestId('qr-code-preview')).toBeInTheDocument();
|
expect(screen.getByTestId('qr-code-preview')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('应该渲染输入区域的系统标签(对齐新版 Label 机制)', () => {
|
it('应该渲染输入区域的系统标签(对齐新版 Label 机制)', () => {
|
||||||
render(<QrCodePage />);
|
render(<QrCodePage />);
|
||||||
expect(screen.getByText('qrCode:urlInputLabel')).toBeInTheDocument();
|
expect(screen.getByText('输入 URL 或文本')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('应该渲染双翼响应式卡片网格布局', () => {
|
it('应该渲染双翼响应式卡片网格布局', () => {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import TextInputArea from '@/components/TextInputArea';
|
import TextInputArea from '@/components/TextInputArea';
|
||||||
import QrCodePreview from '@/components/QrCodePreview';
|
import QrCodePreview from '@/components/QrCodePreview';
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
import { useQrCodeContext } from '../contexts/QrCodeContext';
|
import { useQrCodeContext } from '../contexts/QrCodeContext';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
export default function GeneratePanel() {
|
export default function GeneratePanel() {
|
||||||
const { t } = useLazyTranslation('qrCode');
|
const { t } = useI18n('qrCode');
|
||||||
const { generatorState, setTextToEncode, downloadQrCode, copyQrCode } = useQrCodeContext();
|
const { generatorState, setTextToEncode, downloadQrCode, copyQrCode } = useQrCodeContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { useCallback, useEffect } from 'react';
|
|||||||
import TextInputArea from '@/components/TextInputArea';
|
import TextInputArea from '@/components/TextInputArea';
|
||||||
import ImageUploader from '@/components/ImageUploader';
|
import ImageUploader from '@/components/ImageUploader';
|
||||||
import { useSnackbar } from '@/components/GlobalSnackbar';
|
import { useSnackbar } from '@/components/GlobalSnackbar';
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
import { useQrCodeContext } from '../contexts/QrCodeContext';
|
import { useQrCodeContext } from '../contexts/QrCodeContext';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
export default function ParsePanel() {
|
export default function ParsePanel() {
|
||||||
const { t } = useLazyTranslation('qrCode');
|
const { t } = useI18n('qrCode');
|
||||||
const { showMessage } = useSnackbar();
|
const { showMessage } = useSnackbar();
|
||||||
const { parserState, setParserState, handleFileChange, handleClearFile } = useQrCodeContext();
|
const { parserState, setParserState, handleFileChange, handleClearFile } = useQrCodeContext();
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import QRious from 'qrious';
|
|||||||
import { useSnackbar } from '@/components/GlobalSnackbar';
|
import { useSnackbar } from '@/components/GlobalSnackbar';
|
||||||
import { parseQrCodeFromFile } from '@/utils/qrCodeParser';
|
import { parseQrCodeFromFile } from '@/utils/qrCodeParser';
|
||||||
import { useContextMenuData } from '@/utils/useContextMenuData';
|
import { useContextMenuData } from '@/utils/useContextMenuData';
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
import { useDebounce } from '@/utils/useDebounce';
|
import { useDebounce } from '@/utils/useDebounce';
|
||||||
import type { QrCodeContextValue } from '../contexts/QrCodeContext';
|
import type { QrCodeContextValue } from '../contexts/QrCodeContext';
|
||||||
import type { QrCodeGeneratorState, QrCodeMode, QrCodeParserState } from '../types';
|
import type { QrCodeGeneratorState, QrCodeMode, QrCodeParserState } from '../types';
|
||||||
|
|
||||||
export function useQrCode(): QrCodeContextValue {
|
export function useQrCode(): QrCodeContextValue {
|
||||||
const { t } = useLazyTranslation('qrCode');
|
const { t } = useI18n('qrCode');
|
||||||
const { showMessage } = useSnackbar();
|
const { showMessage } = useSnackbar();
|
||||||
|
|
||||||
// 核心路由视图模式
|
// 核心路由视图模式
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import SwitchButtonGroup from '@/components/SwitchButtonGroup';
|
import SwitchButtonGroup from '@/components/SwitchButtonGroup';
|
||||||
|
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
import { QrCodeContext } from './contexts/QrCodeContext';
|
import { QrCodeContext } from './contexts/QrCodeContext';
|
||||||
import { useQrCode } from './hooks/useQrCode';
|
import { useQrCode } from './hooks/useQrCode';
|
||||||
import GeneratePanel from './components/GeneratePanel';
|
import GeneratePanel from './components/GeneratePanel';
|
||||||
@@ -8,7 +8,7 @@ import ParsePanel from './components/ParsePanel';
|
|||||||
import type { QrCodeMode } from './types';
|
import type { QrCodeMode } from './types';
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
const { t } = useLazyTranslation('qrCode');
|
const { t } = useI18n('qrCode');
|
||||||
const qrCode = useQrCode();
|
const qrCode = useQrCode();
|
||||||
|
|
||||||
// 模式选项驱动骨架
|
// 模式选项驱动骨架
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ describe('RightClickRestorerPage', () => {
|
|||||||
|
|
||||||
it('should render locked status', () => {
|
it('should render locked status', () => {
|
||||||
render(<RightClickRestorerPage />);
|
render(<RightClickRestorerPage />);
|
||||||
expect(screen.getByText(/statusLocked/)).toBeInTheDocument();
|
expect(screen.getByText(/未解锁/)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call unlock when button clicked', () => {
|
it('should call unlock when button clicked', () => {
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import { Label } from '@/components/ui/label';
|
|||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Shield, ShieldCheck, MousePointerClick, AlertTriangle } from 'lucide-react';
|
import { Shield, ShieldCheck, MousePointerClick, AlertTriangle } from 'lucide-react';
|
||||||
import { useRightClickRestorer } from './useRightClickRestorer';
|
import { useRightClickRestorer } from './useRightClickRestorer';
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
|
|
||||||
export default function RightClickRestorerPage() {
|
export default function RightClickRestorerPage() {
|
||||||
const { t } = useLazyTranslation('rightClickRestorer');
|
const { t } = useI18n('rightClickRestorer');
|
||||||
const { domain, isLoading, isUnlocked, isUnsupported, unlock } = useRightClickRestorer();
|
const { domain, isLoading, isUnlocked, isUnsupported, unlock } = useRightClickRestorer();
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
@@ -43,7 +43,7 @@ export default function AutoRefreshToggle({
|
|||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: AutoRefreshToggleProps) {
|
}: AutoRefreshToggleProps) {
|
||||||
const { t } = useLazyTranslation('storageCleaner');
|
const { t } = useI18n('storageCleaner');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import { CheckCircle, XCircle } from 'lucide-react';
|
import { CheckCircle, XCircle } from 'lucide-react';
|
||||||
import type { CleaningResult as CleaningResultType } from '@/types/storage';
|
import type { CleaningResult as CleaningResultType } from '@/types/storage';
|
||||||
import { formatCleaningResult } from '@/utils/storageCleaner';
|
import { formatCleaningResult } from '@/utils/storageCleaner';
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
import { cn } from '@/lib/utils'; // 1. 引入 shadcn 核心类名合并工具
|
import { cn } from '@/lib/utils'; // 1. 引入 shadcn 核心类名合并工具
|
||||||
|
|
||||||
interface CleaningResultProps extends React.HTMLAttributes<HTMLDivElement> {
|
interface CleaningResultProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
@@ -10,7 +10,7 @@ interface CleaningResultProps extends React.HTMLAttributes<HTMLDivElement> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function CleaningResult({ result, className, ...props }: CleaningResultProps) {
|
export default function CleaningResult({ result, className, ...props }: CleaningResultProps) {
|
||||||
const { t } = useLazyTranslation('storageCleaner');
|
const { t } = useI18n('storageCleaner');
|
||||||
|
|
||||||
if (!result) return null;
|
if (!result) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { AlertCircle } from 'lucide-react';
|
import { AlertCircle } from 'lucide-react';
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
interface ErrorDisplayProps extends React.HTMLAttributes<HTMLDivElement> {
|
interface ErrorDisplayProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
@@ -7,7 +7,7 @@ interface ErrorDisplayProps extends React.HTMLAttributes<HTMLDivElement> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ErrorDisplay({ error, className, ...props }: ErrorDisplayProps) {
|
export default function ErrorDisplay({ error, className, ...props }: ErrorDisplayProps) {
|
||||||
const { t } = useLazyTranslation('storageCleaner');
|
const { t } = useI18n('storageCleaner');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// 1. 精简层级:单层外壳直接搞定居中、响应式高度与外部类名扩展
|
// 1. 精简层级:单层外壳直接搞定居中、响应式高度与外部类名扩展
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { formatSize } from '@/utils/storageCleaner';
|
import { formatSize } from '@/utils/storageCleaner';
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
// 引入官方的 Checkbox 原子组件
|
// 引入官方的 Checkbox 原子组件
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
@@ -22,7 +22,7 @@ export default function OptionItem({
|
|||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: OptionItemProps) {
|
}: OptionItemProps) {
|
||||||
const { t } = useLazyTranslation('storageCleaner');
|
const { t } = useI18n('storageCleaner');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { AlertTriangle } from 'lucide-react';
|
import { AlertTriangle } from 'lucide-react';
|
||||||
import type { StorageCleanerOptions } from '@/types/storage';
|
import type { StorageCleanerOptions } from '@/types/storage';
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
export interface StorageCleanerConfirmProps {
|
export interface StorageCleanerConfirmProps {
|
||||||
@@ -26,7 +26,7 @@ export function StorageCleanerConfirm({
|
|||||||
onConfirm,
|
onConfirm,
|
||||||
options,
|
options,
|
||||||
}: StorageCleanerConfirmProps) {
|
}: StorageCleanerConfirmProps) {
|
||||||
const { t } = useLazyTranslation('storageCleaner');
|
const { t } = useI18n('storageCleaner');
|
||||||
|
|
||||||
const selectedOptions = Object.entries(options)
|
const selectedOptions = Object.entries(options)
|
||||||
.filter(([_, value]) => value)
|
.filter(([_, value]) => value)
|
||||||
@@ -99,7 +99,7 @@ export function StorageCleanerConfirm({
|
|||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="w-full text-xs font-semibold shadow-sm h-9 text-muted-foreground hover:text-foreground"
|
className="w-full text-xs font-semibold shadow-sm h-9 text-muted-foreground hover:text-foreground"
|
||||||
>
|
>
|
||||||
{t('common:buttons.cancel')}
|
{t('common_buttons_cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { StorageCleanerOptions } from '@/types/storage';
|
import type { StorageCleanerOptions } from '@/types/storage';
|
||||||
import OptionItem from './OptionItem';
|
import OptionItem from './OptionItem';
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
// 1. 引入官方标准的 Checkbox 原子组件
|
// 1. 引入官方标准的 Checkbox 原子组件
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
@@ -26,7 +26,7 @@ export default function StorageOptionsGrid({
|
|||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: StorageOptionsGridProps) {
|
}: StorageOptionsGridProps) {
|
||||||
const { t } = useLazyTranslation('storageCleaner');
|
const { t } = useI18n('storageCleaner');
|
||||||
|
|
||||||
const optionKeys: { key: keyof StorageCleanerOptions; isCount?: boolean }[] = [
|
const optionKeys: { key: keyof StorageCleanerOptions; isCount?: boolean }[] = [
|
||||||
{ key: 'localStorage' },
|
{ key: 'localStorage' },
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import StorageOptionsGrid from './StorageOptionsGrid';
|
|||||||
import AutoRefreshToggle from './AutoRefreshToggle';
|
import AutoRefreshToggle from './AutoRefreshToggle';
|
||||||
import ErrorDisplay from './ErrorDisplay';
|
import ErrorDisplay from './ErrorDisplay';
|
||||||
import CleaningResult from './CleaningResult';
|
import CleaningResult from './CleaningResult';
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
const { t } = useLazyTranslation('storageCleaner');
|
const { t } = useI18n('storageCleaner');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
error,
|
error,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
isRestrictedUrl,
|
isRestrictedUrl,
|
||||||
} from '@/utils/storageCleaner';
|
} from '@/utils/storageCleaner';
|
||||||
import { MessageAction, sendMessage } from '@/utils/messages';
|
import { MessageAction, sendMessage } from '@/utils/messages';
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
import { toast } from 'sonner'; // 1. 直接引用 shadcn 推荐的 Sonner 单例通知,踢出回调依赖
|
import { toast } from 'sonner'; // 1. 直接引用 shadcn 推荐的 Sonner 单例通知,踢出回调依赖
|
||||||
|
|
||||||
const DEFAULT_OPTIONS: StorageCleanerOptions = {
|
const DEFAULT_OPTIONS: StorageCleanerOptions = {
|
||||||
@@ -56,7 +56,7 @@ export interface UseStorageCleanerReturn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useStorageCleaner(): UseStorageCleanerReturn {
|
export function useStorageCleaner(): UseStorageCleanerReturn {
|
||||||
const { t } = useLazyTranslation(['storageCleaner', 'common']);
|
const { t } = useI18n(['storageCleaner', 'common']);
|
||||||
const [domain, setDomain] = useState<string>('');
|
const [domain, setDomain] = useState<string>('');
|
||||||
const [error, setError] = useState<string>('');
|
const [error, setError] = useState<string>('');
|
||||||
const [isInitializing, setIsInitializing] = useState<boolean>(true);
|
const [isInitializing, setIsInitializing] = useState<boolean>(true);
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import TextInputArea from '@/components/TextInputArea';
|
import TextInputArea from '@/components/TextInputArea';
|
||||||
import { formatByteSize, getTextStats } from '@/utils/textStatistics';
|
import { formatByteSize, getTextStats } from '@/utils/textStatistics';
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
import { useContextMenuData } from '@/utils/useContextMenuData';
|
import { useContextMenuData } from '@/utils/useContextMenuData';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
const { t } = useLazyTranslation('textStatistics');
|
const { t } = useI18n('textStatistics');
|
||||||
const [text, setText] = useState('');
|
const [text, setText] = useState('');
|
||||||
|
|
||||||
const handleContextMenuData = useCallback((payload: string) => {
|
const handleContextMenuData = useCallback((payload: string) => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Clock } from 'lucide-react';
|
|||||||
import CopyButton from '@/components/CopyButton';
|
import CopyButton from '@/components/CopyButton';
|
||||||
import { useSnackbar } from '@/components/GlobalSnackbar';
|
import { useSnackbar } from '@/components/GlobalSnackbar';
|
||||||
import type { UnitType } from './constants';
|
import type { UnitType } from './constants';
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
import { cn } from '@/lib/utils'; // 引入标准的 shadcn 工具函数
|
import { cn } from '@/lib/utils'; // 引入标准的 shadcn 工具函数
|
||||||
|
|
||||||
interface LiveClockProps extends React.HTMLAttributes<HTMLDivElement> {
|
interface LiveClockProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
@@ -12,7 +12,7 @@ interface LiveClockProps extends React.HTMLAttributes<HTMLDivElement> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const LiveClock = React.memo(({ unit, onUseNow, className, ...props }: LiveClockProps) => {
|
const LiveClock = React.memo(({ unit, onUseNow, className, ...props }: LiveClockProps) => {
|
||||||
const { t } = useLazyTranslation('timestamp');
|
const { t } = useI18n('timestamp');
|
||||||
const { showMessage } = useSnackbar();
|
const { showMessage } = useSnackbar();
|
||||||
const onUseNowRef = useRef(onUseNow);
|
const onUseNowRef = useRef(onUseNow);
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import dayjs from '@/utils/dayjs';
|
|||||||
import CopyButton from '@/components/CopyButton';
|
import CopyButton from '@/components/CopyButton';
|
||||||
import type { UnitType } from './constants';
|
import type { UnitType } from './constants';
|
||||||
import { DATE_FORMAT } from './constants';
|
import { DATE_FORMAT } from './constants';
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
import { cn } from '@/lib/utils'; // shadcn 核心类名合并工具
|
import { cn } from '@/lib/utils'; // shadcn 核心类名合并工具
|
||||||
|
|
||||||
interface ResultViewProps extends React.HTMLAttributes<HTMLDivElement> {
|
interface ResultViewProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
@@ -25,7 +25,7 @@ const ResultView = React.memo(
|
|||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: ResultViewProps) => {
|
}: ResultViewProps) => {
|
||||||
const { t } = useLazyTranslation('timestamp');
|
const { t } = useI18n('timestamp');
|
||||||
|
|
||||||
// 严谨计算时间衍生的附加时区/相对时间状态
|
// 严谨计算时间衍生的附加时区/相对时间状态
|
||||||
const extraInfo = useMemo(() => {
|
const extraInfo = useMemo(() => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { ZONES } from './constants';
|
|||||||
import LiveClock from './LiveClock';
|
import LiveClock from './LiveClock';
|
||||||
import ResultView from './ResultView';
|
import ResultView from './ResultView';
|
||||||
import { useTimestampConverter } from './useTimestampConverter';
|
import { useTimestampConverter } from './useTimestampConverter';
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
// 1. 引入标准的 shadcn/ui 原子表单组件(代替原生的原生 Input 和 Select)
|
// 1. 引入标准的 shadcn/ui 原子表单组件(代替原生的原生 Input 和 Select)
|
||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
const { t } = useLazyTranslation('timestamp');
|
const { t } = useI18n('timestamp');
|
||||||
|
|
||||||
// 2. 完美对接全新重构后的统一单源响应式 Hook
|
// 2. 完美对接全新重构后的统一单源响应式 Hook
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useCallback, useMemo, useState } from 'react';
|
|||||||
import dayjs from '@/utils/dayjs';
|
import dayjs from '@/utils/dayjs';
|
||||||
import type { UnitType, ZoneType } from './constants';
|
import type { UnitType, ZoneType } from './constants';
|
||||||
import { DATE_FORMAT } from './constants';
|
import { DATE_FORMAT } from './constants';
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
import { useContextMenuData } from '@/utils/useContextMenuData';
|
import { useContextMenuData } from '@/utils/useContextMenuData';
|
||||||
|
|
||||||
export interface UseTimestampConverterReturn {
|
export interface UseTimestampConverterReturn {
|
||||||
@@ -26,7 +26,7 @@ function isTimestampLike(input: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useTimestampConverter(): UseTimestampConverterReturn {
|
export function useTimestampConverter(): UseTimestampConverterReturn {
|
||||||
const { t } = useLazyTranslation('timestamp');
|
const { t } = useI18n('timestamp');
|
||||||
const [mode, setMode] = useState<'ts2dt' | 'dt2ts'>('ts2dt');
|
const [mode, setMode] = useState<'ts2dt' | 'dt2ts'>('ts2dt');
|
||||||
const [unit, setUnit] = useState<UnitType>('ms');
|
const [unit, setUnit] = useState<UnitType>('ms');
|
||||||
const [zone, setZone] = useState<ZoneType>('Asia/Shanghai');
|
const [zone, setZone] = useState<ZoneType>('Asia/Shanghai');
|
||||||
|
|||||||
@@ -0,0 +1,982 @@
|
|||||||
|
{
|
||||||
|
"buttons_copy": {
|
||||||
|
"message": "复制",
|
||||||
|
"description": "Translation key: buttons_copy"
|
||||||
|
},
|
||||||
|
"buttons_clear": {
|
||||||
|
"message": "清理",
|
||||||
|
"description": "Translation key: buttons_clear"
|
||||||
|
},
|
||||||
|
"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_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_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_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_leftPlaceholder": {
|
||||||
|
"message": "输入原始 JSON...",
|
||||||
|
"description": "Translation key: jsonDiff_leftPlaceholder"
|
||||||
|
},
|
||||||
|
"jsonDiff_rightPlaceholder": {
|
||||||
|
"message": "输入目标 JSON...",
|
||||||
|
"description": "Translation key: jsonDiff_rightPlaceholder"
|
||||||
|
},
|
||||||
|
"jsonDiff_leftLabel": {
|
||||||
|
"message": "原始 JSON",
|
||||||
|
"description": "Translation key: jsonDiff_leftLabel"
|
||||||
|
},
|
||||||
|
"jsonDiff_rightLabel": {
|
||||||
|
"message": "目标 JSON",
|
||||||
|
"description": "Translation key: jsonDiff_rightLabel"
|
||||||
|
},
|
||||||
|
"jsonDiff_sideBySideMode": {
|
||||||
|
"message": "并排",
|
||||||
|
"description": "Translation key: jsonDiff_sideBySideMode"
|
||||||
|
},
|
||||||
|
"jsonDiff_unifiedMode": {
|
||||||
|
"message": "统一",
|
||||||
|
"description": "Translation key: jsonDiff_unifiedMode"
|
||||||
|
},
|
||||||
|
"jsonDiff_previousDiff": {
|
||||||
|
"message": "上一个",
|
||||||
|
"description": "Translation key: jsonDiff_previousDiff"
|
||||||
|
},
|
||||||
|
"jsonDiff_nextDiff": {
|
||||||
|
"message": "下一个",
|
||||||
|
"description": "Translation key: jsonDiff_nextDiff"
|
||||||
|
},
|
||||||
|
"jsonDiff_noDiffs": {
|
||||||
|
"message": "无差异",
|
||||||
|
"description": "Translation key: jsonDiff_noDiffs"
|
||||||
|
},
|
||||||
|
"jsonDiff_invalidJson": {
|
||||||
|
"message": "无效的 JSON 格式",
|
||||||
|
"description": "Translation key: jsonDiff_invalidJson"
|
||||||
|
},
|
||||||
|
"jsonDiff_emptyHint": {
|
||||||
|
"message": "输入两侧 JSON 后点击比较",
|
||||||
|
"description": "Translation key: jsonDiff_emptyHint"
|
||||||
|
},
|
||||||
|
"jsonDiff_fixErrorHint": {
|
||||||
|
"message": "请修正上方 JSON 的语法错误以开启实时流式比对",
|
||||||
|
"description": "Translation key: jsonDiff_fixErrorHint"
|
||||||
|
},
|
||||||
|
"jsonDiff_added": {
|
||||||
|
"message": "新增",
|
||||||
|
"description": "Translation key: jsonDiff_added"
|
||||||
|
},
|
||||||
|
"jsonDiff_removed": {
|
||||||
|
"message": "删除",
|
||||||
|
"description": "Translation key: jsonDiff_removed"
|
||||||
|
},
|
||||||
|
"jsonDiff_modified": {
|
||||||
|
"message": "修改",
|
||||||
|
"description": "Translation key: jsonDiff_modified"
|
||||||
|
},
|
||||||
|
"jsonFormat_inputPlaceholder": {
|
||||||
|
"message": "输入需要格式化的 JSON...",
|
||||||
|
"description": "Translation key: jsonFormat_inputPlaceholder"
|
||||||
|
},
|
||||||
|
"jsonFormat_sortKeys": {
|
||||||
|
"message": "键名排序",
|
||||||
|
"description": "Translation key: jsonFormat_sortKeys"
|
||||||
|
},
|
||||||
|
"jsonFormat_indentSize": {
|
||||||
|
"message": "缩进",
|
||||||
|
"description": "Translation key: jsonFormat_indentSize"
|
||||||
|
},
|
||||||
|
"jsonFormat_outputLabel": {
|
||||||
|
"message": "格式化结果",
|
||||||
|
"description": "Translation key: jsonFormat_outputLabel"
|
||||||
|
},
|
||||||
|
"jsonFormat_invalidJson": {
|
||||||
|
"message": "无效的 JSON 格式",
|
||||||
|
"description": "Translation key: jsonFormat_invalidJson"
|
||||||
|
},
|
||||||
|
"jsonFormat_emptyHint": {
|
||||||
|
"message": "输入 JSON 后点击格式化",
|
||||||
|
"description": "Translation key: jsonFormat_emptyHint"
|
||||||
|
},
|
||||||
|
"jsonFormat_fixErrorHint": {
|
||||||
|
"message": "请修正上方 JSON 的语法错误以开启实时流式格式化",
|
||||||
|
"description": "Translation key: jsonFormat_fixErrorHint"
|
||||||
|
},
|
||||||
|
"jsonFormat_originalSize": {
|
||||||
|
"message": "原始大小",
|
||||||
|
"description": "Translation key: jsonFormat_originalSize"
|
||||||
|
},
|
||||||
|
"jsonFormat_formattedSize": {
|
||||||
|
"message": "格式化后大小",
|
||||||
|
"description": "Translation key: jsonFormat_formattedSize"
|
||||||
|
},
|
||||||
|
"jsonFormat_diffMode": {
|
||||||
|
"message": "差异比较",
|
||||||
|
"description": "Translation key: jsonFormat_diffMode"
|
||||||
|
},
|
||||||
|
"jsonFormat_formatMode": {
|
||||||
|
"message": "格式化",
|
||||||
|
"description": "Translation key: jsonFormat_formatMode"
|
||||||
|
},
|
||||||
|
"jsonFormat_yamlMode": {
|
||||||
|
"message": "YAML",
|
||||||
|
"description": "Translation key: jsonFormat_yamlMode"
|
||||||
|
},
|
||||||
|
"jsonFormat_tomlMode": {
|
||||||
|
"message": "TOML",
|
||||||
|
"description": "Translation key: jsonFormat_tomlMode"
|
||||||
|
},
|
||||||
|
"jsonFormat_minifyMode": {
|
||||||
|
"message": "压缩",
|
||||||
|
"description": "Translation key: jsonFormat_minifyMode"
|
||||||
|
},
|
||||||
|
"jsonFormat_yamlModeInputPlaceholder": {
|
||||||
|
"message": "输入需要转换的 JSON...",
|
||||||
|
"description": "Translation key: jsonFormat_yamlModeInputPlaceholder"
|
||||||
|
},
|
||||||
|
"jsonFormat_yamlModeOutputLabel": {
|
||||||
|
"message": "YAML 结果",
|
||||||
|
"description": "Translation key: jsonFormat_yamlModeOutputLabel"
|
||||||
|
},
|
||||||
|
"jsonFormat_yamlModeEmptyHint": {
|
||||||
|
"message": "输入 JSON 后点击转换",
|
||||||
|
"description": "Translation key: jsonFormat_yamlModeEmptyHint"
|
||||||
|
},
|
||||||
|
"jsonFormat_tomlModeInputPlaceholder": {
|
||||||
|
"message": "输入需要转换的 JSON...",
|
||||||
|
"description": "Translation key: jsonFormat_tomlModeInputPlaceholder"
|
||||||
|
},
|
||||||
|
"jsonFormat_tomlModeOutputLabel": {
|
||||||
|
"message": "TOML 结果",
|
||||||
|
"description": "Translation key: jsonFormat_tomlModeOutputLabel"
|
||||||
|
},
|
||||||
|
"jsonFormat_tomlModeEmptyHint": {
|
||||||
|
"message": "输入 JSON 后点击转换",
|
||||||
|
"description": "Translation key: jsonFormat_tomlModeEmptyHint"
|
||||||
|
},
|
||||||
|
"jsonFormat_minifyModeInputPlaceholder": {
|
||||||
|
"message": "输入需要压缩的 JSON...",
|
||||||
|
"description": "Translation key: jsonFormat_minifyModeInputPlaceholder"
|
||||||
|
},
|
||||||
|
"jsonFormat_minifyModeOutputLabel": {
|
||||||
|
"message": "压缩结果",
|
||||||
|
"description": "Translation key: jsonFormat_minifyModeOutputLabel"
|
||||||
|
},
|
||||||
|
"jsonFormat_minifyModeEmptyHint": {
|
||||||
|
"message": "输入 JSON 后点击压缩",
|
||||||
|
"description": "Translation key: jsonFormat_minifyModeEmptyHint"
|
||||||
|
},
|
||||||
|
"jwt_pageTitle": {
|
||||||
|
"message": "JWT 解析",
|
||||||
|
"description": "Translation key: jwt_pageTitle"
|
||||||
|
},
|
||||||
|
"jwt_placeholder": {
|
||||||
|
"message": "在此粘贴 JWT 令牌 (Encoded JWT)...",
|
||||||
|
"description": "Translation key: jwt_placeholder"
|
||||||
|
},
|
||||||
|
"jwt_headerTitle": {
|
||||||
|
"message": "HEADER: 算法 & 令牌类型",
|
||||||
|
"description": "Translation key: jwt_headerTitle"
|
||||||
|
},
|
||||||
|
"jwt_payloadTitle": {
|
||||||
|
"message": "PAYLOAD: 数据",
|
||||||
|
"description": "Translation key: jwt_payloadTitle"
|
||||||
|
},
|
||||||
|
"jwt_signatureTitle": {
|
||||||
|
"message": "签名",
|
||||||
|
"description": "Translation key: jwt_signatureTitle"
|
||||||
|
},
|
||||||
|
"jwt_noSignature": {
|
||||||
|
"message": "无签名",
|
||||||
|
"description": "Translation key: jwt_noSignature"
|
||||||
|
},
|
||||||
|
"jwt_invalidFormat": {
|
||||||
|
"message": "无法解析",
|
||||||
|
"description": "Translation key: jwt_invalidFormat"
|
||||||
|
},
|
||||||
|
"jwt_errors_invalidBase64String": {
|
||||||
|
"message": "无效的 Base64URL 字符串",
|
||||||
|
"description": "Translation key: jwt_errors_invalidBase64String"
|
||||||
|
},
|
||||||
|
"jwt_errors_failedToDecode": {
|
||||||
|
"message": "Base64URL 解码失败:",
|
||||||
|
"description": "Translation key: jwt_errors_failedToDecode"
|
||||||
|
},
|
||||||
|
"jwt_errors_invalidFormat": {
|
||||||
|
"message": "JWT 格式错误:必须包含三个由 . 分隔的部分",
|
||||||
|
"description": "Translation key: jwt_errors_invalidFormat"
|
||||||
|
},
|
||||||
|
"jwt_errors_parseHeaderFailed": {
|
||||||
|
"message": "解析 Header 失败:",
|
||||||
|
"description": "Translation key: jwt_errors_parseHeaderFailed"
|
||||||
|
},
|
||||||
|
"jwt_errors_parsePayloadFailed": {
|
||||||
|
"message": "解析 Payload 失败:",
|
||||||
|
"description": "Translation key: jwt_errors_parsePayloadFailed"
|
||||||
|
},
|
||||||
|
"markdownToHtml_pageTitle": {
|
||||||
|
"message": "Markdown 转 HTML",
|
||||||
|
"description": "Translation key: markdownToHtml_pageTitle"
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"qrCode_pageTitle": {
|
||||||
|
"message": "二维码工具",
|
||||||
|
"description": "Translation key: qrCode_pageTitle"
|
||||||
|
},
|
||||||
|
"qrCode_urlToQr": {
|
||||||
|
"message": "文本转二维码",
|
||||||
|
"description": "Translation key: qrCode_urlToQr"
|
||||||
|
},
|
||||||
|
"qrCode_qrToUrl": {
|
||||||
|
"message": "二维码转文本",
|
||||||
|
"description": "Translation key: qrCode_qrToUrl"
|
||||||
|
},
|
||||||
|
"qrCode_urlInputLabel": {
|
||||||
|
"message": "输入 URL 或文本",
|
||||||
|
"description": "Translation key: qrCode_urlInputLabel"
|
||||||
|
},
|
||||||
|
"qrCode_urlInputPlaceholder": {
|
||||||
|
"message": "请输入 URL 或文本内容,将自动生成二维码",
|
||||||
|
"description": "Translation key: qrCode_urlInputPlaceholder"
|
||||||
|
},
|
||||||
|
"qrCode_generating": {
|
||||||
|
"message": "生成中...",
|
||||||
|
"description": "Translation key: qrCode_generating"
|
||||||
|
},
|
||||||
|
"qrCode_qrCodeWillShow": {
|
||||||
|
"message": "二维码将显示在这里",
|
||||||
|
"description": "Translation key: qrCode_qrCodeWillShow"
|
||||||
|
},
|
||||||
|
"qrCode_downloadButton": {
|
||||||
|
"message": "下载二维码",
|
||||||
|
"description": "Translation key: qrCode_downloadButton"
|
||||||
|
},
|
||||||
|
"qrCode_copyQrButton": {
|
||||||
|
"message": "复制二维码",
|
||||||
|
"description": "Translation key: qrCode_copyQrButton"
|
||||||
|
},
|
||||||
|
"qrCode_qrCodeDownloadSuccess": {
|
||||||
|
"message": "二维码下载成功",
|
||||||
|
"description": "Translation key: qrCode_qrCodeDownloadSuccess"
|
||||||
|
},
|
||||||
|
"qrCode_qrCodeCopySuccess": {
|
||||||
|
"message": "二维码已复制到剪贴板",
|
||||||
|
"description": "Translation key: qrCode_qrCodeCopySuccess"
|
||||||
|
},
|
||||||
|
"qrCode_parseSuccess": {
|
||||||
|
"message": "二维码解析成功",
|
||||||
|
"description": "Translation key: qrCode_parseSuccess"
|
||||||
|
},
|
||||||
|
"qrCode_noQrDetected": {
|
||||||
|
"message": "未检测到二维码,请确保图片清晰且包含二维码",
|
||||||
|
"description": "Translation key: qrCode_noQrDetected"
|
||||||
|
},
|
||||||
|
"qrCode_parseError": {
|
||||||
|
"message": "解析二维码失败,请重试",
|
||||||
|
"description": "Translation key: qrCode_parseError"
|
||||||
|
},
|
||||||
|
"qrCode_copyError": {
|
||||||
|
"message": "复制失败,请重试",
|
||||||
|
"description": "Translation key: qrCode_copyError"
|
||||||
|
},
|
||||||
|
"qrCode_imagePasted": {
|
||||||
|
"message": "图片粘贴成功,正在解析...",
|
||||||
|
"description": "Translation key: qrCode_imagePasted"
|
||||||
|
},
|
||||||
|
"qrCode_imagePasteError": {
|
||||||
|
"message": "粘贴图片失败,请重试",
|
||||||
|
"description": "Translation key: qrCode_imagePasteError"
|
||||||
|
},
|
||||||
|
"qrCode_imageCleared": {
|
||||||
|
"message": "图片已清除",
|
||||||
|
"description": "Translation key: qrCode_imageCleared"
|
||||||
|
},
|
||||||
|
"qrCode_clickToUpload": {
|
||||||
|
"message": "点击、拖拽或粘贴上传二维码图片",
|
||||||
|
"description": "Translation key: qrCode_clickToUpload"
|
||||||
|
},
|
||||||
|
"qrCode_supportFormats": {
|
||||||
|
"message": "支持 PNG、JPG、WEBP、Base64 格式",
|
||||||
|
"description": "Translation key: qrCode_supportFormats"
|
||||||
|
},
|
||||||
|
"qrCode_resultLabel": {
|
||||||
|
"message": "解析结果",
|
||||||
|
"description": "Translation key: qrCode_resultLabel"
|
||||||
|
},
|
||||||
|
"qrCode_clickToChange": {
|
||||||
|
"message": "点击更换图片",
|
||||||
|
"description": "Translation key: qrCode_clickToChange"
|
||||||
|
},
|
||||||
|
"rightClickRestorer_loading": {
|
||||||
|
"message": "正在加载...",
|
||||||
|
"description": "Translation key: rightClickRestorer_loading"
|
||||||
|
},
|
||||||
|
"rightClickRestorer_currentDomain": {
|
||||||
|
"message": "当前域名",
|
||||||
|
"description": "Translation key: rightClickRestorer_currentDomain"
|
||||||
|
},
|
||||||
|
"rightClickRestorer_statusLocked": {
|
||||||
|
"message": "未解锁",
|
||||||
|
"description": "Translation key: rightClickRestorer_statusLocked"
|
||||||
|
},
|
||||||
|
"rightClickRestorer_statusUnlocked": {
|
||||||
|
"message": "已解锁",
|
||||||
|
"description": "Translation key: rightClickRestorer_statusUnlocked"
|
||||||
|
},
|
||||||
|
"rightClickRestorer_unsupported": {
|
||||||
|
"message": "不支持",
|
||||||
|
"description": "Translation key: rightClickRestorer_unsupported"
|
||||||
|
},
|
||||||
|
"rightClickRestorer_unsupportedDesc": {
|
||||||
|
"message": "当前页面为浏览器内部页面或扩展页面,无法解锁右键功能。请切换到普通网页后重试。",
|
||||||
|
"description": "Translation key: rightClickRestorer_unsupportedDesc"
|
||||||
|
},
|
||||||
|
"rightClickRestorer_unlockDesc": {
|
||||||
|
"message": "点击下方按钮,为当前网站临时解锁右键菜单。刷新页面后需要重新解锁。",
|
||||||
|
"description": "Translation key: rightClickRestorer_unlockDesc"
|
||||||
|
},
|
||||||
|
"rightClickRestorer_unlockBtn": {
|
||||||
|
"message": "解锁当前网站右键",
|
||||||
|
"description": "Translation key: rightClickRestorer_unlockBtn"
|
||||||
|
},
|
||||||
|
"rightClickRestorer_alreadyUnlocked": {
|
||||||
|
"message": "右键已解锁",
|
||||||
|
"description": "Translation key: rightClickRestorer_alreadyUnlocked"
|
||||||
|
},
|
||||||
|
"storageCleaner_pageTitle": {
|
||||||
|
"message": "存储清理",
|
||||||
|
"description": "Translation key: storageCleaner_pageTitle"
|
||||||
|
},
|
||||||
|
"storageCleaner_loading": {
|
||||||
|
"message": "加载中...",
|
||||||
|
"description": "Translation key: storageCleaner_loading"
|
||||||
|
},
|
||||||
|
"storageCleaner_initializing": {
|
||||||
|
"message": "正在读取站点数据...",
|
||||||
|
"description": "Translation key: storageCleaner_initializing"
|
||||||
|
},
|
||||||
|
"storageCleaner_cleaning": {
|
||||||
|
"message": "正在清理...",
|
||||||
|
"description": "Translation key: storageCleaner_cleaning"
|
||||||
|
},
|
||||||
|
"storageCleaner_cleanNow": {
|
||||||
|
"message": "立即清理",
|
||||||
|
"description": "Translation key: storageCleaner_cleanNow"
|
||||||
|
},
|
||||||
|
"storageCleaner_autoRefresh": {
|
||||||
|
"message": "清理后自动刷新页面",
|
||||||
|
"description": "Translation key: storageCleaner_autoRefresh"
|
||||||
|
},
|
||||||
|
"storageCleaner_selectAll": {
|
||||||
|
"message": "全选所有项",
|
||||||
|
"description": "Translation key: storageCleaner_selectAll"
|
||||||
|
},
|
||||||
|
"storageCleaner_noData": {
|
||||||
|
"message": "无数据",
|
||||||
|
"description": "Translation key: storageCleaner_noData"
|
||||||
|
},
|
||||||
|
"storageCleaner_errorNoTab": {
|
||||||
|
"message": "无法获取当前标签页",
|
||||||
|
"description": "Translation key: storageCleaner_errorNoTab"
|
||||||
|
},
|
||||||
|
"storageCleaner_errorRestricted": {
|
||||||
|
"message": "存储清理功能不支持此页面",
|
||||||
|
"description": "Translation key: storageCleaner_errorRestricted"
|
||||||
|
},
|
||||||
|
"storageCleaner_cleanSuccessReload": {
|
||||||
|
"message": "清理成功,即将刷新页面",
|
||||||
|
"description": "Translation key: storageCleaner_cleanSuccessReload"
|
||||||
|
},
|
||||||
|
"storageCleaner_errorStandardOnly": {
|
||||||
|
"message": "存储清理功能仅适用于标准网页",
|
||||||
|
"description": "Translation key: storageCleaner_errorStandardOnly"
|
||||||
|
},
|
||||||
|
"storageCleaner_confirmTitle": {
|
||||||
|
"message": "确认清理数据?",
|
||||||
|
"description": "Translation key: storageCleaner_confirmTitle"
|
||||||
|
},
|
||||||
|
"storageCleaner_confirmDesc": {
|
||||||
|
"message": "您将永久删除当前页面的以下选定存储项。",
|
||||||
|
"description": "Translation key: storageCleaner_confirmDesc"
|
||||||
|
},
|
||||||
|
"storageCleaner_irreversible": {
|
||||||
|
"message": "此操作不可撤销",
|
||||||
|
"description": "Translation key: storageCleaner_irreversible"
|
||||||
|
},
|
||||||
|
"storageCleaner_confirmAction": {
|
||||||
|
"message": "确认清理",
|
||||||
|
"description": "Translation key: storageCleaner_confirmAction"
|
||||||
|
},
|
||||||
|
"storageCleaner_cleanedSummary": {
|
||||||
|
"message": "清理了 {{items}}",
|
||||||
|
"description": "Translation key: storageCleaner_cleanedSummary"
|
||||||
|
},
|
||||||
|
"storageCleaner_noDataToClean": {
|
||||||
|
"message": "该页面没有可清理的存储数据",
|
||||||
|
"description": "Translation key: storageCleaner_noDataToClean"
|
||||||
|
},
|
||||||
|
"storageCleaner_partialFailure": {
|
||||||
|
"message": "部分清理失败",
|
||||||
|
"description": "Translation key: storageCleaner_partialFailure"
|
||||||
|
},
|
||||||
|
"storageCleaner_options_localStorage": {
|
||||||
|
"message": "Local Storage",
|
||||||
|
"description": "Translation key: storageCleaner_options_localStorage"
|
||||||
|
},
|
||||||
|
"storageCleaner_options_sessionStorage": {
|
||||||
|
"message": "Session Storage",
|
||||||
|
"description": "Translation key: storageCleaner_options_sessionStorage"
|
||||||
|
},
|
||||||
|
"storageCleaner_options_indexedDB": {
|
||||||
|
"message": "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_placeholder": {
|
||||||
|
"message": "在此输入或粘贴文本...",
|
||||||
|
"description": "Translation key: textStatistics_placeholder"
|
||||||
|
},
|
||||||
|
"textStatistics_characters": {
|
||||||
|
"message": "字符数",
|
||||||
|
"description": "Translation key: textStatistics_characters"
|
||||||
|
},
|
||||||
|
"textStatistics_words": {
|
||||||
|
"message": "单词数",
|
||||||
|
"description": "Translation key: textStatistics_words"
|
||||||
|
},
|
||||||
|
"textStatistics_lines": {
|
||||||
|
"message": "行数",
|
||||||
|
"description": "Translation key: textStatistics_lines"
|
||||||
|
},
|
||||||
|
"textStatistics_bytes": {
|
||||||
|
"message": "字节大小",
|
||||||
|
"description": "Translation key: textStatistics_bytes"
|
||||||
|
},
|
||||||
|
"timestamp_pageTitle": {
|
||||||
|
"message": "时间戳转换",
|
||||||
|
"description": "Translation key: timestamp_pageTitle"
|
||||||
|
},
|
||||||
|
"timestamp_tsToDate": {
|
||||||
|
"message": "时间戳 → 日期",
|
||||||
|
"description": "Translation key: timestamp_tsToDate"
|
||||||
|
},
|
||||||
|
"timestamp_dateToTs": {
|
||||||
|
"message": "日期 → 时间戳",
|
||||||
|
"description": "Translation key: timestamp_dateToTs"
|
||||||
|
},
|
||||||
|
"timestamp_placeholderTs": {
|
||||||
|
"message": "输入时间戳...",
|
||||||
|
"description": "Translation key: timestamp_placeholderTs"
|
||||||
|
},
|
||||||
|
"timestamp_placeholderDate": {
|
||||||
|
"message": "YYYY-MM-DD HH:mm:ss",
|
||||||
|
"description": "Translation key: timestamp_placeholderDate"
|
||||||
|
},
|
||||||
|
"timestamp_unitMs": {
|
||||||
|
"message": "毫秒 (ms)",
|
||||||
|
"description": "Translation key: timestamp_unitMs"
|
||||||
|
},
|
||||||
|
"timestamp_currentTs": {
|
||||||
|
"message": "当前时间戳",
|
||||||
|
"description": "Translation key: timestamp_currentTs"
|
||||||
|
},
|
||||||
|
"timestamp_useNowTooltip": {
|
||||||
|
"message": "填充到下方",
|
||||||
|
"description": "Translation key: timestamp_useNowTooltip"
|
||||||
|
},
|
||||||
|
"timestamp_copyTsTooltip": {
|
||||||
|
"message": "复制时间戳",
|
||||||
|
"description": "Translation key: timestamp_copyTsTooltip"
|
||||||
|
},
|
||||||
|
"timestamp_usedSuccess": {
|
||||||
|
"message": "已使用当前时间戳",
|
||||||
|
"description": "Translation key: timestamp_usedSuccess"
|
||||||
|
},
|
||||||
|
"timestamp_resultLabel": {
|
||||||
|
"message": "转换结果",
|
||||||
|
"description": "Translation key: timestamp_resultLabel"
|
||||||
|
},
|
||||||
|
"timestamp_copyResultTooltip": {
|
||||||
|
"message": "复制结果",
|
||||||
|
"description": "Translation key: timestamp_copyResultTooltip"
|
||||||
|
},
|
||||||
|
"timestamp_relativeTime": {
|
||||||
|
"message": "相对时间",
|
||||||
|
"description": "Translation key: timestamp_relativeTime"
|
||||||
|
},
|
||||||
|
"timestamp_iso8601": {
|
||||||
|
"message": "ISO 8601",
|
||||||
|
"description": "Translation key: timestamp_iso8601"
|
||||||
|
},
|
||||||
|
"timestamp_utcTime": {
|
||||||
|
"message": "UTC 时间",
|
||||||
|
"description": "Translation key: timestamp_utcTime"
|
||||||
|
},
|
||||||
|
"timestamp_copyTooltip": {
|
||||||
|
"message": "复制",
|
||||||
|
"description": "Translation key: timestamp_copyTooltip"
|
||||||
|
},
|
||||||
|
"timestamp_resultEmpty": {
|
||||||
|
"message": "请输入并点击转换",
|
||||||
|
"description": "Translation key: timestamp_resultEmpty"
|
||||||
|
},
|
||||||
|
"common_buttons_cancel": {
|
||||||
|
"message": "取消",
|
||||||
|
"description": "Translation key: buttons_cancel"
|
||||||
|
},
|
||||||
|
"common_buttons_copy": {
|
||||||
|
"message": "复制",
|
||||||
|
"description": "Translation key: buttons_copy"
|
||||||
|
},
|
||||||
|
"common_buttons_themeMode_light": {
|
||||||
|
"message": "切换到深色模式",
|
||||||
|
"description": "Translation key: buttons_themeMode_light"
|
||||||
|
},
|
||||||
|
"common_buttons_themeMode_dark": {
|
||||||
|
"message": "切换到系统模式",
|
||||||
|
"description": "Translation key: buttons_themeMode_dark"
|
||||||
|
},
|
||||||
|
"common_buttons_themeMode_system": {
|
||||||
|
"message": "切换到浅色模式",
|
||||||
|
"description": "Translation key: buttons_themeMode_system"
|
||||||
|
},
|
||||||
|
"common_buttons_search": {
|
||||||
|
"message": "搜索工具...",
|
||||||
|
"description": "Translation key: buttons_search"
|
||||||
|
},
|
||||||
|
"common_buttons_back": {
|
||||||
|
"message": "返回",
|
||||||
|
"description": "Translation key: buttons_back"
|
||||||
|
},
|
||||||
|
"common_buttons_clearSearch": {
|
||||||
|
"message": "清除搜索",
|
||||||
|
"description": "Translation key: buttons_clearSearch"
|
||||||
|
},
|
||||||
|
"common_buttons_recentSearch": {
|
||||||
|
"message": "最近搜索",
|
||||||
|
"description": "Translation key: buttons_recentSearch"
|
||||||
|
},
|
||||||
|
"common_buttons_noResults": {
|
||||||
|
"message": "未找到相关工具",
|
||||||
|
"description": "Translation key: buttons_noResults"
|
||||||
|
},
|
||||||
|
"common_buttons_openInTab": {
|
||||||
|
"message": "在标签页打开",
|
||||||
|
"description": "Translation key: buttons_openInTab"
|
||||||
|
},
|
||||||
|
"common_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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<typeof vi.fn> };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 💡 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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
/**
|
||||||
|
* chrome.i18n 类型安全 wrapper
|
||||||
|
* 提供与 react-i18next 兼容的接口
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取翻译文本
|
||||||
|
* @param msgId 翻译 key(如 'timestamp_pageTitle')
|
||||||
|
* @param substitutions 占位符替换值(可选)
|
||||||
|
* @returns 翻译后的文本
|
||||||
|
*/
|
||||||
|
export function getMessage(msgId: string, substitutions?: string[]): string {
|
||||||
|
try {
|
||||||
|
return chrome.i18n.getMessage(msgId, substitutions);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`[chrome.i18n] 无法获取翻译: ${msgId}`, error);
|
||||||
|
return msgId; // 回退到 key 本身
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* react-i18next 兼容的 Hook
|
||||||
|
* 返回 t 函数和相关信息
|
||||||
|
*/
|
||||||
|
export function useI18n(namespace?: string | string[]) {
|
||||||
|
const namespaces = Array.isArray(namespace) ? namespace : namespace ? [namespace] : [];
|
||||||
|
|
||||||
|
const t = (key: string, options?: Record<string, unknown>): 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: 'zh',
|
||||||
|
changeLanguage: (_lng?: string) => {
|
||||||
|
// chrome.i18n 无法动态切换语言,需要刷新页面
|
||||||
|
console.warn('[chrome.i18n] 无法动态切换语言,需要刷新页面');
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
isLoaded: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预加载命名空间(无操作,兼容 useLazyTranslation)
|
||||||
|
*/
|
||||||
|
export async function preloadNamespaces(_namespaces: string[]): Promise<void> {
|
||||||
|
// chrome.i18n 是同步的,无需预加载
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
+6
-6
@@ -2,7 +2,7 @@
|
|||||||
* JWT 解析工具
|
* JWT 解析工具
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import i18n from '@/i18n';
|
import { getMessage } from '@/utils/chromeI18n';
|
||||||
|
|
||||||
export interface JwtHeader {
|
export interface JwtHeader {
|
||||||
alg: string;
|
alg: string;
|
||||||
@@ -43,7 +43,7 @@ export function decodeBase64Url(str: string): string {
|
|||||||
const pad = base64.length % 4;
|
const pad = base64.length % 4;
|
||||||
if (pad) {
|
if (pad) {
|
||||||
if (pad === 1) {
|
if (pad === 1) {
|
||||||
throw new Error(i18n.t('jwt:errors.invalidBase64String'));
|
throw new Error(getMessage('jwt_errors_invalidBase64String'));
|
||||||
}
|
}
|
||||||
base64 += new Array(5 - pad).join('=');
|
base64 += new Array(5 - pad).join('=');
|
||||||
}
|
}
|
||||||
@@ -59,7 +59,7 @@ export function decodeBase64Url(str: string): string {
|
|||||||
return decoder.decode(bytes);
|
return decoder.decode(bytes);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(
|
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 },
|
{ cause: e },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -78,7 +78,7 @@ export function parseJwt(token: string): JwtResult {
|
|||||||
payload: null,
|
payload: null,
|
||||||
signature: '',
|
signature: '',
|
||||||
raw: { header: '', payload: '', 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);
|
result.header = JSON.parse(headerJson);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
result.error =
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ export function parseJwt(token: string): JwtResult {
|
|||||||
result.payload = JSON.parse(payloadJson);
|
result.payload = JSON.parse(payloadJson);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
result.error =
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import i18n from '@/i18n';
|
|
||||||
|
|
||||||
// 语言包动态导入映射
|
|
||||||
const localeModules: Record<
|
|
||||||
string,
|
|
||||||
Record<string, () => Promise<{ default: Record<string, unknown> }>>
|
|
||||||
> = {
|
|
||||||
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<string>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清除已加载命名空间的缓存(仅用于测试)
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
export function __test_clearCache(): void {
|
|
||||||
loadedNamespaces.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 动态加载 i18n 命名空间
|
|
||||||
*/
|
|
||||||
async function loadNamespace(ns: string, lng: string): Promise<void> {
|
|
||||||
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<void> {
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
+28
-53
@@ -1,66 +1,41 @@
|
|||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import { afterEach, beforeEach, vi } from 'vitest';
|
import { afterEach, beforeEach, vi } from 'vitest';
|
||||||
import { readFileSync } from 'fs';
|
|
||||||
import { resolve } from 'path';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
// 读取中文翻译文件用于 withTranslation mock
|
// Load actual zh translations for getMessage mock
|
||||||
const zhCommon = JSON.parse(
|
const zhMessages: Record<string, { message: string }> =
|
||||||
readFileSync(resolve(__dirname, 'i18n/locales/zh/common.json'), 'utf-8'),
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
);
|
require('./public/_locales/zh/messages.json');
|
||||||
|
|
||||||
// 支持嵌套 key 查找,如 "errorBoundary.title" → zhCommon.errorBoundary.title
|
vi.mock('@/utils/chromeI18n', () => ({
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
useI18n: (ns?: string | string[]) => ({
|
||||||
function nestedLookup(obj: Record<string, any>, key: string): string | undefined {
|
t: (key: string) => {
|
||||||
const parts = key.split('.');
|
let msgId = key;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// Handle namespace:key format
|
||||||
let current: any = obj;
|
if (key.includes(':')) {
|
||||||
for (const part of parts) {
|
msgId = key.replace(':', '_').replace(/\./g, '_');
|
||||||
if (current == null || typeof current !== 'object') return undefined;
|
}
|
||||||
current = current[part];
|
// Try direct key first
|
||||||
}
|
if (zhMessages[msgId]) return zhMessages[msgId].message;
|
||||||
return typeof current === 'string' ? current : undefined;
|
// Try namespace prefix (using converted msgId)
|
||||||
}
|
if (ns) {
|
||||||
|
const namespaces = Array.isArray(ns) ? ns : [ns];
|
||||||
vi.mock('@/utils/useLazyTranslation', () => ({
|
for (const n of namespaces) {
|
||||||
useLazyTranslation: (ns?: string) => ({
|
const candidate = `${n}_${msgId}`;
|
||||||
// 自动承接命名空间前缀过滤,100% 模拟真实多语种直出
|
if (zhMessages[candidate]) return zhMessages[candidate].message;
|
||||||
t: (key: string) => (ns ? `${ns}:${key}` : key),
|
}
|
||||||
|
}
|
||||||
|
return msgId;
|
||||||
|
},
|
||||||
i18n: {
|
i18n: {
|
||||||
changeLanguage: vi.fn().mockResolvedValue(undefined),
|
changeLanguage: vi.fn().mockResolvedValue(undefined),
|
||||||
language: 'zh-CN',
|
language: 'zh',
|
||||||
},
|
},
|
||||||
isLoaded: true,
|
isLoaded: true,
|
||||||
}),
|
}),
|
||||||
}));
|
getMessage: (msgId: string) => zhMessages[msgId]?.message ?? msgId,
|
||||||
|
getLanguage: () => 'zh',
|
||||||
vi.mock('react-i18next', () => ({
|
preloadNamespaces: vi.fn().mockResolvedValue(undefined),
|
||||||
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<any>) => {
|
|
||||||
// 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),
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('@/components/CopyButton', () => ({
|
vi.mock('@/components/CopyButton', () => ({
|
||||||
|
|||||||
+2
-8
@@ -22,13 +22,6 @@ function manualChunksForHtmlOnly(): Plugin {
|
|||||||
) {
|
) {
|
||||||
return 'vendor-react';
|
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')) {
|
if (id.includes('qr-scanner') || id.includes('qrious')) {
|
||||||
return 'vendor-qr';
|
return 'vendor-qr';
|
||||||
@@ -55,6 +48,7 @@ export default defineConfig({
|
|||||||
name: 'Testing Tool',
|
name: 'Testing Tool',
|
||||||
description: 'A tool for testing web applications.',
|
description: 'A tool for testing web applications.',
|
||||||
version_name: undefined,
|
version_name: undefined,
|
||||||
|
default_locale: 'zh',
|
||||||
permissions: [
|
permissions: [
|
||||||
'storage',
|
'storage',
|
||||||
'unlimitedStorage',
|
'unlimitedStorage',
|
||||||
@@ -69,7 +63,7 @@ export default defineConfig({
|
|||||||
],
|
],
|
||||||
host_permissions: ['<all_urls>'],
|
host_permissions: ['<all_urls>'],
|
||||||
action: {
|
action: {
|
||||||
default_title: '__MSG_extName__',
|
default_title: '__MSG_appName__',
|
||||||
},
|
},
|
||||||
side_panel: {
|
side_panel: {
|
||||||
default_path: 'entrypoints/sidepanel/index.html',
|
default_path: 'entrypoints/sidepanel/index.html',
|
||||||
|
|||||||
Reference in New Issue
Block a user