不改一行 Postman 源码,把它汉化了:Electron preload 注入 + fetch 拦截实战

不改一行 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 里。注入程序做的事情是:

  1. 备份原始 app.asar;
  2. 从备份解包,在 preload 开头插入一行 require 钩子,同时把 pm-chinese.js 和译文 JSON 写到同一个目录;
  3. 用 @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 安装。


如果你也在做 Electron 应用的汉化或者插件化,欢迎在评论区交流。

相关推荐
叼烟扛炮2 小时前
[LangChain] 学习笔记(03):打破失忆症——对话机制与上下文管理
java·前端·javascript·笔记·学习·langchain
卋罖7503 小时前
Razor 视图 + JavaScript + Vue/iView 组件实现的表格自定义列
javascript·vue·iview·razor视图
系统集成-鹰翔17 小时前
ERP图片改名后仍命中识别缓存:缓存键、十分钟到期与调用计数
javascript·经验分享·后端
FlyWIHTSKY19 小时前
vue2和vue3有什么区别,用法上有哪些区别
前端·javascript·vue.js
不可能片场20 小时前
Electron 发布 正文写入的三条通道与读回口径
前端·electron
程序员大阳20 小时前
副队长JavaScript教程(66)-本地存储实战:待办事项网页的开发
javascript·json·实战·待办事项·本地存储·案例
Dream-Y.ocean21 小时前
鸿蒙PC平台逻控 PLC 编辑器适配实战:基于 Electron 壳方案的纯 JavaScript 梯形图编程与仿真引擎开发
javascript·编辑器·harmonyos
前端小D1 天前
JS核心底层
前端·javascript
Dovis(誓平步青云)1 天前
启元 T1 Pro 人机相声搭档:语音-触摸双通道的启元机器人节奏引擎设计
开发语言·前端·javascript·microsoft·交互