feat(i18n): implement internationalization framework and update UI components for language support

This commit is contained in:
HouYunFei
2026-08-05 14:08:03 +08:00
parent 9bccd0ff1a
commit 0f6809251a
116 changed files with 3269 additions and 1870 deletions
@@ -1,30 +1,25 @@
import { Button, Drawer, Input, Segmented, Select, Space } from "antd";
import { ListPlus, Trash2 } from "lucide-react";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { defaultBaseUrlForApiFormat, guessCapability, normalizeChannelModels, type ApiCallFormat, type ChannelModel, type ModelCapability, type ModelChannel } from "@/stores/use-config-store";
import { ModelScriptEditor } from "./model-script-editor";
import { ModelSelectModal } from "./model-select-modal";
const apiFormatOptions: Array<{ label: string; value: ApiCallFormat }> = [
{ label: "OpenAI", value: "openai" },
{ label: "Gemini", value: "gemini" },
{ label: "火山方舟", value: "ark" },
];
const capabilityOptions: Array<{ label: string; value: ModelCapability }> = [
{ label: "生图", value: "image" },
{ label: "视频", value: "video" },
{ label: "文本", value: "text" },
{ label: "音频", value: "audio" },
];
type ScriptTarget = { name: string; capability: ModelCapability; value: string };
export function ChannelEditorDrawer({ open, channel, onSave, onClose }: { open: boolean; channel: ModelChannel | null; onSave: (channel: ModelChannel) => void; onClose: () => void }) {
const { t } = useTranslation();
const [draft, setDraft] = useState<ModelChannel | null>(channel);
const [selectOpen, setSelectOpen] = useState(false);
const [scriptTarget, setScriptTarget] = useState<ScriptTarget | null>(null);
const apiFormatOptions: Array<{ label: string; value: ApiCallFormat }> = [
{ label: "OpenAI", value: "openai" },
{ label: "Gemini", value: "gemini" },
{ label: t("config.protocols.ark"), value: "ark" },
];
const capabilityOptions: Array<{ label: string; value: ModelCapability }> = ["image", "video", "text", "audio"].map((value) => ({ label: t(`config.channelEditor.capabilities.${value}`), value: value as ModelCapability }));
useEffect(() => {
if (open && channel) setDraft(channel);
@@ -50,7 +45,7 @@ export function ChannelEditorDrawer({ open, channel, onSave, onClose }: { open:
const removeModel = (name: string) => setModels(draft.models.filter((model) => model.name !== name));
const save = () => {
onSave({ ...draft, name: draft.name.trim() || "未命名渠道", models: normalizeChannelModels(draft.models) });
onSave({ ...draft, name: draft.name.trim() || t("config.channels.unnamed"), models: normalizeChannelModels(draft.models) });
onClose();
};
@@ -58,29 +53,29 @@ export function ChannelEditorDrawer({ open, channel, onSave, onClose }: { open:
<Drawer
open={open}
width={640}
title="编辑渠道"
title={t("config.channelEditor.title")}
onClose={onClose}
styles={{ body: { paddingTop: 16 } }}
extra={
<Space>
<Button onClick={onClose}></Button>
<Button onClick={onClose}>{t("common.cancel")}</Button>
<Button type="primary" onClick={save}>
{t("common.save")}
</Button>
</Space>
}
>
<div className="grid gap-4 md:grid-cols-2">
<label className="block">
<span className="mb-1 block text-sm font-medium"></span>
<span className="mb-1 block text-sm font-medium">{t("config.channelEditor.name")}</span>
<Input value={draft.name} onChange={(event) => patch({ name: event.target.value })} />
</label>
<label className="block">
<span className="mb-1 block text-sm font-medium"></span>
<span className="mb-1 block text-sm font-medium">{t("config.channelEditor.protocol")}</span>
<Select className="w-full" value={draft.apiFormat} options={apiFormatOptions} onChange={changeApiFormat} />
</label>
<label className="block md:col-span-2">
<span className="mb-1 block text-sm font-medium"></span>
<span className="mb-1 block text-sm font-medium">{t("config.channelEditor.baseUrl")}</span>
<Input value={draft.baseUrl} onChange={(event) => patch({ baseUrl: event.target.value })} placeholder="https://api.example.com" />
</label>
<label className="block md:col-span-2">
@@ -91,11 +86,11 @@ export function ChannelEditorDrawer({ open, channel, onSave, onClose }: { open:
<div className="mt-6 mb-3 flex flex-wrap items-center justify-between gap-2">
<div>
<div className="text-sm font-semibold"></div>
<div className="mt-0.5 text-xs text-stone-500"> {draft.models.length} </div>
<div className="text-sm font-semibold">{t("config.channelEditor.models")}</div>
<div className="mt-0.5 text-xs text-stone-500">{t("config.channelEditor.modelDescription", { count: draft.models.length })}</div>
</div>
<Button type="primary" icon={<ListPlus className="size-4" />} onClick={() => setSelectOpen(true)}>
{t("config.channelEditor.selectModels")}
</Button>
</div>
@@ -109,14 +104,14 @@ export function ChannelEditorDrawer({ open, channel, onSave, onClose }: { open:
<div className="flex shrink-0 items-center gap-2">
<Segmented size="small" value={model.capability} options={capabilityOptions} onChange={(value) => setCapability(model.name, value as ModelCapability)} />
<Button size="small" type={model.script ? "primary" : "default"} ghost={Boolean(model.script)} onClick={() => setScriptTarget({ name: model.name, capability: model.capability, value: model.script || "" })}>
{model.script ? "脚本已设" : "调用脚本"}
{t(model.script ? "config.channelEditor.scriptReady" : "config.channelEditor.script")}
</Button>
<Button size="small" danger type="text" icon={<Trash2 className="size-3.5" />} onClick={() => removeModel(model.name)} />
</div>
</div>
))
) : (
<div className="px-2 py-8 text-center text-sm text-stone-500"></div>
<div className="px-2 py-8 text-center text-sm text-stone-500">{t("config.channelEditor.empty")}</div>
)}
</div>