GML SEO先不继续堆功能了:我为什么把精力转到了GML Translate

最近,我重新给手头几个 WordPress 项目排了排优先级。结论说起来很简单:GML SEO 先不继续堆功能了,后面主要做日常维护、WordPress 兼容、Bug 修复和必要更新。新的开发精力,更多放到 GML Translate。

这不是说 GML SEO 不做了。只是从 SEO、Meta、Schema、Sitemap、robots、重定向,一路想到 Search Console、性能、多语言和 AI,再顺着往下加,很容易变成“WordPress 有什么功能,我都想往里面装一点”。这个毛病我还是有自知之明的。

以前写《SEO 这种枯燥活,我选择让 AI 替我“搬砖”》,就是想少干点重复活。结果功能加着加着,维护的事也跟着多了。再这么下去,砖没少搬,还得顺手管个砖厂。

这段时间让 Codex 继续完善 GML Translate,我问得最多的,已经不是“这句翻得怎么样”,而是:后台说翻好了,为什么这页还是不对?

一开始,我以为翻译插件最麻烦的是 AI

最早想得并不复杂:找出网页上的文字,交给 AI 翻译,保存起来,前台替换,再加个语言切换器。需要兼顾搜索引擎,就继续处理 hreflang 和 sitemap。

当时觉得,最费劲的大概是选模型、调提示词,以及让翻译别太像机器。真正放到 WordPress 网站里跑,问题却越来越不像翻译问题。

页面上有正文,也有导航、页脚、按钮、产品模板。正文改过,主题换过,某些文字只在特定条件下出现。后台显示“翻译完成”,前台打开又夹着几句原文。

于是那个看似简单的问题反复冒出来:这个页面,究竟算不算翻译完成?

没有 API Key,已经翻好的页面怎么办?

先要拆开两个开关:多语言网站是否启用,和 AI 翻译是否启用。它们不是一回事。

API Key 被移除、模型服务临时故障,或者我只是想暂停花钱,都不应该让已经翻好的页面跟着消失。译文已经存下来了,展示它不需要再问一遍 AI。

我现在把这两件事分开:已有译文照常给访客看;新翻译、队列、重试这些事,才去看 AI 服务能不能用。

普通访客打开页面,也不该顺手触发一笔模型费用。我只是请人来看个网站,没打算让他帮我按充值按钮。

“德语完成 95%”,到底是谁完成了?

举个例子,下面的百分比和条数都是为了说明问题,不是实际统计:全站德语完成 37%,首页已经 100%,产品 A 是 99%,产品 B 只有 55%。全站进度条看着直观,但我点开的明明是其中一页。

首页已经翻好了,为什么要等长尾文章?反过来,全站大部分都翻好了,也不能证明刚新增的那个产品页已经准备好。

于是判断单位得从“德语”缩小成“这个页面的德语版本”,也就是 Resource × Language。这里的 Resource 可以是首页、文章、产品或分类页,不只是一个 Post ID。

后台告诉我还缺什么、访客能看到什么、搜索引擎该不该收录,都得先找到这个具体页面。全站百分比留着看总进度就行,别什么事都让它拍板。

接口传了页面 ID,底下却没用它

查早期实现时,有个接口很能说明问题:get_translation_status($object_id, $lang)。看名字和参数,很像是在问某个页面、某种语言的状态。

但那时底层直接丢掉了页面 ID,最后还是用语言全局状态作判断。参数是页面级的,答案却是全站级的。

这就能解释为什么两边可能打架:生成链接和 sitemap 的一边看全站,觉得可以公开;真正打开页面的另一边看到了缺失内容,又收紧索引或语言关联。

门口的人说里面营业,进去以后老板说今天不营业。两边单看都有自己的理由,合在一起就不对了。

后来把这条路径改成先找具体 Resource,再读它的语言状态。这个历史问题已经改了,不是说现在还把页面 ID 扔着不用。

Translation Memory 不是当前网站,Queue 也不是

接下来发现,连计算完成率的底数都不能随便取。

Translation Memory,简称 TM,是译文记忆库。原文和译文对应着保存,后面遇到相同内容可以复用。Queue 是工作队列,记着哪些任务待处理、失败了、要不要重试。

它们很重要,但都不能直接代表“这个页面现在需要什么”。网站改版后,旧按钮的译文还在 TM 里,旧任务的失败记录也可能还在队列里,可现在的页面早就不用那个按钮了。

如果旧的成功和失败都算进来,页面早换了一拨内容,完成率还在替上一版算账。旧译文我当然想留着,下次说不定还能用;但旧失败不能一直拖着新页面。

Manifest:先列清楚这页现在需要什么

所以又加了一层 Manifest。这个词听起来挺架构,理解起来就是一张当前清单。

