feat: i18n 中英双语界面 — i18next + react-i18next
- 安装 i18next / react-i18next / i18next-browser-languagedetector - 新建 src/i18n.ts 配置(fallback zh) - 中/英翻译文件各 ~50 条目 - App.tsx 新增 EN/中 语言切换按钮 - ExportPanel + QrPreview + ModePanel + HistoryList + ErrorBoundary - 全部 7 种模式组件均支持双语 - 12 前端测试通过,tsc 零错误
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQrState } from '../store/qrContext';
|
||||
import { useQrEncode } from '../hooks/useQrEncode';
|
||||
import { buildEmailText } from '../utils/qrText';
|
||||
|
||||
export default function EmailMode() {
|
||||
const { t } = useTranslation();
|
||||
const { state, dispatch } = useQrState();
|
||||
const { encode } = useQrEncode();
|
||||
|
||||
@@ -15,19 +17,19 @@ export default function EmailMode() {
|
||||
return (
|
||||
<div className="flex gap-2 items-center h-full px-4">
|
||||
<input
|
||||
placeholder="收件人"
|
||||
placeholder={t('email.to')}
|
||||
value={state.formData.to || ''}
|
||||
onChange={(e) => update('to', e.target.value)}
|
||||
className="flex-1 px-3 py-1.5 rounded-lg border border-gray-200 dark:border-gray-700 text-sm bg-transparent outline-none focus:ring-2 focus:ring-blue-500/30"
|
||||
/>
|
||||
<input
|
||||
placeholder="主题"
|
||||
placeholder={t('email.subject')}
|
||||
value={state.formData.subject || ''}
|
||||
onChange={(e) => update('subject', e.target.value)}
|
||||
className="flex-1 px-3 py-1.5 rounded-lg border border-gray-200 dark:border-gray-700 text-sm bg-transparent outline-none focus:ring-2 focus:ring-blue-500/30"
|
||||
/>
|
||||
<input
|
||||
placeholder="正文"
|
||||
placeholder={t('email.body')}
|
||||
value={state.formData.body || ''}
|
||||
onChange={(e) => update('body', e.target.value)}
|
||||
className="flex-[2] px-3 py-1.5 rounded-lg border border-gray-200 dark:border-gray-700 text-sm bg-transparent outline-none focus:ring-2 focus:ring-blue-500/30"
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQrState } from '../store/qrContext';
|
||||
import { useQrEncode } from '../hooks/useQrEncode';
|
||||
import { buildPhoneText } from '../utils/qrText';
|
||||
|
||||
export default function PhoneMode() {
|
||||
const { t } = useTranslation();
|
||||
const { state, dispatch } = useQrState();
|
||||
const { encode } = useQrEncode();
|
||||
|
||||
@@ -13,7 +15,7 @@ export default function PhoneMode() {
|
||||
|
||||
return (
|
||||
<input
|
||||
placeholder="输入电话号码"
|
||||
placeholder={t('phone.placeholder')}
|
||||
type="tel"
|
||||
value={state.formData.number || ''}
|
||||
onChange={(e) => update(e.target.value)}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQrState } from '../store/qrContext';
|
||||
import { useQrEncode } from '../hooks/useQrEncode';
|
||||
import { buildSmsText } from '../utils/qrText';
|
||||
|
||||
export default function SmsMode() {
|
||||
const { t } = useTranslation();
|
||||
const { state, dispatch } = useQrState();
|
||||
const { encode } = useQrEncode();
|
||||
|
||||
@@ -15,14 +17,14 @@ export default function SmsMode() {
|
||||
return (
|
||||
<div className="flex gap-2 items-center h-full px-4">
|
||||
<input
|
||||
placeholder="电话号码"
|
||||
placeholder={t('sms.number')}
|
||||
type="tel"
|
||||
value={state.formData.number || ''}
|
||||
onChange={(e) => update('number', e.target.value)}
|
||||
className="flex-1 px-3 py-1.5 rounded-lg border border-gray-200 dark:border-gray-700 text-sm bg-transparent outline-none focus:ring-2 focus:ring-blue-500/30"
|
||||
/>
|
||||
<input
|
||||
placeholder="短信内容"
|
||||
placeholder={t('sms.message')}
|
||||
value={state.formData.message || ''}
|
||||
onChange={(e) => update('message', e.target.value)}
|
||||
className="flex-[2] px-3 py-1.5 rounded-lg border border-gray-200 dark:border-gray-700 text-sm bg-transparent outline-none focus:ring-2 focus:ring-blue-500/30"
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQrState } from '../store/qrContext';
|
||||
import { useQrEncode } from '../hooks/useQrEncode';
|
||||
|
||||
export default function TextMode() {
|
||||
const { t } = useTranslation();
|
||||
const { state, dispatch } = useQrState();
|
||||
const { encode } = useQrEncode();
|
||||
|
||||
@@ -12,7 +14,7 @@ export default function TextMode() {
|
||||
|
||||
return (
|
||||
<textarea
|
||||
placeholder="输入任意文本..."
|
||||
placeholder={t('text.placeholder')}
|
||||
value={state.formData.text || ''}
|
||||
onChange={(e) => handleChange(e.target.value)}
|
||||
rows={3}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQrState } from '../store/qrContext';
|
||||
import { useQrEncode } from '../hooks/useQrEncode';
|
||||
|
||||
export default function UrlMode() {
|
||||
const { t } = useTranslation();
|
||||
const { state, dispatch } = useQrState();
|
||||
const { encode } = useQrEncode();
|
||||
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQrState } from '../store/qrContext';
|
||||
import { useQrEncode } from '../hooks/useQrEncode';
|
||||
import { buildVCardText } from '../utils/qrText';
|
||||
|
||||
const FIELDS = [
|
||||
{ key: 'name', placeholder: '姓名' },
|
||||
{ key: 'phone', placeholder: '电话' },
|
||||
{ key: 'email', placeholder: '邮箱' },
|
||||
{ key: 'company', placeholder: '公司' },
|
||||
{ key: 'address', placeholder: '地址' },
|
||||
{ key: 'name', i18n: 'vcard.name' },
|
||||
{ key: 'phone', i18n: 'vcard.phone' },
|
||||
{ key: 'email', i18n: 'vcard.email' },
|
||||
{ key: 'company', i18n: 'vcard.company' },
|
||||
{ key: 'address', i18n: 'vcard.address' },
|
||||
];
|
||||
|
||||
export default function VCardMode() {
|
||||
const { t } = useTranslation();
|
||||
const { state, dispatch } = useQrState();
|
||||
const { encode } = useQrEncode();
|
||||
|
||||
@@ -25,7 +27,7 @@ export default function VCardMode() {
|
||||
{FIELDS.map((f) => (
|
||||
<input
|
||||
key={f.key}
|
||||
placeholder={f.placeholder}
|
||||
placeholder={t(f.i18n)}
|
||||
value={state.formData[f.key] || ''}
|
||||
onChange={(e) => update(f.key, e.target.value)}
|
||||
className="flex-1 px-3 py-1.5 rounded-lg border border-gray-200 dark:border-gray-700 text-sm bg-transparent outline-none focus:ring-2 focus:ring-blue-500/30"
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQrState } from '../store/qrContext';
|
||||
import { useQrEncode } from '../hooks/useQrEncode';
|
||||
import { buildWifiText } from '../utils/qrText';
|
||||
|
||||
export default function WifiMode() {
|
||||
const { t } = useTranslation();
|
||||
const { state, dispatch } = useQrState();
|
||||
const { encode } = useQrEncode();
|
||||
|
||||
@@ -22,7 +24,7 @@ export default function WifiMode() {
|
||||
className="flex-1 px-3 py-1.5 rounded-lg border border-gray-200 dark:border-gray-700 text-sm bg-transparent outline-none focus:ring-2 focus:ring-blue-500/30"
|
||||
/>
|
||||
<input
|
||||
placeholder="密码"
|
||||
placeholder={t('wifi.password')}
|
||||
type="password"
|
||||
value={state.formData.password || ''}
|
||||
onChange={(e) => update('password', e.target.value)}
|
||||
@@ -35,7 +37,7 @@ export default function WifiMode() {
|
||||
>
|
||||
<option value="WPA">WPA/WPA2</option>
|
||||
<option value="WEP">WEP</option>
|
||||
<option value="nopass">无密码</option>
|
||||
<option value="nopass">无{t('wifi.password')}</option>
|
||||
</select>
|
||||
<label className="flex items-center gap-1 text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
||||
<input
|
||||
@@ -43,7 +45,7 @@ export default function WifiMode() {
|
||||
checked={state.formData.hidden === 'true'}
|
||||
onChange={(e) => update('hidden', e.target.checked ? 'true' : 'false')}
|
||||
/>
|
||||
隐藏
|
||||
{t('wifi.hidden')}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user