WP 博客多语言化实操
(5) 博客菜单翻译实操
在 WordPress + Polylang 多语言站点中,我一直使用一份 PHP 脚本,将新增的中文标签同步复制为英文标签,并建立中英文翻译关系。
这份脚本已经运行了很长时间,但始终存在一个让人困惑的问题:
WordPress 后台显示的中文标签总数,与脚本实际读取到的中文标签总数偶尔对不上。
这个问题并不是第一次出现。
此前我已经围绕它做过两轮完整排查,并分别整理成了文章:
第一次排查将问题指向 W3 Total Cache 中残留的 Polylang 翻译关系缓存;第二次排查则发现,边遍历边创建标签时使用动态分页,会导致分页范围和统计结果发生变化。(永夜)
这两个问题确实都存在,之前的修复也并非无效。
但是直到这一次,我才确认:
脚本长期出现“后台总数正确、直接运行脚本却少读若干标签”的真正根因,是
get_terms()命中了持久化的旧WP_Term_Query查询结果缓存。
此前两次排查解决的是外围问题和并发问题,却没有彻底处理脚本读取源标签列表时的缓存可靠性。
一、这份脚本是做什么的
我的 WordPress 博客使用 Polylang 管理中英文内容。
在新增中文文章时,通常也会增加一些新的中文标签。为了避免每次都在后台手动创建英文标签,我使用一份命令行 PHP 脚本自动完成以下工作:
- 读取所有中文标签;
- 判断每个中文标签是否已有英文翻译;
- 为缺少翻译的标签创建英文标签;
- 设置英文语言归属;
- 建立 Polylang 中英文翻译关系;
- 输出已处理、已跳过和未完成的标签统计。
脚本的运行方式很简单:
cd /data/wwwroot/www.shuijingwanwq.com
php polylang-batch-zh-to-en-tags.php
在正常情况下,脚本读取到的中文标签数量,应该与 WordPress 后台显示的中文标签总数一致。
但这次明显不是这样。
二、故障再次出现:后台有 8928 个,脚本却始终只有 8915 个
WordPress 后台显示:
中文标签总数:8928
但无论运行多少次同步脚本,输出始终都是:
源语言标签总数:8915
已处理新标签:0
已跳过已有翻译:8915
两者相差:
8928 - 8915 = 13
更异常的是,这并不是一次性的统计延迟。
我后来继续:
- 给已发布但没有标签的文章补充标签;
- 删除部分新标签后重新添加;
- 发布带有新标签的文章;
- 再次运行同步脚本。
结果脚本仍然稳定地停留在:
8915
这说明问题已经不是简单的后台统计延迟。
脚本拿到的是一份固定不变的旧标签列表。
三、为什么前两次排查没有真正解决这个问题
回看此前的两轮排查,会发现当时解决的是两个不同层面的问题。
1. 第一次解决的是 Polylang 翻译关系缓存
第一次是在 WordPress 升级后,脚本将几个尚未翻译的标签误判为已经存在英文翻译。
清除 W3 Total Cache 后,脚本重新正常工作。
因此,当时得出的结论是:
W3 Total Cache 中残留了旧的 Polylang 翻译关系缓存,导致
pll_get_term()返回错误结果。
这个判断在当时的场景下是成立的。清缓存后问题也确实消失了。(永夜)
但当时并没有继续确认:
- 究竟是哪个缓存组;
- 是翻译关系缓存还是术语查询缓存;
- 为什么之后还会复发;
- 如何从代码层面避免再次命中旧数据。
也就是说,当时解决了症状,却没有建立长期防护。
2. 第二次解决的是动态分页污染
第二次排查时,脚本出现过这样的统计:
源语言标签总数:8328
已处理新标签:0
已跳过已有翻译:9000
处理率:108.07%
当时发现,脚本一边分页读取标签,一边又在数据库中创建新的英文标签。
如果继续使用 offset 或动态分页查询,后续页面的结果集就可能发生移动,导致:
- 某些标签被跳过;
- 某些标签被重复读取;
- 已跳过数量大于实际源标签总数;
- 最终处理率超过 100%。
最终方案是:
先一次性获取所有中文标签 ID,再通过
array_chunk()在内存中分批处理。
这一修改解决了动态数据集分页问题。(永夜)
但当时用于“一次性获取所有中文标签 ID”的 get_terms(),仍然允许读取缓存。
也就是说,静态列表虽然不会在运行中变化,但它仍可能从一开始就是一份旧列表。
四、这一次最容易误导判断的地方:WP-CLI 查询竟然是正常的
为了确认是不是 get_terms() 本身的问题,我先通过 WP-CLI 分别测试了默认查询和禁用缓存的查询。
结果是:
默认查询:8928
禁用缓存:8928
差值:0
从这个结果看,很容易得出一个结论:
缓存没有问题,因为两种查询结果完全一致。
我一度也据此认为,问题可能在:
- 生产文件与仓库文件不一致;
- 脚本加载顺序不同;
- Polylang 语言过滤条件异常;
- PHP CLI 的 OPcache;
- 某个 MU 插件修改了查询。
但后来按照脚本真正的运行方式,在直接执行 PHP 文件的环境中进行对比,结果完全不同。
五、关键证据:同一个直接 PHP 进程中,默认查询是 8915,禁用缓存是 8928
最终诊断没有继续依赖 WP-CLI,而是完全复现:
php polylang-batch-zh-to-en-tags.php
这种直接 PHP CLI 执行环境。
在同一执行上下文中分别运行:
get_terms([
'taxonomy' => 'post_tag',
'lang' => 'zh',
'hide_empty' => false,
'fields' => 'ids',
]);
以及:
get_terms([
'taxonomy' => 'post_tag',
'lang' => 'zh',
'hide_empty' => false,
'fields' => 'ids',
'cache_results' => false,
]);
得到的结果是:
默认 get_terms():8915
cache_results=false:8928
差值:13
至此,根因已经非常明确:
直接执行 PHP 脚本时,默认的
get_terms()命中了持久化的旧 Term Query 查询缓存。
而设置:
'cache_results' => false,
之后,查询重新从当前数据状态获取完整结果,正确返回了 8928 个中文标签。

