基于AI我做了一个适合程序员的Chrome翻译插件"

写在前面

这是一篇关于"折腾"的记录。

我用业余时间做了一个叫 HalfTrans 的 Chrome 翻译插件——它不是用来和 Google 翻译、沉浸式翻译竞争的,初衷其实很简单:我想自己用得舒服,顺便学一点新东西

文章不打算贴大段代码(仓库都开源了,源码自己看更直观),主要聊三件事:

  1. 为什么我会想做这么一个插件
  2. 它大致是怎么跑起来的
  3. AI 这部分到底是怎么"调"的

一、起心动念:为什么做这个东西?

1. 学习的冲动

说实话,学习才是最初的动力。

平时工作里用的技术栈是固定的,写久了会有一种"温水煮青蛙"的钝感。Chrome Extension、Manifest V3、Service Worker、Content Script——这些词我在工作中很少正经碰,但浏览器又是每天打开时间最长的软件。做一个自己每天会用的浏览器插件,比看一百篇教程都来得扎实。

再加上这两年 LLM 太热闹了,我也想真正动手把"调 AI"这件事玩明白——不是停留在"调一个 chat 接口、把结果打印出来"那种 demo 级别,而是把它放进一个具体场景里,去面对 prompt 怎么写、上下文怎么喂、结果不稳定怎么办、速率限制怎么应付这些真问题。

一句话:学习不一定要有目的,但有个落地的东西会更香

2. 现有翻译工具,真的没让我舒服过

学习是借口,痛点才是真正让我动手的理由。

作为一个每天要读不少英文文档、博客、GitHub Issue 的人,我用过的翻译方案大概是这几种:

  • 全文翻译(Google 翻译、有道、DeepL……)
    把一切都翻译成中文,包括那些我每天都在用英文思考的词。读到"事件循环负责处理异步回调"这种句子,我会下意识地在脑子里翻译回 event loopasync callback 才能继续读下去。翻译没帮我减负,反而多了一道"再翻译回来"的工序

  • 不翻译,硬读
    英文还行的同学可能没问题。但碰到长句、生僻领域、或者只是单纯想快点读完的时候,整段英文还是会让大脑变慢。

  • 沉浸式翻译这一类双语对照
    设计很好,是我用过最久的方案。但它本质上还是"原文 + 全文翻译"两份内容并列,眼睛要在两行之间来回切换,专业术语依然被翻译成了中文。

看了一圈,发现它们都在解决同一个问题:怎么把英文变成中文

但作为程序员,我们想要的根本不是中文

想想我们平时怎么和同事说话:

“这个 bug 是 race condition 导致的,在 event loop 的 next tick 才 resolve。”

中英混着说,专业词留英文,连接词用中文——这是程序员日常最自然、认知负担最低的表达方式。

那为什么翻译工具不能直接给我这种格式呢?

HalfTrans 就是为了回答这个问题。

把"event loop"留着,把"is responsible for handling"翻成"负责处理",把"asynchronous callbacks"留着——这样一段话扫一眼就懂,眼睛不用做翻译,脑子也不用做翻译。

翻译方式 结果
原文 The event loop is responsible for handling asynchronous callbacks in Node.js runtime.
Google 翻译 事件循环负责处理 Node.js 运行时中的异步回调。
HalfTrans event loop 负责处理 Node.js runtime 中的 asynchronous callbacks。

这一刻我才意识到一件事:这种"半翻译"以前做不了,是因为传统机器翻译没有"语义判断"的能力。哪些词该留、哪些该翻,需要根据上下文和领域来判断——而这正是 LLM 擅长的。

时机到了,我就开干。


二、整体思路:这个插件是怎么跑起来的?

不画架构图、不贴代码,用大白话说。

一个 Chrome 插件,本质上是几个角色配合:

  • 页面里的"探子"(Content Script):能直接接触你当前打开的网页,能看到 DOM、能改 DOM。
  • 后台的"调度员"(Service Worker):常驻在浏览器进程里,负责跑请求、做协调。页面里的探子不能直接调外部 API(涉及跨域、密钥安全等问题),得通过它转一手。
  • 设置和弹窗(Popup / Options Page):用户配置 API Key、术语表、风格偏好的地方。

