
自己做翻译插件说实话之前我一直觉得没必要浏览器商店里现成的翻译扩展一抓一大把装一个就能用。但用久了问题就来了免费版时不时弹窗、收集浏览记录、翻译质量在专业术语上非常拉胯尤其是遇到代码、技术文档、医学术语翻出来完全没法看。后来大模型API火起来我试着用免费额度搭了一个属于自己的网页翻译插件没想到效果出奇好整个过程还基本零成本。这篇文章就把我的完整做法写出来——怎么选免费大模型API、怎么写浏览器插件、怎么调提示词、怎么避开各种坑一步步讲清楚。这套方案适合三类人一是对翻译质量和隐私有要求的普通用户二是不想给翻译软件订阅付费的学生或开发者三是想练手浏览器插件和大模型API调用的技术人员。你不需要有很深的编程基础有一点JavaScript经验就能跟下来。整个插件的核心其实就是三件事把网页文字抓出来、把文字发给大模型翻译、再把译文替换回去。听起来简单但每个环节都有值得抠的细节我会在下面逐一展开。1. 整体思路拆解为什么免费API能做翻译1.1 现成翻译工具有哪些绕不开的痛点先说说我为什么非要自己折腾。浏览器上主流的翻译扩展大多走的是传统机器翻译引擎比如统计翻译模型译出来的句子“能看但不够准”碰到长句经常语序混乱。你要想得到更自然的译文就得开会员而会员费一年算下来并不便宜。更让人介意的是隐私——很多在线翻译工具会把整个网页文本、表单内容发到云端你浏览了什么、填了什么它都一清二楚免费版还会拿这些数据去做模型训练。自己搭一个翻译插件最直接的好处是可控发给哪个API完全由你决定不想要隐私风险的可以选数据政策更友好的服务商翻译术语可以自定义比如我把“memory”统一翻译成“内存”而不是“记忆”界面可以做成自己习惯的样子不用忍受广告。而且用免费大模型API的额度日常浏览外文网站的翻译量完全够用选对服务商的话一个月下来账单是0元。1.2 免费大模型API翻译的核心逻辑大模型翻译和传统机器翻译有本质区别。传统模型更多是短语替换和规则重组而大模型靠的是在海量语料上学到的语义理解它会把整句话的意思“读懂”再重新组织成自然的目标语言。所以用大模型翻译长难句、专业术语、口语表达效果明显更好。用API的方式实现零成本目前主要有两个路径。一个是使用大模型服务商提供的免费额度比如新用户注册会送几十万到几百万token一般能用挺长时间。另一个是选择服务商开放的低价模型按量计费非常便宜翻译一段普通网页文本可能只要几厘钱几乎感知不到成本。这两种方式都能做到“不用额外花钱”。我的做法是优先用免费额度然后把插件做成一个“按需请求”的模式——不整页翻译只翻译你正在看的可见区域。这样一来每次推送给API的token量很小免费额度消耗得非常慢真正意义上实现了零成本。后面我会讲到怎么控制这个“按需请求”的细节。1.3 这个方案的边界与预期管理在做之前先把预期设对。用免费大模型API搭的翻译插件不是万能的它有下面几个边界第一必须联网因为API请求要经过网络不适合完全离线环境第二免费额度通常有速率限制一次性翻译大量内容可能被限流第三大模型的上下文窗口有限不能把整本小说一次性丢进去需要分片处理第四插件需要一定的浏览器权限安装时浏览器会弹出提示这是正常现象。想清楚这些边界就不会在用的过程中产生不切实际的期望。我这个方案主要的定位是“日常网页阅读的顺滑翻译”不是“专业级出版翻译”。如果你的需求是后者那还是需要人工译校。但就日常浏览英文资料、读技术文档、看新闻网站来说这套插件完全够用而且比很多商业翻译扩展的默认效果更自然。2. 免费大模型API选型与准备2.1 我现在在用的几个免费API服务商市面上的大模型API服务商不少免费政策也经常调整我这里分享的是我自己的实测选择标准具体免费额度请以各家官方文档为准。我不会给你一个个链接只讲选型思路和大致区别你自己去平台注册一下就能看到最新政策。我在实际测试中比较常用的有三类第一类是以DeepSeek为代表的开源模型服务商中文翻译质量好API价格非常低新用户经常有免费体验额度上下文窗口一般比较大适合处理长文本第二类是智谱AI、Kimi这类国内平台会提供一些免费模型个人用户注册能领到一定额度的token适合做轻量级翻译第三类是海外平台的免费额度同样可以用但对国内网络的调用延迟可能稍高你自己根据实际网络情况判断。我的建议是不要只盯着一家。因为API免费活动变化很快今天这家免费送得多下个月可能就调整了。最稳妥的做法是在插件里把服务商做成可配置的等到哪家的额度用完换一家改一下配置就行。下面的代码会展示如何用抽象接口做到这一点。2.2 获取API Key并安全管理获取API Key的流程都差不多登录服务商的开放平台在控制台创建一个API Key复制保存。这里有几个经验教训必须单独列出来。第一API Key是敏感信息绝对不能写死在插件代码里。尤其是如果你打算把项目传到GitHub一旦Key泄露别人就能用你的额度疯狂调用轻则额度被刷光重则产生费用。第二浏览器的扩展其实没有绝对安全的本地存储方式我的做法是把Key存在chrome.storage.local并且通过配置页让用户自己填写而不是硬编码在代码里。第三很多平台支持创建多个Key并设置限额建议你专门为这个翻译插件创建一个Key同时设置单日消费上限即使泄露也能把损失控制在零。这招非常管用。在实际操作中我还会顺手把Key的前几位和后几位打码后再截图防止在聊天记录或教程里泄露。这些细节看起来啰嗦但踩过坑的人都知道API Key泄露的教训往往很痛。2.3 选型时应该关注的四个技术参数服务商那么多怎么选我总结了四个真正影响体验的参数上下文长度Context Length至少要大于8K token不然还没翻译几段就被截断了。现在主流模型动辄32K、128K足够用。输出速度Tokens per second翻译体验要流畅每秒输出速度最好快一点。你可以用平台提供的在线Playground测试一下多试几家。免费额度和速率限制Rate Limit重点看每分钟请求数限制如果限制太死批量翻译时可能频繁失败。模型翻译质量这个没有固定指标我的笨办法是拿同一段英文技术文档分别给几家的免费模型翻译对比流畅度和术语是否准确15分钟就能看出差距。用这四个参数去筛基本能锁定合适的一家。我最终选的方案可以抽象成一段可配置代码换API就是改改配置项下面章节会把这部分写出来。3. 浏览器翻译插件核心实现3.1 插件的基础骨架Manifest V3现在浏览器插件的主流标准是Manifest V3我整个项目就用这个结构。首先需要一个manifest.json文件它是插件的“身份证”声明了插件名称、权限、入口文件。{ manifest_version: 3, name: AI网页翻译助手, version: 1.0.0, description: 调用免费大模型API的网页翻译插件, permissions: [ storage, activeTab ], host_permissions: [ https://api.deepseek.com/*, https://open.bigmodel.cn/*, https://api.moonshot.cn/* ], action: { default_popup: popup.html, default_icon: icon.png }, background: { service_worker: background.js }, content_scripts: [ { matches: [all_urls], js: [content.js], run_at: document_idle } ] }这里有几个值得解释的点。permissions里我申请了storage和activeTabstorage用来存取配置和API KeyactiveTab是让插件在点击图标后能拿到当前标签页的操作权限。host_permissions是联网访问API的许可每一项对应一个服务商的域名重要的事情说三遍这个数组里的域名必须和你实际调用的API地址完全一致不然请求会被浏览器拦截。background使用service_worker这是MV3的标准做法。content_scripts会注入到所有页面用来读取和改写网页内容。注意run_at选document_idle意思是页面加载完后再执行这样可以避免太早抓取导致文本不全。3.2 从网页里“干净地”抓取正文抓取网页文本是整个插件最容易出Bug的地方。如果直接拿document.body.innerText会把导航栏、广告、脚本内容全部塞进去既浪费token又容易翻译出乱七八糟的东西。我选择的方案是用TreeWalker遍历文本节点同时过滤掉需要忽略的标签。function getVisibleTextNodes(root) { const walker document.createTreeWalker( root, NodeFilter.SHOW_TEXT, { acceptNode(node) { const parent node.parentElement; if (!parent) return NodeFilter.FILTER_REJECT; const tag parent.tagName.toLowerCase(); if ([script, style, code, pre, textarea, iframe].includes(tag)) { return NodeFilter.FILTER_REJECT; } if (parent.isContentEditable) return NodeFilter.FILTER_REJECT; const text node.textContent.trim(); if (!text || text.length 2) return NodeFilter.FILTER_REJECT; return NodeFilter.FILTER_ACCEPT; } } ); const nodes []; while (walker.nextNode()) { nodes.push(walker.currentNode); } return nodes; }你可能会问为什么要把script、style、textarea排除掉因为脚本里的英文全都是代码逻辑翻译了会破坏功能style里的内容本来就不该显示textarea是用户输入框里面的内容没经过同意不应该动。code标签里是代码翻译后代码就没法复制运行了。这些细节就是“干净抓取”和“一锅端”的区别。还有一个容易被忽略的点contentEditable区域。很多在线文档编辑器比如Notion、语雀允许用户直接编辑内容如果贸然翻译这些区域很可能把用户正在写的内容改掉。我加了一个判断isContentEditable属于它的文本节点直接跳过这样就不会误伤。3.3 在Service Worker里调用大模型API抓取到文本之后要把它发给大模型。这里我强烈建议把fetch请求放在background的service worker里而不是content script。因为content script直接跨域请求通常会受到CORS限制而service worker配合host_permissions可以在后台发出跨域请求更稳定也更安全。下面是一段可复用的调用函数我把它抽象成通用方法。你可以根据服务商的不同调整api地址、key字段和请求体格式。// background.js async function translateText(text, config) { const { apiUrl, apiKey, model, targetLang } config; const prompt buildPrompt(text, targetLang); const response await fetch(apiUrl, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${apiKey} }, body: JSON.stringify({ model: model, messages: [ { role: system, content: SYSTEM_PROMPT }, { role: user, content: prompt } ], temperature: 0.3, max_tokens: 4000 }) }); if (!response.ok) { const errText await response.text(); throw new Error(API请求失败 ${response.status}: ${errText}); } const data await response.json(); return data.choices[0].message.content.trim(); }这里的关键参数是temperature。翻译任务我建议设到0.3以下温度越低输出越稳定不容易出现“自由发挥”的情况。max_tokens也要控制好设太多了容易被滥用设少了译文会被截断。对于一般网页段落单次请求设4000基本够用。还要补充一个细节为什么不把SYSTEM_PROMPT也放在用户请求里而是单独用system字段因为在主流大模型API的规范里system是系统角色负责设定整体行为user是用户角色负责传入具体内容。分开写模型更容易理解任务边界翻译时不会把提示词本身也当成待翻译内容。3.4 译文回填与页面防错乱API返回译文后要把译文填充回原来的文本节点。这步有个大坑如果你直接改node.textContent translatedText页面上被翻译的元素可能完全错乱比如原本一个标题被翻成超长句子撑坏布局或者译文里带着换行符导致排版崩掉。我采用的方法是“错位回填”先把页面上的文本节点按顺序分成一组一组的每组累计原文长度控制在几百字符内。然后把这一整组文本合并成一段发给API拿到整段译文后再按原文的段落结构分割开依次填入对应的节点。简单说就是“按组分整段翻按组分”。这样能最大限度保留原始段落层次又不会为了几十个字发太多次请求。async function translatePage(selectedNodes, config) { // 按可见区域或固定长度分组 const groupSize 20; for (let i 0; i selectedNodes.length; i groupSize) { const group selectedNodes.slice(i, i groupSize); const originalText group.map(n n.textContent).join(\n---SPLIT---\n); const translated await translateText(originalText, config); const translatedParts translated.split(\n---SPLIT---\n); if (translatedParts.length group.length) { group.forEach((node, index) { node.textContent translatedParts[index]; }); } } }这个分割符是我自己定义的原文里几乎不会出现的特殊字符串。之所以用这种分隔符而不是用换行是因为很多HTML页面里本身就带着换行直接按换行分割会把一个段落切得七零八落。用唯一的标记字符能保证分组和还原之间不出错。还有一个很实际的问题页面有动态加载内容翻到一半往下滚新的内容又出现了怎么办我的插件里用了一个简单的MutationObserver监听DOM变化发现新增文本节点就自动继续翻译。但对于已经翻译过的节点一定要打上标记比如给父元素添加data-translatedtrue否则观察者一旦触发会把已经翻译的内容再发一次形成无限循环这是新手最常踩的坑。4. 提示词工程决定翻译质量的关键4.1 一套简单却很好用的翻译提示词模板多数人第一次试AI翻译都只写一句“translate this into Chinese”这样出来的效果不稳定。我自己调试下来发现系统提示词越明确翻译质量越高。下面是我目前用着很顺手的模板System Prompt 你是一名专业的网页翻译引擎。你的任务是将用户输入的网页文本翻译成指定语言。 要求 1. 翻译结果要保持原文的语义准确、语气自然。 2. 不要添加任何解释、注释或额外的回复内容只输出译文本身。 3. 保持原文的分段和占位符格式。如果原文用了特殊分割符输出的译文中也必须保留同样的分割符。 4. 遇到专有名词、品牌名、代码、变量名、URL时不需要翻译的部分要原样保留。 5. 用心翻译专业术语确保术语保持一致。这个提示词里最有价值的其实是“只输出译文本身”和“保留分割符”这两条。前者避免了模型重复你刚才说的话后者保证了我们上一节的分组回填能正确对齐。如果你不写这两条模型经常会在译文前后加上“好的这是翻译结果”之类的内容解析起来非常烦。4.2 处理专业术语、代码和无障碍阅读网页翻译的一大场景是技术文档。我刚开始测试时模型把“function”翻译成了“函数”这是对的但把“lambda”翻译成了“拉姆达”就有点怪。后来我在提示词里加了自定义术语表通过用户配置传入。比如“API”保持不译“React”保持不译“深度优先搜索”固定翻译。这本质上就是一种轻量级的few-shot方法能明显提升特定领域的准确性。如果你经常阅读特定领域的内容医学、法律、自动化可以在插件的配置页里维护一份术语表然后用JSON格式拼到提示词里。这样遇到重复术语时模型会主动保持一致。还有一个小技巧翻译代码块或带代码的段落时我建议在提示词中明确写“代码块、变量名、命令行保持不变只翻译自然语言部分”。实测下来工具类网页的翻译体验从“完全没法用”变成了“可以直接照着操作”。4.3 控制成本与上下文窗口的策略免费额度虽然免费但也不能浪费。翻译整页内容时我会先把页面里的文本按“可见区域”进行分段处理只翻译当前视口内的内容。只有当用户滚动或者主动点击“翻译全页”时才继续翻译剩余区域。这个策略能减少大概70%的无效token消耗。上下文窗口方面我的处理逻辑是如果一组文本的字符数较大就自动拆分成多个chunk每个chunk控制在1500字符左右。为什么是1500因为中英文token比不一样英文一个单词约等于1-1.3个token中文一个字约等于1-2个token1500字符对应2K-3K token加上系统提示词不会超过大多数模型的8K上下文限制。我还做了一个响应式分割如果API返回错误提示“maximum context length exceeded”插件会自动把当前组再对半拆开重新请求。这样就可以兼容不同上下文窗口的模型不会因为换了一个模型就导致整个插件罢工。5. 常见问题与排查技巧实录5.1 API请求失败无Key、限流、超时我自己的插件刚写完时测试最常碰到这几个错误。第一是“401 Unauthorized”基本就是API Key错误或者没放对位置。排查方法很简单先用curl或PostMan直接调一次接口如果单独调用成功那就是插件里的赋值或发送逻辑有问题。第二是“429 Too Many Requests”这是触发速率限制了。解决办法是在插件里加一个队列一次只发一个请求前一个完成后再发下一个。为了防止无限等待每个请求设置30秒超时超时就跳过当前组并提示用户。第三是网络超时多见于免费模型负载高的时候重试一两次一般能解决。这里有一个小经验在background.js里加一个简单的错误处理中间层把所有失败原因收集起来通过chrome.runtime.sendMessage推送到弹窗页。这样遇到问题时不用打开控制台看半天直接在插件图标上就能看到错误摘要。5.2 翻译后页面错乱或样式崩塌最常见的错乱有两个表现译文长度失控导致布局撑破或者译文里的特殊字符破坏了HTML结构。第一种可以用CSS解决在content script里给翻译过的节点加上一个类名设置word-break和overflow-wrap属性让长单词自动换行。第二种需要转义保护API返回的译文里如果含“”或“”之类的字符在写入网页前要转义成HTML实体否则会被浏览器当成标签处理页面直接裂开。如果你用textContent写入其实已经天然转义了不会有标签问题。但如果你图省事用innerHTML就必须做严格转义。我一直建议用textContent代价是不支持译文里的加粗、斜体等富文本但对网页翻译这个场景简单可靠比花哨更重要。5.3 动态页面翻译失效很多现代网站是SPA单页应用内容通过JavaScript异步加载。如果你刚注入脚本时页面还没内容TreeWalker抓到的就是空内容。我常用方案是在document_idle执行后延迟几百毫秒再抓一次同时用MutationObserver监听后续变化。另外一些网站会在滚动时频繁添加节点如果不做节流API请求会像洪水一样打出去。我在代码里做了500毫秒的防抖用户停止滚动半秒后才计算是否翻译新增内容。这样既能保证翻译跟上浏览节奏又不会浪费请求。5.4 插件权限被浏览器限制Chrome商店发布插件时“host_permissions”申请过多会被审核员盯着问。如果你是本地开发者模式加载则没有这个限制但如果以后想上架最好把API域名声明成“可选权限”在用户启用翻译功能后再动态申请。这是我后来优化掉的点初期版本因为申请了太多域名权限审查费了不少功夫。5.5 一张排查速查表整理一下我遇到的高频问题做成速查表方便你直接对照现象可能原因解决方向点击翻译无反应content script没注入或API Key为空刷新页面检查storage配置F12看console触发429错误请求太频繁免费额度速率限制加队列串行请求降低并发译文截断max_tokens太小或上下文超限调大max_tokens对文本分片重试部分文本没翻译动态加载或文本节点被过滤优化TreeWalker过滤器增加MutationObserver页面样式错乱长词未换行或用innerHTML误解析加CSS处理改用textContent翻译后原文本被重复翻译已翻译节点没有标记加data-translated标记避免无限循环6. 实测体验与几个值得改进的方向整套插件做完之后我拿它实际翻译了一个英文技术教程网站和一个英文新闻网站。技术教程的翻译质量专业术语基本能对上代码块保持原样新闻网站的长难句翻译很自然阅读流畅度比传统翻译引擎好一个档次。免费的API额度用了一周从每天翻十来页的量来看几乎看不到消耗确实做到了零成本。最后分享几个我自己实测下来的经验。第一一定要做好配置页。虽然核心功能在content script和background但一个友好的配置页能让整个项目好用非常多。我的配置页只做了三件事填API Key、选模型、填目标语言。用起来几乎没有学习成本。第二别一次性翻译整个超级长页面。哪怕免费额度多也要克制。因为页面一旦全部被替换成中文原文就找不回来了。我最后做了一键恢复功能点击恢复可以重新加载页面刷新回原文状态有后悔药吃。第三这个插件的后续扩展空间其实很大。比如你可以改成“选词即译”鼠标选中文本就弹出译文也可以接本地模型把API地址改成Ollama的本地服务实现完全离线翻译还可以加上“导出双语对照”的功能把翻译结果存成markdown文件。这些都是同一个架构上的小改动不需要重写核心逻辑。做这个项目的最大体会是很多看似“免费工具”的日常需求其实用免费大模型API几十行代码就能自己搞定而且效果还能按自己喜好定制。如果你也厌倦了现成翻译插件的种种限制不妨按这个思路搭一个折腾的过程本身也挺有意思。