不改一行 Postman 源码,把它汉化了:Electron preload 注入 + fetch 拦截实战
Postman 官方界面只有英文和日文。老牌汉化项目 Postman-cn 的做法是「下载 app.zip 替换 resources」。这个方法在 Postman 10 之后彻底失效了:新版界面主体是远程网页,文案也不再打包在本地。
于是我换了个思路,写了 postman-chinese-injector:不替换 Postman 的任何代码,只在它加载语言包的那一刻,把中文塞进去。 这篇讲讲实现上的几个关键点。
1. 突破口:Postman 自己就有 i18n
打开 Postman 的 DevTools 看 Network,会发现切换语言时,它会请求这样的文件:
bash
https://.../_ar-assets/locales/en-US/<module>-<hash>.json
https://.../_ar-assets/locales/ja/<module>-<hash>.json
也就是说,Postman 的界面早就做了国际化,按模块拆分语言包,运行时远程加载。日文版能用,就说明整套 key 体系是完整的,只是没有中文这一份。
所以目标就很清楚了:拦截 en-US / ja 语言包的响应,把同模块的中文 key 树 deep-merge 进去,再返回给页面。
2. 在哪里拦截:Electron preload
Postman 桌面端是 Electron 应用。它主窗口的 webPreferences 是这样配置的:
js
contextIsolation: false
nodeIntegration: true
contextIsolation=false 意味着 preload 脚本和页面共享同一个 main world,而且 preload 会先于页面脚本执行 。只要在 preload 里包装 window.fetch 和 XMLHttpRequest,页面后续所有请求都会经过我们的钩子:
js
const origFetch = window.fetch;
window.fetch = async function (input, init) {
const res = await origFetch.call(this, input, init);
const url = typeof input === 'string' ? input : input.url;
const m = url.match(/_ar-assets\/locales\/(en-US|ja)\/([\w-]+?)-[0-9a-f]+\.json/);
if (!m || !I18N[m[2]]) return res;
const json = deepMerge(await res.clone().json(), I18N[m[2]]);
return new Response(JSON.stringify(json), {
status: res.status, statusText: res.statusText, headers: res.headers,
});
};
(以上是示意代码,真实实现见仓库里的 pm-chinese.js。)
改的是 fetch 返回之前的响应体,所以不管数据来自网络还是 Service Worker 缓存,都能生效。
3. 怎么把钩子放进去:处理 app.asar
preload 脚本在 resources/app.asar 里。注入程序做的事情是:
- 备份原始
app.asar; - 从备份解包,在 preload 开头插入一行
require钩子,同时把pm-chinese.js和译文 JSON 写到同一个目录; - 用
@electron/asar重新打包回去。
有几个坑要注意:
- asar 的两种形态 :Electron 优先加载
app.asar,没有的话退而加载resources/app/目录。两种都要识别,后一种直接改文件就行,不用解包。 - preload 的位置随版本变化 :新版在根目录
preload_desktop.js,10.24 左右在preload/desktop/index.js,7.x 则是各个 HTML 页面用<script>加载的js/preload.js。在 7.x 里,require是相对页面目录html/解析的,路径要单独处理。 - 幂等 :每次都从备份打补丁,重复注入不会叠加。
restore直接拿备份覆盖回去。
4. 页面之外的两块硬骨头
登出状态的 Scratch Pad。 它是本地打包的界面,文案直接硬编码成英文,也不请求语言包,拦截 fetch 对它没用。我的做法是给它单独加一个钩子:用 MutationObserver 遍历 DOM,把和词典整串精确匹配 的英文替换成中文。同时跳过 input、textarea、contenteditable、CodeMirror、Monaco 这些编辑区域,绝不碰用户输入的请求体和 URL。
原生菜单和对话框。 「文件 / 编辑 / 视图」菜单、macOS 的应用菜单和 Dock 菜单,都是主进程用 Electron 原生 API 画的。我在主进程入口 main.js 的最前面插了第三个钩子,包装 Menu.buildFromTemplate 和 dialog.showMessageBox,按词典替换 label,并且支持 About {{appName}} 这类占位符。
5. 网页版:同一份钩子,换个载体
网页版没有 asar,但原理完全相同。Chrome / Edge 的 MV3 扩展可以用 world: "MAIN" 加 run_at: "document_start",把同一个 pm-chinese.js 注入页面主世界,并且抢在页面脚本之前执行。数据从 window.__PM_I18N__ 读取。钩子是唯一真源,桌面端和网页版共用同一份。
6. 简体转繁體:OpenCC + 修正表
繁體译文不是手翻的。我先用 OpenCC 的 s2twp(简转繁,附带台湾惯用词转换)生成,再按一张修正表纠正 IT 术语,比如 指令碼→腳本、引數→參數、自定義→自訂。转换时只动译文的值,key、占位符和英文原样保留。
7. 跟上 Postman 的更新节奏
Postman 发版很频繁,新文案不断出现。我的流程是:拉取新版本的 en-US 语言包清单,和上一版做 diff,只翻译新增和改动的词条,再由 CI 打包译文热更新。用户再次运行注入程序时,会自动拿到最新译文,不用等工具发版。
用法
bash
# 先完全退出 Postman
npx postman-chinese-injector # 注入(支持 7.x ~ 12.x)
npx postman-chinese-injector restore # 还原英文
也可以去 Release 下载单文件可执行程序(无需 Node),或者用 Scoop、Homebrew 安装。
- GitHub:github.com/hlmd/postma...
- Gitee:gitee.com/hlmd/postma...
如果你也在做 Electron 应用的汉化或者插件化,欢迎在评论区交流。