2026-03-27 11:48:00 +08:00

535 lines
17 KiB
TypeScript

import React, { createContext, useContext, useEffect, useMemo, useState } from "react";
export type Language = "en" | "zh";
type TranslationKey =
| "workspace"
| "project"
| "runs"
| "localDev"
| "navAssets"
| "navWorkflows"
| "navRuns"
| "navExplore"
| "navLabels"
| "navAdmin"
| "language"
| "english"
| "chinese"
| "assetsTitle"
| "assetsDescription"
| "localPath"
| "registerLocalPath"
| "noAssetsYet"
| "type"
| "source"
| "detected"
| "pending"
| "topLevelEntries"
| "notAvailable"
| "loadingAssetDetail"
| "assetId"
| "status"
| "sourcePath"
| "probeAgain"
| "createExploreArtifact"
| "openExploreView"
| "probeSummary"
| "warnings"
| "none"
| "recommendedNodes"
| "noProbeReportYet"
| "workflowsTitle"
| "createWorkflow"
| "noWorkflowsYet"
| "latestVersion"
| "workflowEditor"
| "runAsset"
| "saveWorkflowVersion"
| "triggerWorkflowRun"
| "reloadLatestSaved"
| "openLatestRun"
| "selectAssetBeforeRun"
| "nodeLibrary"
| "canvas"
| "canvasHint"
| "latestSavedVersions"
| "draftStatus"
| "draftSynced"
| "draftUnsaved"
| "nodeConfiguration"
| "category"
| "definition"
| "executorType"
| "runtimeTarget"
| "artifactTitle"
| "pythonCodeHook"
| "nodeNoHook"
| "selectNode"
| "removeNode"
| "workflowCreatedName"
| "noAssetsAvailable"
| "runsTitle"
| "runsDescription"
| "noRunsYet"
| "createdAt"
| "inputAssets"
| "runDetail"
| "workflow"
| "startedAt"
| "finishedAt"
| "runDuration"
| "runSummary"
| "cancelRun"
| "retryRun"
| "runGraph"
| "boundAssets"
| "selectedTask"
| "executor"
| "executorConfig"
| "codeHook"
| "duration"
| "summary"
| "error"
| "retryTask"
| "artifacts"
| "noTaskArtifactsYet"
| "stdout"
| "stderr"
| "executionLog"
| "resultPreview"
| "noStdout"
| "noStderr"
| "noTaskLogs"
| "noTasksCreated"
| "loadingRun"
| "exploreTitle"
| "exploreEmpty"
| "loadingArtifact"
| "bootstrappingLocalWorkspace"
| "failedLoadAssets"
| "failedRegisterAsset"
| "failedLoadWorkflows"
| "failedLoadWorkflow"
| "failedLoadRuns"
| "failedLoadRunDetail"
| "failedLoadTaskArtifacts"
| "failedCancelRun"
| "failedRetryRun"
| "failedRetryTask"
| "failedLoadArtifact"
| "failedBootstrap"
| "validatedAssetCount"
| "loadedAssetCount"
| "success"
| "failed"
| "running"
| "queued"
| "cancelled"
| "totalTasks"
| "stdoutLines"
| "stderrLines"
| "successCount"
| "failedCount"
| "runningCount"
| "cancelledCount"
| "artifactsCount"
| "viaExecutor"
| "assetCount"
| "artifactCount";
const TRANSLATIONS: Record<Language, Record<TranslationKey, string>> = {
en: {
workspace: "Workspace",
project: "Project",
runs: "Runs",
localDev: "Local Dev",
navAssets: "Assets",
navWorkflows: "Workflows",
navRuns: "Runs",
navExplore: "Explore",
navLabels: "Labels",
navAdmin: "Admin",
language: "Language",
english: "English",
chinese: "中文",
assetsTitle: "Assets",
assetsDescription:
"Register local folders, archives, or dataset files, then probe them into managed asset metadata.",
localPath: "Local Path",
registerLocalPath: "Register Local Path",
noAssetsYet: "No assets have been registered yet.",
type: "Type",
source: "Source",
detected: "Detected",
pending: "pending",
topLevelEntries: "Top-level entries",
notAvailable: "n/a",
loadingAssetDetail: "Loading asset detail...",
assetId: "Asset ID",
status: "Status",
sourcePath: "Source path",
probeAgain: "Probe Again",
createExploreArtifact: "Create Explore Artifact",
openExploreView: "Open Explore View",
probeSummary: "Probe Summary",
warnings: "Warnings",
none: "none",
recommendedNodes: "Recommended nodes",
noProbeReportYet: "No probe report yet.",
workflowsTitle: "Workflows",
createWorkflow: "Create Workflow",
noWorkflowsYet: "No workflows yet.",
latestVersion: "Latest version",
workflowEditor: "Workflow Editor",
runAsset: "Run Asset",
saveWorkflowVersion: "Save Workflow Version",
triggerWorkflowRun: "Trigger Workflow Run",
reloadLatestSaved: "Reload Latest Saved",
openLatestRun: "Open Latest Run",
selectAssetBeforeRun: "Select an asset before triggering a workflow run.",
nodeLibrary: "Node Library",
canvas: "Canvas",
canvasHint: "Drag nodes freely, connect handles, zoom, and pan.",
latestSavedVersions: "Latest saved versions",
draftStatus: "Draft status",
draftSynced: "synced",
draftUnsaved: "unsaved changes",
nodeConfiguration: "Node Configuration",
category: "Category",
definition: "Definition",
executorType: "Executor Type",
runtimeTarget: "Runtime Target",
artifactTitle: "Artifact Title",
pythonCodeHook: "Python Code Hook",
nodeNoHook: "This node does not expose a code hook in V1.",
selectNode: "Select a node.",
removeNode: "Remove Node",
workflowCreatedName: "Delivery Normalize {count}",
noAssetsAvailable: "No assets available",
runsTitle: "Runs",
runsDescription: "Recent workflow executions for the current project.",
noRunsYet: "No workflow runs yet.",
createdAt: "Created at",
inputAssets: "Input assets",
runDetail: "Run Detail",
workflow: "Workflow",
startedAt: "Started at",
finishedAt: "Finished at",
runDuration: "Run duration",
runSummary: "Run summary",
cancelRun: "Cancel Run",
retryRun: "Retry Run",
runGraph: "Run Graph",
boundAssets: "Bound assets",
selectedTask: "Selected Task",
executor: "Executor",
executorConfig: "Executor config",
codeHook: "Code hook",
duration: "Duration",
summary: "Summary",
error: "Error",
retryTask: "Retry Task",
artifacts: "Artifacts",
noTaskArtifactsYet: "No task artifacts yet.",
stdout: "Stdout",
stderr: "Stderr",
executionLog: "Execution Log",
resultPreview: "Result Preview",
noStdout: "No stdout lines.",
noStderr: "No stderr lines.",
noTaskLogs: "No task logs yet.",
noTasksCreated: "No tasks created.",
loadingRun: "Loading run...",
exploreTitle: "Explore",
exploreEmpty: "Create an artifact from asset detail to inspect it here.",
loadingArtifact: "Loading artifact...",
bootstrappingLocalWorkspace: "Bootstrapping local workspace...",
failedLoadAssets: "Failed to load assets",
failedRegisterAsset: "Failed to register local asset",
failedLoadWorkflows: "Failed to load workflows",
failedLoadWorkflow: "Failed to load workflow",
failedLoadRuns: "Failed to load runs",
failedLoadRunDetail: "Failed to load run detail",
failedLoadTaskArtifacts: "Failed to load task artifacts",
failedCancelRun: "Failed to cancel run",
failedRetryRun: "Failed to retry run",
failedRetryTask: "Failed to retry task",
failedLoadArtifact: "Failed to load artifact",
failedBootstrap: "Failed to bootstrap local context",
validatedAssetCount: "validated {count} asset{suffix}",
loadedAssetCount: "loaded {count} bound asset{suffix}",
success: "success",
failed: "failed",
running: "running",
queued: "queued",
cancelled: "cancelled",
totalTasks: "{count} total tasks",
stdoutLines: "{count} stdout lines",
stderrLines: "{count} stderr lines",
successCount: "{count} success",
failedCount: "{count} failed",
runningCount: "{count} running",
cancelledCount: "{count} cancelled",
artifactsCount: "artifacts {count}",
viaExecutor: "{outcome} via {executor}",
assetCount: "assets {count}",
artifactCount: "artifacts {count}",
},
zh: {
workspace: "工作空间",
project: "项目",
runs: "运行",
localDev: "本地开发",
navAssets: "数据资产",
navWorkflows: "工作流",
navRuns: "运行记录",
navExplore: "查看",
navLabels: "标注",
navAdmin: "管理",
language: "语言",
english: "English",
chinese: "中文",
assetsTitle: "数据资产",
assetsDescription: "注册本地目录、压缩包或数据集文件,并将其探测为受管资产元数据。",
localPath: "本地路径",
registerLocalPath: "注册本地路径",
noAssetsYet: "还没有注册任何资产。",
type: "类型",
source: "来源",
detected: "识别结果",
pending: "待探测",
topLevelEntries: "顶层条目",
notAvailable: "暂无",
loadingAssetDetail: "正在加载资产详情...",
assetId: "资产 ID",
status: "状态",
sourcePath: "源路径",
probeAgain: "重新探测",
createExploreArtifact: "创建查看产物",
openExploreView: "打开查看页",
probeSummary: "探测摘要",
warnings: "告警",
none: "无",
recommendedNodes: "推荐节点",
noProbeReportYet: "还没有探测报告。",
workflowsTitle: "工作流",
createWorkflow: "新建工作流",
noWorkflowsYet: "还没有工作流。",
latestVersion: "最新版本",
workflowEditor: "工作流编辑器",
runAsset: "运行资产",
saveWorkflowVersion: "保存工作流版本",
triggerWorkflowRun: "触发工作流运行",
reloadLatestSaved: "重新加载最新保存版本",
openLatestRun: "打开最新运行",
selectAssetBeforeRun: "触发工作流运行前请先选择资产。",
nodeLibrary: "节点面板",
canvas: "画布",
canvasHint: "支持自由拖动节点、拖拽连线、缩放和平移。",
latestSavedVersions: "最近保存版本",
draftStatus: "草稿状态",
draftSynced: "已同步",
draftUnsaved: "有未保存修改",
nodeConfiguration: "节点配置",
category: "分类",
definition: "定义",
executorType: "执行器类型",
runtimeTarget: "运行目标",
artifactTitle: "产物标题",
pythonCodeHook: "Python 代码钩子",
nodeNoHook: "该节点在 V1 中不开放代码钩子。",
selectNode: "请选择一个节点。",
removeNode: "删除节点",
workflowCreatedName: "交付标准化 {count}",
noAssetsAvailable: "没有可用资产",
runsTitle: "运行记录",
runsDescription: "当前项目最近的工作流执行记录。",
noRunsYet: "还没有工作流运行记录。",
createdAt: "创建时间",
inputAssets: "输入资产",
runDetail: "运行详情",
workflow: "工作流",
startedAt: "开始时间",
finishedAt: "结束时间",
runDuration: "运行时长",
runSummary: "运行摘要",
cancelRun: "取消运行",
retryRun: "重试运行",
runGraph: "运行图",
boundAssets: "绑定资产",
selectedTask: "当前任务",
executor: "执行器",
executorConfig: "执行器配置",
codeHook: "代码钩子",
duration: "耗时",
summary: "摘要",
error: "错误",
retryTask: "重试任务",
artifacts: "产物",
noTaskArtifactsYet: "当前任务还没有产物。",
stdout: "标准输出",
stderr: "标准错误",
executionLog: "执行日志",
resultPreview: "结果预览",
noStdout: "没有标准输出。",
noStderr: "没有标准错误。",
noTaskLogs: "当前任务还没有日志。",
noTasksCreated: "还没有生成任务。",
loadingRun: "正在加载运行...",
exploreTitle: "查看",
exploreEmpty: "先在资产详情或运行详情里创建产物,再来这里查看。",
loadingArtifact: "正在加载产物...",
bootstrappingLocalWorkspace: "正在初始化本地工作空间...",
failedLoadAssets: "加载资产失败",
failedRegisterAsset: "注册本地资产失败",
failedLoadWorkflows: "加载工作流失败",
failedLoadWorkflow: "加载工作流失败",
failedLoadRuns: "加载运行列表失败",
failedLoadRunDetail: "加载运行详情失败",
failedLoadTaskArtifacts: "加载任务产物失败",
failedCancelRun: "取消运行失败",
failedRetryRun: "重试运行失败",
failedRetryTask: "重试任务失败",
failedLoadArtifact: "加载产物失败",
failedBootstrap: "初始化本地上下文失败",
validatedAssetCount: "已校验 {count} 个资产",
loadedAssetCount: "已加载 {count} 个绑定资产",
success: "成功",
failed: "失败",
running: "运行中",
queued: "排队中",
cancelled: "已取消",
totalTasks: "共 {count} 个任务",
stdoutLines: "{count} 条标准输出",
stderrLines: "{count} 条标准错误",
successCount: "{count} 个成功",
failedCount: "{count} 个失败",
runningCount: "{count} 个运行中",
cancelledCount: "{count} 个已取消",
artifactsCount: "产物 {count}",
viaExecutor: "{outcome},执行器 {executor}",
assetCount: "资产 {count}",
artifactCount: "产物 {count}",
},
};
const BUILTIN_NODE_TRANSLATIONS: Record<string, { en: { name: string; description: string }; zh: { name: string; description: string } }> = {
"source-asset": {
en: { name: "Source Asset", description: "Load an uploaded asset or registered storage path." },
zh: { name: "数据源", description: "加载上传资产或已注册的存储路径。" },
},
"extract-archive": {
en: { name: "Extract Archive", description: "Expand a compressed archive into a managed folder artifact." },
zh: { name: "解压归档", description: "将压缩包展开成受管目录产物。" },
},
"rename-folder": {
en: { name: "Rename Delivery Folder", description: "Rename the top-level delivery folder to the business naming convention." },
zh: { name: "重命名交付目录", description: "将顶层交付目录重命名为业务命名规范。" },
},
"validate-structure": {
en: { name: "Validate Structure", description: "Validate required directories and metadata files." },
zh: { name: "校验目录结构", description: "校验必需目录和元数据文件。" },
},
"validate-metadata": {
en: { name: "Validate Metadata", description: "Validate meta.json, intrinsics.json, and video_meta.json." },
zh: { name: "校验元数据", description: "校验 meta.json、intrinsics.json 和 video_meta.json。" },
},
"export-delivery-package": {
en: { name: "Export Delivery Package", description: "Produce the final delivery package artifact for upload." },
zh: { name: "导出交付包", description: "生成最终交付包产物用于上传或交付。" },
},
};
type InterpolationValues = Record<string, string | number>;
function interpolate(template: string, values?: InterpolationValues) {
if (!values) {
return template;
}
return template.replace(/\{(\w+)\}/gu, (_match, key) => String(values[key] ?? ""));
}
export function translate(
language: Language,
key: TranslationKey,
values?: InterpolationValues,
) {
return interpolate(TRANSLATIONS[language][key], values);
}
export function getInitialLanguage(): Language {
if (typeof window === "undefined") {
return "zh";
}
const stored = window.localStorage.getItem("emboflow-language");
if (stored === "en" || stored === "zh") {
return stored;
}
return window.navigator.language.toLowerCase().startsWith("zh") ? "zh" : "en";
}
export function localizeNodeDefinition<T extends { id: string; name?: string; description?: string; category?: string }>(
language: Language,
definition: T,
) {
const localized = BUILTIN_NODE_TRANSLATIONS[definition.id];
const categoryMap = {
Source: language === "zh" ? "数据源" : "Source",
Transform: language === "zh" ? "处理" : "Transform",
Inspect: language === "zh" ? "检查" : "Inspect",
Annotate: language === "zh" ? "标注" : "Annotate",
Export: language === "zh" ? "导出" : "Export",
Utility: language === "zh" ? "工具" : "Utility",
} as const;
return {
...definition,
name: localized ? localized[language].name : definition.name,
description: localized ? localized[language].description : definition.description,
category:
definition.category && definition.category in categoryMap
? categoryMap[definition.category as keyof typeof categoryMap]
: definition.category,
};
}
type I18nContextValue = {
language: Language;
setLanguage: (language: Language) => void;
t: (key: TranslationKey, values?: InterpolationValues) => string;
};
const I18nContext = createContext<I18nContextValue | null>(null);
export function I18nProvider(props: React.PropsWithChildren) {
const [language, setLanguage] = useState<Language>(() => getInitialLanguage());
useEffect(() => {
if (typeof document !== "undefined") {
document.documentElement.lang = language === "zh" ? "zh-CN" : "en";
}
if (typeof window !== "undefined") {
window.localStorage.setItem("emboflow-language", language);
}
}, [language]);
const value = useMemo<I18nContextValue>(
() => ({
language,
setLanguage,
t: (key, values) => translate(language, key, values),
}),
[language],
);
return <I18nContext.Provider value={value}>{props.children}</I18nContext.Provider>;
}
export function useI18n() {
const context = useContext(I18nContext);
if (!context) {
throw new Error("useI18n must be used within I18nProvider");
}
return context;
}