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
+3 -2
View File
@@ -2,6 +2,7 @@ import { create } from "zustand";
import { persist, type PersistStorage, type StorageValue } from "zustand/middleware";
import { nanoid } from "nanoid";
import i18n from "@/i18n";
import { localForageStorage } from "@/lib/localforage-storage";
import type { CanvasBackgroundMode } from "@/lib/canvas-theme";
import type { CanvasAssistantSession, CanvasConnection, CanvasNodeData, ViewportTransform } from "@/types/canvas";
@@ -64,7 +65,7 @@ export const useCanvasStore = create<CanvasStore>()(
(set, get) => ({
hydrated: false,
projects: [],
createProject: (title = "未命名画布") => {
createProject: (title = i18n.t("canvas.project.untitled")) => {
const now = new Date().toISOString();
const id = nanoid();
const project: CanvasProject = {
@@ -87,7 +88,7 @@ export const useCanvasStore = create<CanvasStore>()(
const now = new Date().toISOString();
const project: CanvasProject = {
id: nanoid(),
title: source.title || "导入画布",
title: source.title || i18n.t("canvas.project.imported"),
createdAt: source.createdAt || now,
updatedAt: now,
nodes: source.nodes || [],
+4 -3
View File
@@ -1,4 +1,5 @@
import { create } from "zustand";
import i18n from "@/i18n";
import { fetchCodexSkills, type AgentSkillDraft, type AgentSkillSummary } from "@/services/api/canvas-agent";
import { useAgentStore } from "@/stores/use-agent-store";
@@ -52,7 +53,7 @@ export const useAgentSkillStore = create<AgentSkillStore>((set, get) => ({
set({ skills, selectedSkill, autoPrompt, selectionRevision: current.selectionRevision + (selectionChanged ? 1 : 0), loading: false, loaded: true, errors: (response.errors || []).map(skillErrorText) });
} catch (error) {
if (sequence !== loadSequence) return;
set({ loading: false, loaded: true, errors: [error instanceof Error ? error.message : "读取 Skill 失败"] });
set({ loading: false, loaded: true, errors: [error instanceof Error ? error.message : i18n.t("agent.state.skillReadFailed")] });
}
},
selectSkill: (selectedSkill) => {
@@ -86,8 +87,8 @@ function replaceAutoPrompt(previous: string, next: string, fillEmpty: boolean) {
function skillErrorText(value: unknown) {
if (typeof value === "string") return value;
if (!value || typeof value !== "object") return "Skill 解析失败";
if (!value || typeof value !== "object") return i18n.t("agent.state.skillParseFailed");
const item = value as { path?: unknown; error?: unknown; message?: unknown };
const message = typeof item.error === "string" ? item.error : typeof item.message === "string" ? item.message : "Skill 解析失败";
const message = typeof item.error === "string" ? item.error : typeof item.message === "string" ? item.message : i18n.t("agent.state.skillParseFailed");
return typeof item.path === "string" ? `${item.path}:${message}` : message;
}
+6 -5
View File
@@ -1,4 +1,5 @@
import { create } from "zustand";
import i18n from "@/i18n";
import type { CanvasAgentOp, CanvasAgentSnapshot } from "@/lib/canvas/canvas-agent-ops";
@@ -117,7 +118,7 @@ export const useAgentStore = create<AgentStore>((set, get) => ({
models: [],
model: typeof window === "undefined" ? "" : localStorage.getItem("canvas-agent-model") || "",
reasoningEffort: typeof window === "undefined" ? "" : (localStorage.getItem("canvas-agent-reasoning-effort") as AgentReasoningEffort) || "",
activity: "就绪",
activity: i18n.t("agent.state.ready"),
conversation: { revision: 0, conversationId: "", threadId: "", status: "idle", mcpStatuses: {} },
bootstrapStatus: null,
mcpStartupStatuses: {},
@@ -139,24 +140,24 @@ export const useAgentStore = create<AgentStore>((set, get) => ({
const silent = options?.silent ?? false;
const endpoint = get().url.trim().replace(/\/$/, "");
const token = get().token.trim();
if (!endpoint || !token) return set({ connectError: silent ? "" : "请填写 Local URL 和 Connect token" });
if (!endpoint || !token) return set({ connectError: silent ? "" : i18n.t("agent.state.connectionRequired") });
try {
const parsed = new URL(endpoint);
if (!["http:", "https:"].includes(parsed.protocol)) throw new Error();
} catch {
return set({ connectError: silent ? "" : "Local URL 格式不正确" });
return set({ connectError: silent ? "" : i18n.t("agent.state.invalidUrl") });
}
localStorage.setItem("canvas-agent-url", endpoint);
localStorage.setItem("canvas-agent-token", token);
// 只设 enabled=true,由 LocalAgentPanel 的 useEffect 统一负责开 SSE
set({ url: endpoint, token, enabled: true, silentConnect: silent, activity: "连接中", connectError: "" });
set({ url: endpoint, token, enabled: true, silentConnect: silent, activity: i18n.t("agent.status.connecting"), connectError: "" });
},
disconnectAgent: (patch = {}) => {
agentSource?.close();
agentSource = null;
if (connectTimer) clearTimeout(connectTimer);
connectTimer = null;
set({ enabled: false, connected: false, silentConnect: false, activity: "离线", conversation: { revision: 0, conversationId: "", threadId: "", status: "idle", mcpStatuses: {} }, bootstrapStatus: null, mcpStartupStatuses: {}, ...patch });
set({ enabled: false, connected: false, silentConnect: false, activity: i18n.t("agent.state.offline"), conversation: { revision: 0, conversationId: "", threadId: "", status: "idle", mcpStatuses: {} }, bootstrapStatus: null, mcpStartupStatuses: {}, ...patch });
},
addMessage: (item) => set((state) => ({ messages: [...state.messages, item] })),
addEventLog: (item) => set((state) => ({ eventLogs: [...state.eventLogs.slice(-160), item] })),
+7 -5
View File
@@ -3,6 +3,8 @@ import { create } from "zustand";
import { persist } from "zustand/middleware";
import { nanoid } from "nanoid";
import i18n from "@/i18n";
export type ApiCallFormat = "openai" | "gemini" | "ark";
export type ModelCapability = "image" | "video" | "text" | "audio";
export type ReasoningEffort = "auto" | "low" | "medium" | "high" | "xhigh";
@@ -74,7 +76,7 @@ export const defaultConfig: AiConfig = {
channels: [
{
id: "default",
name: "默认渠道",
name: i18n.t("config.channels.defaultName"),
baseUrl: OPENAI_BASE_URL,
apiKey: "",
apiFormat: "openai",
@@ -276,7 +278,7 @@ export function createModelChannel(channel?: Partial<ModelChannel>): ModelChanne
const apiFormat = normalizeApiFormat(channel?.apiFormat);
return {
id: channel?.id?.trim() || nanoid(),
name: channel?.name?.trim() || "新渠道",
name: channel?.name?.trim() || i18n.t("config.channels.newName"),
baseUrl: channel?.baseUrl?.trim() || defaultBaseUrlForApiFormat(apiFormat),
apiKey: channel?.apiKey || "",
apiFormat,
@@ -329,7 +331,7 @@ export function resolveModelChannel(config: AiConfig, value: string) {
const decoded = decodeChannelModel(value);
const model = decoded?.model || value;
const matched = decoded ? config.channels.find((channel) => channel.id === decoded.channelId) : config.channels.find((channel) => channel.models.some((item) => item.name === model));
return matched || config.channels[0] || createModelChannel({ id: "default", name: "默认渠道", baseUrl: config.baseUrl, apiKey: config.apiKey, apiFormat: config.apiFormat, models: config.models.map(modelOptionName).map((name) => ({ name, capability: guessCapability(name) })) });
return matched || config.channels[0] || createModelChannel({ id: "default", name: i18n.t("config.channels.defaultName"), baseUrl: config.baseUrl, apiKey: config.apiKey, apiFormat: config.apiFormat, models: config.models.map(modelOptionName).map((name) => ({ name, capability: guessCapability(name) })) });
}
export function resolveModelRequestConfig(config: AiConfig, value: string) {
@@ -349,7 +351,7 @@ function normalizeChannels(config: AiConfig) {
createModelChannel({
...channel,
id: channel.id || (index === 0 ? "default" : `channel-${index + 1}`),
name: channel.name || (index === 0 ? "默认渠道" : `渠道 ${index + 1}`),
name: channel.name || (index === 0 ? i18n.t("config.channels.defaultName") : i18n.t("config.channels.indexedName", { index: index + 1 })),
models: normalizeChannelModels(channel.models),
}),
);
@@ -357,7 +359,7 @@ function normalizeChannels(config: AiConfig) {
channels.push(
createModelChannel({
id: "default",
name: "默认渠道",
name: i18n.t("config.channels.defaultName"),
baseUrl: config.baseUrl || defaultConfig.baseUrl,
apiKey: config.apiKey || "",
apiFormat: config.apiFormat || defaultConfig.apiFormat,
+1 -7
View File
@@ -15,13 +15,7 @@ const defaultSchedule: PromptSourceSchedule = {
lastFetchedAt: "",
};
export const PROMPT_SOURCE_INTERVAL_OPTIONS = [
{ label: "关闭定时", value: 0 },
{ label: "每 30 分钟", value: 30 },
{ label: "每 1 小时", value: 60 },
{ label: "每 6 小时", value: 360 },
{ label: "每 24 小时", value: 1440 },
];
export const PROMPT_SOURCE_INTERVALS = [0, 30, 60, 360, 1440];
type PromptSourceStore = {
sources: PromptSource[];