Codex完整汉化方法:界面字符串、模型输出与终端渲染三层实践
Codex 装好第一次跑起来满屏英文提示符的时候我第一反应不是它能力怎么样而是我得先把它改成中文。这不是矫情是因为这类终端里的编码代理交互密度实在太高——你敲一句它回三段状态行、工具调用日志、错误提示、确认菜单全挤在一个窗口里滚动。英文界面下每处理一次任务我都要额外花精力去读界面而不是读代码。所谓 Codex 完整汉化方法本质上不是找一个中文语言包点一下安装而是把三个不同层面的东西分别处理掉第一层是交互外壳里的静态英文字符串提示符、菜单、报错、帮助文本第二层是模型回复本身的语言它由指令文件和上下文决定跟界面无关第三层是终端渲染中文字符宽度、编码、字体、对齐这层不做前两层做完也是歪的。很多人只做了第二层就宣称汉化了结果界面照样英文中文回复还因为宽字符计算错位把表格撑烂。这套方法适合谁日常在终端里高频使用编码代理的开发者、需要给团队统一工作环境的负责人、以及单纯受不了中英混杂界面的强迫症患者。下面我按三层拆开讲给到可以直接抄的配置、脚本和替换表也把我在几个版本迭代里踩过的坑一并写出来。1. 先把汉化这件事拆开三层结构决定你的工作量1.1 三层分别是什么为什么不能混着做第一层交互外壳。这是程序编译或打包时就固定进产物的字符串包含命令帮助、交互式提问、状态栏、确认对话框、错误码说明。它不联网、不经过模型纯粹是静态文案。你想改它只能改产物本身或者在外面套一层包装脚本。第二层模型输出。你问一句它答一段这段文字用什么语言取决于系统指令、项目级指令文件、以及你当轮提示的措辞。这一层完全不涉及修改程序文件改的是配置文件里的一段文本。收益最大、风险最低、耗时最短通常十分钟内能搞定。第三层终端渲染。终端里中文占两列、英文占一列这叫宽字符。程序如果按字符数而不是显示宽度来做截断、补齐、换行中文一进来立刻错位状态行右边框对不齐、表格竖线歪掉、长中文被截成半个字、有时还出现方块和问号。这一层跟翻译无关是显示层的问题。三层混着做的典型后果是你把界面字符串替换了中文变长了原来按英文字符数预留的宽度不够界面直接崩成两行或者你只改了模型语言发现它还是用英文回因为项目目录里有个更高优先级的指令文件把全局设置盖掉了。先把层次分清后面每一步才知道自己在改什么。1.2 投入产出对照先做哪一层最划算我按自己的实际耗时和体验提升列了个对照你可以直接照这个顺序推进。层级处理手段大致耗时体验提升风险第二层模型输出语言改全局指令文件10 分钟高占日常交互八成极低可随时删第三层终端渲染改 locale、字体、终端设置30 分钟中高决定中文能不能看低只影响本机第一层界面字符串替换打包产物中的字符串2 到 4 小时中界面全中文更顺中升级即失效顺序建议是二、三、一。原因很实际第二层做完你已经能读懂绝大部分内容因为真正的信息量在模型回复里界面字符串更多是操作提示。第三层做完中文不再错位读起来不费眼。第一层是锦上添花适合对界面一致性有执念或者要给不熟悉英文的同事做统一环境的时候再动手。注意动手改程序产物之前先确认你手上的软件许可条款允许本地修改并且这份修改只用于你自己的机器。对外分发修改版通常是不被允许的这条红线别踩。2. 最省事的一层把模型输出语言锁死为中文2.1 全局指令文件的位置与写法这类工具普遍支持一个全局指令文件位置一般在用户主目录下的配置目录里命名约定各不相同常见的是AGENTS.md或instructions.md。以常见约定为例路径大致是~/.codex/AGENTS.md不同版本可能挪到~/.config/codex/下面拿不准就先翻一遍官方文档或看程序启动时打印的配置路径。文件不存在就自己建一个纯文本 Markdown 即可。写这个文件有个核心原则用正向、可判定的约束不要用请尽量最好这种软措辞。模型对尽量的理解极不稳定同一个提示词今天全中文明天就给你中英混杂。我现在的写法是这样的## 语言规则 - 所有面向我的解释、总结、计划、提问一律使用简体中文。 - 代码、命令、文件路径、变量名、函数名、API 名称、报错原文保持英文原样不要翻译。 - 代码块内的注释如果原文件注释是英文保持英文如果是中文保持中文。 - 提交信息commit message使用中文但类型前缀feat/fix/docs/refactor保持英文。 - 表格、列表的列名和条目标题使用中文。 - 不要使用翻译腔。宁可句子短一点也不要出现让我们来一起看看这类表述。最后一条看着像废话实际很有用。不写这句模型很容易滑向那种生硬的翻译腔读起来比英文还累。把不要翻译腔写进约束输出质量的差别肉眼可见。另外一个细节明确列出不要翻译的白名单。我吃过亏某次让它解释一段报错它把错误信息本身翻成了中文我拿去搜索完全搜不到对应结果。白名单里写清楚报错原文保留英文这个坑就填上了。2.2 项目级指令与全局指令打架时的处理多数这类工具支持两级指令全局的放主目录项目级的放仓库根目录。项目级优先级更高会覆盖全局。这个设计本身没问题坑在于你换了台机器、或者接手了一个别人配过的仓库项目级文件里往往写着Always respond in English于是你的全局中文设置当场失效你还以为是工具坏了。排查顺序很简单先在仓库根目录找有没有指令文件再看有没有嵌套的子目录指令文件。找到了先读一遍通常一眼就能看出问题。处理方式有两种取决于你的角色仓库是你自己的直接改项目级文件把语言规则写进去顺手提交团队其他人也受益。仓库是别人的、不方便改在你的全局文件里加一句更强的覆盖声明同时在当轮提示里补一句本轮用中文回答。双保险比单保险稳。还有一种情况是项目级文件本身内容很长、写了一堆工程规范你不方便动。这时候可以在全局文件里明确写出优先级声明比如当项目指令与本文件的语言规则冲突时语言规则优先。实测下来大部分情况能生效但不保证百分百毕竟指令优先级最终由工具实现决定所以当轮提示兜底还是必要的。2.3 术语统一避免同一个概念三种叫法中文技术表达最大的问题是同一个词有多种译法。repository你可能看到仓库版本库代码库三种prompt有提示词提示语指令diff有差异补丁变更。混着来读起来极其难受。我在全局指令文件里附了一张术语表效果很好你可以按自己习惯改英文统一用词不要用repository仓库版本库、代码库prompt提示词提示语、指令语diff差异补丁、变更集commit提交版本、快照branch分支支线merge合并归并dependency依赖依存项build构建编译除非确实指编译把这张表写进指令文件模型输出的稳定性会上一个台阶。代价是文件长了点但这是一次性成本。顺便说这张表在你跟同事协作时也有用——大家输出风格一致评审代码时少很多歧义。3. 界面字符串本地化从字符串表到可复用补丁3.1 先摸清产物形态别上来就动手界面字符串藏在哪取决于这个工具是怎么发布的。常见三种形态处理方式完全不同第一种打包成单个 JS 文件的 Node 工具。所有字符串都在那个大文件里以字符串字面量的形式存在。这种最好办直接用文本替换就能改改完还能跑。第二种带 sourcemap 的产物。除了单文件还附带.map文件里面保留了原始源码结构。这种更舒服你可以顺着 sourcemap 找到原始模块名定位准确不用在一堆压缩代码里瞎猜。第三种原生编译的二进制。字符串以常量形式编进可执行文件长度固定替换时新字符串不能比原字符串长否则会破坏二进制结构。这种最麻烦通常只能做等长替换中文一个字符占三字节长度预算非常紧张实际能改的条目很有限。我的建议是先花五分钟确认形态再决定要不要做第一层。如果是第三种而你的中文需求又不算极端直接放弃第一层把精力放在第二、三层上性价比高得多。3.2 定位字符串的实操路径假设是第一种形态产物在类似node_modules/包名/dist/cli.js的位置。定位分三步走。第一步全局搜索几个你每天都会看到的高频字符串。挑那种长一点、不容易误伤的比如一段确认提示的完整句子。用grep -n带着行号搜能快速定位到大致的区域。grep -n Are you sure you want to ./dist/cli.js | head -20第二步把目标行附近的内容打出来看结构。多数打包产物的字符串是集中放在一个对象或数组里的找到这个结构你就拿到了整张字符串表可以一次性全改而不是一条一条找。sed -n 1180,1240p ./dist/cli.js第三步确认字符串的拼接方式。有些界面文案是模板拼接出来的比如正在处理 ${n} 个文件这种你不能整体替换只能替换固定片段变量部分保留。看清楚了再动手不然改完变量丢了界面直接显示undefined。提示动手之前先整个复制一份产物文件做备份命名成cli.js.bak。这一步省不得我见过不止一次改到一半发现括号没闭合整个程序起不来没有备份只能重装。3.3 建一张替换表别写死在脚本里字符串几十条上百条如果每次都手写一堆sed命令维护成本高到崩溃。我的做法是维护一张 JSON 替换表脚本读表打补丁。表长这样{ Are you sure you want to continue?: 确认继续, Press Enter to confirm: 按回车确认, Esc to cancel: Esc 取消, Thinking: 思考中, Running command: 执行命令, File changed: 文件已修改, No changes detected: 未检测到变更, Error: command failed: 错误命令执行失败 }几个原则译文尽量不比原文长太多尤其是状态行和按钮位置英文 20 字符的地方塞 15 个汉字必然溢出保留原有的标点风格原文明明用问号你别改成句号专业术语按你第 2 节维护的术语表来别这张表说仓库下张表说代码库。打补丁的脚本我用 Python 写逻辑很简单读 JSON逐条替换统计命中数没命中的报出来。import json, shutil, sys from pathlib import Path TARGET Path(node_modules/包名/dist/cli.js) TABLE Path(i18n/zh-CN.json) def main(): text TARGET.read_text(encodingutf-8) table json.loads(TABLE.read_text(encodingutf-8)) hit, miss [], [] for src, dst in table.items(): if src in text: text text.replace(src, dst) hit.append(src) else: miss.append(src) if len(hit) 0: print(没有命中任何字符串可能版本已变化终止。) sys.exit(1) backup TARGET.with_suffix(.js.bak) if not backup.exists(): shutil.copy2(TARGET, backup) TARGET.write_text(text, encodingutf-8) print(f命中 {len(hit)} 条未命中 {len(miss)} 条) for m in miss: print( 未命中:, m[:60]) if __name__ __main__: main()这段脚本有几个刻意的设计。命中数为零直接退出不写文件——这是版本升级后的典型场景字符串全变了如果照写不误你会得到一个被替换了一半的产物。备份只在首次创建避免第二次运行时把已汉化的版本覆盖成备份。未命中列表打出来方便你判断是这条英文压根不存在还是这条被上游改写了。3.4 校验、回滚和一条容易被忽略的检查改完之后不要急着用先做三件事。第一件语法校验。JS 产物改完用 node 做一次空跑能过就说明没把语法搞坏。node --check node_modules/包名/dist/cli.js第二件启动烟测。直接跑一次最简单的命令比如打印帮助或者版本号看界面有没有乱码、有没有undefined、有没有半个汉字被截断。第三件功能回归。至少跑一遍你日常最常用的两三个动作比如发起一次对话、读一次文件、执行一次命令。界面字符串改动理论上不影响逻辑但如果某条字符串同时被用作判断条件是的真有这种写法有人拿提示文本做状态判断就会出事。回滚很简单把.bak复制回去覆盖即可。所以我建议在脚本里顺手加一个--restore参数省得每次手打路径。cp node_modules/包名/dist/cli.js.bak node_modules/包名/dist/cli.js4. 终端渲染中文能不能看全在这一层4.1 编码链路locale、终端、程序三者要一致中文显示成方块、问号、或者干脆是乱码九成是编码链路某一环没对上。这条链路上有三个角色系统 locale、终端模拟器、程序自身的输出编码。三个都必须是 UTF-8缺一不可。先查系统 localelocale重点看LANG和LC_ALL。如果是C或者POSIX那就是纯 ASCII 环境中文必挂。改成 UTF-8 就好写进 shell 的配置文件里让它持久生效export LANGzh_CN.UTF-8 export LC_ALLzh_CN.UTF-8有些系统上zh_CN.UTF-8这个 locale 没有生成需要先本地化一下。如果嫌麻烦用en_US.UTF-8也能正确显示中文——关键在UTF-8这个后缀前面的语言代码不影响字符能不能显示只影响日期、数字格式。再看终端模拟器。绝大多数现代终端默认就是 UTF-8但你如果用的是某种老旧组合或者是通过跳板环境转发进来的会话编码可能被改成别的。这个设置位置因终端而异一般在偏好设置的编码或文本里确认是 UTF-8。第三看程序自身。有些工具会读环境变量决定输出编码有些则写死。如果是前者确认没有被别的地方覆盖掉如果是后者又写死了非 UTF-8那就只能走第 3 节的产物修改路线了。4.2 宽字符宽度计算中文错位的真正元凶这是最容易被忽略、也最让人抓狂的一层。终端里一个汉字占两列一个英文字母占一列。但程序里做字符串处理时length属性数的是字符个数不是显示宽度。中文.length是 2但它实际占 4 列。所有依赖长度的操作——截断、补齐、画边框、算换行——只要没考虑宽字符中文一进来就错位。具体表现有几种你对号入座现象原因能不能从外部修状态行右边框对不齐补齐时按字符数算宽度不能除非改程序长中文被截成半个字截断位置落在双字节中间不能表格竖线歪斜列宽按字符数而非显示宽度部分能靠缩短译文文字换行位置诡异按字符数算行宽中文实际超宽部分能靠缩短译文第一、二、四种基本只能在程序内部修外部改不了。但第三种有救把译文控制在原文长度的上下两个字符以内。原文 30 个英文字符的表格列标题你塞 15 个汉字30 列宽刚好塞 20 个汉字40 列宽就溢出。这就是为什么我在第 3.3 节强调译文尽量不比原文长太多不是风格问题是硬约束。提示如果程序用的是成熟的终端 UI 库宽字符一般已经被处理好你只要保证译文长度别太夸张就行。判断方法很简单改完跑一次看表格有没有歪。歪了就是没处理好回去压缩译文。4.3 字体与配色决定你一天看八小时累不累字体这块我说得直白点。终端里同时有中文和英文你必须用一款中英文等宽比例正确的字体。什么叫正确一个汉字的宽度正好等于两个英文字母的宽度。如果比例不对中英文混排的代码行会对不齐看起来像有轻微的抖动。选择标准三条有完整中文字形、半角全角宽度比例严格 1:2、字重切换清晰正常和粗体区分明显。很多系统自带的默认等宽字体只有英文字形中文会回退到别的字体比例往往对不上这就是你看到中英文混排高低不齐的原因。配色方面中文笔画密度远高于拉丁字母同一个前景色中文看起来比英文更黑、更重。如果你的配色方案是给英文调的换成中文界面后会觉得整屏发闷。建议把正文字号往上调半档或者把前景色亮度提一点。这个纯属个人偏好但你汉化完觉得哪哪都不对劲又说不上来八成就是这个原因。5. 升级之后补丁失效把汉化做成可维护的工程5.1 版本一变锚点全废怎么办这类工具迭代很快界面字符串隔三差五就改。我统计过自己用的这套东西大大小小的版本更新里能有六成以上的字符串保持不变剩下四成里一部分是措辞微调一部分是新功能新增还有一部分是旧的被删掉。所以你的替换表不能是一次性写完就扔得当成资产维护。我的处理方式是给每条替换表加一个状态字段分三种active稳定多次命中、fragile命中过但容易被改、orphan连续两个版本没命中标记待清理。脚本跑完打印未命中清单人工扫一眼把orphan的删掉把新出现的补进去。整个维护动作每次升级大概五分钟。5.2 目录结构让补丁和版本可以共存把汉化相关的东西集中到一个目录里别散落在各处。我的结构大概是这样i18n/ ├── zh-CN.json # 主替换表 ├── zh-CN.orphan.json # 待清理条目 ├── apply.py # 打补丁脚本 ├── verify.sh # 校验脚本 └── history/ ├── v1.2.0.json # 每个版本用过的表快照 └── v1.3.1.jsonhistory这个目录很关键。升级出问题时你能立刻回退到上一个版本的替换表配合上一个版本的产物备份几分钟恢复可用状态。没有这个目录出一次事故就得从头再来一遍。5.3 自动化把升级后重打补丁变成一条命令手工重打补丁的次数多了一定会烦烦了就会拖拖着就会一直用英文界面。所以最后一公里是自动化。我用 shell 把整个流程串起来一条命令搞定#!/usr/bin/env bash set -euo pipefail PKG_DIRnode_modules/包名 TARGET$PKG_DIR/dist/cli.js echo [1/4] 记录当前版本 node -p require(./$PKG_DIR/package.json).version echo [2/4] 检查产物存在 test -f $TARGET || { echo 产物不存在先安装依赖; exit 1; } echo [3/4] 打补丁 python3 i18n/apply.py echo [4/4] 语法校验 node --check $TARGET echo 校验通过set -euo pipefail这行别省。打补丁脚本中途失败如果没这行后面的校验会基于半成品跑可能误报通过。加上之后任何一步出错立刻中断你就知道卡在哪。注意如果你的依赖是通过包管理器安装的重装依赖会把node_modules整个重建补丁自然没了。这个时候把打补丁的动作挂到安装流程的钩子上或者干脆在每次装完之后手动跑一遍上面这个脚本养成习惯。6. 常见问题速查与踩坑记录6.1 问题速查表我把这一路遇到的问题整理成表遇到症状直接对号找原因省得重新推理。症状最可能的原因处理方式界面还是英文但回复变中文了只做了第二层正常第一层需要单独处理回复突然变英文了项目级指令文件覆盖了全局查仓库根目录的指令文件界面出现undefined替换时把模板变量一起换掉了回滚恢复变量占位程序启动直接报语法错误替换破坏了引号或括号配对node --check定位回滚中文显示成方块locale 不是 UTF-8改LANG/LC_ALL表格竖线歪斜译文宽度超过原文预留压缩译文控制在 ±2 字符半个汉字被截断截断按字符数算缩短该条译文或放弃这条升级后补丁全失效上游改了产物结构重跑脚本看命中率更新替换表重新装依赖后汉化消失node_modules被重建重跑补丁脚本挂到安装流程6.2 几条用实际教训换来的经验第一条先做损失最小的那层。我一开始就是冲着改界面去的折腾了一下午改完发现升级一次全没了心态直接崩。后来才想明白第二层十分钟就能拿到八成收益风险还接近零。顺序错了付出和回报完全不成比例。第二条任何修改产物之前先问一句这个功能是不是本来就有。有些工具其实支持通过配置切换界面语言只是默认没开、文档里写得比较隐蔽。花十分钟翻一遍配置项列表和帮助输出可能省掉你两小时的活儿。我吃过这个亏改完才发现配置文件里有个语言字段。第三条替换表里不要放短字符串。像Yes、OK、Run这种在代码里到处都是你替换了界面文案顺手把某个变量名或者配置值也改了程序行为直接变化。判断标准很简单少于六个字符的英文短语一律不进替换表。收益太小风险太大。第四条翻译腔是汉化质量的分水岭。让我们来看一下这个函数的实现和看下这个函数是两种东西。前者你读三遍才反应过来后者一眼就懂。在指令文件里明确写句子要短、去掉客套、保留技术准确性输出质量的差别非常明显。这条比替换一百条界面字符串都值。第五条给自己留一条随时能回滚的路。不管是产物备份、替换表历史、还是配置文件的版本管理本质上都是同一个思路让改坏了这件事的代价趋近于零。有这个底气你才敢放心折腾才不会因为怕出事而一直用着别扭的英文界面。最后分享一个我最近才用上的小技巧把汉化脚本和终端启动配置绑在一起。每次开新终端窗口脚本先检查一遍产物里有没有中文没有就自动打补丁有就跳过。整个过程静默一两秒完成。这样即使某次升级把补丁冲掉了下次开窗口自动恢复你甚至不会察觉到中间出过问题。这一步做完汉化才算真正完整——不是改一次就完事的动作而是一个自己能维持住的状态。