// ==UserScript== // @name ADO 韩中翻译(ALCHERA ADO 3D Cuboid) // @namespace https://ado.treed.ai/ // @version 1.2.0 // @description 将 ALCHERA ADO 3D Cuboid 标注平台界面(韩文 + 英文)实时翻译为中文。术语依据《PJT64 Cuboid_Guide》官方中文版与标注行业规范。v1.2.0 覆盖平台新增的「尺寸上下限例外」功能:对象列表的尺寸告警徽标、「上下限例外」复选框及其悬浮说明、以及走帧/保存时被尺寸超范围拦截的提示框(并修复同一条消息内多处「(허용 a~b)」只译第一处的问题)。 // @author Bro // @match https://ado.treed.ai/* // @run-at document-idle // @grant none // @noframes // ==/UserScript== /* * ──────────────────────────────────────────────────────────────── * ADO 界面韩中/英中对照翻译(专为 PJT64 Cuboid 项目) * ──────────────────────────────────────────────────────────────── * 术语依据: * 1) 《02_PJT64 Cuboid_Guide_CN V2》官方中文版(ALCHERA Inc.) * - P14/P15 类别·子类别定义 → 英文子类别名译法 * - P24-P26 停车障碍物定义 → cone=锥桶 / bollard=护柱 … * - P5 术语表 → Width 宽 / Height 高 / Length 长 * 2) 3D 立方体标注行业通用译法 * * 翻译原则: * · 只替换「界面文案」,绝不改动任何业务数据 * - 不动 input / textarea 的 value(ID、尺寸、TC/OC、帧号等) * - 不动 select 的 value(只改 option 显示文字,提交值仍是原英文码) * - 不动 canvas / WebGL 内的绘制内容 * · 英文只做「白名单整段精确匹配」: * PCD 文件名、序列名、ROI 预设名(PJT64_Cuboid 等)、tracking_id、 * JSON / ROI / PCD / ID / Shift / Ctrl 等技术标识一律保持原样, * 避免破坏与项目方的字段对照关系。 * · 类别名 / 子类别名属数据标签,可用 Ctrl+Alt+L 单独关闭(保持英文) * · 无法命中的长句保留原文,不猜测、不生造。 * 控制台 adoCN.report() 查看未命中的韩文。 * * v1.2.0 变更(平台新增「尺寸上下限例外」功能后): * ① 顶部提示框(走帧 / 保存被尺寸超范围拦截) * 크기 허용 범위 이탈 객체 N개 — 프레임을 이동할 수 없습니다. * 新增整句规则;并修复「同一条消息里 (허용 a~b) 只译出第一处」的 bug * —— 根因是 String.replace 不带 /g 只替换第一处,现改为循环替换到稳定。 * ② 对象列表的尺寸告警徽标 * 크기 → 尺寸异常 (红色 = 尺寸超出允许范围) * 크기 예외 → 尺寸例外 (琥珀色 = 已指定上下限例外) * 注意「크기」单独出现时不能全局译成「尺寸异常」——「类别」面板里 * 还有一处同名 span("将以所选类别的 [크기] 创建立方体"), * 故徽标按父元素 title 前缀上下文识别,只改徽标那一处。 * ③ 尺寸面板的「上下限例外」复选框及其悬浮说明(原文整句收录) * * 使用: * · 右下角小圆点 —— 左键:中/韩切换;右键:类别名翻译开关 * 绿 = 全中文 橙 = 译文但类别名保持英文 灰 = 韩文原文 * · Ctrl+Alt+T 中/韩切换 | Ctrl+Alt+L 类别名翻译开关 * · 两项状态均自动记忆 * ──────────────────────────────────────────────────────────────── */ (function () { "use strict"; var HANGUL = /[\uAC00-\uD7A3\u1100-\u11FF\u3130-\u318F]/; var STORE_KEY = "ado_cn_translate_on"; var LABELS_KEY = "ado_cn_labels_on"; /* ============ 1. 整段精确词典(静态界面文案) ============ */ var DICT = { /* ---- 顶栏 ---- */ "ALCHERA ADO": "ALCHERA ADO", "3D Cuboid Annotation Tool": "3D 立方体标注工具", "단축키 안내": "快捷键说明", "JSON 불러오기": "载入 JSON", "CLEAR": "清空本帧", "SAVE FRAME": "保存帧", "닫기": "关闭", "한국어": "韩语", "영문": "英文", /* ---- 左侧面板:三个页签 ---- */ "표시": "显示", "조작": "操作", "ROI 영역": "ROI 区域", "객체": "对象", "클래스": "类别", "설정": "设置", /* ---- 显示设置 ---- */ "포인트 크기": "点大小", "선 두께": "线宽", "카메라 선 두께": "摄像头线宽", "착색 기준": "着色基准", "높이": "高度", "INTENSITY": "强度", "착색 배율": "着色倍率", "선택 박스만 표시 (O키)": "仅显示选中框 (O 键)", "선택 박스만 표시": "仅显示选中框", "선택 박스만 표시 토글": "切换仅显示选中框", "객체 ID 표시 (I키)": "显示对象 ID (I 键)", "객체 ID 표시": "显示对象 ID", "객체 ID 표시 토글": "切换显示对象 ID", "높이(Z) 필터": "高度(Z) 过滤", "높이 범위": "高度范围", "데이터를 불러오면 높이 범위를 조절할 수 있습니다": "载入数据后可调节高度范围", /* ---- 操作设置 ---- */ "이동 스텝": "平移步长", "회전 스텝": "旋转步长", "카메라 속도": "摄像头旋转速度", /* ---- ROI 区域 / 预设 ---- */ "전체 지우기": "全部清除", "기본값으로 초기화": "恢复默认值", "프리셋 적용": "应用预设", "프리셋 적용 (경계 테두리 포함)": "应用预设(含边界外框)", /* ---- 类面板 ---- */ "선택한 클래스의 크기로 새 큐보이드가 생성됩니다 (Shift+클릭). 클래스는 미지정으로 시작하니 객체 탭에서 지정하세요.": "将以所选类别的尺寸创建立方体(Shift+点击)。新建时类别为未指定,请在「对象」页签中指定。", /* JSX 中该提示被拆成 3 个文本节点,故分别收录以保证拼接顺序正确 */ "선택한 클래스의": "将以所选类别的", "로 새 큐보이드가 생성됩니다 (Shift+클릭). 클래스는 미지정으로 시작하니 객체 탭에서 지정하세요.": "创建立方体(Shift+点击)。新建时类别为未指定,请在「对象」页签中指定。", "크기": "尺寸", "큐보이드를 선택하면 단면이 표시됩니다": "选中立方体后显示截面", "카메라 이미지와 2D 박스가 표시됩니다": "显示摄像头图像和 2D 框", /* ---- 对象面板 ---- */ "전체 클래스": "全部类别", "미지정": "未指定", "전체 선택": "全选", "클래스 필터": "类别筛选", "필터된 클래스의 객체를 모두 선택": "全选筛选类别下的对象", "해당 클래스의 객체가 없습니다.": "该类别下没有对象。", "이 프레임에 객체가 없습니다.": "本帧没有对象。", "데이터셋을 불러오면 객체 목록이 표시됩니다.": "载入数据集后显示对象列表。", "데이터셋 디렉토리를 불러오세요.": "请载入数据集目录。", /* ---- 属性面板 ---- */ "ID": "ID", "서브클래스": "子类别", "카테고리": "类别", "디멘션": "尺寸", "카메라 속성": "摄像头属性", "반대면 고정": "对面固定", "위치·회전은 유지": "保持位置·旋转不变", "정방형 강제로 연동됨": "正方形强制联动", "허용 범위 이탈": "超出允许范围", "(없음)": "(无)", "(빈 값)": "(空值)", "보정": "修正", "숨김": "隐藏", "재결합": "重新合并", "이 객체의": "该对象的", "전체 프레임에 적용": "应用到全部帧", "전체 프레임에 적용 (즉시 저장)": "应用到全部帧(立即保存)", /* ---- 尺寸轴 / tracking_id 提示 ---- */ "length (전후, 로컬 +x)": "length(前后,局部 +x)", "width (좌우, 로컬 +y)": "width(左右,局部 +y)", "height (상하)": "height(上下)", "tracking_id — 다른 프레임의 같은 물체와 이어붙일 때 사용": "tracking_id — 用于与其他帧中的同一物体关联", /* ---- 快捷键弹窗:分组标题 ---- */ "프레임 · 시퀀스": "帧 · 序列", "생성 · 선택": "创建 · 选择", "이동 · 크기 (3D)": "平移 · 尺寸 (3D)", "회전 · 정렬": "旋转 · 对齐", "편집 · 클립보드": "编辑 · 剪贴板", "화면 조작": "视图操作", "스텝 설정": "步长设置", "카메라 뷰 (2D 박스 모드)": "摄像头视图(2D 框模式)", /* ---- 快捷键弹窗:功能名 ---- */ "이전 프레임 (저장 후 이동)": "上一帧(保存后移动)", "다음 프레임 (저장 후 이동)": "下一帧(保存后移动)", "현재 프레임 전체 지우기": "清空当前帧", "이전 프레임 어노테이션 복사": "复制上一帧标注", "선택 객체 치수 시퀀스 통일": "选中对象尺寸全序列统一", "큐보이드 생성 (활성 클래스)": "创建立方体(当前类别)", "박스 선택": "选择框", "다중 선택 토글": "切换多选", "전체 선택 / 선택 해제": "全选 / 取消全选", "다음 / 이전 박스": "下一个 / 上一个框", "선택 박스 삭제": "删除选中框", "진행 중 취소 / 선택 해제": "取消进行中 / 取消选择", "앞 / 뒤 이동": "前后平移", "좌 / 우 이동": "左右平移", "위 / 아래 이동": "上下平移", "크기 변경 (길이·폭·높이)": "调整尺寸(长·宽·高)", "표준 크기 즉시 적용": "立即应用标准尺寸", "단면 뷰 코너 드래그": "截面视图角点拖拽", "Yaw 회전": "Yaw 旋转(偏航)", "Pitch 회전": "Pitch 旋转(俯仰)", "Roll 회전": "Roll 旋转(翻滚)", "최소 각도 정렬 (자차 방향 0°)": "最小角度对齐(自车方向 0°)", "방향 자동 맞춤": "方向自动适配", "긴 변 앞으로": "长边朝前", "실행 취소 / 다시 실행": "撤销 / 重做", "박스 복사": "复制框", "커서 위치에 붙여넣기": "粘贴到光标位置", "선택 박스에 덮어쓰기": "覆盖到选中框", "tracking ID 변경": "修改跟踪 ID", "화면 이동 (팬)": "平移视图", "화면 회전": "旋转视图", "줌": "缩放", "시점 초기화": "重置视角", "이동 스텝 감소 / 증가": "减小 / 增大平移步长", "회전 스텝 감소 / 증가": "减小 / 增大旋转步长", "2D 박스 모드 전환": "切换 2D 框模式", "2D 박스 이동": "2D 框平移", "2D 박스 크기 조절": "2D 框尺寸调整", "이 카메라에서 강제 숨김": "在该摄像头中强制隐藏", "자동 투영으로 재결합": "自动投影重新合并", "단일뷰 / 4분할 전환": "单视图 / 四分屏 切换", /* ---- 快捷键弹窗:备注 ---- */ "확인창 후 삭제 — Ctrl+Z로 복원 가능": "确认后删除 — 可用 Ctrl+Z 恢复", "현재 내용 교체 — Ctrl+Z로 복원 가능": "替换当前内容 — 可用 Ctrl+Z 恢复", "같은 ID의 W/H/L을 전체 프레임에 적용 (즉시 저장)": "将同一 ID 的 W/H/L 应用到全部帧(立即保存)", "Shift 조합 시 2배 속도 (이동 공통) · 다중 선택 시 W/A/S/D는 화면 기준 위/좌/아래/우 일괄 이동": "加 Shift 速度翻倍(平移通用)· 多选时 W/A/S/D 按屏幕方向整体平移上/左/下/右", "Shift 조합 시 90°씩": "加 Shift 每次 90°", "Shift 조합 시 2배 속도": "加 Shift 速度翻倍", "카메라 이미지의 ID 텍스트만 숨김 — 박스는 그대로": "仅隐藏摄像头图像上的 ID 文字 — 框保持不变", "위치·회전은 유지": "保持位置·旋转不变", "TOP/SIDE/FRONT에서 크기 조절": "在 TOP/SIDE/FRONT 视图中调整尺寸", "빈 공간에서 시작 — 휠 버튼 드래그·Space+좌드래그도 동일": "从空白处开始 — 中键拖拽、Space+左键拖拽等效", "프레임 내 중복 불가": "帧内不可重复", "코너 드래그로도 가능": "也可拖拽角点", "마우스": "鼠标", "클릭": "点击", "Ctrl+클릭": "Ctrl+点击", "Shift+클릭": "Shift+点击", "좌드래그": "左键拖拽", "우드래그": "右键拖拽", "휠": "滚轮", "이미지 우클릭": "在图像上右键", "채널 칩 클릭": "点击通道标签", "우측 패널 ID 입력": "在右侧面板输入 ID", "Ctrl+이동키": "Ctrl+方向键", "단축키 안내 탭": "快捷键说明页签", "툴 우측 상단 단축키 탭 참고": "参见工具右上角快捷键页签", /* ---- 角色模式(URL ?role=) ---- */ "객체 작업자": "对象作业员", "속성 작업자": "属性作业员", "검수자": "质检员", "객체 작업자 모드에서는 클래스·속성 편집이 제한됩니다.": "对象作业员模式下,类别·属性编辑受限。", "속성 작업자 모드에서는 객체·지오메트리 편집이 제한됩니다.": "属性作业员模式下,对象·几何编辑受限。", "검수자 모드에서는 클래스·속성 편집이 제한됩니다.": "质检员模式下,类别·属性编辑受限。", "URL 의 ?role= 로 정해진다": "由 URL 的 ?role= 参数决定", /* ---- 尺寸/范围校验提示 ---- */ "단면이 원형·정사각인 클래스라 L·W가 연동됩니다 — 한쪽을 바꾸면 다른 쪽이 따라옵니다.": "该类别截面为圆形/正方形,长(L)·宽(W) 联动 — 改动一侧另一侧会跟随变化。", ". 크기가 맞다면 subclass 오지정일 수 있으니 subclass를 확인하세요.": "。若尺寸正确,可能是子类别指定错误,请检查子类别。", "truncation/occlusion 미입력": "截断/遮挡未填写", "길이(length)가 너비(width)보다 긴 객체에는 적용할 수 없습니다.": "长(length)大于宽(width)的对象无法应用。", "도형의 변이 서로 교차합니다.": "图形各边互相交叉。", "범위 이탈은 대개 subclass 오지정입니다 — subclass가 맞는지 확인하세요.": "超出范围通常是子类别指定错误 — 请确认子类别是否正确。", "확인 — 그대로 저장": "确定 — 照原样保存", "취소 — 저장 취소 후 첫 해당 객체 선택": "取消 — 取消保存并选中第一个相关对象", "취소 — 저장 취소 후 첫 이탈 객체 선택": "取消 — 取消保存并选中第一个超范围对象", "확인 — 새 ID 발급 (트래킹 분리)": "确定 — 分配新 ID(跟踪分离)", "취소 — 이 객체 붙여넣기 취소": "取消 — 放弃粘贴该对象", "확인 — 기존 객체를 붙여넣는 내용으로 교체": "确定 — 用粘贴内容替换已有对象", "취소 — 다음 선택지 (새 ID / 붙여넣기 취소)": "取消 — 查看下一选项(新 ID / 取消粘贴)", /* ---- 尺寸「上下限例外」(v1.2.0 平台新增功能) ---- 背景:对象尺寸超出类别允许范围时会拦截保存 / 走帧; 若实物确实就是这个尺寸,可勾选「上下限例外」放行, 并在标注里留下「已设例外」的标记。 */ "크기 예외": "尺寸例外", " (예외 지정됨)": "(已指定例外)", "상하한 예외 — 실물이 이 크기가 맞아 범위를 벗어난 경우에만 켠다. 켜면 저장·이동이 통과하고 예외 표시가 어노테이션에 남는다.": "上下限例外 — 仅当实物确实是该尺寸、因而超出允许范围时才勾选。勾选后保存·移动可放行,标注中会留下「已设例外」的标记。", "실물이 이 크기가 맞을 때만 켠다 — 켜면 이 객체는 저장·이동 차단에서 빠지고, 예외로 저장됐다는 표시가 어노테이션에 남는다": "仅当实物确实是该尺寸时才勾选 — 勾选后该对象不再被保存·移动拦截,标注中会留下「已设例外」的标记", "예외 지정은 객체 작업자·검수자 몫이다": "例外指定由对象作业员·质检员负责", /* ---- 平台更新后新增 / 改写的界面文案(全量扫描产物,v1.2.0 补齐) ---- */ "불러오는 중...": "正在载入…", "불러오는 중입니다.": "正在载入。", "PCD 파일명 검색...": "搜索 PCD 文件名…", "이미 시퀀스를 불러오는 중입니다.": "已在载入序列。", "지금 로드가 끝난 뒤에 다시 선택해 주세요.": "请在当前载入完成后再选择。", "자동 투영으로 재결합 (R)": "自动投影重新合并 (R)", /* ⚠ DICT 是按 trim 后的正文查表,键首尾不能带空格,否则永远命不中 */ "— 시퀀스 JSON(.json)을 놓아주세요.": "— 请拖入序列 JSON (.json) 文件。", "(예외 지정됨)": "(已指定例外)", "수동 보정된 2D 박스를 자동 투영으로 재결합할까요? 보정 내용은 사라집니다.": "是否将手动修正过的 2D 框用自动投影重新合并?修正内容将丢失。", /* ---- 状态 / 错误 / 提示 ---- */ "전체 삭제 완료": "已全部删除", "프레임 저장 완료": "帧保存完成", "프레임 저장 실패 — SAVE FRAME으로 재시도할 수 있습니다.": "帧保存失败 — 可用 SAVE FRAME 重试。", "프레임 저장 실패 — 이동을 취소했습니다.": "帧保存失败 — 已取消移动。", "저장이 이미 진행 중입니다.": "保存已在进行中。", "시퀀스 로드 실패": "序列载入失败", "시퀀스 로드 완료": "序列载入完成", "시퀀스 로드에 실패했습니다.": "序列载入失败。", "시퀀스 불러오는 중...": "正在载入序列…", "프레임 로드에 실패했습니다.": "帧载入失败。", "프레임을 찾을 수 없습니다.": "找不到该帧。", "이 프레임은 서버에 어노테이션 파일이 등록되어 있지 않습니다.": "该帧的标注文件尚未在服务器登记。", "어노테이션 파일 미등록 프레임 — 저장할 수 없습니다.": "标注文件未登记的帧 — 无法保存。", "어노테이션 파일 미등록 프레임 — 저장할 수 없어 이동을 취소했습니다.": "标注文件未登记的帧 — 无法保存,已取消移动。", "업로드 URL 발급에 실패했습니다 (파일 미등록).": "获取上传 URL 失败(文件未登记)。", "업로드에 실패했습니다.": "上传失败。", "토큰을 찾을 수 없습니다.": "找不到令牌。", "JSON 파일만 열 수 있습니다.": "只能打开 JSON 文件。", "PCD에 x/y/z 필드가 없습니다.": "PCD 中缺少 x/y/z 字段。", "ID는 0 이상의 정수여야 합니다.": "ID 必须为 0 及以上的整数。", "시퀀스 JSON 파일을 놓아 열기": "拖入序列 JSON 文件以打开", "시퀀스 JSON 파일을 직접 놓아주세요.": "请直接拖入序列 JSON 文件。", "시퀀스 JSON 형식이 아닙니다. (payload.files 없음)": "不是序列 JSON 格式。(缺少 payload.files)", "시퀀스 config.json을 해석할 수 없어 기본 클래스 정의를 사용합니다.": "无法解析序列 config.json,将使用默认类别定义。", "파일을 1개만 놓아주세요.": "请只拖入 1 个文件。", "폴더는 열 수 없습니다.": "无法打开文件夹。", "이 프레임에 해당 채널 이미지가 없습니다": "该帧没有此通道的图像", "이전 프레임에 복사할 객체가 없습니다.": "上一帧没有可复制的对象。", "파일명을 입력하세요": "请输入文件名", "검색 결과가 없습니다": "无搜索结果", "PCD 파일명 검색": "搜索 PCD 文件名", "시점 초기화 (Ctrl+X)": "重置视角 (Ctrl+X)" /* 视图名 / 相机方向(TOP·SIDE·FRONT·front·rear…)统一由下方英文词典处理 */ }; /* ============ 2. 动态文案规则(含变量/数字的界面句) ============ */ var RULES = [ /* 对象计数 / 属性面板标题 */ [/^객체 \((\d+)\/(\d+)\)$/, "对象 ($1/$2)"], [/^객체 \((\d+) \/ (\d+)\)$/, "对象 ($1/$2)"], [/^클래스 — #(\d+)$/, "类别 — #$1"], [/^크기 — #(\d+)$/, "尺寸 — #$1"], [/^카메라 속성 — #(\d+)$/, "摄像头属性 — #$1"], [/^이동 스텝 (\d+)\/10 \((\d+)cm\/키\)$/, "平移步长 $1/10 ($2cm/次)"], [/^회전 스텝 (\d+)\/10 \(([\d.]+)°\/키\)$/, "旋转步长 $1/10 ($2°/次)"], /* 尺寸超范围警告 */ [/\(허용 ([\d.]+)~([\d.]+)\)/, "(允许 $1~$2)"], [/([\d.]+) — 허용 범위 이탈/, "$1 — 超出允许范围"], [/([\d.]+) — 정방형 강제로 연동됨/, "$1 — 正方形强制联动"], [/^크기 허용 범위 이탈 — (.+) · subclass 확인 필요$/, "尺寸超出允许范围 — $1 · 需确认子类别"], [/^크기 허용 범위 이탈 — (.+) · 상하한 예외로 지정됨$/, "尺寸超出允许范围 — $1 · 已指定为上下限例外"], /* 列表徽标的 tooltip:크기 허용 범위 이탈 — L 3.46 (허용 a~b) (必须先于下面的通用规则,否则「크기」会被当成 $1 原样留下) */ [/^크기 허용 범위 이탈 — (.+)$/, "尺寸超出允许范围 — $1"], [/^(.+) 허용 범위 이탈 — (.+)$/, "$1 超出允许范围 — $2"], /* 顶部提示框:走帧 / 保存被尺寸超范围拦截(v1.2.0) 原文:크기 허용 범위 이탈 객체 N개 — 프레임을 이동할 수 없습니다. 说明:<清单> — subclass가 맞는지 확인하고 크기를 고친 뒤 이동하세요. 실물이 이 크기가 맞으면 크기 패널에서 상하한 예외를 켜세요. ⚠ 整句规则必须排在下面的片段兜底之前,否则会被拆碎而永不命中 */ [/^크기 허용 범위 이탈 객체 (\d+)개 — 프레임을 이동할 수 없습니다\.$/, "有 $1 个对象尺寸超出允许范围 — 无法移动帧。"], [/^크기 허용 범위 이탈 객체 (\d+)개 — 저장할 수 없습니다\.$/, "有 $1 个对象尺寸超出允许范围 — 无法保存。"], [/^저장된 프레임에 크기 허용 범위 이탈 객체 (\d+)개$/, "已保存的帧中有 $1 个对象尺寸超出允许范围"], [/— subclass가 맞는지 확인하고 크기를 고친 뒤 이동하세요\. 실물이 이 크기가 맞으면 크기 패널에서 상하한 예외를 켜세요\./, "— 请确认子类别是否正确、修正尺寸后再移动。若实物确实是该尺寸,请在尺寸面板中开启「上下限例外」。"], [/— subclass가 맞는지 확인하고 크기를 고친 뒤 저장하세요\. 실물이 이 크기가 맞으면 크기 패널에서 상하한 예외를 켜세요\./, "— 请确认子类别是否正确、修正尺寸后再保存。若实物确实是该尺寸,请在尺寸面板中开启「上下限例外」。"], /* 保存 / 移动 / 清空的确认与拦截 */ [/^truncation\/occlusion 미입력 객체가 (\d+)개 있습니다\./, "有 $1 个对象的截断/遮挡未填写。"], [/^크기 허용 범위를 벗어난 객체가 (\d+)개 있습니다\./, "有 $1 个对象尺寸超出允许范围。"], [/^클래스 미지정 객체 (\d+)개 — 저장할 수 없습니다\.$/, "有 $1 个对象未指定类别 — 无法保存。"], [/^클래스 미지정 객체 (\d+)개 — 프레임을 이동할 수 없습니다\.$/, "有 $1 个对象未指定类别 — 无法移动帧。"], [/^저장된 프레임에 truncation\/occlusion 미입력 객체 (\d+)개$/, "已保存的帧中有 $1 个对象截断/遮挡未填写"], [/^저장된 프레임에 크기 허용 범위 이탈 객체 (\d+)개 — subclass 확인 필요$/, "已保存的帧中有 $1 个对象尺寸超出允许范围 — 需确认子类别"], [/— 클래스를 채운 뒤 저장하세요\.$/, "— 请填写类别后再保存。"], [/— 클래스를 채운 뒤 이동하세요\.$/, "— 请填写类别后再移动。"], [/^현재 프레임의 모든 객체\((\d+)개\)를 삭제할까요\?/, "是否删除当前帧的全部对象($1 个)?"], [/^현재 프레임의 객체 (\d+)개를 이전 프레임 내용\((\d+)개\)으로 교체할까요\?/, "是否将当前帧的 $1 个对象替换为上一帧内容($2 个)?"], /* 粘贴 / ID 相关 */ [/^#(\d+)를 새 ID로 붙여넣을까요\?/, "是否将 #$1 作为新 ID 粘贴?"], [/^이미 이 프레임에서 사용 중인 ID입니다\. \(#(\d+)\)$/, "该 ID 已被本帧使用。(#$1)"], [/^#(\d+) 객체가 있는 다른 프레임이 없습니다\.$/, "不存在包含 #$1 对象的其他帧。"], [/^(.+?) 객체가 현재 프레임에 이미 있습니다 \(하이라이트 표시\)\./, "$1 对象已存在于当前帧(已高亮显示)。"], /* 批量应用结果 */ [/^#(\d+) 클래스를 (\d+)개 프레임에 적용했습니다\.$/, "已将 #$1 的类别应用到 $2 个帧。"], [/^#(\d+)의 클래스\((.+)\)를 시퀀스 전체 프레임에 적용할까요\?/, "是否将 #$1 的类别($2)应用到序列全部帧?"], [/^다른 프레임은 즉시 서버에 저장되며 되돌릴 수 없습니다\.$/, "其他帧将立即保存到服务器且无法撤销。"], [/^#(\d+) 치수를 (\d+)개 프레임에 통일했습니다\.$/, "已将 #$1 的尺寸统一到 $2 个帧。"], [/^치수 통일 — (\d+)개 프레임 반영, (\d+)개 실패$/, "尺寸统一 — 成功 $1 个帧,失败 $2 个"], [/^클래스 일괄 적용 — (\d+)개 프레임 반영, (\d+)개 실패$/, "类别批量应用 — 成功 $1 个帧,失败 $2 个"], [/^이전 프레임 객체 (\d+)개를 복사했습니다\.$/, "已复制上一帧的 $1 个对象。"], [/^선택 객체 (\d+)개의 2D 박스가 수동 보정되어 있습니다\. 3D 편집을 계속하면 보정이 초기화\(자동 투영으로 재결합\)됩니다\. 계속할까요\?$/, "选中 $1 个对象的 2D 框已被手动修正。继续 3D 编辑将重置修正(自动投影重新合并)。是否继续?"], [/^이 객체의 2D 박스가 수동 보정되어 있습니다\. 3D 편집을 계속하면 보정이 초기화\(자동 투영으로 재결합\)됩니다\. 계속할까요\?$/, "该对象的 2D 框已被手动修正。继续 3D 编辑将重置修正(自动投影重新合并)。是否继续?"], [/^(\d+)개가 드롭되었습니다 — 시퀀스 JSON 1개만 열 수 있습니다\.$/, "有 $1 个被丢弃 — 只能打开 1 个序列 JSON。"], [/^선택 객체 (\d+)개의$/, "选中 $1 个对象的"], /* 网络 / 状态 */ [/^로그인 실패 \(status: (\d+)\)$/, "登录失败 (status: $1)"], [/^작업 상태 저장 실패 \((\d+)\)/, "作业状态保存失败 ($1)"], [/^작업 상태 조회 실패 \((\d+)\)$/, "作业状态查询失败 ($1)"], [/^파일 다운로드 실패 \((\d+)\)$/, "文件下载失败 ($1)"], [/^파일 업로드 실패 \((\d+)\)$/, "文件上传失败 ($1)"], [/^프리사인 URL 요청 실패 \((\d+)\)/, "预签名 URL 请求失败 ($1)"], [/^지원하지 않는 PCD DATA 형식: (.+)$/, "不支持的 PCD 数据格式: $1"], [/^(.+) — 시퀀스 JSON\(\.json\)을 놓아주세요\.$/, "$1 — 请拖入序列 JSON (.json) 文件。"], [/^#(\d+) \(카메라 엔트리 없음\)$/, "#$1(无摄像头条目)"], /* 片段级兜底(供被 React 拆分的文本节点与拼接句使用) */ [/크기 허용 범위 이탈 — /, "尺寸超出允许范围 — "], [/허용 범위 이탈/, "超出允许范围"], [/ · subclass 확인 필요/, " · 需确认子类别"], [/\. 크기가 맞다면 subclass 오지정일 수 있으니 subclass를 확인하세요\./, "。若尺寸正确,可能是子类别指定错误,请检查子类别。"], /* 尺寸超范围拦截提示的片段兜底(规则措辞变了也不会漏译) */ [/크기 허용 범위 이탈 객체 /, "尺寸超出允许范围的对象 "], [/(\d+)개 — 프레임을 이동할 수 없습니다\./, "共 $1 个 — 无法移动帧。"], [/(\d+)개 — 저장할 수 없습니다\./, "共 $1 个 — 无法保存。"], [/subclass가 맞는지 확인하고 크기를 고친 뒤 이동하세요/, "请确认子类别是否正确、修正尺寸后再移动"], [/subclass가 맞는지 확인하고 크기를 고친 뒤 저장하세요/, "请确认子类别是否正确、修正尺寸后再保存"], [/실물이 이 크기가 맞으면 크기 패널에서 상하한 예외를 켜세요/, "若实物确实是该尺寸,请在尺寸面板中开启「上下限例外」"], /* 上下限例外(复选框说明 / 徽标悬浮提示 / 尺寸行角标) */ [/ · 상하한 예외로 지정됨/, " · 已指定为上下限例外"], [/ \(예외 지정됨\)/, "(已指定例外)"], [/\(없음\)/, "(无)"], [/상하한 예외/, "上下限例外"], [/크기 패널/, "尺寸面板"], [/저장·이동 차단/, "保存·移动拦截"], /* 尺寸行 tooltip:轴名 + 状态(轴名单独出现时由 DICT 处理, 这里是拼接进长句后的形态:「length (전후, 로컬 +x) — 허용 범위 이탈 (예외 지정됨)」) */ [/^length \(전후, 로컬 \+x\)/, "length(前后,局部 +x)"], [/^width \(좌우, 로컬 \+y\)/, "width(左右,局部 +y)"], [/^height \(상하\)/, "height(上下)"], [/ — 허용 범위 이탈/, " — 超出允许范围"], [/ — 정방형 강제로 연동됨/, " — 正方形强制联动"], /* 载入 / 上传进度(平台更新后的新文案,v1.2.0 补齐) */ [/네트워크가 느립니다 — 수신이 멈춰 있습니다/, "网络较慢 — 接收已停止"], [/^(.+) 받음$/, "已接收 $1"], /* ⚠ 注意:正则会拿 trim 后的正文去匹配,模式里的尾随空格会永远匹配不到本行末尾 */ [/^파일 (\d+)\/(\d+)/, "文件 $1/$2"], /* 后台标签页时的 document.title 提示 */ [/✅ 로드 완료 — /, "✅ 载入完成 — "], [/^객체 수 기록 실패 \((\d+)\)/, "对象数记录失败 ($1)"], [/Ctrl\+Z로 되돌릴 수 있습니다/, "可用 Ctrl+Z 恢复"], [/Ctrl\+Z로 복원 가능/, "可用 Ctrl+Z 恢复"], [/한국어/, "韩语"], [/영문/, "英文"] ]; /* ============ 3. 词组兜底(仅用于短文本,保证用词规范) ============ */ var PHRASES = [ ["큐보이드", "立方体"], ["어노테이션", "标注"], ["서브클래스", "子类别"], ["카테고리", "类别"], ["클래스", "类别"], ["시퀀스", "序列"], ["프레임", "帧"], ["카메라", "摄像头"], ["채널", "通道"], ["객체", "对象"], ["속성", "属性"], ["크기", "尺寸"], ["높이", "高度"], ["길이", "长"], ["너비", "宽"], ["두께", "粗细"], ["배율", "倍率"], ["필터", "过滤"], ["스텝", "步长"], ["속도", "速度"], ["정렬", "对齐"], ["회전", "旋转"], ["이동", "平移"], ["생성", "创建"], ["삭제", "删除"], ["저장", "保存"], ["불러오기", "载入"], ["초기화", "重置"], ["지우기", "清除"], ["선택", "选择"], ["표시", "显示"], ["숨김", "隐藏"], ["보정", "修正"], ["재결합", "重新合并"], ["투영", "投影"], ["복사", "复制"], ["붙여넣기", "粘贴"], ["되돌릴", "恢复"], ["복원", "恢复"], ["확인", "确定"], ["취소", "取消"], ["닫기", "关闭"], ["전체", "全部"], ["미지정", "未指定"], ["미입력", "未填写"], ["미등록", "未登记"], ["허용 범위", "允许范围"], ["이탈", "超出"], ["기준", "基准"], ["프리셋", "预设"], ["적용", "应用"], ["테두리", "边框"], ["경계", "边界"], ["단면", "截面"], ["강제", "强制"], ["반대면", "对面"], ["고정", "固定"], ["자차", "自车"], ["방향", "方向"], ["맞춤", "适配"], ["앞으로", "朝前"], ["화면", "视图"], ["줌", "缩放"], ["휠", "滚轮"], ["클릭", "点击"], ["우클릭", "右键"], ["드래그", "拖拽"], ["커서", "光标"], ["위치", "位置"], ["무시", "忽略"], ["교차", "交叉"], ["실패", "失败"], ["완료", "完成"], /* 尺寸上下限例外术语(v1.2.0) */ ["상하한 예외", "上下限例外"], ["예외", "例外"], ["지정됨", "已指定"], ["실물", "实物"], ["차단", "拦截"], ["통과", "放行"] ]; /* ============ 4. 英文标签词典(依据 PJT64 指南官方中文版) ============ */ /* 仅做「整段精确匹配」,绝不触碰数字、ID、文件名、序列名等技术标识。 * 类别 / 子类别名属数据标签,可用 Ctrl+Alt+L 单独开关(默认开)。 */ var EN_DICT = { /* ---- 顶栏 / 通用按钮 ---- */ "clear": "清空本帧", "save frame": "保存帧", "3d cuboid annotation tool": "3D 立方体标注工具", "on": "开", "off": "关", "intensity": "强度", "2d box": "2D 框", "json": "JSON", /* ---- 大类别(Title Case / UPPER 两种写法) ---- */ "vehicle": "车辆", "person": "人", "cycle": "两轮车", "mobility": "代步工具", "cart": "推车", "parking obstacle": "停车障碍物", "parking_obstacle": "停车障碍物", /* ---- VEHICLE 子类别(指南 P14) ---- */ "unknown_vehicle": "其他车辆", "compact": "微型车", "sedan": "乘用车", "suv": "运动型多用途车", "van": "厢式车", "bus": "巴士", "truck": "卡车", "special": "特种车辆", /* ---- PERSON 子类别(指南 P14) ---- */ "unknown_person": "其他人员", "adult_pedestrian": "成人行人", "child_pedestrian": "儿童行人", "unknown_rider": "其他骑乘者", "motorcycle_rider": "摩托车骑手", "bicycle_rider": "自行车骑手", "mobility_rider": "代步工具乘坐者", /* ---- MOBILITY 子类别(指南 P15) ---- */ "unknown_mobility": "其他代步工具", "kick_scooter": "电动滑板车", "wheelchair": "轮椅", /* ---- CYCLE 子类别(指南 P15) ---- */ "unknown_cycle": "其他两轮车", "motorcycle": "摩托车", "bicycle": "自行车", /* ---- CART 子类别(指南 P15) ---- */ "unknown_cart": "其他推车", "shopping_single": "购物车(单辆)", "shopping_stacked": "购物车(叠放)", /* ---- PARKING_OBSTACLE 子类别(指南 P24-P26) ---- */ "unknown_parking_obstacle": "其他停车障碍物", "cone": "锥桶", "parking_locker": "车位锁", "parking_gate": "停车道闸", "parking_gate_body": "道闸本体", "unknown_pillar": "其他停车柱", "pillar": "停车柱", "pillar_circle": "圆形停车柱", "speed_bump": "减速带", "stopper": "挡车器", "stopper_long": "长挡车器", "bollard": "护柱", "bollard_u": "U 型护柱", "no_parking_sign": "禁止停车标志", "no_park_sign_circle": "禁止停车标志(圆形)", "no_park_sign_square": "禁止停车标志(方形)", "pillar_unknown": "其他停车柱", "speedbump": "减速带", "close_bar": "关闭杆", "moving_bar": "移动中杆", "open_bar": "打开杆", /* ---- 视图 / 相机方向 ---- */ "top": "顶视", "side": "侧视", "front": "前视", "rear": "后视", "left": "左视", "right": "右视", "unknown": "未知", /* ---- 尺寸字段(指南 P5:Width 宽 / Height 高 / Length 长) ---- */ "l": "长(L)", "w": "宽(W)", "h": "高(H)", "length": "长(length)", "width": "宽(width)", "height": "高(height)" }; /* 这些 key 属「类别 / 子类别」数据标签,受 Ctrl+Alt+L 开关控制 */ var LABEL_KEYS = { "vehicle": 1, "person": 1, "cycle": 1, "mobility": 1, "cart": 1, "parking obstacle": 1, "parking_obstacle": 1, "unknown_vehicle": 1, "compact": 1, "sedan": 1, "suv": 1, "van": 1, "bus": 1, "truck": 1, "special": 1, "unknown_person": 1, "adult_pedestrian": 1, "child_pedestrian": 1, "unknown_rider": 1, "motorcycle_rider": 1, "bicycle_rider": 1, "mobility_rider": 1, "unknown_mobility": 1, "kick_scooter": 1, "wheelchair": 1, "unknown_cycle": 1, "motorcycle": 1, "bicycle": 1, "unknown_cart": 1, "shopping_single": 1, "shopping_stacked": 1, "unknown_parking_obstacle": 1, "cone": 1, "parking_locker": 1, "parking_gate": 1, "parking_gate_body": 1, "unknown_pillar": 1, "pillar": 1, "pillar_circle": 1, "speed_bump": 1, "stopper": 1, "stopper_long": 1, "bollard": 1, "bollard_u": 1, "no_parking_sign": 1, "no_park_sign_circle": 1, "no_park_sign_square": 1, "pillar_unknown": 1, "speedbump": 1, "close_bar": 1, "moving_bar": 1, "open_bar": 1 }; /* ============ 5. 翻译核心 ============ */ var MISS = Object.create(null); function matchEn(raw) { var m = raw.match(/^(\s*)([\s\S]*?)(\s*)$/); var core = m[2]; if (!core) return null; var key = core.toLowerCase(); var hit = EN_DICT[key]; if (hit === undefined) return null; if (!labelsOn && LABEL_KEYS[key]) return null; return m[1] + hit + m[3]; } function translateText(raw) { if (!raw) return raw; /* 纯英文:只做白名单精确匹配,命中则译,否则原样返回 */ if (!HANGUL.test(raw)) { var en = matchEn(raw); return en === null ? raw : en; } var m = raw.match(/^(\s*)([\s\S]*?)(\s*)$/); var lead = m[1], core = m[2], tail = m[3]; if (!core) return raw; if (Object.prototype.hasOwnProperty.call(DICT, core)) { return lead + DICT[core] + tail; } var out = core, i; /* 依次应用全部规则(每条只针对韩文模式,译后为中文不会再被后续规则命中) ⚠ 循环替换:同一条消息里同一句式可能出现多次 —— 例如尺寸超范围清单 「#128 sedan: L 3.46 (허용 3.937~4.663) #142 sedan: W 2.19 (허용 1.5~2.1) …」 而 String.replace 不带 /g 时只替换第一处,导致后面几条原样留韩文。 这里对每条规则反复替换到不再变化(上限 40 次,防规则自匹配死循环)。 */ for (i = 0; i < RULES.length; i++) { var re = RULES[i][0], rp = RULES[i][1]; for (var guard = 0; guard < 40; guard++) { var next = out.replace(re, rp); if (next === out) break; out = next; } } if (!HANGUL.test(out)) return lead + out + tail; /* 兜底:仅对短文本逐词替换,长句宁保留韩文也不乱译 */ if (out.length <= 24) { var tmp = out; for (i = 0; i < PHRASES.length; i++) { if (tmp.indexOf(PHRASES[i][0]) !== -1) { tmp = tmp.split(PHRASES[i][0]).join(PHRASES[i][1]); } } if (!HANGUL.test(tmp)) return lead + tmp + tail; } MISS[core] = (MISS[core] || 0) + 1; return lead + out + tail; } var ATTRS = ["title", "aria-label", "placeholder", "alt", "data-tooltip"]; var SKIP_TAGS = { SCRIPT: 1, STYLE: 1, NOSCRIPT: 1, TEXTAREA: 1, CODE: 1, PRE: 1 }; var LATIN = /[A-Za-z]/; /* 纯数字 / 纯符号节点直接跳过,减少无谓调用 */ var NEED = /[\uAC00-\uD7A3\u1100-\u11FF\u3130-\u318F A-Za-z]/; function translateAttrs(el) { if (!el || el.nodeType !== 1 || SKIP_TAGS[el.tagName]) return; for (var i = 0; i < ATTRS.length; i++) { var a = ATTRS[i], v = el.getAttribute && el.getAttribute(a); if (v && NEED.test(v)) { var t = translateText(v); if (t !== v) { el.__adoRaw = el.__adoRaw || {}; if (el.__adoRaw[a] === undefined) el.__adoRaw[a] = v; el.setAttribute(a, t); } } } } function translateNode(node) { if (!node) return; if (node.nodeType === 3) { var p = node.parentNode; if (!p || SKIP_TAGS[p.tagName]) return; if (p.isContentEditable) return; var v = node.nodeValue; if (!v || !NEED.test(v)) return; var t; var core = v.trim(); /* 对象列表的「尺寸告警徽标」:크기 ⚠ 「크기」不能进 DICT 全局译成「尺寸异常」——「类别」面板里还有一处同名 span("将以所选类别的 [크기] 创建立方体"),全局译会毁掉那句话。 故按父元素 title 前缀做上下文识别,只改徽标这一处。 译完后 title 已变中文,所以中韩两种前缀都要认(保证幂等)。 */ var pTitle = p.getAttribute ? p.getAttribute("title") : null; if (core === "크기" && pTitle && /^(크기 허용 범위 이탈|尺寸超出允许范围)/.test(pTitle)) { t = v.replace(core, "尺寸异常"); } else if (p.tagName === "LABEL" && (core === "T" || core === "O")) { t = v.replace(core, core === "T" ? "截断" : "遮挡"); } else { t = translateText(v); } if (t !== v) { if (!rawText.has(node)) rawText.set(node, v); node.nodeValue = t; } } else if (node.nodeType === 1) { if (SKIP_TAGS[node.tagName]) return; translateAttrs(node); var w = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, null); var n; while ((n = w.nextNode())) translateNode(n); var all = node.querySelectorAll ? node.querySelectorAll("*") : []; for (var i = 0; i < all.length; i++) translateAttrs(all[i]); } } /* ============ 6. 开关与开关状态 ============ */ var rawText = new WeakMap(); var enabled = true; var labelsOn = true; try { if (localStorage.getItem(STORE_KEY) === "0") enabled = false; if (localStorage.getItem(LABELS_KEY) === "0") labelsOn = false; } catch (e) { /* 隐私模式下忽略 */ } function applyAll() { translateNode(document.body); if (window.__ADO_TITLE_RAW === undefined) window.__ADO_TITLE_RAW = document.title; var t = translateText(document.title || ""); if (t && t !== document.title) document.title = t; } /* 卸载旧实例(重复注入 / 热更新时使用) */ function destroy() { if (mo) { mo.disconnect(); mo = null; } if (timer) { clearTimeout(timer); timer = null; } if (tailTimer) { clearTimeout(tailTimer); tailTimer = null; } try { restoreAll(); } catch (e) { } var bx = document.getElementById("ado-cn-toggle"); if (bx && bx.parentNode) bx.parentNode.removeChild(bx); } function restoreAll() { /* 1) 文本节点还原 */ var tw = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null); var n; while ((n = tw.nextNode())) { if (rawText.has(n)) { try { n.nodeValue = rawText.get(n); } catch (e) { /* textarea 等只读场景 */ } } } /* 2) 属性还原 */ var all = document.querySelectorAll("[title],[aria-label],[placeholder],[alt]"); for (var i = 0; i < all.length; i++) { var el = all[i], raw = el.__adoRaw; if (!raw) continue; for (var k in raw) { if (Object.prototype.hasOwnProperty.call(raw, k) && raw[k] !== undefined) { el.setAttribute(k, raw[k]); } } } /* 3) 标题还原 */ if (window.__ADO_TITLE_RAW !== undefined) document.title = window.__ADO_TITLE_RAW; } /* ============ 7. MutationObserver(增量翻译,静默期防自触发循环) ============ */ var mo = null, timer = null, tailTimer = null, queued = []; function observeTarget() { if (!mo) return; mo.observe(document.documentElement, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ATTRS }); } function flush() { timer = null; if (!enabled) { queued = []; return; } var list = queued; queued = []; /* 关键:翻译自身会改动 DOM,先断开观察器,避免回调自触发导致循环 */ if (mo) mo.disconnect(); try { for (var i = 0; i < list.length; i++) { try { translateNode(list[i]); } catch (e) { /* 忽略单个节点异常 */ } } applyAll(); } finally { if (mo && enabled) observeTarget(); } } function schedule(immediate) { if (timer) clearTimeout(timer); timer = setTimeout(flush, immediate ? 0 : 60); /* 尾部补偿:展开动画 / WebGL 阻塞可能让内容晚于观察回调出现 */ if (!tailTimer) { tailTimer = setTimeout(function () { tailTimer = null; flush(); }, 400); } } function startObserver() { mo = new MutationObserver(function (muts) { if (!enabled) return; if (!muts.length) return; for (var i = 0; i < muts.length; i++) { var m = muts[i]; if (m.type === "characterData") { queued.push(m.target.parentNode || m.target); } else if (m.type === "childList") { for (var j = 0; j < m.addedNodes.length; j++) queued.push(m.addedNodes[j]); } else if (m.type === "attributes") { queued.push(m.target); } } schedule(false); }); observeTarget(); } /* ============ 8. 原生弹窗(confirm/alert)翻译 ============ */ function patchDialogs() { var wConfirm = window.confirm, wAlert = window.alert; if (wConfirm && !window.__adoCnConfirm) { window.confirm = function (msg) { return wConfirm.call(window, translateText(String(msg))); }; window.__adoCnConfirm = true; } if (wAlert && !window.__adoCnAlert) { window.alert = function (msg) { return wAlert.call(window, translateText(String(msg))); }; window.__adoCnAlert = true; } } /* ============ 9. 悬浮开关 ============ */ function buildUI() { var box = document.createElement("div"); box.id = "ado-cn-toggle"; box.style.cssText = [ "position:fixed", "right:10px", "bottom:10px", "z-index:2147483647", "width:30px", "height:30px", "border-radius:50%", "display:flex", "align-items:center", "justify-content:center", "font:600 12px/1 system-ui,'Microsoft YaHei',sans-serif", "color:#fff", "cursor:pointer", "user-select:none", "box-shadow:0 2px 8px rgba(0,0,0,.45)", "opacity:.9", "transition:background .15s,opacity .15s" ].join(";"); box.title = "ADO 界面翻译:左键切换中/韩(Ctrl+Alt+T)|右键切换类别名是否翻译(Ctrl+Alt+L)"; function paint() { box.textContent = enabled ? "中" : "韩"; if (!enabled) { box.style.background = "rgba(100,116,139,.95)"; /* 灰 = 韩文原文 */ } else if (!labelsOn) { box.style.background = "rgba(217,119,6,.95)"; /* 橙 = 译文,类别名保持英文 */ } else { box.style.background = "rgba(22,163,74,.95)"; /* 绿 = 全中文 */ } } box.addEventListener("click", function () { toggle(); }); box.addEventListener("contextmenu", function (e) { e.preventDefault(); toggleLabels(); }); paint(); box.__paint = paint; (document.body || document.documentElement).appendChild(box); window.__adoCnBox = box; } function toggle() { enabled = !enabled; try { localStorage.setItem(STORE_KEY, enabled ? "1" : "0"); } catch (e) { } if (enabled) { applyAll(); } else { restoreAll(); } if (window.__adoCnBox && window.__adoCnBox.__paint) window.__adoCnBox.__paint(); } /* 类别 / 子类别名属数据标签,可单独切换是否翻译 */ function toggleLabels() { labelsOn = !labelsOn; try { localStorage.setItem(LABELS_KEY, labelsOn ? "1" : "0"); } catch (e) { } if (enabled) { restoreAll(); applyAll(); } if (window.__adoCnBox && window.__adoCnBox.__paint) window.__adoCnBox.__paint(); console.log("[adoCN] 类别/子类别名翻译:" + (labelsOn ? "已开启" : "已关闭(保持英文)")); } /* ============ 10. 启动 ============ */ function boot() { /* 防重复注入:先卸载上一个实例 */ if (typeof window.__adoCnDestroy === "function") { try { window.__adoCnDestroy(); } catch (e) { } } window.__adoCnDestroy = destroy; window.__adoCnInstalled = true; patchDialogs(); if (enabled) applyAll(); startObserver(); buildUI(); document.addEventListener("keydown", function (e) { if (e.ctrlKey && e.altKey && (e.key === "t" || e.key === "T")) { e.preventDefault(); toggle(); } else if (e.ctrlKey && e.altKey && (e.key === "l" || e.key === "L")) { e.preventDefault(); toggleLabels(); } }, true); window.adoCN = { on: function () { if (!enabled) toggle(); }, off: function () { if (enabled) toggle(); }, toggle: toggle, labels: function (v) { if (v === undefined) { console.log("[adoCN] 类别名翻译:" + labelsOn); return labelsOn; } if (!!v !== labelsOn) toggleLabels(); return labelsOn; }, reapply: applyAll, destroy: destroy, report: function () { var keys = Object.keys(MISS); if (!keys.length) { console.log("[adoCN] 全部韩文均已翻译,无遗漏。"); return []; } console.log("[adoCN] 未命中词典的韩文(" + keys.length + " 条):"); keys.sort(function (a, b) { return MISS[b] - MISS[a]; }); keys.forEach(function (k) { console.log(" " + MISS[k] + " × " + k); }); if (console.table) { console.table(keys.map(function (k) { return { 韩文: k, 次数: MISS[k] }; })); } return keys; } }; console.log("[adoCN] ADO 韩中翻译已加载。Ctrl+Alt+T 切换中/韩。adoCN.report() 查看未译条目。"); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", boot); } else { boot(); } })();