Spaces:
Running
Running
stouchinglhh
Claude
sync processor from GitLab c697b6e: 版本 1.19.4 / 前端 1.12.4 —— 服务端 i18n(首屏不再闪英文)、推理模块并入模型页、前端抽出 shared/、缓存键改用前端号
bb59211 Download processor/web/workflow-studio/src/i18n.ts from Stouching-Robotics/DataFlowing: direct link, hf CLI and curl.
- Browser
- Download file 10.2 kB
-
https://huggingface.co/spaces/Stouching-Robotics/DataFlowing/resolve/main/processor/web/workflow-studio/src/i18n.ts
- Command line
-
hf download hf://spaces/Stouching-Robotics/DataFlowing/processor/web/workflow-studio/src/i18n.ts
-
curl -L -o i18n.ts https://huggingface.co/spaces/Stouching-Robotics/DataFlowing/resolve/main/processor/web/workflow-studio/src/i18n.ts
10.2 kB
| /** Workflow Studio 的中英切换。 | |
| * | |
| * 这是**独立的 React 应用**,不加载 `web/static/js/i18n.js`(那套是给 Jinja 模板 | |
| * 用的)。两边靠两个约定打通: | |
| * | |
| * 1. 同一个 localStorage 键 `lang` —— 模板侧 `setLang()` 写、这里读,刷新后一致 | |
| * 2. 自定义事件 `egodata:lang` —— 模板侧切换时派发,这里监听 | |
| * | |
| * ★ 为什么不用 `storage` 事件:那个**只在别的标签页**触发,同一个标签页里 | |
| * localStorage 变了它不会响。而语言切换按钮就在本页的侧边栏上,正好是同标签页。 | |
| * | |
| * 缺词时回落 key 本身(如 `palette.search`)而不是静默显示英文 —— 漏翻译一眼看得见。 | |
| */ | |
| import { useEffect, useMemo, useState } from 'react'; | |
| export type Lang = 'en' | 'zh'; | |
| const LANG_KEY = 'lang'; | |
| const LANG_EVENT = 'egodata:lang'; | |
| /** 当前语言。默认英文(与模板侧一致)。隐私模式禁 storage 时回落默认。 */ | |
| export function currentLang(): Lang { | |
| try { | |
| return localStorage.getItem(LANG_KEY) === 'zh' ? 'zh' : 'en'; | |
| } catch { | |
| return 'en'; | |
| } | |
| } | |
| const DICT: Record<string, { en: string; zh: string }> = { | |
| // ── 调色板 ── | |
| 'palette.input': { en: 'Input', zh: '输入' }, | |
| 'palette.review': { en: 'Review', zh: '审核' }, | |
| 'palette.process': { en: 'Process', zh: '处理' }, | |
| 'palette.export': { en: 'Export', zh: '导出' }, | |
| 'palette.search': { en: 'Search nodes...', zh: '搜索节点...' }, | |
| 'palette.hint': { en: 'Drag nodes onto canvas', zh: '拖拽节点到画布' }, | |
| // ── 工具栏 ── | |
| 'toolbar.new': { en: 'New', zh: '新建' }, | |
| 'toolbar.save': { en: 'Save', zh: '保存' }, | |
| 'toolbar.saveAs': { en: 'Save As', zh: '另存为' }, | |
| 'toolbar.export': { en: 'Export', zh: '导出' }, | |
| 'toolbar.workflowList': { en: 'Workflow list', zh: '工作流列表' }, | |
| 'toolbar.run': { en: 'Run', zh: '运行' }, | |
| // ── 通用 ── | |
| 'common.cancel': { en: 'Cancel', zh: '取消' }, | |
| 'common.save': { en: 'Save', zh: '保存' }, | |
| 'common.loading': { en: 'Loading...', zh: '加载中...' }, | |
| // ── AI 标注设置弹窗 ── | |
| 'ai.vendor': { en: 'API vendor', zh: 'API 供应商' }, | |
| 'ai.model': { en: 'API model', zh: 'API 模型' }, | |
| 'ai.key': { en: 'API key (stored with this workflow)', zh: 'API Key(随本工作流保存)' }, | |
| 'ai.labelLanguage': { en: 'Label language', zh: '标注语言' }, | |
| // ── 抽屉 / 连线 / 节点 ── | |
| 'drawer.workflows': { en: 'Workflows', zh: '工作流' }, | |
| 'drawer.noWorkflows': { en: 'No workflows yet', zh: '暂无工作流' }, | |
| 'edge.delete': { en: 'Delete connection', zh: '删除连线' }, | |
| 'node.projectOverride': { en: 'Project override', zh: '项目级覆盖' }, | |
| 'node.saveForProject': { en: 'Save for this project', zh: '仅保存到本项目' }, | |
| 'node.clearOverride': { en: 'Clear project override', zh: '清除项目级覆盖' }, | |
| 'node.clear': { en: 'Clear', zh: '清除' }, | |
| 'node.clearOverrideHint': { en: 'Project override — click to clear', zh: '项目级覆盖 —— 点击清除' }, | |
| 'node.aiSettings': { en: 'AI annotation settings', zh: 'AI 标注设置' }, | |
| 'node.aiSettingsShort': { en: 'Data quality settings', zh: '数据质检设置' }, | |
| // ── AI 设置弹窗的测试结果 ── | |
| 'ai.modelRequired': { en: 'Model required', zh: '请先填写模型' }, | |
| 'ai.keyRequired': { en: 'API key required', zh: '请先填写 API Key' }, | |
| 'ai.testFailed': { en: 'Test failed', zh: '测试失败' }, | |
| 'ai.requestFailed': { en: 'Request failed', zh: '请求失败' }, | |
| 'ai.networkError': { en: 'network error', zh: '网络错误' }, | |
| 'ai.appliesNextRun': { en: 'Applies to the next AI annotation run.', zh: '对下一次 AI 标注生效。' }, | |
| 'common.close': { en: 'Close', zh: '关闭' }, | |
| // ── 画布提示 ── | |
| 'toast.dupEdge': { en: 'Already connected: this port pair is already linked.', | |
| zh: '已连接:这条端口对已经连过了。' }, | |
| 'toast.selfEdge': { en: 'Self-connection is not allowed', zh: '不能连接到自身' }, | |
| 'toast.cannotConnect': { en: 'Cannot connect: ', zh: '无法连接:' }, | |
| 'toast.portMismatch': { | |
| en: 'Port mismatch: this input expects different data.', | |
| zh: '端口类型不匹配:这个输入要的数据类型与上游输出不同。' }, | |
| 'toast.stereoPair': { en: 'Stereo pair connected', zh: '双目已配对连接' }, | |
| // ── 数据清洗设置弹窗 ── | |
| 'quality.title': { en: 'Data Cleaning · Settings', zh: '数据清洗 · 设置' }, | |
| 'quality.loadFailed': { en: 'Failed to load the check catalog: ', zh: '加载检查项目录失败:' }, | |
| 'quality.noDevice': { en: 'This node has no device connected yet', zh: '该节点还没有连接任何设备' }, | |
| 'quality.noChecks': { en: 'has no checks available yet', zh: '暂时没有可用的检查项' }, | |
| // ── 顶栏 ── | |
| 'app.template': { en: 'Template', zh: '模板' }, | |
| 'app.makeTemplate': { en: 'Make Template', zh: '设为模板' }, | |
| 'app.save': { en: 'Save', zh: '保存' }, | |
| 'app.del': { en: 'Del', zh: '删除' }, | |
| 'app.cancel': { en: 'Cancel', zh: '取消' }, | |
| 'app.newWorkflow': { en: 'New Workflow', zh: '新建工作流' }, | |
| 'app.startFromTemplate': { en: 'Start from Template', zh: '从模板开始' }, | |
| 'app.unsetTemplate': { en: 'Unset Template', zh: '取消模板' }, | |
| 'app.noTemplates': { en: 'No templates available — an admin can mark workflows as templates.', | |
| zh: '暂无模板 —— 管理员可将工作流标记为模板。' }, | |
| 'app.templateHint': { en: 'Applying a template replaces the current canvas — your workflow name and ID stay the same.', | |
| zh: '应用模板会替换当前画布 —— 工作流名称与 ID 保持不变。' }, | |
| }; | |
| export function translate(key: string, lang: Lang): string { | |
| const entry = DICT[key]; | |
| if (!entry) return key; | |
| return entry[lang] || entry.en; | |
| } | |
| // ── 节点卡片名 ────────────────────────────────────────────── | |
| // | |
| // 卡片名是**后端声明的**(module_catalog 的 label),前端只是显示。这里给中文 | |
| // 对照,key = 模块 slug。查不到就回落到英文原文 —— 后端新增模块时不会显示成 | |
| // 空白的 key。 | |
| const NODE_LABELS_ZH: Record<string, string> = { | |
| mono_camera: 'RGB 相机', | |
| rgb_camera: 'RGB 相机', | |
| fisheye_camera: 'RGB 相机', | |
| rgbd_camera: 'RGB-D 相机', | |
| stereo_camera: '双目 RGB 相机', | |
| stereo_rgbd_camera: '双目 RGB-D 相机', | |
| glove_sensor: '触觉手套', | |
| gripper_device: 'UMI 夹爪', | |
| ai_annotation: 'AI 标注', | |
| annotation: '人工标注', | |
| mediapipe_hand: 'MediaPipe 手部', | |
| rgb_to_2d_bare_hand: 'RGB_2D_裸手', | |
| rgb_to_2d_black_glove: 'RGB_2D_黑手套', | |
| rgbd_to_3d_bare_hand: 'RGB-D_3D_裸手', | |
| rgbd_to_3d_black_glove: 'RGB-D_3D_黑手套', | |
| umi_slam_action: 'UMI SLAM 动作', | |
| data_quality: '数据清洗', | |
| human_review: '人工审核', | |
| lerobot_export: 'LeRobot 导出', | |
| hdf5_export: 'HDF5 导出', | |
| }; | |
| /** 相机卡的分类名走的是另一条路(cameraCategoryLabel 直接返回展示串), | |
| * 按英文原文再兜一层。 */ | |
| const DISPLAY_LABELS_ZH: Record<string, string> = { | |
| 'RGB Camera': 'RGB 相机', | |
| 'Stereo RGB Camera': '双目 RGB 相机', | |
| 'RGB-D Camera': 'RGB-D 相机', | |
| 'Stereo RGB-D Camera': '双目 RGB-D 相机', | |
| 'Glove Sensor': '触觉手套', | |
| 'UMI Gripper': 'UMI 夹爪', | |
| }; | |
| export function nodeLabel(type: string, fallback: string, lang: Lang): string { | |
| if (lang !== 'zh') return fallback; | |
| return NODE_LABELS_ZH[type] || DISPLAY_LABELS_ZH[fallback] || fallback; | |
| } | |
| // ── 端口名 ────────────────────────────────────────────────── | |
| // | |
| // key 用 ``*:端口key`` 表示"所有节点都这么叫",``节点:端口key`` 用于同一个 | |
| // 端口在不同卡上叫法不同的情况 —— 最典型的是 ``data``:导出卡上是"可导出数据", | |
| // 质检卡上就是"数据",审核卡上是"待审核数据"。只按端口 key 查会串味。 | |
| const PORT_LABELS_ZH: Record<string, string> = { | |
| '*:video': 'RGB 视频', | |
| '*:depth': '深度', | |
| '*:video_left': '左目 RGB 视频', | |
| '*:video_right': '右目 RGB 视频', | |
| '*:rgb_video': 'RGB 视频', | |
| '*:gripper_state': 'ESP 夹爪状态', | |
| '*:slam_trajectory': 'SLAM 轨迹', | |
| '*:tactile_force_matrices': '左右力矩阵', | |
| '*:sensor_data': '手套传感器数据', | |
| '*:annotation': '标注', | |
| '*:hand_keypoints': '手部 2D', | |
| '*:hand_3d': '手部 3D', | |
| '*:action': '动作', | |
| '*:reviewed': '已审核数据', | |
| '*:dataset': '数据集', | |
| 'data_quality:data': '数据', | |
| 'human_review:data': '待审核数据', | |
| 'lerobot_export:data': '可导出数据', | |
| 'hdf5_export:data': '可导出数据', | |
| 'ai_annotation:data': 'RGB 视频', | |
| 'annotation:data': 'RGB 视频', | |
| }; | |
| export function portLabel( | |
| nodeType: string, key: string, fallback: string, lang: Lang, | |
| ): string { | |
| if (lang !== 'zh') return fallback; | |
| return PORT_LABELS_ZH[`${nodeType}:${key}`] || PORT_LABELS_ZH[`*:${key}`] || fallback; | |
| } | |
| /** | |
| * 订阅当前语言,返回一个取词函数。 | |
| * | |
| * 语言变化时**所有用到它的组件都会重渲染** —— 这正是需要的:调色板、工具栏、 | |
| * 节点卡片都要跟着换文字。 | |
| */ | |
| export function useLang(): Lang { | |
| const [lang, setLang] = useState<Lang>(currentLang); | |
| useEffect(() => { | |
| const onChange = () => setLang(currentLang()); | |
| window.addEventListener(LANG_EVENT, onChange); | |
| // 兜底:万一别处(如 devtools)直接改了 localStorage 而没派发事件 | |
| window.addEventListener('storage', onChange); | |
| return () => { | |
| window.removeEventListener(LANG_EVENT, onChange); | |
| window.removeEventListener('storage', onChange); | |
| }; | |
| }, []); | |
| return lang; | |
| } | |
| /** 取词函数。需要节点名/端口名的组件再单独调 useLang()。 */ | |
| export function useT(): (key: string) => string { | |
| const lang = useLang(); | |
| return useMemo(() => (key: string) => translate(key, lang), [lang]); | |
| } | |