HalfTrans 的工作流大致是这样的:

  1. 你按下快捷键或点击图标,告诉插件:开始翻译这个页面。
  2. 探子开始扫描页面,找出"看起来是英文段落"的元素:<p><li><h2> 这些。
  3. 关键:只翻译你能看到的那部分。一篇长文可能有几百段,没必要一次全翻——既慢又费 token。只翻视口里的,滚动到哪翻到哪。
  4. 打包送给后台调度员。一次性塞十几段给它,让它批量发给 AI,省请求次数。
  5. 调度员调用 AI 接口,拿到翻译结果,再发回页面。
  6. 探子把翻译结果插进原段落里,原文不删——这样你随时可以对照原文,也不会破坏页面原有的链接、按钮、样式。

整个过程里,我自己觉得最有意思的两个设计点是:

"视口优先"是怎么想到的?

最早的版本是一上来就翻全文。结果在长博客上一次性发出去几十个请求,要么把 API 速率限制打爆,要么用户等了好久还是空白。

后来想清楚了一件事:用户根本不需要"整页翻译好",他需要的是"现在屏幕上这段已经翻译好"。其他段落等他滚到那里再说就行。

这其实是个非常老的工程经验——别做用户不需要的工作。但要在产品设计里时刻提醒自己,并不容易。

不替换原文,而是"追加"翻译结果

最开始我想的是"翻译完了把原文换成中文"。后来意识到这样有几个问题:

  • 原文里的链接、<code> 标签、行内样式全没了
  • 用户想看原文校对一下都做不到
  • 万一翻译翻砸了,页面看起来就乱了

所以现在的做法是:原段落留着,翻译结果作为一个 <span> 追加在原段落里。视觉上看起来就是"英文 + 中文"挨着出现,但 DOM 上原文完全没动过。

这个决定让后续很多事都变得简单:要不要显示原文?给那个 span 加个 CSS class 就行。要不要清掉翻译?把 span 删掉就行。完全不需要"还原页面"这种危险操作。


三、和 AI 打交道:这部分最值得聊

这是整个项目我学到最多的地方,单独展开说说。

调 AI,远不止"发个请求"

刚开始做的时候,我以为"调 AI 翻译"就是:

请翻译这段话:xxx

发给 OpenAI 兼容接口,拿到结果就行。

真做起来才发现,这只是 1% 的工作量,剩下 99% 都是在解决"怎么让 AI 翻得让我满意"。

第一关:怎么写 prompt,让它知道我想要什么

“半翻译"这件事,我自己说着很顺,但 AI 不知道什么叫"半翻译”。你得用它能理解的语言把规则讲清楚。

我现在的 system prompt 大致分成几个部分(用分隔符明确分段,让 AI 一眼就知道每段是干嘛的):

  • 硬规则:什么东西绝对不能翻——代码标识符、API 字段名、命令、固定搭配(HTTP request 这种)。这是兜底,必须最优先。
  • 强术语表:一份我内置的 80 多个常见技术术语(event loopclosuremiddlewarepodschema……),告诉 AI 在技术语境下这些词通常保留英文。
  • 用户词库:用户自己加的词。这部分优先级最高——你说要留就留,你说要翻就翻,不接受 AI 反驳。
  • 指导原则:理解语义优先、不要逐词翻译、同一概念前后一致、长度不要超过原文 1.3 倍等等。
  • 输出格式:用 XML 标签包住上下文和待翻译文本,让 AI 清楚哪部分是参考、哪部分是任务。

写到这里,我才理解为什么大家都说 prompt engineering 是一门玄学——它不像写代码有编译报错,你只能不停地试、不停地观察输出、不停地补规则。我前后改过差不多十几个版本,每次以为"这次稳了",结果第二天用又会发现一种新的翻车姿势。

第二关:上下文这件事,AI 比你想象的更需要

你给 AI 的就一个孤零零的句子:

