feat(i18n): implement internationalization framework and update UI components for language support
This commit is contained in:
@@ -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 || [],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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] })),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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[];
|
||||
|
||||
Reference in New Issue
Block a user