六、为什么 WP-CLI 和直接 PHP 脚本会得到不同结果
这也是整个排查中最容易产生矛盾的地方。
WP-CLI 查询得到:
8928
直接 PHP 脚本默认查询却得到:
8915
它们看起来都加载了同一个 WordPress,但运行上下文并不完全相同。
WP-CLI 会定义和初始化自己的运行环境,例如:
WP_CLI = true
而直接执行普通 PHP 文件时,并不存在相同的 CLI 上下文。
在当前网站中,还同时存在:
- W3 Total Cache Object Cache;
- Redis 持久化对象缓存;
- Polylang 语言过滤;
- 自定义 MU 插件;
- 多域名 Host 识别;
- PHP CLI OPcache。
因此,不同入口可能使用不同的:
- 缓存命名空间;
- Host 上下文;
- 缓存绕过逻辑;
- 插件初始化路径;
- 缓存键组合。
这意味着:
WP-CLI 中查询正常,不能证明普通
php file.php执行环境中的缓存也正常。
真正可靠的诊断,必须复现出现问题的原始执行方式。
七、SQL 本身没有问题,问题发生在查询结果缓存层
诊断过程中还检查了实际生成的 SQL。
查询没有异常的 LIMIT,也没有错误的语言条件:
SELECT t.term_id
FROM wp_terms AS t
INNER JOIN wp_term_taxonomy AS tt
ON t.term_id = tt.term_id
INNER JOIN wp_term_relationships AS pll_tr
ON pll_tr.object_id = t.term_id
WHERE tt.taxonomy IN ('post_tag')
AND pll_tr.term_taxonomy_id IN (8837)
ORDER BY t.name ASC
也就是说:
- 数据库中存在完整的中文标签;
- Polylang 的中文语言关系正确;
- SQL 查询条件没有漏掉新标签;
- 数据库实时查询能够返回 8928 个结果。
真正返回 8915 的,是此前保存下来的旧查询结果。
这也解释了为什么数量会长期固定不变。
八、13 个缓存遗漏标签,与 9 个真正缺失翻译标签并不矛盾
诊断过程中还出现了一个看似矛盾的数据:
旧缓存遗漏:13 个中文标签
真正缺少英文翻译:9 个中文标签
这两个数字并不要求相等。
13 个标签没有进入旧的中文源标签列表,但其中有 4 个标签已经通过其他流程建立了英文翻译关系,例如:
- 后台手动操作;
- 文章翻译流程自动同步;
- 其他插件或脚本处理。
因此,当前实际需要补齐英文翻译的只有 9 个。
这 9 个标签分别是:
代码审查
占位符保护
Token 校验
翻译稳定性
安全加速流量
域名拆分
安全加速请求
加量包
网站成本控制
九、最终修复其实只有两行
确认根因后,没有重新设计脚本,也没有大范围重构标签创建流程。
只在获取全部中文标签 ID 的查询中加入一条注释和一个参数:
// 批处理维护脚本必须绕过持久化的 term-query 缓存,确保读取最新完整标签集合。
$source_term_ids = get_terms([
'taxonomy' => 'post_tag',
'lang' => $source_lang,
'hide_empty' => false,
'fields' => 'ids',
'cache_results' => false,
]);
核心变化只有:
'cache_results' => false,
对应 Git 提交为:
f079037925dcf048bf1d455cf2a6badfb830df64
fix: bypass stale term query cache in Polylang tag batch
这个修复没有关闭整个 WordPress 的缓存,也没有影响前台页面性能。
它只规定:
这份维护脚本在读取待处理源标签集合时,不可信任旧的查询结果缓存。
对于日常页面请求,缓存可以提高性能。
但对于负责补齐、迁移、校验数据的维护脚本,准确性比复用查询缓存更加重要。
十、生产部署后的第一次运行结果
部署前先对生产文件进行了备份,然后检查临时文件与生产文件语法:
No syntax errors detected in /tmp/polylang-batch-zh-to-en-tags.php.new
No syntax errors detected in /data/wwwroot/www.shuijingwanwq.com/polylang-batch-zh-to-en-tags.php
部署后 SHA256 与仓库文件完全一致:
9b8e1993914571da1bd4f2d6849d6fa318b2572093d86ed5fc686636ce12ac68
随后重新运行脚本:
php polylang-batch-zh-to-en-tags.php
脚本终于读取到了后台的完整数量:
源语言标签总数:8928
最终统计为:
源语言标签总数:8928
已处理新标签:9
已跳过已有翻译:8919
处理率:100.00%
此前长期固定在 8915 的问题已经消失。
这也从生产运行结果反向证明:
cache_results=false确实命中了真正的故障点。
十一、剩余 3 个标签未建立关系,是另一个独立问题
第一次完整运行结束后,脚本提示有 3 个标签仍未成功建立英文翻译关系:
Token 校验
占位符保护
翻译稳定性
在新的 PHP 进程中再次调用:
pll_get_term($source_id, 'en');
三个标签仍然返回:
英文 ID:0
因此,这并不是脚本末尾同一进程缓存造成的误报,而是标签创建或翻译关系绑定阶段的另一个独立问题。
由于只剩 3 个标签,继续改写批处理脚本的成本和风险已经高于手动处理,因此最终直接在 WordPress 后台补充:
Token 校验 → Token Validation
占位符保护 → Placeholder Protection
翻译稳定性 → Translation Stability
对应 slug 为:
token-validation
placeholder-protection
translation-stability
这一小段异常不影响本次关于“源标签总数读取不完整”的根因判断。
相反,它也提醒我:
“进入了处理流程”不等于“最终成功建立了翻译关系”。
今后脚本中的 processed 统计,最好只在最终验证成功后增加,而不是在开始尝试创建标签时就增加。
十二、为什么说这一次才算真正解决
此前两次排查都得到了一个可以让脚本暂时恢复正常的方案:
第一次:
清除 W3 Total Cache
第二次:
一次性获取源标签 ID,再通过 array_chunk() 分批处理
它们分别解决了:
- Polylang 翻译关系缓存残留;
- 动态分页过程中结果集变化。
但脚本最关键的源数据入口仍然是:
get_terms()
只要这个查询继续允许读取持久化旧缓存,就仍然可能再次拿到过期的中文标签 ID 集合。
本次修复则将保证写进了代码:
'cache_results' => false,
今后无论对象缓存中是否还残留旧的 Term Query 结果,这份批处理脚本都会重新读取当前完整的源标签集合。
这才是一个不依赖人工记忆、不依赖每次先清缓存、也不依赖当前缓存状态的长期修复。
十三、这次排查带来的几个经验
1. 必须在真正出错的执行入口中复现
WP-CLI 正常,不代表:
php custom-script.php
也正常。
不同运行入口可能拥有不同的缓存与插件上下文。
2. 清缓存可以验证方向,但不一定是永久修复
清除 W3TC 或 Redis 后恢复正常,只能证明问题与缓存有关。
仍需进一步确认:
- 是哪个查询;
- 是哪个缓存层;
- 为什么没有正确失效;
- 是否可以在关键脚本中主动绕过。
3. 静态列表也可能是一份过期的静态列表
提前获取全部 ID,再用 array_chunk() 处理,确实比动态分页稳定。
但还需要保证这份初始 ID 列表来自实时数据,而不是旧查询缓存。
4. 数据维护脚本应优先保证准确性
前台页面需要缓存性能。
批处理、迁移、修复和校验脚本则应该优先保证:
完整
实时
可验证
可重复运行
对于这种一次运行处理几千个标签的脚本,节省一次术语查询的收益,远低于漏掉标签造成的长期数据不一致。
5. 统计必须代表真正成功,而不是尝试次数
本次输出中的:
已处理新标签:9
实际上包含了 3 个最终未建立翻译关系的标签。
更严谨的统计应该拆分为:
尝试处理数量
成功创建数量
成功绑定翻译关系数量
失败数量
跳过数量
否则,表面上的 100% 处理率仍可能掩盖局部失败。
十四、结语
这个问题前后排查了三次。
第一次,我以为是 WordPress 7.0 升级后残留的 Polylang 缓存。
第二次,我以为是脚本在动态分页过程中创建新标签,导致数据集发生移动。
这两个判断都没有错,但都只是问题的一部分。
真正让脚本长期固定读取旧总数的原因,是:
直接 PHP 执行环境中的
get_terms()命中了持久化的旧 Term Query 查询结果缓存。
最终修复只有一个参数:
'cache_results' => false,
但为了确认这一行,前后经历了:
- 两篇排查文章;
- 多轮数据库与缓存检查;
- WP-CLI 与直接 PHP 环境对比;
- SQL 查询核对;
- 缓存与非缓存结果差集比较;
- 仓库提交与生产部署验证。
这也是维护大型 WordPress 多语言站点时很典型的一类问题:
最难排查的并不是直接报错,而是代码正常执行、输出看起来也合理,但读取到的数据从一开始就是旧的。
这一次,关于标签同步脚本“总数长期对不上”的问题,终于不再依赖清缓存碰运气,而是从代码层面得到了稳定解决。
WordPress 网站维护、性能优化与博客运营咨询
本站已持续运营超过 10 年,累计发布 1000+ 篇原创技术文章,长期实践 WordPress 网站建设、CDN / Cloudflare 配置、缓存优化、Google SEO、广告变现和多语言网站运营。
如果你的 WordPress 网站遇到访问慢、缓存异常、插件冲突、广告不显示、SEO 基础结构混乱、CDN 配置不确定等问题,可以联系我做一次远程技术排查。
适合以下用户:
✅ 个人博客站长
✅ WordPress 网站运营者
✅ 独立开发者与内容创作者
✅ SaaS 产品官网运营团队
✅ 希望优化网站速度与稳定性的站点
服务内容:
✅ WordPress 速度优化
✅ Cloudflare / CDN / 缓存配置排查
✅ 插件冲突与页面异常排查
✅ AdSense 广告显示问题排查
✅ SEO 基础结构检查
✅ 博客运营与商业化咨询
如需了解方案或交流相关问题,请直接联系我,并注明:WordPress 维护咨询。
联系方式:
Telegram:@shuijingwan
微信:13980074657
邮箱:shuijingwanwq@gmail.com


发表回复