The container manages the lifecycle.

它怎么知道这个 container 该不该翻译?是 Docker 文档里的 container(保留),还是 CSS 里的 container(可能翻成"容器"),还是某个 Java 框架里的 container?

没有上下文,AI 就靠猜

所以 HalfTrans 在每次翻译时都会顺手把这几样东西也喂给 AI:

  • 页面是什么:页面标题、所在的导航路径(比如"Kubernetes 文档 → Concepts → Workloads")
  • 当前段落在哪个章节:让它知道这段话在文章里的位置
  • 前后几段在说什么:让它知道上下文在讨论什么主题
  • 周围有没有代码块:邻近的代码片段是判断技术领域的最强信号

效果是显而易见的——加了上下文之后,AI 对"这个词在这里到底算不算专有名词"的判断准确率明显上了一个台阶。

我后来意识到,很多人觉得"AI 翻得不准",其实不是 AI 不行,而是你给它的信息太少。你自己在脑子里读这句话的时候,是带着对整篇文章的理解去读的;你只把一句话扔给 AI,等于要求它在没有上下文的情况下做得和你一样好——这不公平。

第三关:批量翻译,省钱也省心

每翻一段调一次 API,几百段就是几百个请求。慢、费钱、还容易被限流。

办法是把多段塞进一个请求里,让 AI 一次翻完,再用一个分隔符(我用的是 [SEP])把它们分开。

听起来简单,做起来有坑:不同的 LLM 对"用 [SEP] 分隔"这个指令的遵守程度参差不齐。有的稳如老狗,有的会偷偷换成换行,有的会忘了加。

所以我做了一个三级保底策略:

  1. 首选:按 [SEP] 切,段数对得上就用。
  2. 次选:按双换行切,段数对得上就用。
  3. 兜底:上面都失败了?降级成"一段一段单独翻",慢一点但保证每段都翻得到。

这个设计后来给我上了一课:和 AI 协作的代码里,你永远要假设它会犯错,写代码的时候就要把"它没按格式输出"这件事当成一种正常状态来处理。这和我们以往写"调用 RESTful 接口拿 JSON"的心态完全不一样——传统接口你信它,AI 接口你不能全信。

第四关:并发,不能太贪心

视口里十几段、批量打包之后变成几个请求、几个请求又同时往外发——一个长文档轻轻松松就能把 API 速率限制(429)打出来。

我的做法是滑动窗口:最多同时跑 3 个批次,跑完一个再放一个进来。

数字是拍脑袋拍出来的,对绝大多数 API 都够用,也不会让用户等得太焦虑。这就是工程——理论上的最优 ≠ 体感上的最优


四、做完之后的一些感受

关于"做一个东西"

我以前总觉得做一个完整的产品很麻烦,一直停在"想做"的阶段。这次硬着头皮做下来,才知道很多东西想 100 次不如做 1 次

做的过程中你会自然遇到一堆原本想不到的问题——Service Worker 怎么休眠、innerText 为什么会把翻译结果也读进去、Manifest V3 的权限怎么配——这些都是你不动手永远不知道存在的细节。

关于 AI

调 AI 这事,写 prompt 比想象中要工程化得多

它不像玄学,更像在写一份"特别能容忍歧义的需求文档"——你越能把规则写清楚、把例外说明白、把格式定死,AI 就越靠谱。

而那些 AI 翻得不好的地方,大多数时候不是模型的问题,是我没把话讲清楚

关于这个插件

它现在不完美。术语判断偶尔翻车、跨段一致性还不够好、流式输出没做、缓存还没加……但它已经是我每天都在用的东西。

能解决自己问题的工具,做得糙一点也没关系;做不了的工具,做得再精致也没用

如果你也受够了"事件循环"和"依赖注入",欢迎来试试。仓库地址在文末,MIT 协议,随便折腾。


效果图

在这里插入图片描述

开源地址:https://github.com/huolihua123-crypto/HalfTrans
技术栈:React 18 + TypeScript + Vite + Chrome Extension Manifest V3

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