feat: add i18n with language switcher and English/Chinese translations

This commit is contained in:
shiyu
2025-09-09 16:50:43 +08:00
parent 59c017a05b
commit db453ef09b
40 changed files with 1381 additions and 469 deletions

View File

@@ -3,19 +3,21 @@ import { Button, Typography, Upload, message, Modal } from 'antd';
import PageCard from '../../components/PageCard';
import { UploadOutlined, DownloadOutlined } from '@ant-design/icons';
import { backupApi } from '../../api/backup';
import { useI18n } from '../../i18n';
const { Paragraph, Text } = Typography;
const BackupPage = memo(function BackupPage() {
const [loading, setLoading] = useState(false);
const { t } = useI18n();
const handleExport = async () => {
setLoading(true);
try {
await backupApi.export();
message.success('导出已开始,请检查您的下载。');
message.success(t('Export started, check your downloads.'));
} catch (e: any) {
message.error(e.message || '导出失败');
message.error(e.message || t('Export failed'));
} finally {
setLoading(false);
}
@@ -23,24 +25,24 @@ const BackupPage = memo(function BackupPage() {
const handleImport = (file: File) => {
Modal.confirm({
title: '确认导入备份?',
title: t('Confirm import backup?'),
content: (
<Typography>
<Paragraph>?</Paragraph>
<Paragraph strong></Paragraph>
<Paragraph>{t('Are you sure to import from this file?')}</Paragraph>
<Paragraph strong>{t('Warning: This will overwrite all data including users (with passwords), settings, storages and tasks. Irreversible!')}</Paragraph>
</Typography>
),
okText: '确认导入',
okText: t('Confirm Import'),
okType: 'danger',
cancelText: '取消',
cancelText: t('Cancel'),
onOk: async () => {
setLoading(true);
try {
const response = await backupApi.import(file);
message.success(response.message || '导入成功!页面将刷新。');
message.success(response.message || t('Import succeeded! The page will refresh.'));
setTimeout(() => window.location.reload(), 2000);
} catch (e: any) {
message.error(e.message || '导入失败');
message.error(e.message || t('Import failed'));
} finally {
setLoading(false);
}
@@ -50,33 +52,33 @@ const BackupPage = memo(function BackupPage() {
};
return (
<PageCard title="备份和恢复">
<PageCard title={t('Backup & Restore')}>
<div style={{ display: 'flex', gap: '16px' }}>
<PageCard title="导出" style={{ flex: 1 }}>
<PageCard title={t('Export')} style={{ flex: 1 }}>
<Paragraph>
JSON
<Text strong></Text>
{t('Export all data (adapters, users, tasks, shares) into a JSON file.')}
<Text strong>{t('Keep your backup file safe.')}</Text>
</Paragraph>
<Button
icon={<DownloadOutlined />}
onClick={handleExport}
loading={loading}
>
{t('Export Backup')}
</Button>
</PageCard>
<PageCard title="恢复" style={{ flex: 1 }}>
<PageCard title={t('Import')} style={{ flex: 1 }}>
<Paragraph>
JSON文件恢复数据
<Text strong type="danger"></Text>
{t('Restore data from a previously exported JSON file.')}
<Text strong type="danger">{t('Warning: This will clear and overwrite existing data.')}</Text>
</Paragraph>
<Upload
beforeUpload={handleImport}
showUploadList={false}
>
<Button icon={<UploadOutlined />} loading={loading}>
{t('Choose File and Restore')}
</Button>
</Upload>
</PageCard>
@@ -85,4 +87,4 @@ const BackupPage = memo(function BackupPage() {
);
});
export default BackupPage;
export default BackupPage;

View File

@@ -6,24 +6,25 @@ import { vectorDBApi } from '../../api/vectorDB';
import { AppstoreOutlined, RobotOutlined, DatabaseOutlined, SkinOutlined } from '@ant-design/icons';
import { useTheme } from '../../contexts/ThemeContext';
import '../../styles/settings-tabs.css';
import { useI18n } from '../../i18n';
const APP_CONFIG_KEYS: {key: string, label: string, default?: string}[] = [
{ key: 'APP_NAME', label: '应用名称' },
{ key: 'APP_LOGO', label: 'LOGO地址' },
{ key: 'APP_DOMAIN', label: '应用域名' },
{ key: 'FILE_DOMAIN', label: '文件域名' },
{ key: 'APP_NAME', label: 'App Name' },
{ key: 'APP_LOGO', label: 'Logo URL' },
{ key: 'APP_DOMAIN', label: 'App Domain' },
{ key: 'FILE_DOMAIN', label: 'File Domain' },
];
const VISION_CONFIG_KEYS = [
{ key: 'AI_VISION_API_URL', label: '视觉模型 API 地址' },
{ key: 'AI_VISION_MODEL', label: '视觉模型', default: 'Qwen/Qwen2.5-VL-32B-Instruct' },
{ key: 'AI_VISION_API_KEY', label: '视觉模型 API Key' },
{ key: 'AI_VISION_API_URL', label: 'Vision API URL' },
{ key: 'AI_VISION_MODEL', label: 'Vision Model', default: 'Qwen/Qwen2.5-VL-32B-Instruct' },
{ key: 'AI_VISION_API_KEY', label: 'Vision API Key' },
];
const EMBED_CONFIG_KEYS = [
{ key: 'AI_EMBED_API_URL', label: '嵌入模型 API 地址' },
{ key: 'AI_EMBED_MODEL', label: '嵌入模型', default: 'Qwen/Qwen3-Embedding-8B' },
{ key: 'AI_EMBED_API_KEY', label: '嵌入模型 API Key' },
{ key: 'AI_EMBED_API_URL', label: 'Embedding API URL' },
{ key: 'AI_EMBED_MODEL', label: 'Embedding Model', default: 'Qwen/Qwen3-Embedding-8B' },
{ key: 'AI_EMBED_API_KEY', label: 'Embedding API Key' },
];
const ALL_AI_KEYS = [...VISION_CONFIG_KEYS, ...EMBED_CONFIG_KEYS];
@@ -42,6 +43,7 @@ export default function SystemSettingsPage() {
const [config, setConfigState] = useState<Record<string, string> | null>(null);
const [activeTab, setActiveTab] = useState('appearance');
const { refreshTheme, previewTheme } = useTheme();
const { t } = useI18n();
useEffect(() => {
getAllConfig().then((data) => setConfigState(data as Record<string, string>));
@@ -53,14 +55,14 @@ export default function SystemSettingsPage() {
for (const [key, value] of Object.entries(values)) {
await setConfig(key, String(value ?? ''));
}
message.success('保存成功');
message.success(t('Saved successfully'));
setConfigState({ ...config, ...values });
// trigger theme refresh if related keys changed
if (Object.keys(values).some(k => Object.values(THEME_KEYS).includes(k))) {
await refreshTheme();
}
} catch (e: any) {
message.error(e.message || '保存失败');
message.error(e.message || t('Save failed'));
}
setLoading(false);
};
@@ -73,12 +75,12 @@ export default function SystemSettingsPage() {
}, [activeTab]);
if (!config) {
return <PageCard title='系统设置'><div>...</div></PageCard>;
return <PageCard title={t('System Settings')}><div>{t('Loading...')}</div></PageCard>;
}
return (
<PageCard
title='系统设置'
title={t('System Settings')}
>
<Space direction="vertical" style={{ width: '100%' }} size={32}>
<Tabs
@@ -93,7 +95,7 @@ export default function SystemSettingsPage() {
label: (
<span>
<SkinOutlined style={{ marginRight: 8 }} />
{t('Appearance Settings')}
</span>
),
children: (
@@ -130,39 +132,39 @@ export default function SystemSettingsPage() {
// Validate JSON if provided
if (vals[THEME_KEYS.TOKENS]) {
try { JSON.parse(vals[THEME_KEYS.TOKENS]); }
catch { return message.error('高级 Token 需为合法 JSON'); }
catch { return message.error(t('Advanced tokens must be valid JSON')); }
}
await handleSave(vals);
}}
style={{ marginTop: 24 }}
key={'appearance-' + JSON.stringify(config)}
>
<Card title="主题">
<Form.Item name={THEME_KEYS.MODE} label="主题模式">
<Card title={t('Theme')}>
<Form.Item name={THEME_KEYS.MODE} label={t('Theme Mode')}>
<Radio.Group buttonStyle="solid">
<Radio.Button value="light"></Radio.Button>
<Radio.Button value="dark"></Radio.Button>
<Radio.Button value="system"></Radio.Button>
<Radio.Button value="light">{t('Light')}</Radio.Button>
<Radio.Button value="dark">{t('Dark')}</Radio.Button>
<Radio.Button value="system">{t('Follow System')}</Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item name={THEME_KEYS.PRIMARY} label="主色">
<Form.Item name={THEME_KEYS.PRIMARY} label={t('Primary Color')}>
<Input type="color" size="large" />
</Form.Item>
<Form.Item name={THEME_KEYS.RADIUS} label="圆角">
<Form.Item name={THEME_KEYS.RADIUS} label={t('Border Radius')}>
<InputNumber min={0} max={24} style={{ width: '100%' }} />
</Form.Item>
</Card>
<Card title="高级" style={{ marginTop: 24 }}>
<Form.Item name={THEME_KEYS.TOKENS} label="覆盖 AntD TokenJSON" tooltip="例如:{ &quot;colorText&quot;: &quot;#222&quot; }">
<Card title={t('Advanced')} style={{ marginTop: 24 }}>
<Form.Item name={THEME_KEYS.TOKENS} label={t('Override AntD Tokens (JSON)')} tooltip={t('e.g. {"colorText": "#222"}') }>
<Input.TextArea autoSize={{ minRows: 4 }} placeholder='{ "colorText": "#222" }' />
</Form.Item>
<Form.Item name={THEME_KEYS.CSS} label="自定义 CSS">
<Input.TextArea autoSize={{ minRows: 6 }} placeholder={":root{ }\n/* 支持任意 CSS */"} />
<Form.Item name={THEME_KEYS.CSS} label={t('Custom CSS')}>
<Input.TextArea autoSize={{ minRows: 6 }} placeholder={":root{ }\n/* CSS */"} />
</Form.Item>
</Card>
<Form.Item style={{ marginTop: 24 }}>
<Button type="primary" htmlType="submit" loading={loading} block>
{t('Save')}
</Button>
</Form.Item>
</Form>
@@ -173,7 +175,7 @@ export default function SystemSettingsPage() {
label: (
<span>
<AppstoreOutlined style={{ marginRight: 8 }} />
{t('App Settings')}
</span>
),
children: (
@@ -187,13 +189,13 @@ export default function SystemSettingsPage() {
key={JSON.stringify(config)}
>
{APP_CONFIG_KEYS.map(({ key, label }) => (
<Form.Item key={key} name={key} label={label}>
<Form.Item key={key} name={key} label={t(label)}>
<Input size="large" />
</Form.Item>
))}
<Form.Item>
<Button type="primary" htmlType="submit" loading={loading} block>
{t('Save')}
</Button>
</Form.Item>
</Form>
@@ -204,7 +206,7 @@ export default function SystemSettingsPage() {
label: (
<span>
<RobotOutlined style={{ marginRight: 8 }} />
AI设置
{t('AI Settings')}
</span>
),
children: (
@@ -217,23 +219,23 @@ export default function SystemSettingsPage() {
style={{ marginTop: 24 }}
key={JSON.stringify(config)}
>
<Card title="视觉模型" style={{ marginBottom: 24 }}>
<Card title={t('Vision Model')} style={{ marginBottom: 24 }}>
{VISION_CONFIG_KEYS.map(({ key, label }) => (
<Form.Item key={key} name={key} label={label}>
<Form.Item key={key} name={key} label={t(label)}>
<Input size="large" />
</Form.Item>
))}
</Card>
<Card title="嵌入模型">
<Card title={t('Embedding Model')}>
{EMBED_CONFIG_KEYS.map(({ key, label }) => (
<Form.Item key={key} name={key} label={label}>
<Form.Item key={key} name={key} label={t(label)}>
<Input size="large" />
</Form.Item>
))}
</Card>
<Form.Item style={{ marginTop: 24 }}>
<Button type="primary" htmlType="submit" loading={loading} block>
{t('Save')}
</Button>
</Form.Item>
</Form>
@@ -244,16 +246,16 @@ export default function SystemSettingsPage() {
label: (
<span>
<DatabaseOutlined style={{ marginRight: 8 }} />
{t('Vector Database')}
</span>
),
children: (
<Card title="向量数据库设置" style={{ marginTop: 24 }}>
<Card title={t('Vector Database Settings')} style={{ marginTop: 24 }}>
<Form layout="vertical">
<Form.Item label="数据库类型">
<Form.Item label={t('Database Type')}>
<Select
size="large"
value="Milvus Lite"
value={'Milvus Lite'}
disabled
options={[{ value: 'Milvus Lite', label: 'Milvus Lite' }]}
/>
@@ -264,23 +266,23 @@ export default function SystemSettingsPage() {
block
onClick={() => {
Modal.confirm({
title: '确认清空向量数据库?',
content: '此操作将删除所有集合中的所有数据,且不可逆。',
okText: '确认清空',
title: t('Confirm clear vector database?'),
content: t('This will delete all collections irreversibly.'),
okText: t('Confirm Clear'),
okType: 'danger',
cancelText: '取消',
cancelText: t('Cancel'),
onOk: async () => {
try {
await vectorDBApi.clearAll();
message.success('向量数据库已清空');
message.success(t('Vector database cleared'));
} catch (e: any) {
message.error(e.message || '清空失败');
message.error(e.message || t('Clear failed'));
}
},
});
}}
>
{t('Clear Vector DB')}
</Button>
</Form.Item>
</Form>