2026-03-30 03:02:44 +08:00

799 lines
28 KiB
TypeScript

import React, { createContext, useContext, useEffect, useMemo, useState } from "react";
export type Language = "en" | "zh";
export type TranslationKey =
| "workspace"
| "project"
| "runs"
| "localDev"
| "navProjects"
| "navAssets"
| "navNodes"
| "navWorkflows"
| "navRuns"
| "navExplore"
| "navLabels"
| "navAdmin"
| "language"
| "english"
| "chinese"
| "assetsTitle"
| "assetsDescription"
| "projectsTitle"
| "projectsDescription"
| "projectNameLabel"
| "projectDescriptionLabel"
| "createProject"
| "noProjectsYet"
| "activeProject"
| "openProject"
| "storageConnectionsTitle"
| "storageConnectionsDescription"
| "createStorageConnection"
| "storageProvider"
| "bucket"
| "endpoint"
| "region"
| "basePath"
| "rootPath"
| "noStorageConnectionsYet"
| "nodesTitle"
| "nodesDescription"
| "createCustomNode"
| "noCustomNodesYet"
| "customNodeName"
| "customNodeDescription"
| "customNodeCategory"
| "customNodeSourceKind"
| "customNodeSourceImage"
| "customNodeSourceDockerfile"
| "customNodeImage"
| "customNodeDockerfile"
| "customNodeDockerfileUpload"
| "customNodeCommand"
| "customNodeInputMode"
| "customNodeOutputMode"
| "customNodeArtifactType"
| "customNodeSingleAssetSet"
| "customNodeMultiAssetSet"
| "customNodeReport"
| "customNodeAssetSet"
| "customNodeAssetSetWithReport"
| "datasetsTitle"
| "datasetsDescription"
| "datasetName"
| "datasetDescription"
| "sourceAsset"
| "sourceAssets"
| "storageConnection"
| "storagePathLabel"
| "createDataset"
| "noDatasetsYet"
| "latestDatasetVersion"
| "localPath"
| "registerLocalPath"
| "noAssetsYet"
| "type"
| "source"
| "detected"
| "pending"
| "topLevelEntries"
| "notAvailable"
| "loadingAssetDetail"
| "assetId"
| "status"
| "sourcePath"
| "probeAgain"
| "createExploreArtifact"
| "openExploreView"
| "probeSummary"
| "warnings"
| "none"
| "recommendedNodes"
| "noProbeReportYet"
| "workflowsTitle"
| "workflowTemplatesTitle"
| "workflowTemplatesDescription"
| "createWorkflow"
| "createBlankWorkflow"
| "createWorkflowFromTemplate"
| "saveAsTemplate"
| "templateName"
| "templateDescription"
| "templateSaved"
| "noWorkflowTemplatesYet"
| "noWorkflowsYet"
| "latestVersion"
| "workflowEditor"
| "runAsset"
| "saveWorkflowVersion"
| "triggerWorkflowRun"
| "reloadLatestSaved"
| "openLatestRun"
| "selectAssetBeforeRun"
| "nodeLibrary"
| "nodeLibraryHint"
| "canvas"
| "canvasHint"
| "dragNodeToCanvas"
| "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"
| "failedLoadStorageConnections"
| "failedCreateStorageConnection"
| "failedLoadDatasets"
| "failedCreateDataset"
| "failedLoadCustomNodes"
| "failedCreateCustomNode"
| "failedRegisterAsset"
| "failedLoadWorkflows"
| "failedLoadTemplates"
| "failedCreateTemplate"
| "failedCreateWorkflowFromTemplate"
| "failedLoadWorkflow"
| "failedLoadRuns"
| "failedLoadRunDetail"
| "failedLoadTaskArtifacts"
| "failedCancelRun"
| "failedRetryRun"
| "failedRetryTask"
| "failedLoadArtifact"
| "failedBootstrap"
| "failedLoadProjects"
| "failedCreateProject"
| "validatedAssetCount"
| "loadedAssetCount"
| "success"
| "failed"
| "running"
| "queued"
| "cancelled"
| "totalTasks"
| "stdoutLines"
| "stderrLines"
| "successCount"
| "failedCount"
| "runningCount"
| "cancelledCount"
| "artifactsCount"
| "viaExecutor"
| "assetCount"
| "artifactCount"
| "invalidConnectionMissingEndpoint"
| "invalidConnectionSelf"
| "invalidConnectionDuplicate"
| "invalidConnectionSourceDisallowsOutgoing"
| "invalidConnectionTargetDisallowsIncoming"
| "invalidConnectionTargetAlreadyHasIncoming"
| "invalidConnectionCycle";
const TRANSLATIONS: Record<Language, Record<TranslationKey, string>> = {
en: {
workspace: "Workspace",
project: "Project",
runs: "Runs",
localDev: "Local Dev",
navProjects: "Projects",
navAssets: "Assets",
navNodes: "Nodes",
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.",
projectsTitle: "Projects",
projectsDescription:
"Create project spaces, switch the active project, and manage project-scoped assets, datasets, workflows, and runs.",
projectNameLabel: "Project Name",
projectDescriptionLabel: "Project Description",
createProject: "Create Project",
noProjectsYet: "No projects yet.",
activeProject: "Active project",
openProject: "Open Project",
storageConnectionsTitle: "Storage Connections",
storageConnectionsDescription:
"Define where project datasets are stored, including local paths and object storage providers.",
nodesTitle: "Custom Nodes",
nodesDescription:
"Register project-level docker nodes from an image or a self-contained Dockerfile. Containers must read the EmboFlow input path and write a result object to the EmboFlow output path.",
createCustomNode: "Create Custom Node",
noCustomNodesYet: "No custom nodes have been created yet.",
customNodeName: "Node Name",
customNodeDescription: "Node Description",
customNodeCategory: "Node Category",
customNodeSourceKind: "Container Source",
customNodeSourceImage: "Docker Image",
customNodeSourceDockerfile: "Dockerfile",
customNodeImage: "Image",
customNodeDockerfile: "Dockerfile Content",
customNodeDockerfileUpload: "Upload Dockerfile",
customNodeCommand: "Command (one argument per line)",
customNodeInputMode: "Input Contract",
customNodeOutputMode: "Output Contract",
customNodeArtifactType: "Artifact Type",
customNodeSingleAssetSet: "Single asset set",
customNodeMultiAssetSet: "Multiple asset sets",
customNodeReport: "Report only",
customNodeAssetSet: "Asset set",
customNodeAssetSetWithReport: "Asset set with report",
createStorageConnection: "Create Storage Connection",
storageProvider: "Storage Provider",
bucket: "Bucket",
endpoint: "Endpoint",
region: "Region",
basePath: "Base Path",
rootPath: "Root Path",
noStorageConnectionsYet: "No storage connections yet.",
datasetsTitle: "Datasets",
datasetsDescription:
"Create project datasets from source assets and bind them to a storage connection.",
datasetName: "Dataset Name",
datasetDescription: "Dataset Description",
sourceAsset: "Source Asset",
sourceAssets: "Source Assets",
storageConnection: "Storage Connection",
storagePathLabel: "Storage Path",
createDataset: "Create Dataset",
noDatasetsYet: "No datasets have been created yet.",
latestDatasetVersion: "Latest dataset version",
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",
workflowTemplatesTitle: "Workflow Templates",
workflowTemplatesDescription:
"Start workflows from reusable templates or create a blank workflow directly in the project.",
createWorkflow: "Create Workflow",
createBlankWorkflow: "Create Blank Workflow",
createWorkflowFromTemplate: "Create From Template",
saveAsTemplate: "Save As Template",
templateName: "Template Name",
templateDescription: "Template Description",
templateSaved: "Saved template",
noWorkflowTemplatesYet: "No workflow templates yet.",
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",
nodeLibraryHint: "Click to append or drag a node onto the canvas.",
canvas: "Canvas",
canvasHint: "Drag nodes freely, connect handles, zoom, and pan.",
dragNodeToCanvas: "Drop the node here to place it on the canvas.",
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",
failedLoadStorageConnections: "Failed to load storage connections",
failedCreateStorageConnection: "Failed to create storage connection",
failedLoadDatasets: "Failed to load datasets",
failedCreateDataset: "Failed to create dataset",
failedLoadCustomNodes: "Failed to load custom nodes",
failedCreateCustomNode: "Failed to create custom node",
failedRegisterAsset: "Failed to register local asset",
failedLoadWorkflows: "Failed to load workflows",
failedLoadTemplates: "Failed to load workflow templates",
failedCreateTemplate: "Failed to create workflow template",
failedCreateWorkflowFromTemplate: "Failed to create workflow from template",
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",
failedLoadProjects: "Failed to load projects",
failedCreateProject: "Failed to create project",
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}",
invalidConnectionMissingEndpoint: "The connection is missing a valid source or target node.",
invalidConnectionSelf: "A node cannot connect to itself.",
invalidConnectionDuplicate: "This edge already exists.",
invalidConnectionSourceDisallowsOutgoing: "Export nodes cannot create outgoing connections in V1.",
invalidConnectionTargetDisallowsIncoming: "Source nodes cannot accept incoming connections.",
invalidConnectionTargetAlreadyHasIncoming: "This node already has an upstream connection in V1.",
invalidConnectionCycle: "This edge would create a cycle.",
},
zh: {
workspace: "工作空间",
project: "项目",
runs: "运行",
localDev: "本地开发",
navProjects: "项目",
navAssets: "数据资产",
navNodes: "节点",
navWorkflows: "工作流",
navRuns: "运行记录",
navExplore: "查看",
navLabels: "标注",
navAdmin: "管理",
language: "语言",
english: "English",
chinese: "中文",
assetsTitle: "数据资产",
assetsDescription: "注册本地目录、压缩包或数据集文件,并将其探测为受管资产元数据。",
projectsTitle: "项目",
projectsDescription: "创建项目、切换当前项目,并管理项目级资产、数据集、工作流与运行记录。",
projectNameLabel: "项目名称",
projectDescriptionLabel: "项目描述",
createProject: "创建项目",
noProjectsYet: "还没有项目。",
activeProject: "当前项目",
openProject: "打开项目",
storageConnectionsTitle: "存储连接",
storageConnectionsDescription: "定义项目数据集的存储位置,包括本地路径和对象存储提供方。",
nodesTitle: "自定义节点",
nodesDescription:
"通过镜像或自包含 Dockerfile 注册项目级 Docker 节点。容器必须读取 EmboFlow 输入路径,并把结果对象写入 EmboFlow 输出路径。",
createCustomNode: "创建自定义节点",
noCustomNodesYet: "当前还没有自定义节点。",
customNodeName: "节点名称",
customNodeDescription: "节点描述",
customNodeCategory: "节点分类",
customNodeSourceKind: "容器来源",
customNodeSourceImage: "Docker 镜像",
customNodeSourceDockerfile: "Dockerfile",
customNodeImage: "镜像",
customNodeDockerfile: "Dockerfile 内容",
customNodeDockerfileUpload: "上传 Dockerfile",
customNodeCommand: "启动命令(每行一个参数)",
customNodeInputMode: "输入契约",
customNodeOutputMode: "输出契约",
customNodeArtifactType: "产物类型",
customNodeSingleAssetSet: "单资产集",
customNodeMultiAssetSet: "多资产集",
customNodeReport: "仅报告",
customNodeAssetSet: "资产集",
customNodeAssetSetWithReport: "资产集加报告",
createStorageConnection: "创建存储连接",
storageProvider: "存储提供方",
bucket: "Bucket",
endpoint: "Endpoint",
region: "Region",
basePath: "基础路径",
rootPath: "根路径",
noStorageConnectionsYet: "还没有存储连接。",
datasetsTitle: "数据集",
datasetsDescription: "从源资产创建项目数据集,并绑定到一个存储连接。",
datasetName: "数据集名称",
datasetDescription: "数据集描述",
sourceAsset: "源资产",
sourceAssets: "源资产",
storageConnection: "存储连接",
storagePathLabel: "存储路径",
createDataset: "创建数据集",
noDatasetsYet: "还没有创建任何数据集。",
latestDatasetVersion: "最新数据集版本",
localPath: "本地路径",
registerLocalPath: "注册本地路径",
noAssetsYet: "还没有注册任何资产。",
type: "类型",
source: "来源",
detected: "识别结果",
pending: "待探测",
topLevelEntries: "顶层条目",
notAvailable: "暂无",
loadingAssetDetail: "正在加载资产详情...",
assetId: "资产 ID",
status: "状态",
sourcePath: "源路径",
probeAgain: "重新探测",
createExploreArtifact: "创建查看产物",
openExploreView: "打开查看页",
probeSummary: "探测摘要",
warnings: "告警",
none: "无",
recommendedNodes: "推荐节点",
noProbeReportYet: "还没有探测报告。",
workflowsTitle: "工作流",
workflowTemplatesTitle: "工作流模板",
workflowTemplatesDescription: "从可复用模板创建工作流,或者直接在项目里创建空白工作流。",
createWorkflow: "新建工作流",
createBlankWorkflow: "创建空白工作流",
createWorkflowFromTemplate: "从模板创建工作流",
saveAsTemplate: "另存为模板",
templateName: "模板名称",
templateDescription: "模板描述",
templateSaved: "已保存模板",
noWorkflowTemplatesYet: "还没有工作流模板。",
noWorkflowsYet: "还没有工作流。",
latestVersion: "最新版本",
workflowEditor: "工作流编辑器",
runAsset: "运行资产",
saveWorkflowVersion: "保存工作流版本",
triggerWorkflowRun: "触发工作流运行",
reloadLatestSaved: "重新加载最新保存版本",
openLatestRun: "打开最新运行",
selectAssetBeforeRun: "触发工作流运行前请先选择资产。",
nodeLibrary: "节点面板",
nodeLibraryHint: "支持点击追加,也支持将节点拖入画布指定位置。",
canvas: "画布",
canvasHint: "支持自由拖动节点、拖拽连线、缩放和平移。",
dragNodeToCanvas: "将节点拖放到这里即可在画布中创建。",
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: "加载资产失败",
failedLoadStorageConnections: "加载存储连接失败",
failedCreateStorageConnection: "创建存储连接失败",
failedLoadDatasets: "加载数据集失败",
failedCreateDataset: "创建数据集失败",
failedLoadCustomNodes: "加载自定义节点失败",
failedCreateCustomNode: "创建自定义节点失败",
failedRegisterAsset: "注册本地资产失败",
failedLoadWorkflows: "加载工作流失败",
failedLoadTemplates: "加载工作流模板失败",
failedCreateTemplate: "创建工作流模板失败",
failedCreateWorkflowFromTemplate: "从模板创建工作流失败",
failedLoadWorkflow: "加载工作流失败",
failedLoadRuns: "加载运行列表失败",
failedLoadRunDetail: "加载运行详情失败",
failedLoadTaskArtifacts: "加载任务产物失败",
failedCancelRun: "取消运行失败",
failedRetryRun: "重试运行失败",
failedRetryTask: "重试任务失败",
failedLoadArtifact: "加载产物失败",
failedBootstrap: "初始化本地上下文失败",
failedLoadProjects: "加载项目失败",
failedCreateProject: "创建项目失败",
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}",
invalidConnectionMissingEndpoint: "该连线缺少有效的起点或终点节点。",
invalidConnectionSelf: "节点不能连接自己。",
invalidConnectionDuplicate: "这条连线已经存在。",
invalidConnectionSourceDisallowsOutgoing: "V1 中导出节点不允许继续向外连线。",
invalidConnectionTargetDisallowsIncoming: "数据源节点不允许接收入边。",
invalidConnectionTargetAlreadyHasIncoming: "V1 中该节点只能保留一条上游入边。",
invalidConnectionCycle: "这条连线会形成环路。",
},
};
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。" },
},
"union-assets": {
en: { name: "Union Assets", description: "Merge multiple upstream asset sets into one deduplicated asset set." },
zh: { name: "资产并集", description: "将多个上游资产集合合并为一个去重后的资产集合。" },
},
"intersect-assets": {
en: { name: "Intersect Assets", description: "Keep only the assets that exist in every upstream asset set." },
zh: { name: "资产交集", description: "只保留所有上游资产集合共同包含的资产。" },
},
"difference-assets": {
en: { name: "Difference Assets", description: "Subtract downstream asset sets from the first upstream asset set." },
zh: { name: "资产差集", description: "从第一个上游资产集合中减去后续上游资产集合。" },
},
"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;
}