fix(i18n): 统一使用 useLazyTranslation 避免翻译 key 闪烁

This commit is contained in:
雨霖铃
2026-05-21 00:50:01 +08:00
parent 604fbb4d1f
commit 8e2ac5200d
19 changed files with 70 additions and 34 deletions
+2 -2
View File
@@ -4,7 +4,7 @@ import ImageIcon from '@mui/icons-material/Image';
import ClearIcon from '@mui/icons-material/Clear'; import ClearIcon from '@mui/icons-material/Clear';
import { qrCodePageStyles } from '@/config/pageTheme'; import { qrCodePageStyles } from '@/config/pageTheme';
import { useSnackbar } from '@/components/GlobalSnackbar'; import { useSnackbar } from '@/components/GlobalSnackbar';
import { useTranslation } from 'react-i18next'; import { useLazyTranslation } from '@/utils/useLazyTranslation';
interface ImageUploaderProps { interface ImageUploaderProps {
/** 选中的文件 */ /** 选中的文件 */
@@ -32,7 +32,7 @@ const ImageUploader = ({
dragging, dragging,
onDraggingChange, onDraggingChange,
}: ImageUploaderProps) => { }: ImageUploaderProps) => {
const { t } = useTranslation(['qrCode']); const { t } = useLazyTranslation('qrCode');
const { showMessage } = useSnackbar(); const { showMessage } = useSnackbar();
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
+2 -2
View File
@@ -2,7 +2,7 @@ import { Box, Button, Typography } from '@mui/material';
import DownloadIcon from '@mui/icons-material/Download'; import DownloadIcon from '@mui/icons-material/Download';
import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import { qrCodePageStyles } from '@/config/pageTheme'; import { qrCodePageStyles } from '@/config/pageTheme';
import { useTranslation } from 'react-i18next'; import { useLazyTranslation } from '@/utils/useLazyTranslation';
interface QrCodePreviewProps { interface QrCodePreviewProps {
/** 二维码 Data URL */ /** 二维码 Data URL */
@@ -14,7 +14,7 @@ interface QrCodePreviewProps {
} }
const QrCodePreview = ({ qrCodeDataUrl, onDownload, onCopy }: QrCodePreviewProps) => { const QrCodePreview = ({ qrCodeDataUrl, onDownload, onCopy }: QrCodePreviewProps) => {
const { t } = useTranslation(['qrCode']); const { t } = useLazyTranslation('qrCode');
if (!qrCodeDataUrl) { if (!qrCodeDataUrl) {
return ( return (
@@ -2,6 +2,24 @@ import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react'; import { render, screen, fireEvent } 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
vi.mock('@/config/features', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/config/features')>();
return {
...actual,
getEntryPointType: () => 'sidepanel',
};
});
// Mock 子组件 // Mock 子组件
vi.mock('../TextMode', () => ({ vi.mock('../TextMode', () => ({
default: () => <div data-testid="text-mode">TextMode</div>, default: () => <div data-testid="text-mode">TextMode</div>,
+2 -2
View File
@@ -1,7 +1,7 @@
import { Box, IconButton, Typography } from '@mui/material'; import { Box, IconButton, Typography } from '@mui/material';
import NavigateBeforeIcon from '@mui/icons-material/NavigateBefore'; import NavigateBeforeIcon from '@mui/icons-material/NavigateBefore';
import NavigateNextIcon from '@mui/icons-material/NavigateNext'; import NavigateNextIcon from '@mui/icons-material/NavigateNext';
import { useTranslation } from 'react-i18next'; import { useLazyTranslation } from '@/utils/useLazyTranslation';
import { jsonDiffPageStyles } from '@/config/pageTheme'; import { jsonDiffPageStyles } from '@/config/pageTheme';
interface DiffNavigatorProps { interface DiffNavigatorProps {
@@ -13,7 +13,7 @@ interface DiffNavigatorProps {
} }
export default function DiffNavigator({ total, currentIndex, onPrev, onNext }: DiffNavigatorProps) { export default function DiffNavigator({ total, currentIndex, onPrev, onNext }: DiffNavigatorProps) {
const { t } = useTranslation(['jsonDiff']); const { t } = useLazyTranslation('jsonDiff');
if (total === 0) { if (total === 0) {
return ( return (
+2 -2
View File
@@ -1,6 +1,6 @@
import { Box, Stack, Typography, useTheme } from '@mui/material'; import { Box, Stack, Typography, useTheme } from '@mui/material';
import type { Theme } from '@mui/material/styles'; import type { Theme } from '@mui/material/styles';
import { useTranslation } from 'react-i18next'; import { useLazyTranslation } from '@/utils/useLazyTranslation';
import { jsonDiffPageStyles, surfaceTint } from '@/config/pageTheme'; import { jsonDiffPageStyles, surfaceTint } from '@/config/pageTheme';
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';
@@ -12,7 +12,7 @@ interface DiffResultProps {
} }
export default function DiffResult({ result, viewMode, activePath }: DiffResultProps) { export default function DiffResult({ result, viewMode, activePath }: DiffResultProps) {
const { t } = useTranslation(['jsonDiff']); const { t } = useLazyTranslation('jsonDiff');
if (viewMode === 'sideBySide') { if (viewMode === 'sideBySide') {
return ( return (
+2 -2
View File
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { Box, Button, Stack, Typography } from '@mui/material'; import { Box, Button, Stack, Typography } from '@mui/material';
import { useTranslation } from 'react-i18next'; import { useLazyTranslation } from '@/utils/useLazyTranslation';
import { formatByteSize } from '@/utils/textStatistics'; import { formatByteSize } from '@/utils/textStatistics';
import { useSnackbar } from '@/components/GlobalSnackbar'; import { useSnackbar } from '@/components/GlobalSnackbar';
import CopyButton from '@/components/CopyButton'; import CopyButton from '@/components/CopyButton';
@@ -43,7 +43,7 @@ export default function JsonConvertSection({
convertFunction, convertFunction,
convertButtonKey = 'convertButton', convertButtonKey = 'convertButton',
}: JsonConvertSectionProps) { }: JsonConvertSectionProps) {
const { t } = useTranslation(['jsonFormat']); const { t } = useLazyTranslation('jsonFormat');
const { showMessage } = useSnackbar(); const { showMessage } = useSnackbar();
const [input, setInput] = useState(''); const [input, setInput] = useState('');
+2 -2
View File
@@ -9,7 +9,7 @@ import {
TextField, TextField,
Typography, Typography,
} from '@mui/material'; } from '@mui/material';
import { useTranslation } from 'react-i18next'; import { useLazyTranslation } from '@/utils/useLazyTranslation';
import { import {
formatJson, formatJson,
validateJson, validateJson,
@@ -32,7 +32,7 @@ const INDENT_OPTIONS = [2, 4, 6, 8] as const;
* 支持一键复制格式化后的 JSON。 * 支持一键复制格式化后的 JSON。
*/ */
export default function JsonFormatSection() { export default function JsonFormatSection() {
const { t } = useTranslation(['jsonFormat']); const { t } = useLazyTranslation('jsonFormat');
const { showMessage } = useSnackbar(); const { showMessage } = useSnackbar();
const [input, setInput] = useState(''); const [input, setInput] = useState('');
+18
View File
@@ -2,6 +2,24 @@ import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react'; import { render, screen, fireEvent } from '@testing-library/react';
import QrCodePage from '../index'; import QrCodePage from '../index';
// Mock useLazyTranslation
vi.mock('@/utils/useLazyTranslation', () => ({
useLazyTranslation: () => ({
t: (key: string) => key,
i18n: { changeLanguage: vi.fn(), language: 'zh-CN' },
isLoaded: true,
}),
}));
// Mock getEntryPointType
vi.mock('@/config/features', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/config/features')>();
return {
...actual,
getEntryPointType: () => 'sidepanel',
};
});
// Mock 子组件 // Mock 子组件
vi.mock('@/components/QrCodePreview', () => ({ vi.mock('@/components/QrCodePreview', () => ({
default: () => <div data-testid="qr-code-preview">QrCodePreview</div>, default: () => <div data-testid="qr-code-preview">QrCodePreview</div>,
+2 -2
View File
@@ -1,6 +1,6 @@
import { Box, Switch, Typography } from '@mui/material'; import { Box, Switch, Typography } from '@mui/material';
import { storageCleanerPageStyles } from '@/config/pageTheme'; import { storageCleanerPageStyles } from '@/config/pageTheme';
import { useTranslation } from 'react-i18next'; import { useLazyTranslation } from '@/utils/useLazyTranslation';
interface AutoRefreshToggleProps { interface AutoRefreshToggleProps {
autoRefresh: boolean; autoRefresh: boolean;
@@ -8,7 +8,7 @@ interface AutoRefreshToggleProps {
} }
export default function AutoRefreshToggle({ autoRefresh, onChange }: AutoRefreshToggleProps) { export default function AutoRefreshToggle({ autoRefresh, onChange }: AutoRefreshToggleProps) {
const { t } = useTranslation(['storageCleaner']); const { t } = useLazyTranslation('storageCleaner');
return ( return (
<Box sx={storageCleanerPageStyles.AUTO_REFRESH_CONTAINER}> <Box sx={storageCleanerPageStyles.AUTO_REFRESH_CONTAINER}>
<Typography variant="body2" fontWeight={700} sx={{ fontSize: '0.8rem', px: 1.2 }}> <Typography variant="body2" fontWeight={700} sx={{ fontSize: '0.8rem', px: 1.2 }}>
+2 -2
View File
@@ -2,14 +2,14 @@ import { Alert, Box } from '@mui/material';
import type { CleaningResult } from '@/types/storage'; import type { CleaningResult } from '@/types/storage';
import { formatCleaningResult } from '@/utils/storageCleaner'; import { formatCleaningResult } from '@/utils/storageCleaner';
import { storageCleanerPageStyles } from '@/config/pageTheme'; import { storageCleanerPageStyles } from '@/config/pageTheme';
import { useTranslation } from 'react-i18next'; import { useLazyTranslation } from '@/utils/useLazyTranslation';
interface CleaningResultProps { interface CleaningResultProps {
result: CleaningResult | null; result: CleaningResult | null;
} }
export default function CleaningResult({ result }: CleaningResultProps) { export default function CleaningResult({ result }: CleaningResultProps) {
const { t } = useTranslation(['storageCleaner']); const { t } = useLazyTranslation('storageCleaner');
if (!result) return null; if (!result) return null;
return ( return (
+2 -2
View File
@@ -3,7 +3,7 @@ import StorageIcon from '@mui/icons-material/Storage';
import PageHeader from '@/components/PageHeader'; import PageHeader from '@/components/PageHeader';
import { formatSize } from '@/utils/storageCleaner'; import { formatSize } from '@/utils/storageCleaner';
import { storageCleanerPageStyles } from '@/config/pageTheme'; import { storageCleanerPageStyles } from '@/config/pageTheme';
import { useTranslation } from 'react-i18next'; import { useLazyTranslation } from '@/utils/useLazyTranslation';
/** /**
* DomainHeader 组件属性接口 * DomainHeader 组件属性接口
@@ -29,7 +29,7 @@ interface DomainHeaderProps {
* ``` * ```
*/ */
export default function DomainHeader({ domain, totalSize }: DomainHeaderProps) { export default function DomainHeader({ domain, totalSize }: DomainHeaderProps) {
const { t } = useTranslation(['storageCleaner']); const { t } = useLazyTranslation('storageCleaner');
return ( return (
<PageHeader <PageHeader
icon={<StorageIcon sx={{ fontSize: 22 }} />} icon={<StorageIcon sx={{ fontSize: 22 }} />}
+2 -2
View File
@@ -1,14 +1,14 @@
import { Box, Container, Typography } from '@mui/material'; import { Box, Container, Typography } from '@mui/material';
import WarningIcon from '@mui/icons-material/Warning'; import WarningIcon from '@mui/icons-material/Warning';
import { storageCleanerPageStyles } from '@/config/pageTheme'; import { storageCleanerPageStyles } from '@/config/pageTheme';
import { useTranslation } from 'react-i18next'; import { useLazyTranslation } from '@/utils/useLazyTranslation';
interface ErrorDisplayProps { interface ErrorDisplayProps {
error: string; error: string;
} }
export default function ErrorDisplay({ error }: ErrorDisplayProps) { export default function ErrorDisplay({ error }: ErrorDisplayProps) {
const { t } = useTranslation(['storageCleaner']); const { t } = useLazyTranslation('storageCleaner');
return ( return (
<Container sx={storageCleanerPageStyles.ERROR_DISPLAY_CONTAINER}> <Container sx={storageCleanerPageStyles.ERROR_DISPLAY_CONTAINER}>
<Box sx={{ width: '100%', maxWidth: 320 }}> <Box sx={{ width: '100%', maxWidth: 320 }}>
+2 -2
View File
@@ -1,7 +1,7 @@
import { Box, Checkbox, Typography } from '@mui/material'; import { Box, Checkbox, Typography } from '@mui/material';
import { formatSize } from '@/utils/storageCleaner'; import { formatSize } from '@/utils/storageCleaner';
import { storageCleanerPageStyles } from '@/config/pageTheme'; import { storageCleanerPageStyles } from '@/config/pageTheme';
import { useTranslation } from 'react-i18next'; import { useLazyTranslation } from '@/utils/useLazyTranslation';
interface OptionItemProps { interface OptionItemProps {
labelKey: string; labelKey: string;
@@ -18,7 +18,7 @@ export default function OptionItem({
isCount = false, isCount = false,
onChange, onChange,
}: OptionItemProps) { }: OptionItemProps) {
const { t } = useTranslation(['storageCleaner']); const { t } = useLazyTranslation('storageCleaner');
return ( return (
<Box sx={storageCleanerPageStyles.OPTION_ITEM(checked)}> <Box sx={storageCleanerPageStyles.OPTION_ITEM(checked)}>
<Box sx={{ flex: 1, minWidth: 0, mr: 1.5 }}> <Box sx={{ flex: 1, minWidth: 0, mr: 1.5 }}>
@@ -10,7 +10,7 @@ import {
import type { StorageCleanerOptions } from '@/types/storage'; import type { StorageCleanerOptions } from '@/types/storage';
import Button from '@/components/Button'; import Button from '@/components/Button';
import { storageCleanerPageStyles } from '@/config/pageTheme'; import { storageCleanerPageStyles } from '@/config/pageTheme';
import { useTranslation } from 'react-i18next'; import { useLazyTranslation } from '@/utils/useLazyTranslation';
export interface StorageCleanerConfirmProps { export interface StorageCleanerConfirmProps {
open: boolean; open: boolean;
@@ -25,7 +25,7 @@ export function StorageCleanerConfirm({
onConfirm, onConfirm,
options, options,
}: StorageCleanerConfirmProps) { }: StorageCleanerConfirmProps) {
const { t } = useTranslation(['storageCleaner']); const { t } = useLazyTranslation('storageCleaner');
const selectedOptions = Object.entries(options) const selectedOptions = Object.entries(options)
.filter(([_, value]) => value) .filter(([_, value]) => value)
+2 -2
View File
@@ -2,7 +2,7 @@ import { Box, Checkbox, Divider, Grid, Typography } from '@mui/material';
import type { StorageCleanerOptions } from '@/types/storage'; import type { StorageCleanerOptions } from '@/types/storage';
import OptionItem from './OptionItem'; import OptionItem from './OptionItem';
import { storageCleanerPageStyles } from '@/config/pageTheme'; import { storageCleanerPageStyles } from '@/config/pageTheme';
import { useTranslation } from 'react-i18next'; import { useLazyTranslation } from '@/utils/useLazyTranslation';
interface StorageOptionsGridProps { interface StorageOptionsGridProps {
options: StorageCleanerOptions; options: StorageCleanerOptions;
@@ -21,7 +21,7 @@ export default function StorageOptionsGrid({
onOptionChange, onOptionChange,
onSelectAll, onSelectAll,
}: StorageOptionsGridProps) { }: StorageOptionsGridProps) {
const { t } = useTranslation(['storageCleaner']); const { t } = useLazyTranslation('storageCleaner');
const optionKeys: { key: keyof StorageCleanerOptions; isCount?: boolean }[] = [ const optionKeys: { key: keyof StorageCleanerOptions; isCount?: boolean }[] = [
{ key: 'localStorage' }, { key: 'localStorage' },
+2 -2
View File
@@ -18,7 +18,7 @@ import {
isRestrictedUrl, isRestrictedUrl,
} from '@/utils/storageCleaner'; } from '@/utils/storageCleaner';
import { MessageAction, sendMessage } from '@/utils/messages'; import { MessageAction, sendMessage } from '@/utils/messages';
import { useTranslation } from 'react-i18next'; import { useLazyTranslation } from '@/utils/useLazyTranslation';
const DEFAULT_OPTIONS: StorageCleanerOptions = { const DEFAULT_OPTIONS: StorageCleanerOptions = {
localStorage: true, localStorage: true,
@@ -66,7 +66,7 @@ export interface UseStorageCleanerOptions {
export function useStorageCleaner({ export function useStorageCleaner({
showMessage, showMessage,
}: UseStorageCleanerOptions): UseStorageCleanerReturn { }: UseStorageCleanerOptions): UseStorageCleanerReturn {
const { t } = useTranslation(['storageCleaner', 'common']); const { t } = useLazyTranslation(['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);
+2 -2
View File
@@ -5,7 +5,7 @@ import CopyButton from '@/components/CopyButton';
import { useSnackbar } from '@/components/GlobalSnackbar'; import { useSnackbar } from '@/components/GlobalSnackbar';
import type { UnitType } from '@/config/pageTheme'; import type { UnitType } from '@/config/pageTheme';
import { timestampPageStyles } from '@/config/pageTheme'; import { timestampPageStyles } from '@/config/pageTheme';
import { useTranslation } from 'react-i18next'; import { useLazyTranslation } from '@/utils/useLazyTranslation';
interface LiveClockProps { interface LiveClockProps {
unit: UnitType; unit: UnitType;
@@ -14,7 +14,7 @@ interface LiveClockProps {
const LiveClock = React.memo(({ unit, onUseNow }: LiveClockProps) => { const LiveClock = React.memo(({ unit, onUseNow }: LiveClockProps) => {
const [now, setNow] = useState(() => Date.now()); const [now, setNow] = useState(() => Date.now());
const { t } = useTranslation(['timestamp']); const { t } = useLazyTranslation('timestamp');
const { showMessage } = useSnackbar(); const { showMessage } = useSnackbar();
const onUseNowRef = useRef(onUseNow); const onUseNowRef = useRef(onUseNow);
+2 -2
View File
@@ -4,7 +4,7 @@ import dayjs from '@/utils/dayjs';
import CopyButton from '@/components/CopyButton'; import CopyButton from '@/components/CopyButton';
import type { UnitType } from '@/config/pageTheme'; import type { UnitType } from '@/config/pageTheme';
import { DATE_FORMAT, timestampPageStyles } from '@/config/pageTheme'; import { DATE_FORMAT, timestampPageStyles } from '@/config/pageTheme';
import { useTranslation } from 'react-i18next'; import { useLazyTranslation } from '@/utils/useLazyTranslation';
interface ResultViewProps { interface ResultViewProps {
result: string; result: string;
@@ -17,7 +17,7 @@ interface ResultViewProps {
const ResultView = React.memo( const ResultView = React.memo(
({ result, mode, unit, zone, showEmptyPlaceholder = false }: ResultViewProps) => { ({ result, mode, unit, zone, showEmptyPlaceholder = false }: ResultViewProps) => {
const { t } = useTranslation(['timestamp']); const { t } = useLazyTranslation('timestamp');
const extraInfo = useMemo(() => { const extraInfo = useMemo(() => {
if (!result) return null; if (!result) return null;
+2 -2
View File
@@ -2,7 +2,7 @@ import { useCallback, useState } from 'react';
import dayjs from '@/utils/dayjs'; import dayjs from '@/utils/dayjs';
import type { UnitType, ZoneType } from '@/config/pageTheme'; import type { UnitType, ZoneType } from '@/config/pageTheme';
import { DATE_FORMAT } from '@/config/pageTheme'; import { DATE_FORMAT } from '@/config/pageTheme';
import { useTranslation } from 'react-i18next'; import { useLazyTranslation } from '@/utils/useLazyTranslation';
import { useContextMenuData } from '@/utils/useContextMenuData'; import { useContextMenuData } from '@/utils/useContextMenuData';
export interface UseTimestampConverterReturn { export interface UseTimestampConverterReturn {
@@ -34,7 +34,7 @@ function isTimestampLike(input: string): boolean {
} }
export function useTimestampConverter(): UseTimestampConverterReturn { export function useTimestampConverter(): UseTimestampConverterReturn {
const { t } = useTranslation(['timestamp']); const { t } = useLazyTranslation('timestamp');
const [mode, setMode] = useState<'ts2dt' | 'dt2ts'>('ts2dt'); const [mode, setMode] = useState<'ts2dt' | 'dt2ts'>('ts2dt');
const [tsInput, setTsInput] = useState(() => String(Date.now())); const [tsInput, setTsInput] = useState(() => String(Date.now()));
const [dtInput, setDtInput] = useState(() => dayjs().format(DATE_FORMAT)); const [dtInput, setDtInput] = useState(() => dayjs().format(DATE_FORMAT));