refactor(i18n): 移除 chrome.i18n 国际化,统一使用中文硬编码
移除 chromeI18n 工具、_locales 翻译文件和 manifest default_locale 配置, 将所有 UI 文案改为直接硬编码中文,并更新相关测试与文档。
This commit is contained in:
@@ -4,7 +4,6 @@ import { copyTextToClipboard } from '@/utils/clipboard';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { buttonVariants, type ButtonProps } from '@/components/ui/button';
|
||||
import { toast } from 'sonner';
|
||||
import { useI18n } from '@/utils/chromeI18n';
|
||||
|
||||
interface CopyButtonProps extends Omit<ButtonProps, 'children' | 'onClick'> {
|
||||
text: string;
|
||||
@@ -19,7 +18,6 @@ export const CopyButton: React.FC<CopyButtonProps> = ({
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
const { t } = useI18n('common');
|
||||
const [copied, setCopied] = useState(false);
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
@@ -33,18 +31,18 @@ export const CopyButton: React.FC<CopyButtonProps> = ({
|
||||
e.stopPropagation();
|
||||
|
||||
if (!text) {
|
||||
toast.error(t('messages.copyEmpty'));
|
||||
toast.error('无内容可复制');
|
||||
return;
|
||||
}
|
||||
|
||||
const success = await copyTextToClipboard(text);
|
||||
if (success) {
|
||||
toast.success(t('messages.copySuccess'));
|
||||
toast.success('已复制到剪贴板');
|
||||
setCopied(true);
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
timerRef.current = setTimeout(() => setCopied(false), 1500);
|
||||
} else {
|
||||
toast.error(t('messages.copyError'));
|
||||
toast.error('复制失败');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -52,8 +50,8 @@ export const CopyButton: React.FC<CopyButtonProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
title={tooltip ?? t('buttons.copy')}
|
||||
aria-label={tooltip ?? t('buttons.copy')}
|
||||
title={tooltip ?? '复制'}
|
||||
aria-label={tooltip ?? '复制'}
|
||||
className={cn(
|
||||
buttonVariants({ variant, size }),
|
||||
copied &&
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Component, ErrorInfo, ReactNode } from 'react';
|
||||
import { AlertCircle, RefreshCw } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { getMessage } from '@/utils/chromeI18n';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
@@ -44,11 +43,9 @@ class ErrorBoundary extends Component<Props, State> {
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-destructive/10 text-destructive mx-auto mb-4">
|
||||
<AlertCircle className="h-8 w-8" />
|
||||
</div>
|
||||
<h2 className="text-xl font-extrabold text-destructive mb-2">
|
||||
{getMessage('errorBoundary_title')}
|
||||
</h2>
|
||||
<h2 className="text-xl font-extrabold text-destructive mb-2">糟糕,出了点问题</h2>
|
||||
<p className="text-sm text-muted-foreground mb-6">
|
||||
{getMessage('errorBoundary_description')}
|
||||
应用遇到了一些意外错误。您可以尝试刷新页面或重置应用。
|
||||
</p>
|
||||
{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">
|
||||
@@ -63,7 +60,7 @@ class ErrorBoundary extends Component<Props, State> {
|
||||
className="rounded-lg font-bold shadow-sm"
|
||||
>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
{getMessage('errorBoundary_refresh')}
|
||||
刷新应用
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Component, ErrorInfo, ReactNode } from 'react';
|
||||
import { AlertCircle, RefreshCw } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { getMessage } from '@/utils/chromeI18n';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
@@ -46,11 +45,9 @@ class PageErrorBoundary extends Component<Props, State> {
|
||||
<AlertCircle className="h-6 w-6" />
|
||||
</div>
|
||||
|
||||
<h3 className="text-base font-semibold text-foreground mb-1.5">
|
||||
{getMessage('pageErrorBoundary_title')}
|
||||
</h3>
|
||||
<h3 className="text-base font-semibold text-foreground mb-1.5">该功能运行异常</h3>
|
||||
<p className="text-xs text-muted-foreground mb-5">
|
||||
{getMessage('pageErrorBoundary_description')}
|
||||
该页面在加载或渲染时遇到了内部脚本错误。您可以尝试重试,或者通过导航菜单切换到其他工具。
|
||||
</p>
|
||||
|
||||
{this.state.error && (
|
||||
@@ -68,7 +65,7 @@ class PageErrorBoundary extends Component<Props, State> {
|
||||
className="font-medium shadow-sm"
|
||||
>
|
||||
<RefreshCw className="mr-1.5 h-3.5 w-3.5" />
|
||||
{getMessage('errorBoundary_retry')}
|
||||
重新尝试
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { FEATURES, getEntryPointType } from '@/config/features';
|
||||
import { useRouter } from '@/providers/RouterProvider';
|
||||
import { Suspense } from 'react';
|
||||
import { useI18n } from '@/utils/chromeI18n';
|
||||
import PageErrorBoundary from '@/components/PageErrorBoundary';
|
||||
import PageSkeleton from '@/components/PageSkeleton';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -11,7 +10,6 @@ const entryPointType = getEntryPointType();
|
||||
|
||||
export default function RouterContainer() {
|
||||
const { currentPage, isLoaded } = useRouter();
|
||||
const { t } = useI18n('common');
|
||||
|
||||
const animationClass =
|
||||
currentPage === 'dashboard' ? 'page-transition-dashboard' : 'page-transition-enter';
|
||||
@@ -43,9 +41,9 @@ export default function RouterContainer() {
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-destructive/10 text-destructive mb-4">
|
||||
<AlertTriangle className="h-6 w-6" />
|
||||
</div>
|
||||
<h3 className="text-sm font-semibold text-foreground">{t('router.notFound')}</h3>
|
||||
<h3 className="text-sm font-semibold text-foreground">{'页面未找到'}</h3>
|
||||
<p className="text-xs text-muted-foreground mt-1 max-w-[240px]">
|
||||
{t('router.notFoundDescription', { entryPointType })}
|
||||
{`该功能在当前运行环境(${entryPointType})下不可用或已被移除。`}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react';
|
||||
import { X } from 'lucide-react';
|
||||
import { useI18n } from '@/utils/chromeI18n';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { toast } from 'sonner'; // 推荐使用 shadcn 的默认 Toast
|
||||
import { CopyButton } from '@/components/CopyButton';
|
||||
@@ -114,8 +113,7 @@ const TextInputArea = forwardRef<HTMLTextAreaElement, TextInputAreaProps>((props
|
||||
const [internalValue, setInternalValue] = useState(defaultValue);
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
const { t } = useI18n('common');
|
||||
const placeholder = placeholderProp ?? t('textInputArea.placeholder');
|
||||
const placeholder = placeholderProp ?? '请输入文本';
|
||||
|
||||
const isControlled = controlledValue !== undefined;
|
||||
const value = isControlled ? controlledValue : internalValue;
|
||||
@@ -159,7 +157,7 @@ const TextInputArea = forwardRef<HTMLTextAreaElement, TextInputAreaProps>((props
|
||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const newVal = e.target.value;
|
||||
if (maxLength && newVal.length > maxLength) {
|
||||
const msg = t('charCount', { count: maxLength });
|
||||
const msg = `内容不能超过 ${maxLength} 个字符`;
|
||||
setError(msg);
|
||||
toast.warning(msg);
|
||||
return;
|
||||
@@ -181,9 +179,9 @@ const TextInputArea = forwardRef<HTMLTextAreaElement, TextInputAreaProps>((props
|
||||
onChange?.('');
|
||||
setError('');
|
||||
internalRef.current?.focus();
|
||||
toast.success(t('textInputArea.cleared'));
|
||||
toast.success('已清空');
|
||||
onClear?.();
|
||||
}, [isControlled, onChange, onClear, t]);
|
||||
}, [isControlled, onChange, onClear]);
|
||||
|
||||
const handleAction = useCallback(
|
||||
(action: ToolbarAction) => {
|
||||
@@ -272,18 +270,13 @@ const TextInputArea = forwardRef<HTMLTextAreaElement, TextInputAreaProps>((props
|
||||
{/* 右侧系统按钮组 */}
|
||||
<div className="flex items-center gap-1.5 ml-auto shrink-0">
|
||||
{allowCopy && value && (
|
||||
<CopyButton
|
||||
text={value}
|
||||
tooltip={t('textInputArea.copyContent')}
|
||||
size="sm"
|
||||
className="h-7 w-7 p-1"
|
||||
/>
|
||||
<CopyButton text={value} tooltip={'复制内容'} size="sm" className="h-7 w-7 p-1" />
|
||||
)}
|
||||
{showClear && value && !disabled && !readOnly && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClear}
|
||||
aria-label={t('textInputArea.clear')}
|
||||
aria-label={'清空'}
|
||||
className="p-1 h-7 w-7 flex items-center justify-center rounded-md text-muted-foreground hover:text-destructive hover:bg-destructive/10 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
|
||||
+23
-21
@@ -5,7 +5,6 @@ import { useThemeMode } from '@/providers/ThemeModeProvider';
|
||||
import { FeatureConfig, FEATURES } from '@/config/features';
|
||||
import { storageUtil } from '@/utils/chromeStorage';
|
||||
import { openExtensionPage } from '@/utils/chromeTabs';
|
||||
import { useI18n } from '@/utils/chromeI18n';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const SEARCH_HISTORY_LIMIT = 10;
|
||||
@@ -14,7 +13,6 @@ const SEARCH_HISTORY_DISPLAY = 5;
|
||||
export default function TopBar() {
|
||||
const { currentPage, goBack, navigateTo } = useRouter();
|
||||
const { mode, setMode } = useThemeMode();
|
||||
const { t } = useI18n(['common', 'features']);
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [showResults, setShowResults] = useState(false);
|
||||
@@ -66,12 +64,9 @@ export default function TopBar() {
|
||||
if (!query) return [];
|
||||
return FEATURES.filter((f) => {
|
||||
if (f.key === 'dashboard') return false;
|
||||
return (
|
||||
t(f.labelKey).toLowerCase().includes(query) ||
|
||||
t(f.descriptionKey).toLowerCase().includes(query)
|
||||
);
|
||||
return f.label.toLowerCase().includes(query) || f.description.toLowerCase().includes(query);
|
||||
});
|
||||
}, [searchQuery, t]);
|
||||
}, [searchQuery]);
|
||||
|
||||
const displayedHistory = useMemo(() => {
|
||||
if (searchQuery.trim()) return [];
|
||||
@@ -144,7 +139,7 @@ export default function TopBar() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={goBack}
|
||||
aria-label={t('common_buttons_back')}
|
||||
aria-label={'返回'}
|
||||
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" />
|
||||
@@ -159,7 +154,7 @@ export default function TopBar() {
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
placeholder={t('common_buttons_search')}
|
||||
placeholder={'搜索工具...'}
|
||||
value={searchQuery}
|
||||
onChange={(e) => {
|
||||
setSearchQuery(e.target.value);
|
||||
@@ -168,7 +163,7 @@ export default function TopBar() {
|
||||
}}
|
||||
onFocus={() => setShowResults(true)}
|
||||
onKeyDown={handleKeyDown}
|
||||
aria-label={t('common_buttons_search')}
|
||||
aria-label={'搜索工具...'}
|
||||
className="w-full h-9 pl-9 pr-16 text-sm rounded-lg border border-border/60 bg-muted/40 transition-all placeholder:text-muted-foreground/50 focus:bg-background focus:outline-none focus:ring-1 focus:ring-ring focus:border-input"
|
||||
/>
|
||||
{!searchQuery && (
|
||||
@@ -183,7 +178,7 @@ export default function TopBar() {
|
||||
setSearchQuery('');
|
||||
setSelectedIndex(-1);
|
||||
}}
|
||||
aria-label={t('common:buttons.clearSearch')}
|
||||
aria-label={'清除搜索'}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 rounded-md text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
@@ -219,24 +214,22 @@ export default function TopBar() {
|
||||
{feature.icon && <feature.icon className="h-4 w-4" />}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium text-foreground truncate">
|
||||
{t(feature.labelKey)}
|
||||
</p>
|
||||
<p className="font-medium text-foreground truncate">{feature.label}</p>
|
||||
<p className="text-xs text-muted-foreground truncate mt-0.5">
|
||||
{t(feature.descriptionKey)}
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
))
|
||||
) : (
|
||||
<li className="px-4 py-6 text-center text-sm text-muted-foreground">
|
||||
{t('common:buttons.noResults')}
|
||||
{'未找到相关工具'}
|
||||
</li>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<div className="px-3 py-1.5 text-[11px] font-semibold tracking-wider text-muted-foreground/60 uppercase">
|
||||
{t('common:buttons.recentSearch')}
|
||||
{'最近搜索'}
|
||||
</div>
|
||||
{displayedHistory.map((item, index) => (
|
||||
<li
|
||||
@@ -261,10 +254,10 @@ export default function TopBar() {
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium text-foreground truncate">
|
||||
{item.feature && t(item.feature.labelKey)}
|
||||
{item.feature?.label}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground truncate mt-0.5">
|
||||
{item.feature && t(item.feature.descriptionKey)}
|
||||
{item.feature?.description}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
@@ -278,10 +271,19 @@ export default function TopBar() {
|
||||
|
||||
{/* 右侧:操作区 */}
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<IconButton onClick={cycleThemeMode} title={t(`common:buttons.themeMode.${mode}`)}>
|
||||
<IconButton
|
||||
onClick={cycleThemeMode}
|
||||
title={
|
||||
mode === 'light'
|
||||
? '切换到深色模式'
|
||||
: mode === 'dark'
|
||||
? '切换到系统模式'
|
||||
: '切换到浅色模式'
|
||||
}
|
||||
>
|
||||
<ThemeIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
<IconButton onClick={handleOpenInTab} title={t('common:buttons.openInTab')}>
|
||||
<IconButton onClick={handleOpenInTab} title={'在标签页打开'}>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
@@ -18,12 +18,12 @@ describe('TextInputArea 组件', () => {
|
||||
|
||||
it('默认显示清空按钮', () => {
|
||||
render(<TextInputArea value="有内容" onChange={() => {}} />);
|
||||
expect(screen.getByRole('button', { name: 'textInputArea.clear' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '清空' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('无内容时清空按钮应隐藏', () => {
|
||||
render(<TextInputArea value="" onChange={() => {}} />);
|
||||
expect(screen.queryByRole('button', { name: 'textInputArea.clear' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: '清空' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('disabled 时清空按钮应隐藏', () => {
|
||||
@@ -57,7 +57,7 @@ describe('TextInputArea 组件', () => {
|
||||
const handleChange = vi.fn();
|
||||
render(<TextInputArea value="内容" onChange={handleChange} />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'textInputArea.clear' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '清空' }));
|
||||
|
||||
expect(handleChange).toHaveBeenCalledWith('');
|
||||
});
|
||||
@@ -81,7 +81,7 @@ describe('TextInputArea 组件', () => {
|
||||
it('清空按钮应清空内容', () => {
|
||||
render(<TextInputArea defaultValue="内容" />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'textInputArea.clear' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '清空' }));
|
||||
|
||||
expect(screen.getByRole('textbox')).toHaveValue('');
|
||||
});
|
||||
@@ -90,14 +90,12 @@ describe('TextInputArea 组件', () => {
|
||||
describe('allowCopy 复制功能', () => {
|
||||
it('allowCopy 且有内容时显示复制按钮', () => {
|
||||
render(<TextInputArea value="可复制的内容" onChange={() => {}} allowCopy />);
|
||||
expect(screen.getByRole('button', { name: 'textInputArea.copyContent' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '复制内容' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('allowCopy 但无内容时隐藏复制按钮', () => {
|
||||
render(<TextInputArea value="" onChange={() => {}} allowCopy />);
|
||||
expect(
|
||||
screen.queryByRole('button', { name: 'textInputArea.copyContent' }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: '复制内容' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('allowCopy=false 时不显示复制按钮', () => {
|
||||
@@ -113,7 +111,7 @@ describe('TextInputArea 组件', () => {
|
||||
|
||||
render(<TextInputArea value="测试" onChange={() => {}} allowCopy />);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'textInputArea.copyContent' }));
|
||||
await user.click(screen.getByRole('button', { name: '复制内容' }));
|
||||
|
||||
expect(writeTextSpy).toHaveBeenCalledWith('测试');
|
||||
});
|
||||
@@ -126,7 +124,7 @@ describe('TextInputArea 组件', () => {
|
||||
|
||||
render(<TextInputArea value="测试" onChange={() => {}} allowCopy />);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'textInputArea.copyContent' }));
|
||||
await user.click(screen.getByRole('button', { name: '复制内容' }));
|
||||
|
||||
expect(writeTextSpy).toHaveBeenCalledWith('测试');
|
||||
});
|
||||
@@ -158,6 +156,7 @@ describe('TextInputArea 组件', () => {
|
||||
fireEvent.change(textarea, { target: { value: '123456' } });
|
||||
|
||||
expect(handleChange).not.toHaveBeenCalledWith('123456');
|
||||
expect(screen.getByText('内容不能超过 5 个字符')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('未超出 maxLength 的输入应正常触发', () => {
|
||||
@@ -370,7 +369,7 @@ describe('TextInputArea 组件', () => {
|
||||
const writeTextSpy = vi.spyOn(navigator.clipboard, 'writeText').mockResolvedValue(undefined);
|
||||
|
||||
render(<TextInputArea value="测试" onChange={() => {}} allowCopy />);
|
||||
await user.click(screen.getByRole('button', { name: 'textInputArea.copyContent' }));
|
||||
await user.click(screen.getByRole('button', { name: '复制内容' }));
|
||||
|
||||
expect(writeTextSpy).toHaveBeenCalledWith('测试');
|
||||
});
|
||||
@@ -381,7 +380,7 @@ describe('TextInputArea 组件', () => {
|
||||
const handleClear = vi.fn();
|
||||
render(<TextInputArea value="内容" onChange={() => {}} onClear={handleClear} />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'textInputArea.clear' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '清空' }));
|
||||
|
||||
expect(handleClear).toHaveBeenCalledOnce();
|
||||
});
|
||||
@@ -389,7 +388,7 @@ describe('TextInputArea 组件', () => {
|
||||
it('不传 onClear 时清空按钮应正常工作', () => {
|
||||
render(<TextInputArea defaultValue="内容" />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'textInputArea.clear' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '清空' }));
|
||||
|
||||
expect(screen.getByRole('textbox')).toHaveValue('');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user