前面有人问过我,部署好OpenClaw之后,第一件让人头疼的事是什么?我的答案不是模型配额,不是skill配置,而是那个全英文的管理界面。我自己的OpenClaw实例跑起来之后,每次调整代理参数、查看任务状态都要在英文界面里翻半天。网页翻译插件试过,一键翻译确实快,但把代码里的变量、按钮里的英文全给翻了,点了还容易出问题。后来我花了两个晚上,用Tampermonkey脚本给OpenClaw界面做了精准中文化,只翻译界面文案,不动代码逻辑,效果比我预想中好得多。这篇就把我的完整思路、踩过的坑和可直接用的脚本框架写出来,给有同样需求的人一个参考。
1. 为什么最终选了Tampermonkey脚本而不是改源码或浏览器翻译
1.1 先说说我的三个方案对比
OpenClaw界面中文化的路线其实不止一条。我最早的想法是直接改OpenClaw的前端源码,把语言包里的英文文案全部替换成中文。这个方案听起来最干净,但实际操作会碰到几个绕不开的问题:OpenClaw升级之后源码会被覆盖,每次升级都要重新改一遍;本地改动需要重新构建前端资源,如果你部署的是Docker版本,还得把构建产物重新打镜像;更麻烦的是,OpenClaw的界面文案并不全在一个语言包里,不少是后端传回来的动态字符串,光改前端根本覆盖不全。
第二个方案是浏览器自带的翻译功能。Edge、Chrome的网页翻译对OpenClaw这种单页应用不算友好,页面加载后翻译一次,你切到某个Tab或者打开弹窗,新渲染出来的内容还是英文,得手动再翻一次。而且翻译引擎对"Deploy""Run""Status"这类短词的翻译经常不太对劲,甚至会把API路径里的单词也翻掉,造成界面信息失真。
最后我选择了Tampermonkey脚本。它不需要碰OpenClaw任何源码,升级不受影响;脚本在浏览器端运行,对每一帧界面都有控制权;替换规则完全由我定义,真正做到"只翻译该翻译的"。从长期维护角度看,Tampermonkey脚本是最划算的路线。
1.2 Tampermonkey脚本在OpenClaw汉化里的工作方式
Tampermonkey脚本本质上就是一个浏览器端JavaScript注入器。它通过脚本头部的@match规则声明自己只跑在OpenClaw管理界面的域名下,通过@run-at设置注入时机,在页面文档解析完成后执行。对于OpenClaw这种大量使用异步渲染的单页应用,脚本还需要搭配MutationObserver去监听DOM变化,这样才能把动态加载出来的英文文案也一并处理掉。
这里有个关键认知:我做的不是"翻译",而是"界面文案映射"。我的脚本维护一张中英文对照表,遍历OpenClaw界面里可见的文本节点,命中英文词条就替换成对应的中文。这样做的好处是精确——不经过翻译引擎,不存在上下文语义问题,也不会误杀代码内容。代价是词条需要手工维护,OpenClaw界面更新新增的文案需要你自己补充。
2. 梳理OpenClaw界面文案:定位导航、按钮、状态提示三类核心区域
2.1 先决定中文化的覆盖范围
在写任何代码之前,我做的第一件事是把OpenClaw管理界面的文案分布摸清楚。我用的部署版本界面结构大致分这几块:顶部导航栏、左侧功能菜单、中间的工作区、底部的状态信息栏,以及各种弹出的配置对话框。不同区域的文案性质不一样,优先级也不一样。
我给自己定的覆盖顺序是:高频操作区优先。左侧菜单里的代理管理、任务列表、技能配置这些入口,是每天都要点的地方,文案不理解会直接影响操作效率;其次是配置弹窗里的表单标签和按钮,比如"Save""Cancel""API Key",这类文案填错代价高;最后才是状态提示区,像"Running""Completed""Failed"这类状态词,配合颜色图标其实猜也能猜个大概,但翻成中文会更直观。
2.2 用开发者工具快速提取文案清单
这部分是纯体力活,但对后续写替换规则至关重要。我打开Chrome DevTools,用Elements面板逐个区域点开查看,把界面里出现的英文文案和DOM结构对应起来记录下来。我的记录方式是建一个表格,每一行是一个词条:英文原文、中文译法、出现位置、元素特征。
| 英文原文 | 中文译法 | 出现位置 | 元素特征 |
|---|---|---|---|
| Dashboard | 仪表盘 | 左侧导航 | .nav-item |
| Agents | 代理列表 | 左侧导航 | .nav-item |
| Deploy | 部署 | 工作区按钮 | button.btn-primary |
| Running | 运行中 | 任务状态标签 | .status-badge |
| Configuration | 配置 | 顶部Tab | .tab-item |
除了肉眼看到的静态文案,我还用Network面板观察了页面加载和交互时的XHR请求。OpenClaw界面不少文案是后端接口返回后在浏览器端渲染出来的,这些动态文案在初始DOM里不存在,只有在触发某些操作后才出现。比如任务详情弹窗里的一段英文描述,就是点开弹窗时通过接口拉取的。这类文案必须靠MutationObserver监听才能覆盖到,我会在后面的实现部分详细讲。
3. 中文化脚本的核心实现:字典表、文本节点替换和动态内容监听
3.1 脚本骨架与关键配置
我直接给出脚本的基础骨架,这份代码框架我在OpenClaw当前版本上验证过,可以直接用。
// ==UserScript== // @name OpenClaw 界面中文化 // @namespace https://your-namespace.example/ // @version 0.1.0 // @description OpenClaw管理界面精准中文化脚本 // @author your-name // @match http://localhost:3000/* // @match https://openclaw.example.com/* // @run-at document-idle // @grant GM_addStyle // ==/UserScript==@match字段要替换成你自己的OpenClaw管理界面地址。如果你用的是本地默认端口,http://localhost:3000/*一般够用;如果是远程部署,把域名写进另一行@match规则里就行。这里有一个容易被忽略的细节:@run-at document-idle表示DOM解析完成后执行,但OpenClaw是单页应用,首次加载后还会继续渲染,所以脚本不能只跑一次,后面需要用MutationObserver持续监听。@grant GM_addStyle是用来注入自定义样式的,我在处理中文字体回退时会用到它。
3.2 字典表与文本节点替换函数
字典表我建议用一个Map结构维护,比普通对象更适合做大量词条匹配。替换逻辑的核心是遍历文本节点,命中词典后替换textContent。
const dict = new Map([ ['Dashboard', '仪表盘'], ['Agents', '代理列表'], ['Deploy', '部署'], ['Running', '运行中'], ['Configuration', '配置'], ['Save', '保存'], ['Cancel', '取消'], ]); function translateNode(node) { if (node.nodeType === Node.TEXT_NODE) { const text = node.nodeValue; if (text && text.trim()) { for (const [en, zh] of dict.entries()) { if (text.includes(en)) { node.nodeValue = text.split(en).join(zh); } } } } else if ( node.nodeType === Node.ELEMENT_NODE && !['SCRIPT', 'STYLE', 'CODE', 'PRE'].includes(node.tagName) ) { node.childNodes.forEach(translateNode); } }这个函数有几个设计决策值得说明。第一,判断节点类型时,我只处理TEXT_NODE,不处理属性节点,避免把href、class这些属性里的英文单词也翻译掉。第二,SCRIPT、STYLE、CODE、PRE这些标签里的内容天然需要跳过,否则脚本代码块里的英文会被误伤。第三,我用split加join的方式做整词替换,而不是正则,主要是性能考虑——在遍历DOM节点时,字符串拆分方式比编译正则更快,也更安全。
3.3 用MutationObserver解决异步渲染漏译问题
OpenClaw界面大量使用前端框架渲染,第一次脚本执行完后,用户点击按钮、切换Tab都会触发新DOM节点插入。要覆盖这些动态内容,MutationObserver是标配方案。
const observer = new MutationObserver((mutations) => { let shouldTranslate = false; for (const mutation of mutations) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { shouldTranslate = true; break; } } if (shouldTranslate) { translateNode(document.body); } }); observer.observe(document.body, { childList: true, subtree: true, });这里有个性能细节:MutationObserver回调在每次DOM变化时都会触发,如果每次触发都做全量文本遍历,OpenClaw这种高频刷新界面会有明显卡顿。我的做法是先判断addedNodes是否有新增节点,有才执行翻译,而且翻译函数本身有节点类型过滤,能省掉大量无意义遍历。如果你发现界面交互时脚本还有性能问题,可以在回调里加一个setTimeout做节流,把多次DOM变化合并成一次翻译操作。
4. 实测中的意外:选择器失效、重复替换、字体渲染与性能问题
4.1 重复替换问题:同一段英文被翻译两次
我的脚本第一版跑起来后,很快发现一个诡异的问题:部分中文文案会叠加。比如"仪表盘"变成了"仪表盘仪表盘"。排查后发现原因很简单——MutationObserver触发了两次翻译,第一次把"Dashboard"替换成"仪表盘",第二次遍历时字典里没有"仪表盘",但文本节点里已经没有英文词条了,理论上不该有问题。问题出在OpenClaw某些组件会重新渲染整个DOM子树,这个过程中旧节点被移除、新节点被插入,如果新节点里的文案已经是中文,我的脚本就不该再处理,可实测中还是会重复处理。
解决方案是给文本节点加一个自定义标记:
function translateNode(node) { if (node.nodeType === Node.TEXT_NODE) { if (node.parentElement && node.parentElement.dataset.translated === 'true') { return; } const text = node.nodeValue; if (text && text.trim()) { let changed = false; for (const [en, zh] of dict.entries()) { if (text.includes(en)) { node.nodeValue = text.split(en).join(zh); changed = true; } } if (changed && node.parentElement) { node.parentElement.dataset.translated = 'true'; } } } }这个标记的含义是"这个节点已经被脚本处理过,不要重复翻译"。它的原理是利用DOM元素上的>GM_addStyle(` .btn { min-width: 88px; padding-left: 16px; padding-right: 16px; } .status-badge { white-space: nowrap; padding: 2px 10px; } `);
这套样式只针对我确认过会溢出的几个组件类名做了微调,没有全局改变布局。这里要注意的是,选择器一定要和OpenClaw实际使用的类名一致,不同版本的类名可能不同,建议用DevTools确认后再写进样式里。如果不想维护这么多规则,也可以用font-family全局指定中文字体优先,让中文渲染更紧凑,这个方案更省事,但也更依赖系统字体环境。
4.4 频繁DOM操作导致的界面卡顿
脚本进入正常使用阶段后,我注意到OpenClaw在展示任务日志的页面有明显卡顿,滚动时帧率下降。用Performance面板分析后发现,高频日志输出会不断触发MutationObserver,每次触发都执行全量翻译,导致主线程被大量的DOM操作占用。
最终我采用了两管齐下的优化方案。箭头方向有两个优化:
- 在MutationObserver回调中加节流控制,多个DOM变化合并成一次翻译操作,避免重复遍历。
- 缩小遍历范围。把翻译的目标从
document.body收缩到mutation.target所在的局部区域,新增节点通常就在这个区域内,不需要全页扫描。
let translateTimer = null; const observer = new MutationObserver((mutations) => { if (translateTimer) return; translateTimer = setTimeout(() => { for (const mutation of mutations) { mutation.addedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { translateNode(node); } }); } translateTimer = null; }, 200); });这个改法让脚本在日志高频刷新时基本不再影响页面流畅度。核心思路是"批量处理,局部扫描",这在处理任何动态渲染密集的界面时都适用。
5. 把这个脚本变成可维护的小工具:版本管理与规则升级思路
5.1 字典外置,让词条维护不再痛苦
脚本使用一段时间后,你会面临一个现实问题:OpenClaw升级了,界面多了几个新的英文词条,怎么办?如果字典表直接写在脚本主体里,每次都改脚本代码,违背了脚本管理的初衷。我采用的方式是把字典表独立成一份配置,在脚本运行时动态合并。
const DICT_URL = 'https://your-host.com/openclaw-zh-dict.json'; async function loadDict() { const resp = await fetch(DICT_URL); const remoteDict = await resp.json(); Object.entries(remoteDict).forEach(([en, zh]) => { dict.set(en, zh); }); }远程字典的好处是词条更新不需要用户重新安装或刷新脚本,脚本启动时会自动拉取最新词条。我实际用下来,这个方案最适合服务端集中维护:有人发现漏翻或者错翻,直接改JSON文件,所有用户次日启动脚本就自动同步。需要注意一点:如果OpenClaw管理界面所在的域名和字典所在的域名不同,脚本会跨域请求,需要在Tampermonkey的权限设置里给脚本开通对应的跨域访问权限。
5.2 调试技巧:用日志模式快速定位问题
调试中文化脚本有一个很实用的技巧:在脚本里增加一个调试开关,通过日志查看哪些节点被翻译了、哪些被跳过了。我在浏览器console里用console.debug输出被替换的词条和对应DOM路径,配合Tampermonkey的"脚本控制台"功能,能快速定位漏译和误翻的位置。
function logTranslate(en, zh, el) { if (debugMode) { console.debug(`[OpenClaw-zh] "${en}" -> "${zh}"`, el); } }调试模式建议只在开发和排障时开启,正式使用时关掉,否则高频率的日志输出也会造成可感知的性能消耗。
5.3 分享与版本发布的经验
用着顺手之后,我把它整理发布到了脚本分享平台。如果你是第一次发布,有几个细节值得注意:脚本头部@version字段务必遵循语义化版本规则(主版本号.次版本号.修订号),Tampermonkey会依据这个值判断是否有更新;@updateURL要指向脚本文件的最新地址,这样用户安装后能自动收到更新提示;描述字段里建议写清楚脚本适用的OpenClaw版本,避免新版本界面不兼容时误装。
清理不必要的@match规则同样重要。发布版本里我只保留了一行通用域名规则,把调试期的localhost和临时IP都删掉了。这既是安全考虑,也是为了让脚本更聚焦——它只运行在OpenClaw管理界面,不会干扰其他站点。
我个人的体会是,界面中文化这件事看起来小,但牵扯的问题其实不少。你要同时处理DOM遍历、动态渲染、性能优化、样式兼容,还得预留后续维护的通道。从一个简单的"翻译按钮"到一套可长期使用的汉化工具,中间这些细节就是差距所在。如果你也在给OpenClaw或者其他英文界面做汉化,不妨从上面这套框架起步,踩过的坑我都写在前面了,能帮你少走不少弯路。