假设这页现在需要 A、B、C、D 四段文字,就拿这张清单去问 TM:A 有德语吗?B 有吗?哪些译文有效,哪些还缺?不是把数据库从古到今的记录倒出来求个比例。

页面改版了,清单也要变。fingerprint 用来辨认内容是否还是那一份,generation 用来识别状态属于哪一代。旧清单不能披着新页面的外衣继续说“我已经完成了”。

到这里,Readiness 才有了一个比较可靠的起点:根据当前页面的依赖和有效译文,算出当前状态,而不是根据历史库存猜。

改一句导航,为什么不只影响一个页面?

WordPress 还有一个特点:很多内容是共用的。Header、Footer、Navigation、Widget、主题全局元素,以及 WooCommerce 模板,都可能同时出现在一批页面里。

假设网站有几百页,而且都用了同一个导航,那改一句导航,影响的可能就是几百页。不是点开哪篇文章编辑,就只算哪篇的事。

TM 没必要为每页重复存一份相同导航的译文,但系统必须知道哪些 Resource 依赖它。于是又有了资源关系和依赖跟踪:共享内容变了,相关页面的旧状态不能继续无条件作数。

这也是为什么只盯着“文章更新”不够。正文没动,不代表整张页面没动。

100% 不代表翻译是对的

就算每段文字都有译文,事情也还没结束。完成度和质量是两件事。

自动译文可能已经成功入库,机器数一遍,觉得一个不缺;人看一眼,却发现明显错译,或者把不该出现的内容带进来了。

Quality Hold 就是给这种情况留一个明确状态:译文存在,但被标记为不可采用,不能再当成有效译文去证明“这里已经准备好了”。

在最新这轮开发实现里,被 Hold 的片段不会继续作为有效翻译输出,可以回退到源文;这也不等于只要有一条 Hold,就把整页一律封掉。整页是否能访问、是否适合收录,还要分别判断。

机器可以告诉我都有了,但不能替我决定都对了。这个人工判断的位置不能被一个绿色进度条挤掉。

然后发现,字符串本身也没有那么老实

主题和插件给出的文字,不一定就是浏览器最后显示的文字。

比如 gettext 里是 Showing %s results,经过 sprintf 填入数量,页面上才变成 Showing 12 results。模板和最终句子不是同一个源文本身份,不能只看着差不多就混着记。

HTML entity 也类似。页面源码里的编码表示,和人眼看到的字符之间需要正确解码,否则抓取、查找和替换会对不上。

还有更绕的一类问题:前一个环节已经翻成德语的内容,在同一个请求的后续环节里,又被当作源文送去翻译。翻译不光要记得“我要替换谁”,还得知道“这句已经是我的输出”。

后来补的就是完整的 source identity、完整文本节点的实体解码,以及请求内防止二次翻译的保护。带占位符的模板,也不能在还没形成最终句子时乱改。

至于“在整页 HTML 里搜到一段就全局替换”,看着省事,实际很容易顺手伤到别的文字或结构。这里宁可处理得明确一点,也不能靠一个大范围替换赌运气。

页面优先,不是让 AI 先把全站跑完

站在我这种实际用网站的人这边,最想先解决的往往是:首页能不能看,导航能不能用,重要分类和产品能不能读懂。不是先等所有冷门文章凑齐一个漂亮的百分比。

所以我想要的顺序是:先首页和共享导航,再重要分类、重要产品,长尾慢慢处理。这就是现在往 Page First 调整的原因,底下按 Resource × Language 来安排任务。

后台最好直接告诉我“首页德语还差 7 条”,而不只是“全站还有 4600 条没翻”。

前一种回答让我知道下一步做什么,后一种回答主要让我知道事情很多。

不过这套方式还在继续做。底层能按页面记状态,不代表后台已经能替我把所有轻重缓急都排好,更不代表什么主题扔进去都不用管了。

98% 到底能不能放出来?这个判断后来也改了

早期我倾向于把门槛设严:缺关键内容就不要放,后来还经历过以 100% 完成度作为公开条件的阶段。出发点是避免半成品被访问或收录,但实际也会把已经有用的部分译文一起锁住。

只缺“查看更多”和缺产品关键规格,当然不是同一种业务风险。不过,把这个直觉写成“缺 SEO Title 就必定封页”,又不符合最新实现了。

最新开发代码已经改成渐进公开:机器完成度继续记录,但不再是所有页面共同的一把门锁。在当前清单有效、已有有效译文等条件满足时,部分翻译也可以提供访问;缺失或被 Hold 的片段回退源文。

但能访问和能收录,还是要分开。源页本来就 noindex、语言路由不对、Manifest 已经过期,或者根本没有有效译文,都不能因为“可以回退原文”就当作没事。人工审核策略也得继续起作用。

