refactor(i18n): 移除 chrome.i18n 国际化,统一使用中文硬编码

移除 chromeI18n 工具、_locales 翻译文件和 manifest default_locale 配置,
将所有 UI 文案改为直接硬编码中文,并更新相关测试与文档。
This commit is contained in:
雨霖铃
2026-06-18 14:22:14 +00:00
parent 618f3c36f2
commit ec92afa294
68 changed files with 531 additions and 2148 deletions
@@ -1,7 +1,6 @@
import { Loader2, Pencil, QrCode } from 'lucide-react';
import TextInputArea from '@/components/TextInputArea';
import QrCodePreview from './QrCodePreview';
import { useI18n } from '@/utils/chromeI18n';
import { useQrCodeContext } from '../contexts/QrCodeContext';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
@@ -11,7 +10,6 @@ import { cn } from '@/lib/utils';
const TEXT_PREVIEW_MAX_LENGTH = 80;
export default function GeneratePanel() {
const { t } = useI18n('qrCode');
const {
generatorState,
setTextToEncode,
@@ -42,13 +40,13 @@ export default function GeneratePanel() {
>
<div className="flex flex-col space-y-2.5">
<Label className="text-[10px] font-bold text-muted-foreground/90 uppercase tracking-wider select-none pl-0.5">
{t('qrCode:urlInputLabel')}
{'输入 URL 或文本'}
</Label>
<TextInputArea
value={generatorState.textToEncode}
onChange={setTextToEncode}
placeholder={t('qrCode:urlInputPlaceholder')}
placeholder={'请输入 URL 或文本内容,将自动生成二维码'}
showCount={true}
showClear={true}
allowCopy={false}
@@ -66,12 +64,12 @@ export default function GeneratePanel() {
{generatorState.generating ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
{t('qrCode:generating')}
{'生成中...'}
</>
) : (
<>
<QrCode className="w-4 h-4 mr-2" />
{t('qrCode:generateButton')}
{'生成二维码'}
</>
)}
</Button>
@@ -88,11 +86,11 @@ export default function GeneratePanel() {
<div className="border border-border rounded-xl bg-card text-card-foreground shadow-sm p-3 mb-3">
<div className="flex items-center justify-between mb-1.5">
<Label className="text-[10px] font-bold text-muted-foreground/90 uppercase tracking-wider select-none pl-0.5">
{t('qrCode:textPreviewLabel')}
{'原始文本'}
</Label>
<Button variant="ghost" size="sm" onClick={backToEdit} className="h-6 px-2 text-xs">
<Pencil className="w-3 h-3 mr-1" />
{t('qrCode:editButton')}
{'编辑'}
</Button>
</div>
<div
+8 -10
View File
@@ -3,7 +3,6 @@ import { Image, X } from 'lucide-react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { useI18n } from '@/utils/chromeI18n';
interface ImageUploaderProps {
/** 选中的文件 */
@@ -31,7 +30,6 @@ const ImageUploader = ({
dragging,
onDraggingChange,
}: ImageUploaderProps) => {
const { t } = useI18n('qrCode');
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileChange = useCallback(
@@ -47,8 +45,8 @@ const ImageUploader = ({
URL.revokeObjectURL(previewUrl);
}
onClearFile();
toast.success(t('qrCode:imageCleared'));
}, [previewUrl, onClearFile, t]);
toast.success('图片已清除');
}, [previewUrl, onClearFile]);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) {
@@ -88,10 +86,10 @@ const ImageUploader = ({
if (file) {
try {
handleFileChange(file);
toast.success(t('qrCode:imagePasted'));
toast.success('图片粘贴成功,正在解析...');
} catch (error) {
console.error('处理粘贴图片失败:', error);
toast.error(t('qrCode:imagePasteError'));
toast.error('粘贴图片失败,请重试');
}
}
break;
@@ -104,7 +102,7 @@ const ImageUploader = ({
return () => {
document.removeEventListener('paste', handlePaste);
};
}, [handleFileChange, t]);
}, [handleFileChange]);
return (
<div
@@ -151,7 +149,7 @@ const ImageUploader = ({
</Button>
</div>
<span className="block text-sm text-muted-foreground mt-2">{selectedFile.name}</span>
<span className="block text-xs text-muted-foreground">{t('qrCode:clickToChange')}</span>
<span className="block text-xs text-muted-foreground">{'点击更换图片'}</span>
</div>
) : (
<>
@@ -160,10 +158,10 @@ const ImageUploader = ({
className="w-12 h-12 text-muted-foreground mx-auto mb-2"
/>
<span className="block text-sm text-muted-foreground mb-1">
{t('qrCode:clickToUpload')}
{'点击、拖拽或粘贴上传二维码图片'}
</span>
<span className="block text-xs text-muted-foreground">
{t('qrCode:supportFormats')}
{'支持 PNG、JPG、WEBP、Base64 格式'}
</span>
</>
)}
+9 -11
View File
@@ -3,14 +3,12 @@ import { RefreshCw } from 'lucide-react';
import { toast } from 'sonner';
import TextInputArea from '@/components/TextInputArea';
import ImageUploader from './ImageUploader';
import { useI18n } from '@/utils/chromeI18n';
import { useQrCodeContext } from '../contexts/QrCodeContext';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { cn } from '@/lib/utils';
export default function ParsePanel() {
const { t } = useI18n('qrCode');
const { parserState, setParserState, handleFileChange, handleClearFile } = useQrCodeContext();
const hasFile = parserState.selectedFile !== null;
@@ -27,7 +25,7 @@ export default function ParsePanel() {
const file = items[i].getAsFile();
if (file) {
handleFileChange(file);
toast.success(t('qrCode:imagePasted'));
toast.success('图片粘贴成功,正在解析...');
}
return;
}
@@ -41,14 +39,14 @@ export default function ParsePanel() {
const blob = await response.blob();
const file = new File([blob], 'pasted-image.png', { type: blob.type });
handleFileChange(file);
toast.success(t('qrCode:imagePasted'));
toast.success('图片粘贴成功,正在解析...');
} catch (error) {
console.error('处理 Base64 图片失败:', error);
toast.error(t('qrCode:imagePasteError'));
toast.error('粘贴图片失败,请重试');
}
}
},
[handleFileChange, t],
[handleFileChange],
);
useEffect(() => {
@@ -82,11 +80,11 @@ export default function ParsePanel() {
<div className="border border-border rounded-xl bg-card text-card-foreground shadow-sm p-3 mb-3">
<div className="flex items-center justify-between mb-1.5">
<Label className="text-[10px] font-bold text-muted-foreground/90 uppercase tracking-wider select-none pl-0.5">
{t('qrCode:uploadedImage')}
{'已上传图片'}
</Label>
<Button variant="ghost" size="sm" onClick={handleClearFile} className="h-6 px-2 text-xs">
<RefreshCw className="w-3 h-3 mr-1" />
{t('qrCode:reuploadButton')}
{'重新上传'}
</Button>
</div>
<div className="flex items-center gap-3 bg-muted/50 rounded-md p-2">
@@ -98,7 +96,7 @@ export default function ParsePanel() {
<div className="flex-1 min-w-0">
<p className="text-sm text-foreground truncate">{parserState.selectedFile?.name}</p>
{parserState.parsing && (
<p className="text-xs text-primary animate-pulse mt-1">{t('qrCode:parsing')}</p>
<p className="text-xs text-primary animate-pulse mt-1">{'解析中...'}</p>
)}
</div>
</div>
@@ -113,7 +111,7 @@ export default function ParsePanel() {
>
<div className="flex flex-col space-y-2.5">
<Label className="text-[10px] font-bold text-muted-foreground/90 uppercase tracking-wider pl-0.5">
{t('qrCode:resultLabel')}
{'解析结果'}
</Label>
<TextInputArea
@@ -121,7 +119,7 @@ export default function ParsePanel() {
readOnly={true}
showClear={false}
allowCopy={true}
placeholder={parserState.parsing ? '' : t('qrCode:resultPlaceholder')}
placeholder={parserState.parsing ? '' : '解析结果将显示在此处'}
minRows={4}
maxRows={8}
externalError={parserState.parseError || undefined}
@@ -1,6 +1,5 @@
import React from 'react';
import { Copy, Download } from 'lucide-react';
import { useI18n } from '@/utils/chromeI18n';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
@@ -24,8 +23,6 @@ const QrCodePreview = ({
className,
...props
}: QrCodePreviewProps) => {
const { t } = useI18n('qrCode');
// 空状态下的虚线骨架屏
if (!qrCodeDataUrl) {
return (
@@ -37,7 +34,7 @@ const QrCodePreview = ({
{...props}
>
<p className="text-sm text-muted-foreground text-center">
{placeholderText || t('qrCode:qrCodeWillShow')}
{placeholderText || '二维码将显示在这里'}
</p>
</div>
);
@@ -68,12 +65,12 @@ const QrCodePreview = ({
<div className="flex w-full gap-2 mt-3">
<Button variant="outline" size="sm" onClick={onDownload} className="flex-1 h-8">
<Download className="w-3.5 h-3.5 text-muted-foreground" />
<span className="truncate text-xs">{t('qrCode:downloadButton')}</span>
<span className="truncate text-xs">{'下载二维码'}</span>
</Button>
<Button variant="default" size="sm" onClick={onCopy} className="flex-1 h-8">
<Copy className="w-3.5 h-3.5" />
<span className="truncate text-xs">{t('qrCode:copyQrButton')}</span>
<span className="truncate text-xs">{'复制二维码'}</span>
</Button>
</div>
</div>
+88 -100
View File
@@ -3,7 +3,6 @@ import QRious from 'qrious';
import { toast } from 'sonner';
import { parseQrCodeFromFile } from '@/utils/qrCodeParser';
import { useContextMenuData } from '@/utils/useContextMenuData';
import { useI18n } from '@/utils/chromeI18n';
import type { QrCodeContextValue } from '../contexts/QrCodeContext';
import type { QrCodeGeneratorState, QrCodeMode, QrCodeParserState } from '../types';
@@ -78,8 +77,6 @@ function generateQrCodeDataUrl(text: string): string {
}
export function useQrCode(): QrCodeContextValue {
const { t } = useI18n('qrCode');
const [mode, setMode] = useState<QrCodeMode>('generate');
const [generatorState, setGeneratorState] = useState<QrCodeGeneratorState>({
@@ -112,40 +109,37 @@ export function useQrCode(): QrCodeContextValue {
}, []);
/** 自动检测URL并生成二维码 */
const autoGenerateIfUrl = useCallback(
(text: string) => {
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
const autoGenerateIfUrl = useCallback((text: string) => {
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
if (!isUrl(text)) {
return;
}
debounceTimerRef.current = setTimeout(() => {
setGeneratorState((prev) => ({ ...prev, generating: true, inputError: '' }));
const qrCodeDataUrl = generateQrCodeDataUrl(text);
if (qrCodeDataUrl) {
setGeneratorState((prev) => ({
...prev,
step: 'preview',
savedText: text.trim(),
qrCodeDataUrl,
generating: false,
}));
} else {
setGeneratorState((prev) => ({
...prev,
generating: false,
inputError: '生成二维码失败,请重试',
}));
}
if (!isUrl(text)) {
return;
}
debounceTimerRef.current = setTimeout(() => {
setGeneratorState((prev) => ({ ...prev, generating: true, inputError: '' }));
const qrCodeDataUrl = generateQrCodeDataUrl(text);
if (qrCodeDataUrl) {
setGeneratorState((prev) => ({
...prev,
step: 'preview',
savedText: text.trim(),
qrCodeDataUrl,
generating: false,
}));
} else {
setGeneratorState((prev) => ({
...prev,
generating: false,
inputError: t('qrCode:generateError'),
}));
}
}, DEBOUNCE_DELAY);
},
[t],
);
}, DEBOUNCE_DELAY);
}, []);
const setTextToEncode = useCallback(
(text: string) => {
@@ -160,8 +154,8 @@ export function useQrCode(): QrCodeContextValue {
const text = generatorState.textToEncode.trim();
if (!text) {
setGeneratorState((prev) => ({ ...prev, inputError: t('qrCode:inputRequired') }));
toast.error(t('qrCode:inputRequired'));
setGeneratorState((prev) => ({ ...prev, inputError: '请输入内容' }));
toast.error('请输入内容');
return;
}
@@ -176,9 +170,9 @@ export function useQrCode(): QrCodeContextValue {
setGeneratorState((prev) => ({
...prev,
generating: false,
inputError: t('qrCode:generateError'),
inputError: '生成二维码失败,请重试',
}));
toast.error(t('qrCode:generateError'));
toast.error('生成二维码失败,请重试');
return;
}
@@ -190,7 +184,7 @@ export function useQrCode(): QrCodeContextValue {
generating: false,
}));
}, 0);
}, [generatorState.textToEncode, t]);
}, [generatorState.textToEncode]);
/** 返回编辑态,保留上次输入内容 */
const backToEdit = useCallback(() => {
@@ -207,46 +201,43 @@ export function useQrCode(): QrCodeContextValue {
}, []);
/** 从图片URL解析二维码 */
const parseQrCodeFromUrl = useCallback(
async (imageUrl: string) => {
try {
setParserState((prev) => ({
...prev,
parsing: true,
parseError: '',
decodedResult: '',
previewUrl: imageUrl,
selectedFile: null,
}));
const parseQrCodeFromUrl = useCallback(async (imageUrl: string) => {
try {
setParserState((prev) => ({
...prev,
parsing: true,
parseError: '',
decodedResult: '',
previewUrl: imageUrl,
selectedFile: null,
}));
// 从URL获取图片并转换为File对象
const response = await fetch(imageUrl);
const blob = await response.blob();
const file = new File([blob], 'qrcode-image.png', { type: blob.type });
// 从URL获取图片并转换为File对象
const response = await fetch(imageUrl);
const blob = await response.blob();
const file = new File([blob], 'qrcode-image.png', { type: blob.type });
setParserState((prev) => ({ ...prev, selectedFile: file }));
setParserState((prev) => ({ ...prev, selectedFile: file }));
const result = await parseQrCodeFromFile(file);
const result = await parseQrCodeFromFile(file);
if (result.success && result.data) {
setParserState((prev) => ({ ...prev, decodedResult: result.data! }));
toast.success(t('qrCode:parseSuccess'));
} else {
const errorMsg = result.error || t('qrCode:noQrDetected');
setParserState((prev) => ({ ...prev, parseError: errorMsg }));
toast.error(errorMsg);
}
} catch (error) {
console.error('解析图片二维码失败:', error);
const errorMsg = error instanceof Error ? error.message : t('qrCode:parseError');
if (result.success && result.data) {
setParserState((prev) => ({ ...prev, decodedResult: result.data! }));
toast.success('二维码解析成功');
} else {
const errorMsg = result.error || '未检测到二维码,请确保图片清晰且包含二维码';
setParserState((prev) => ({ ...prev, parseError: errorMsg }));
toast.error(errorMsg);
} finally {
setParserState((prev) => ({ ...prev, parsing: false }));
}
},
[t],
);
} catch (error) {
console.error('解析图片二维码失败:', error);
const errorMsg = error instanceof Error ? error.message : '解析二维码失败,请重试';
setParserState((prev) => ({ ...prev, parseError: errorMsg }));
toast.error(errorMsg);
} finally {
setParserState((prev) => ({ ...prev, parsing: false }));
}
}, []);
/** 右键菜单传入URL时,自动生成二维码或解析图片 */
const handleContextMenuData = useCallback(
@@ -294,32 +285,29 @@ export function useQrCode(): QrCodeContextValue {
useContextMenuData({ featureKey: 'qrCode', onData: handleContextMenuData });
// 反向活态解析二维码算法
const parseQrCode = useCallback(
async (file: File) => {
try {
setParserState((prev) => ({ ...prev, parsing: true, parseError: '', decodedResult: '' }));
const parseQrCode = useCallback(async (file: File) => {
try {
setParserState((prev) => ({ ...prev, parsing: true, parseError: '', decodedResult: '' }));
const result = await parseQrCodeFromFile(file);
const result = await parseQrCodeFromFile(file);
if (result.success && result.data) {
setParserState((prev) => ({ ...prev, decodedResult: result.data! }));
toast.success(t('qrCode:parseSuccess'));
} else {
const errorMsg = result.error || t('qrCode:noQrDetected');
setParserState((prev) => ({ ...prev, parseError: errorMsg }));
toast.error(errorMsg);
}
} catch (error) {
console.error('解析二维码失败:', error);
const errorMsg = error instanceof Error ? error.message : t('qrCode:parseError');
if (result.success && result.data) {
setParserState((prev) => ({ ...prev, decodedResult: result.data! }));
toast.success('二维码解析成功');
} else {
const errorMsg = result.error || '未检测到二维码,请确保图片清晰且包含二维码';
setParserState((prev) => ({ ...prev, parseError: errorMsg }));
toast.error(errorMsg);
} finally {
setParserState((prev) => ({ ...prev, parsing: false }));
}
},
[t],
);
} catch (error) {
console.error('解析二维码失败:', error);
const errorMsg = error instanceof Error ? error.message : '解析二维码失败,请重试';
setParserState((prev) => ({ ...prev, parseError: errorMsg }));
toast.error(errorMsg);
} finally {
setParserState((prev) => ({ ...prev, parsing: false }));
}
}, []);
const downloadQrCode = useCallback(() => {
if (!generatorState.qrCodeDataUrl) return;
@@ -328,8 +316,8 @@ export function useQrCode(): QrCodeContextValue {
link.href = generatorState.qrCodeDataUrl;
link.download = 'qrcode.png';
link.click();
toast.success(t('qrCode:qrCodeDownloadSuccess'));
}, [generatorState.qrCodeDataUrl, t]);
toast.success('二维码下载成功');
}, [generatorState.qrCodeDataUrl]);
const copyQrCode = useCallback(async () => {
if (!generatorState.qrCodeDataUrl) return;
@@ -344,12 +332,12 @@ export function useQrCode(): QrCodeContextValue {
}),
]);
toast.success(t('qrCode:qrCodeCopySuccess'));
toast.success('二维码已复制到剪贴板');
} catch (error) {
console.error('复制二维码失败:', error);
toast.error(t('qrCode:copyError'));
toast.error('复制失败,请重试');
}
}, [generatorState.qrCodeDataUrl, t]);
}, [generatorState.qrCodeDataUrl]);
const handleFileChange = useCallback(
(file: File) => {
+2 -4
View File
@@ -1,6 +1,5 @@
import SwitchButtonGroup from '@/components/SwitchButtonGroup';
import { useI18n } from '@/utils/chromeI18n';
import { QrCodeContext } from './contexts/QrCodeContext';
import { useQrCode } from './hooks/useQrCode';
import GeneratePanel from './components/GeneratePanel';
@@ -8,13 +7,12 @@ import ParsePanel from './components/ParsePanel';
import type { QrCodeMode } from './types';
export default function Index() {
const { t } = useI18n('qrCode');
const qrCode = useQrCode();
// 模式选项驱动骨架
const modeOptions = [
{ value: 'generate' as QrCodeMode, label: t('qrCode:urlToQr') },
{ value: 'parse' as QrCodeMode, label: t('qrCode:qrToUrl') },
{ value: 'generate' as QrCodeMode, label: '文本转二维码' },
{ value: 'parse' as QrCodeMode, label: '二维码转文本' },
];
return (