Veeam Backup & Replication 13 Linux Appliance 中文化实战:从版本不兼容到提交上游 PR
文章目录15 节
一、先说最终结果
这次我没有把一个版本不匹配的中文包强行装进 Veeam,而是以实际运行的 Veeam Backup & Replication Linux Appliance 为基准,重新完成了一套 13.0.2.29 专用 Web UI 简体中文包。
最终成果包括:
- 为 VBR Web UI build
13.0.2.29建立独立适配分支。 - 从已经审校的
13.1.0.411中文资源迁移可复用翻译。 - 人工补齐旧版专有资源,最终覆盖
6100 / 6102条,覆盖率为99.97%。 - 对目标版本、原始文件和安装结果实施 SHA-256 校验。
- 验证首次安装、重复安装、卸载还原、版本不匹配拒绝和文件篡改保护。
- 修复 macOS 打包元数据导致的 Linux 解压警告。
- 修复切换中文后浏览器持续转圈、主线程卡死的问题。
- 修复“移除代理/Worker”弹窗的变量占位符错误,并把占位符检查扩展到整个翻译目录。
- 发布最终
v13.0.2.29-linux-r6Release。 - 向上游项目提交 PR #1。
截至本文整理时,PR 状态仍是 Open,还没有合并到上游 main。最终可用包已经在我的 Fork 中发布,但它仍然是非官方本地化包,不是 Veeam 官方补丁。
二、为什么不能直接套用现有 13.1 中文包
最初参考的是上游项目 Coku2015/Veeam-Chinese-UI-Packages。项目已经提供 VBR 13.1.0.411 中文包和使用步骤,但我当时实际运行的 Web UI build 是 13.0.2.29。
真正阻止安装的不是脚本形式,而是两个版本的 Web UI 结构并不相同:
| 对比项 | VBR 13.0.2.29 | VBR 13.1.0.411 中文包预期 |
|---|---|---|
| Web UI 根目录 | /opt/veeam/vbr/GatewayApiService/app | 新版 Web UI 目录结构 |
| 主模块 | assets/index-e21aa407.js | assets/index-BJPSD5I9.js |
| 插件文件 | plugin/plugin.js | 还涉及 vdpPlugin.js |
| 旧版附加资源 | assets/assets-9c8c6fd9.js | 资源组织方式已经变化 |
| 登录语言选择器 | /opt/veeam/vbr/wwwroot/oauth/static/lib/languageSelector.js | 需要按新版结构处理 |
如果跳过版本检查,把 13.1 的文件名、补丁位置和哈希直接套到 13.0,轻则安装脚本找不到文件,重则把错误内容写入生产 Web UI。这里最重要的决定,是不使用类似 ALLOW_UNVERIFIED=1 的方式绕过兼容性保护,而是从服务器实际文件重新建立适配基线。
风险等级:INFO(只读)
作用: 在 Veeam Appliance 的 root Shell 中查看 Web UI 软件包版本,并计算目标静态文件的 SHA-256,用于确认环境是否与本文的
13.0.2.29基线一致。 注意事项: 这些命令不修改系统。路径、文件名或哈希不一致时,应停止安装并寻找匹配当前 build 的中文包,不要凭产品大版本相同就继续。
rpm -q veeam-vbr-webui
sha256sum \
/opt/veeam/vbr/GatewayApiService/app/index.html \
/opt/veeam/vbr/GatewayApiService/app/assets/index-e21aa407.js \
/opt/veeam/vbr/GatewayApiService/app/plugin/plugin.js \
/opt/veeam/vbr/wwwroot/oauth/static/lib/languageSelector.js本次适配记录的四个原始文件哈希如下:
| 文件 | SHA-256 |
|---|---|
index.html | 7d5d30326a648dea90e1fa16f818e1d46c9d5bb2ea596131b273b4a246bba7e8 |
index-e21aa407.js | a56354fa06e4857d87ddb50b29ab53702896a85334ec0a20bfa4103ca53324bb |
plugin.js | e1af0c38752ae7669a577bfa70e8119b80d0ac906bdb3548ed76a21c83ab8ec2 |
languageSelector.js | ae389601d582ea5ea6ca20bb77204493b77837ee79e5ba4166f2fe00fcec078d |
这些哈希不是“所有 13.0 都应该一样”的通用答案,而是安装脚本用于识别本次目标 build 原始状态的保护边界。只要 Veeam 发布了补丁、重构了前端或文件被其他工具修改,校验就可能不再通过。
三、从 Fork 和适配分支开始
我保留了上游历史,并在自己的 Fork 中创建了专用分支:
上游仓库:Coku2015/Veeam-Chinese-UI-Packages
我的 Fork:MrXJG/Veeam-Chinese-UI-Packages
适配分支:adapt-vbr-13.0.2.29-linux
目标 build:13.0.2.29没有直接修改上游 main 的原因很简单:13.0 与 13.1 必须长期保持各自的目录、安装脚本、校验哈希和发布包。版本专用分支既方便审阅,也能让每次故障修复通过独立提交和 Release 追踪。
整个适配过程最终形成六个主要提交:
| 提交 | 作用 | 对应阶段 |
|---|---|---|
9d8bc84 | 新增 VBR 13.0.2.29 Linux 中文包 | 初始适配 |
197592c | 提升翻译覆盖率 | 13.1 文案变化审校 |
a692660 | 翻译剩余旧版专有资源 | 覆盖率达到 99.97% |
bdca77f | 清理 macOS 元数据 | r4 打包修复 |
1b259a1 | 修复中文界面监听器循环 | r5 运行时修复 |
27ed366 | 校验翻译占位符 | r6 最终修复 |
这条提交历史后来完整进入了上游 PR,而不是只提交一个无法解释来源的二进制压缩包。
四、把 13.1 翻译迁移到 13.0,而不是机械覆盖
13.0 原始英文目录共有 6102 条资源。迁移时,生成器按不同证据层匹配 13.1 中已经审校的中文内容:
- 同命名空间、同资源键。
- 全局唯一的同名资源键。
- 英文原文完全一致。
- 对 13.1 与 13.0 文案轻微变化的资源进行人工审校映射。
- 对旧版独有文本进行人工翻译。
最后的目录统计是:
| 项目 | 数量 |
|---|---|
| 13.0 英文资源总数 | 6102 |
| 已覆盖中文资源 | 6100 |
| 13.1 文案变化审校映射 | 77 |
| 人工翻译的旧版专有资源 | 602 |
| 未加入目录的资源 | 2 |
剩余两条的英文原文本身就是空字符串,不会在页面中显示。我没有为了把数字写成 100% 而加入无意义空翻译。
迁移过程中还必须保护 {{hostName}}、%JobName%、{url} 等变量。翻译文字看起来正确,并不代表运行时一定安全;只要中文比英文多一个变量,或者少一个变量,前端格式化函数就可能在用户点击按钮时直接抛错。
五、安装和卸载必须有明确的回退边界
这个中文包不会替换整个 Veeam RPM,也不会覆盖 13.0.2.29 原有的 English 和日本語;安装脚本只在旧版语言注册表中追加 zh-CN。它主要完成以下动作:
- 在旧版主模块和插件语言注册表中增加
zh-CN。 - 加载中文目录和旧版兼容脚本。
- 在登录语言菜单中加入“简体中文”。
- 修改前先校验四个原始文件的 SHA-256。
- 把原始文件备份到
/var/lib/veeam-webui-zh-cn/13.0.2.29/。 - 记录原始哈希、安装后哈希和新增资源哈希。
安装脚本还处理了一个很容易被忽略的状态:如果清单存在且当前文件完全等于本包的安装结果,重复运行会提示“已经安装”,不会再次叠加补丁;如果清单存在但文件已经被升级或人工修改,脚本会拒绝继续。
卸载脚本同样不会无条件覆盖文件。它先确认当前文件仍然等于本包安装后的哈希,才使用备份恢复原始文件。这样可以避免 Veeam 已经升级后,卸载旧中文包又把旧版文件覆盖回去。
离线副本测试验证了以下场景:
- 第一次安装成功。
- 第二次安装保持幂等,不重复修改。
- 卸载后四个原始文件哈希全部恢复。
- 原始文件哈希不匹配时拒绝安装。
- 安装后文件被篡改时拒绝卸载覆盖。
六、从 r1 到 r6:真正耗时的是故障复现和保护补齐
1. r1 到 r3:先让版本适配完整可用
初始包完成了 13.0.2.29 的目录、安装脚本、卸载脚本和翻译生成器。后续继续对照 13.1 文案变化,并人工翻译旧版专有资源,r3 时覆盖率达到 6100 / 6102。
这一步解决的是“有没有中文”和“覆盖是否足够”,但还没有覆盖不同操作系统打包、浏览器动态 DOM 和运行时变量这些边界。
2. r4:修复 macOS 打包元数据
在 macOS 上直接制作 Linux tar.gz 后,Appliance 解压时出现了 ._* AppleDouble 文件以及类似下面的警告:
Ignoring unknown extended header keyword
LIBARCHIVE.xattr.com.apple.provenance这些内容不属于 Veeam 中文资源,而是 macOS 扩展属性被写进了归档。r4 增加专用构建脚本,使用 COPYFILE_DISABLE=1、禁用 xattr、排除 ._*,并在发布前扫描残留元数据。
3. r5:切换中文后持续转圈
安装后,英文界面可以正常进入;一切换到简体中文,浏览器主线程就被占满,页面一直转圈。服务器端的 NGINX、Backup Service、Web Service、REST API 和后端端口都正常,故障最终定位到旧版兼容脚本的 DOM 翻译逻辑。
当时的实现同时存在三个问题:
- 遍历整页所有元素和文本节点。
- 监听整棵 DOM 的
characterData与childList变化。 - 每 750 毫秒再次扫描整个页面。
翻译目录中还有一些原文与译文相同的产品名,例如 Veeam ONE。脚本把相同文本再次写回节点后,会触发 MutationObserver;Observer 再次写入相同文本,于是形成自触发循环。
r5 的修复包括:
- 只有译文与当前文本不同时才写入节点。
- 过滤原文与译文相同的映射。
- 删除 750 毫秒整页轮询。
- 只处理真正新增或发生变化的节点。
- 新增
NATIVE_GUARD_TEST回归测试,确保相同文本不会产生写操作,真实翻译只写入一次。
同一提交还把构建流程从 tar -czf 调整为 tar -cf - | gzip -n。gzip -n 不记录原文件名和时间戳,使同一源码重复构建时能够得到一致的 SHA-256。
修复包重新安装后,中文概览页能够稳定加载,这才证明问题确实来自中文兼容脚本,而不是后端服务。
4. r6:代理按钮暴露占位符错误
r5 修复转圈后,我继续操作不同页面。点击“代理和 Worker”中的移除按钮时,确认框还没有调用后端接口就报错:
Missing prop: 'name' in '是否移除代理/Worker {{name}}?'VBR 13.0 原文是 Remove proxy?,没有 {{name}} 参数;错误中文却从 13.1 文案带入了这个变量。调用方没有传递 name,所以前端在渲染弹窗时直接中断。
全目录审计后共发现 5 处占位符或语义不匹配,包括文件级恢复标题、主机搜索提示、代理确认框和邮件主题提示。r6 不只修正这 5 条,还调整了生成器规则:
- 资源级人工审校覆盖优先于自动匹配。
- 无论翻译来自自动迁移还是人工覆盖,都必须统一校验占位符。
- 资源目录和源文本映射任何一处占位符不一致,构建立即失败。
- 新增回归检查,固定验证这 5 个容易回归的资源。
最终验证结果为:
resourceMismatchCount: 0
sourceMismatchCount: 0
regressionTranslationsChecked: 5“移除代理/Worker”确认框随后在实际 VBR 13.0.2.29 页面中恢复正常。验证只确认弹窗能够正确显示,没有把测试动作扩展为真正删除代理。
七、不是所有 502 都是中文包造成的
汉化验证期间,我还排查过一次 Appliance 升级到 13.1.0.411 后出现的 NGINX 502。那次 NGINX 本身正常,但上游 127.0.0.1:34813 没有进程监听,多个 Veeam 服务都因为下面的程序集错误退出:
Could not load file or assembly
'Veeam.Backup.LicenseLib, Version=13.1.0.0'继续核对文件版本、时间和哈希后发现,升级后的 13.1 目录被复制回了旧版许可证组件,其中 DLL 产品版本为 13.0.1.180。旧进程在重启前可能仍然使用已经加载的程序集,重启后 13.1 服务重新加载文件才集中失败,最终表现为 NGINX 502。
中文包只修改 Web UI 的 HTML、JavaScript 和语言资源,并不触碰 Veeam.Backup.LicenseLib.dll 或 libVeeamLicense.so。这次故障提醒我:看到“安装中文包后出现异常”只能建立时间相关性,不能直接证明因果关系。必须继续检查服务、监听端口、反向代理上游、应用日志和实际被修改的文件范围。
八、最终 Release 与上游 PR
最终版本为 v13.0.2.29-linux-r6:
| 项目 | 结果 |
|---|---|
| 文件 | VeeamWebUiZhCN-13.0.2.29-linux.tar.gz |
| 大小 | 143560 字节 |
| SHA-256 | 70bd47a9c19d2a5c97bc1557dbd33bd3fde2cba756680118e1439f180693110a |
| 翻译覆盖 | 6100 / 6102,99.97% |
| 资源占位符不匹配 | 0 |
| 源文本映射占位符不匹配 | 0 |
| 现场验证 | 中文概览稳定加载,代理确认框正常 |
2026 年 8 月 8 日,我把完整适配分支提交到上游,PR 为:
PR 包含安装和卸载脚本、翻译生成工具、人工审校映射、构建脚本、占位符测试、运行时回归测试、发布包及使用说明,共涉及 14 个文件。当前状态为 Open,因此在上游合并前,使用者应从我的 Fork 的 r6 Release 下载,不应假设上游 main 已经包含 13.0.2.29 包。
九、r6 安装与回退方法
安装前先从 r6 Release 页面 下载压缩包和 SHA256SUMS-13.0.2.29.txt,再上传到 Appliance 的 /tmp/。本文命令仅适用于 VBR Web UI build 13.0.2.29。
风险等级:INFO(只读)
作用: 在 Appliance root Shell 中确认软件包版本,并校验已经上传到
/tmp/的 r6 压缩包。 需要调整: 如果文件不在/tmp/,请把路径替换为实际上传位置。 完成验证: 版本应为13.0.2.29,压缩包 SHA-256 应为70bd47a9c19d2a5c97bc1557dbd33bd3fde2cba756680118e1439f180693110a。
rpm -q veeam-vbr-webui
cd /tmp
sha256sum VeeamWebUiZhCN-13.0.2.29-linux.tar.gz风险等级:CAUTION(修改 Veeam Web UI 静态文件)
作用: 在
/tmp/解压 r6,并执行版本专用安装脚本。 执行对象: VBR13.0.2.29Linux Appliance 的 Web UI 静态文件;需要 Appliance root Shell。 执行前检查: 必须先完成版本与压缩包 SHA-256 校验。安装脚本还会校验四个原始文件,任一哈希不匹配都会停止,不应绕过。 预期影响: 增加简体中文语言选项,刷新页面时可能短暂重新加载前端;不需要为了安装主动重启整台 Appliance。 回退方法: 使用同一目录中的卸载脚本恢复安装前备份。如果当前文件已被升级或其他工具修改,卸载脚本会拒绝覆盖,应先保留现场并重新判断版本。 完成验证: 安装脚本应显示目标 build 安装成功;浏览器强制刷新后可选择“简体中文”,概览页应能稳定加载。
cd /tmp
tar xvf VeeamWebUiZhCN-13.0.2.29-linux.tar.gz
bash VeeamWebUiZhCN-13.0.2.29/linux/install-veeam-webui-zh-cn.sh风险等级:CAUTION(恢复原始 Web UI 文件)
作用: 卸载中文包并恢复安装前的四个原始文件。 执行前检查: 确认仍在同一 VBR build,且
/var/lib/veeam-webui-zh-cn/13.0.2.29/中的安装清单和备份完整。Veeam 升级前应优先执行本步骤。 预期影响: 简体中文选项被移除,Web UI 恢复到安装前状态。 回退与异常处理: 如果脚本提示当前文件哈希不属于本包,不要手工覆盖;保留备份目录,先判断是否发生 Veeam 升级或其他修改。 完成验证: 脚本应提示原始文件哈希验证通过,浏览器刷新后中文选项消失,原有语言正常使用。
cd /tmp
bash VeeamWebUiZhCN-13.0.2.29/linux/uninstall-veeam-webui-zh-cn.sh十、给后续版本适配留下的测试基线
这次最有价值的结果,不只是多了一个中文包,而是把后续适配需要守住的边界固定了下来。
风险等级:INFO(本地只读测试)
作用: 在中文包仓库根目录运行两个 Node.js 回归测试,检查运行时监听器和全目录占位符。 执行环境: 已拉取
adapt-vbr-13.0.2.29-linux分支、安装可用 Node.js,并保留生成器所需的 13.0 原始资源基线。 完成验证: 第一项输出NATIVE_GUARD_TEST=ok;第二项的资源和源文本不匹配计数都应为 0。
node tools/test-vbr-13.0.2.29-native-guard.js
node tools/test-vbr-13.0.2.29-catalog-placeholders.js以后适配新 build 时,至少应重新完成:
- 识别实际 RPM/Web UI build 和静态文件布局。
- 保存原始文件并记录 SHA-256。
- 从已审校目录迁移翻译,但不跨版本硬套文件路径。
- 全量检查变量占位符,而不是只检查人工翻译。
- 验证登录页、概览、作业、存储库、代理和弹窗等动态页面。
- 验证安装、重复安装、卸载、版本不匹配与篡改保护。
- 在 Linux 上检查归档元数据,并验证构建可复现。
- 现场故障先检查后端服务和网络链路,再判断是否属于前端中文资源。
十一、这次工作的结论
Veeam Web UI 中文化并不是简单的字符串替换。真正决定一个版本包能否长期使用的,是版本边界、资源映射、变量约束、动态 DOM 行为、安装回退和现场故障归因。
这次从“不适配”走到 r6,最关键的三个修复分别来自三个不同层面:
- 操作系统层:清理 macOS 写入 Linux 压缩包的扩展属性。
- 浏览器运行时层:阻止 MutationObserver 因相同文本写回而自循环。
- 国际化资源层:保证英文与中文占位符严格一致。
最终包已经在 VBR 13.0.2.29 Linux Appliance 上完成中文页面和代理确认框验证,上游 PR 也已提交。后续如果 PR 收到审阅意见或 Veeam 发布新的 Web UI build,我会继续沿用这套“先识别真实版本、再适配、可回退、可验证”的方式维护,而不会把现有 r6 跨版本复用。