产品规格、付款信息这些内容,我肯定还是要认真看。但不能写得好像插件已经能自动认出所有重要句子,发现不对就替我把门关上。它还没这么省心。

所以 98% 或 99.8% 已经不能替我下结论。现在分别看完成度、访问和收录。这是最新开发实现的变化,真实网站上的验收还得接着做。

SEO:翻译插件不能到处抢活

处理到公开状态,就绕不开 SEO。但多语言插件知道语言和 URL,不代表它应该再造一整套 SEO 插件。

当 GML SEO 与 GML Translate 一起使用时,SEO authority 留给 GML SEO,由它承担最终 SEO 输出;Translate 提供语言、URL mapping、页面状态和译文数据。canonical、robots、sitemap、hreflang、Meta、Schema 这些输出不能两边各写一份。

如果没有 GML SEO,也不能假定网站没有 SEO Owner。SEOPress、Yoast、Rank Math 可能已经在工作,就应该按已有插件的接口配合,避免重复 canonical 等输出;真正没有相应 Owner 时,才补最低限度的多语言 SEO。

huwencai.com 本站继续由 SEOPress 负责 SEO,GML SEO 保持未启用。这不是谁比谁强的问题,而是一个页面不需要两个插件争着签字。

重复代码,最后抽成了 Shared Translation Core

GML SEO 原本有翻译相关能力,GML Translate 又是独立插件。如果底层一直复制两份,一边修了 Bug,另一边没同步,过一阵子就会变成名字相近、行为不同的两套东西。

于是把翻译、存储、队列、状态和相关缓存这些共用部分,抽成 Shared Translation Core。各产品继续管自己的设置入口、产品边界和最终输出。

这里容易误会的一点是:用户不用因此再安装第三个 WordPress 插件。共享核心属于开发和构建底座,构建时按锁定版本带进各自安装包,最终仍然安装独立的 GML SEO 或 GML Translate ZIP。

版本锁定和 vendoring,主要是防止我修了这边、忘了那边。装插件的人不用跟着折腾这些,下载对应的 ZIP 安装就行。

Redis 也让我记住了:快,不等于真

状态越来越多,当然会想缓存。Manifest、Readiness、译文查找,都有加速的空间。但 Redis 说 Ready,不能压过数据库里刚刚发生的变化。

这里的原则很直接:Database 是 Authority,Redis 是 Acceleration。数据库是依据,缓存负责少跑几趟腿。

译文、Hold 或相关状态更新后,要让 generation 和失效机制跟着变化;并发处理中,也不能让较晚返回的旧结果把新状态盖回去。

这里还不能偷懒:内部 generation 变了,外面的页面缓存、CDN 不会自动跟着全变。该失效的缓存、该回读的页面,还是得一个个环节对上。

本来只是想让查询快一点,最后又回到了老问题:这份结果现在还算不算数?

最后测的,已经不只是语言切换器

做到后面,测试早就不只是点一下“Deutsch”,看看页面有没有变。

TM 冲突和重复写入、事务回滚、Queue guard、Manifest 与 Readiness、Quality Hold、Redis generation、并发和 Provider 预算保护,都得有覆盖。改版后旧译文能不能复用,正式 ZIP 里共享核心有没有带对,也不能漏掉。

翻现有测试记录,数据库场景已经到了上百种,也有 WordPress、MariaDB、Redis 和插件集成环境的验证。这个数量我以前真没想到,毕竟最初只想让页面换种语言。

不过测试跑过,不能直接写成“所有网站都稳定了”。换个主题、缓存或页面结构,还是得打开那个网站亲自看。这一点吃不得省事的亏。

为什么最后重点变成了 GML Translate

说到底,GML SEO 进入维护阶段,不是放弃,而是不再用“功能越来越多”衡量进度。已有能力该维护就维护,该修的 Bug 就修,该做的兼容和必要更新继续做。

GML Translate 反而让我看到了更具体的问题:网站改版后旧译文怎么复用?共享导航变了影响谁?页面什么时候适合公开?没有 API Key 以后还能不能正常访问?AI 错译怎么拦?历史队列和眼前这张页面到底是什么关系?

我现在更愿意把时间花在这些地方。按钮少几个,暂时不影响我用;状态算错了、旧译文找不到了、页面该不该公开说不清,那才真的麻烦。

后记:本来只是想翻个网页

回头看这个过程,有点像本来只是想换个轮胎,最后蹲地上把轮毂、轴承和半根传动轴都拆了。

也不是说拆得越深就越厉害。我还得看它装回去以后到底好不好用,别最后只是给自己换了一套更难维护的东西。

目前最明确的体会是:一个真正能用的 AI 翻译插件,最难的可能从来不是 AI 翻译,而是管理一个不断变化的网站里的翻译状态。

这些东西还没折腾完,先记到这里。

接着看看这些记录

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注