
1. 从零拆解 Anymaker 汉化补丁一个 3D 建模工具的中文化实践Anymaker 是一款面向 3D 打印和数字雕刻领域的建模软件主打“低门槛上手、高自由度造型”在海外创客圈子里口碑不错。但它的官方界面长期只有英文和日文对国内刚接触 3D 建模的朋友来说光是菜单里的 Sculpt、Remesh、Boolean 这些词就够劝退的了。我最初接触这个工具是在给一个手办模型做修补的时候当时对着满屏英文菜单一个一个查词典效率低得让人抓狂。后来索性花了几个周末把整个界面的汉化补丁做了一遍也就是今天要聊的这个项目。这个汉化补丁做的事情很纯粹把 Anymaker 的界面文本、菜单项、提示信息、错误弹窗全部替换成中文同时保留原始语言包的完整性方便随时切换回去。它解决的核心问题就一个——让中文用户不用在“学软件”和“学英语”之间二选一。适合谁来参考如果你是用 Anymaker 做 3D 打印模型、数字雕刻的普通用户直接拿去用就行如果你是想给自己常用的某个小众工具做汉化的开发者这套流程和踩坑经验同样适用。我做的这个版本覆盖了 Anymaker 主流的几个大版本从资源文件结构到字体适配都做了处理。下面我把整个项目的设计思路、技术细节、实操步骤和踩过的坑完整地梳理一遍尽量做到你照着做就能复现。2. 汉化补丁的整体设计与技术选型2.1 为什么选择资源文件替换而不是外挂注入做汉化这件事技术路线上大致有三条一是外挂式注入通过 Hook 的方式在运行时替换字符串二是修改可执行文件直接改二进制里的字符串表三是替换资源文件把软件自带的语言包文件替换成翻译后的版本。我最终选了第三条路原因有三个。第一Anymaker 的资源文件结构比较清晰语言包以独立的 JSON 和二进制资源形式存放在安装目录的resources/locales文件夹下每个语言一个子目录互不干扰。这意味着我只需要复制一份英文语言包翻译后放进去再改一下配置文件里的语言指向就行完全不用碰主程序。第二外挂注入的方案虽然看起来“高级”但稳定性太差。Anymaker 在启动时会校验部分资源的完整性Hook 方案很容易触发校验失败导致闪退。我早期试过一个基于内存补丁的方案结果每次软件更新版本就得重新找偏移地址维护成本高得离谱。第三替换资源文件的方案天然支持回滚。用户想切回英文只需要把语言配置改回去或者直接删掉中文语言包文件夹就行不会对原始安装造成任何破坏。这一点对普通用户来说非常重要毕竟谁也不想因为装了个汉化把软件搞崩了。提示替换资源文件之前务必先备份原始的locales文件夹。我一般会把它压缩成一个 zip 放在同目录下命名成locales_backup_日期.zip这样即使出了问题也能一分钟恢复。2.2 语言包格式的逆向分析Anymaker 的语言包并不是单一的 JSON 文件而是混合了两种格式一部分是纯文本的 JSON存放菜单项、按钮标签、提示语这类短字符串另一部分是二进制资源文件存放带占位符的动态文本和部分需要嵌入字体的界面元素。JSON 部分好办直接用文本编辑器就能改二进制部分就需要先解析出字符串表翻译后再重新打包。我用的工具组合是strings命令提取二进制里的可读字符串hexdump配合xxd定位字符串偏移然后写了一个 Python 脚本做批量替换。这里的关键是保持字符串长度一致或者处理好长度字段的更新否则会导致资源文件损坏。Anymaker 的二进制资源用的是长度前缀加 UTF-8 编码的格式每个字符串前面有一个 4 字节的长度标识翻译后字符串变长了这个长度字段必须同步更新。import struct def replace_string_in_binary(file_path, offset, new_text): with open(file_path, rb) as f: f.seek(offset) old_len struct.unpack(I, f.read(4))[0] encoded new_text.encode(utf-8) new_len len(encoded) f.seek(offset) f.write(struct.pack(I, new_len)) f.write(encoded) # 如果新字符串比旧的长需要处理后续数据的偏移 if new_len ! old_len: remaining f.read() f.seek(offset 4 new_len) f.write(remaining)上面这段代码是核心逻辑的简化版实际处理时还需要考虑对齐和填充的问题。Anymaker 的资源文件按 4 字节对齐所以长度字段更新后如果新字符串长度不是 4 的倍数需要在末尾补零到对齐边界。2.3 字体适配的坑与解决方案汉化最容易被忽视但最影响体验的就是字体问题。Anymaker 默认的界面字体是英文字体中文字符集覆盖不全直接替换文本后会出现方块或者乱码。我的解决方案是在语言包目录下额外放一个中文字体文件然后在配置里指定界面字体路径。具体操作是在resources/locales/zh_CN目录下放一个font.ttf然后在locale_config.json里加上font_override: font.ttf这一项。Anymaker 启动时会优先加载这个字体来渲染界面文本。字体我选的是思源黑体的一个子集版本只保留了常用汉字和标点文件大小控制在 3MB 左右不会明显拖慢启动速度。注意不要直接用系统字体路径因为 Anymaker 的资源加载器只认相对路径而且不同用户的系统字体目录不一样写死了会导致部分用户加载失败。把字体文件打包进语言包是最稳妥的做法。3. 核心细节解析与实操要点3.1 翻译范围的界定与优先级排序Anymaker 的界面文本量不算特别大我统计下来大概有 2800 多条字符串。但并不是所有字符串都需要翻译有些是内部调试信息有些是日志输出翻译了反而影响排查问题。我的做法是先按模块分类然后按用户可见性排优先级。模块字符串数量优先级是否翻译主菜单与工具栏约 320 条最高是右键上下文菜单约 180 条最高是属性面板标签约 450 条高是弹窗提示与错误信息约 600 条高是偏好设置项约 280 条中是插件与扩展接口约 400 条中部分日志与调试输出约 570 条低否这个优先级排序的逻辑是用户高频接触的界面元素优先翻译低频的、面向开发者的内容可以保留英文。比如“Sculpt”翻译成“雕刻”“Remesh”翻译成“重构网格”这些都是用户每天要点几十次的功能必须准确。而像“Failed to allocate memory for vertex buffer”这种日志信息翻译了普通用户也看不懂保留英文反而方便搜索解决方案。3.2 术语翻译的一致性与风格统一3D 建模领域有很多专业术语翻译的时候最怕前后不一致。比如“Mesh”这个词有的地方翻译成“网格”有的地方翻译成“模型”用户看了会懵。我的做法是建一个术语对照表所有翻译都从这个表里取词确保全局统一。{ Mesh: 网格, Sculpt: 雕刻, Remesh: 重构网格, Boolean: 布尔运算, Extrude: 挤出, Bevel: 倒角, Subdivide: 细分, Vertex: 顶点, Edge: 边, Face: 面, UV Map: UV 展开, Texture: 纹理, Material: 材质, Render: 渲染, Viewport: 视口, Gizmo: 操纵器, Pivot: 轴心, Snap: 吸附, Layer: 图层, Mask: 遮罩 }这个表我放在项目根目录的glossary.json里翻译脚本每次运行都会先加载它遇到表里有的词就直接用对应翻译没有的才走通用翻译流程。这样做的好处是即使多人协作翻译术语也不会跑偏。风格上我选择了偏口语化的表达而不是生硬的直译。比如“Are you sure you want to delete this object?”翻译成“确定要删除这个对象吗”而不是“您是否确定您想要删除此对象”。前者读起来像人话后者像机器翻译。3D 建模本身是个创意工作界面语言太正式反而让人紧张。3.3 占位符与动态文本的处理Anymaker 的提示信息里有不少带占位符的动态文本比如“Object {name} has been deleted.”这种。翻译的时候占位符必须原样保留位置可以根据中文语序调整但花括号里的变量名一个都不能改。我遇到过一个问题有些占位符在英文里是复数形式比如“{count} vertices selected”翻译成中文后“vertices”变成了“个顶点”但占位符{count}还在。这时候需要判断count的值来决定用“个顶点”还是“个顶点”中文没有复数变化所以直接统一成“已选中 {count} 个顶点”就行。还有一种情况是占位符本身包含格式说明比如{price:.2f}表示保留两位小数。这种在翻译时要把整个格式说明一起保留不能只留{price}。我的脚本里专门写了一个正则来提取和校验占位符确保翻译后的字符串里占位符数量和名称与原文一致。import re def extract_placeholders(text): pattern r\{[^}]\} return re.findall(pattern, text) def validate_translation(original, translated): orig_ph set(extract_placeholders(original)) trans_ph set(extract_placeholders(translated)) if orig_ph ! trans_ph: missing orig_ph - trans_ph extra trans_ph - orig_ph raise ValueError(f占位符不匹配: 缺失 {missing}, 多余 {extra}) return True这个校验步骤在批量翻译时救了我好几次有一次一个翻译把{name}写成了{nane}肉眼根本看不出来跑一遍校验立刻就暴露了。4. 完整实操流程从提取到打包的每一步4.1 环境准备与工具清单动手之前先把工具备齐免得做到一半发现缺东西。我用的工具都是跨平台的Windows、macOS、Linux 上都能跑。Python 3.8主力脚本语言处理 JSON 和二进制文件都方便。文本编辑器VS Code 就行装个 JSON 格式化插件翻译的时候看着舒服。二进制查看工具xxdLinux/macOS 自带或 HxDWindows用来定位二进制资源里的字符串。字体编辑工具FontForge 或者 Python 的fonttools库用来做字体子集化。压缩工具7-Zip 或系统自带的 zip用来打包最终的语言包。Anymaker 的安装目录结构大致是这样的Anymaker/ ├── Anymaker.exe ├── resources/ │ ├── locales/ │ │ ├── en_US/ │ │ │ ├── strings.json │ │ │ ├── ui_binary.dat │ │ │ └── locale_config.json │ │ ├── ja_JP/ │ │ └── ... │ └── fonts/ └── ...我们要操作的就是locales目录。先复制一份en_US文件夹重命名为zh_CN然后在这个副本上做翻译原始文件保持不动。4.2 提取待翻译字符串第一步是把所有需要翻译的字符串从 JSON 和二进制文件里提取出来整理成一个统一的翻译工作表。JSON 部分直接读取就行二进制部分需要先解析。import json import struct import os def extract_from_json(json_path): with open(json_path, r, encodingutf-8) as f: data json.load(f) strings {} def traverse(obj, prefix): if isinstance(obj, dict): for k, v in obj.items(): traverse(v, f{prefix}.{k} if prefix else k) elif isinstance(obj, str): strings[prefix] obj traverse(data) return strings def extract_from_binary(bin_path): strings {} with open(bin_path, rb) as f: data f.read() offset 0 index 0 while offset len(data) - 4: length struct.unpack(I, data[offset:offset4])[0] if 0 length 10000 and offset 4 length len(data): try: text data[offset4:offset4length].decode(utf-8) if text.isprintable() and len(text) 1: strings[fbin_{index}] { text: text, offset: offset, length: length } index 1 offset 4 length continue except UnicodeDecodeError: pass offset 1 return strings提取完成后把所有字符串合并到一个 CSV 文件里三列key、original、translation。翻译的时候就在这个 CSV 里填第三列。用 CSV 的好处是可以直接用 Excel 或 Google Sheets 打开批量翻译效率高也方便多人协作。4.3 翻译执行与质量检查翻译这步没什么技术含量但有几个细节要注意。一是长度控制界面按钮和标签的空间有限翻译太长会被截断。我一般会把按钮类文本控制在原文长度的 1.5 倍以内超出的就精简措辞。比如“Subdivision Surface”直译是“细分曲面”但按钮上放不下就简写成“细分”。二是标点符号的统一。中文用全角标点英文用半角这个在翻译时就要定好规则。我的习惯是界面标签不加句号提示信息加句号问句用问号。这些规则写在翻译指南里多人协作时大家照着来。三是敏感词的过滤。有些英文词在中文语境下可能有歧义翻译时要避开。比如“Master”在 3D 建模里是“主”的意思但直接翻译成“主人”就不合适我翻译成“主控”或“主导”。这类词我整理了一个替换表脚本自动处理。翻译完成后跑一遍质量检查脚本主要检查三项占位符是否匹配、是否有未翻译的空项、是否有明显的中文乱码。检查通过后再进入打包环节。4.4 重新打包与安装测试打包就是把翻译好的字符串写回 JSON 和二进制文件然后连同字体文件一起放进zh_CN文件夹。JSON 部分直接覆盖写入就行二进制部分需要按偏移量逐个替换。def repack_binary(original_path, output_path, translations): with open(original_path, rb) as f: data bytearray(f.read()) # 按偏移量从大到小排序避免前面的替换影响后面的偏移 items sorted(translations.items(), keylambda x: x[1][offset], reverseTrue) for key, info in items: offset info[offset] new_text info[translation].encode(utf-8) old_len info[length] new_len len(new_text) # 更新长度字段 data[offset:offset4] struct.pack(I, new_len) # 替换字符串内容 data[offset4:offset4old_len] new_text # 如果长度变了需要调整后续数据 if new_len ! old_len: diff new_len - old_len if diff 0: # 新字符串更长插入填充 padding b\x00 * ((4 - new_len % 4) % 4) data[offset4new_len:offset4new_len] padding else: # 新字符串更短删除多余字节 del data[offset4new_len:offset4old_len] with open(output_path, wb) as f: f.write(data)打包完成后把zh_CN文件夹放到 Anymaker 的locales目录下然后修改locale_config.json里的默认语言为zh_CN。启动软件检查主界面、菜单、弹窗是否都显示中文字体是否正常。我一般会做一个检查清单逐项过一遍主菜单栏所有下拉菜单工具栏按钮的悬停提示右键上下文菜单属性面板的所有标签和输入框提示文件打开/保存对话框错误提示弹窗可以故意触发一个错误来测试偏好设置里的所有选项测试通过后把zh_CN文件夹压缩成 zip就是最终可以分发的汉化补丁了。用户下载后解压到locales目录改一下配置就能用。5. 常见问题与排查技巧实录5.1 汉化后界面出现方块或乱码这是最常见的问题根本原因就是字体没有正确加载。排查步骤是这样的先确认zh_CN目录下有没有font.ttf文件然后检查locale_config.json里的font_override字段是否指向了这个文件名。如果都正确但还是乱码可能是字体文件本身的问题用 FontForge 打开看看有没有损坏或者换一个字体文件试试。还有一种情况是部分字符显示为方块但大部分正常。这说明字体子集化的时候漏掉了一些字符。我的做法是子集化时把 Unicode 范围放宽一点除了常用汉字区4E00-9FFF还要包含全角标点FF00-FFEF和常用符号2000-206F。宁可字体文件大一点也不要出现缺字。5.2 软件启动时报资源加载失败这个问题的原因通常是二进制资源文件在替换字符串时长度字段没有正确更新导致解析器读到了错误的位置。排查方法是把汉化后的二进制文件和原始文件做二进制对比看看长度字段和字符串内容是否匹配。我踩过的一个坑是替换字符串时只更新了长度字段但没有处理对齐填充导致后续字符串的偏移全部错位。Anymaker 的资源解析器是按顺序读取的一个字符串读错后面的全乱。解决办法就是在替换时严格按 4 字节对齐处理新字符串长度不是 4 的倍数时补零。提示修改二进制资源前先用xxd把原始文件的前 256 字节 dump 出来改完后对比一下能快速发现明显的结构错误。5.3 部分菜单项仍然是英文这种情况一般是漏翻译了或者翻译后的字符串没有被正确加载。先检查 CSV 里对应的条目有没有填翻译如果填了再看打包后的 JSON 或二进制文件里有没有写入。有时候是 JSON 的键名写错了导致读取时找不到对应项就回退到英文了。还有一种可能是某些字符串是硬编码在可执行文件里的不在资源文件中。这种就需要用十六进制编辑器直接改 exe 文件风险比较大我一般不建议普通用户这么做。如果确实需要一定要先备份原始 exe改完后校验文件哈希确保没有损坏。5.4 汉化补丁与软件更新冲突Anymaker 更新版本后资源文件的结构可能会变旧的汉化补丁直接覆盖上去可能导致软件无法启动。我的做法是在补丁包里加一个版本检测脚本安装前先检查 Anymaker 的版本号如果和补丁支持的版本不匹配就提示用户。版本检测的实现很简单读取 Anymaker 安装目录下的version.txt或者从 exe 文件的版本信息里提取然后和补丁包里记录的版本号做比对。如果不一致提示用户“当前补丁支持 Anymaker x.x.x 版本您的版本是 y.y.y可能存在兼容性问题是否继续”让用户自己决定。问题现象可能原因排查方法解决方案界面显示方块字体未加载或字体缺字检查 font.ttf 和配置补充字体字符集或更换字体启动报资源错误二进制长度字段错误对比原始和修改后的二进制修正长度字段和对齐部分菜单仍为英文漏翻译或键名错误检查 CSV 和打包文件补翻译或修正键名更新后无法启动资源结构不兼容检查版本号等待补丁更新或回滚5.5 独家避坑经验分享做了这么久的汉化有几个经验是文档里不会写的。第一翻译的时候不要一个人闷头干找两三个懂 3D 建模的朋友帮你审一遍很多术语你自己觉得翻得对但实际用户看着别扭。比如我把“Gizmo”翻译成“操纵器”有个朋友说他们圈子里都叫“手柄”后来我改成了“操纵手柄”接受度就高多了。第二二进制资源的修改一定要做单元测试。我写了一个小脚本每次修改后自动跑一遍检查所有字符串的长度字段和实际内容是否一致偏移量是否连续。这个脚本帮我省了无数次的重新打包时间。第三发布补丁的时候把原始语言包一起打包进去。有些用户装完汉化想切回英文发现原始文件被覆盖了还得重新安装软件。我在补丁包里放了一个restore脚本一键恢复英文界面用户反馈很好。第四版本号管理要严格。每次 Anymaker 更新我都用 git 打一个 tag记录对应的汉化补丁版本。这样用户反馈问题时我能快速定位到是哪个版本的补丁出的问题。git 的 diff 功能也能帮我快速看出新版本资源文件的变化只翻译新增的部分不用全部重来。6. 汉化补丁的维护与后续扩展6.1 自动化构建流程的搭建手动打包一次两次还行版本多了就受不了。我后来搭了一个简单的 CI 流程用 GitHub Actions 自动跑翻译校验和打包。每次我 push 翻译更新到仓库Actions 就自动提取字符串、校验占位符、打包二进制、生成 zip 文件然后上传到 release 页面。整个过程不用我手动干预省下来的时间可以多翻译几个版本。CI 脚本的核心就是前面提到的那些 Python 函数把它们串起来做成一个build.py然后在 workflow 里调用。关键是要处理好依赖安装和环境变量特别是字体子集化那步需要fonttools库在 workflow 里要显式安装。name: Build Localization Patch on: push: paths: - translations/** jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 - name: Set up Python uses: actions/setup-pythonv4 with: python-version: 3.10 - name: Install dependencies run: pip install fonttools - name: Validate translations run: python scripts/validate.py - name: Build patch run: python scripts/build.py - name: Upload artifact uses: actions/upload-artifactv3 with: name: zh_CN_patch path: dist/*.zip6.2 社区协作翻译的管理一个人翻译 2800 条字符串工作量不小后来我拉了几个朋友一起做。多人协作最大的问题是术语不统一和格式混乱。我的解决办法是定好规则然后自动化检查。规则包括术语必须从glossary.json取词、占位符必须保留、标点符号用全角、按钮文本不超过 20 个字符。检查脚本在 CI 里跑不符合规则的 PR 直接打回。协作平台我用的是 Crowdin 的免费版它支持 JSON 和 CSV 导入翻译界面友好还能自动检查占位符。翻译完成后导出成 CSV再跑我的打包脚本。这样即使不懂技术的朋友也能参与翻译降低了协作门槛。6.3 后续可以扩展的方向这个汉化补丁目前只覆盖了界面文本其实还有几个方向可以继续做。一是帮助文档的汉化Anymaker 自带的教程和手册都是英文的翻译过来对新手更友好。二是预设和模板的本地化比如把常用的建模参数预设加上中文说明。三是语音提示的汉化如果 Anymaker 后续支持语音反馈的话。另外这套汉化流程其实可以抽象成一个通用工具支持其他类似结构的软件。我目前正在把提取、翻译、打包的逻辑封装成一个命令行工具输入软件安装目录和语言代码自动生成汉化补丁。如果做成了以后汉化其他工具就不用每次都写一遍脚本了。我在实际维护这个补丁的过程中最大的体会是汉化不只是翻译更是对软件本身的理解。你得知道每个功能是干什么的用户会在什么场景下用到它才能翻出准确又自然的词。有时候一个词翻错了用户可能找半天找不到功能在哪。所以每次翻译完我都会自己用一遍把常用功能都点一遍看看有没有别扭的地方。这个习惯帮我发现了好几个翻译问题也让我对 Anymaker 这个工具的理解越来越深。