博客标签翻译实操|8060个标签,基于 PHP 脚本实现全流程
前言
在前面的系列文章中,我已经完成了分类的多语言适配,而今天我要解决的,是整个多语言化中最繁琐的一环:8060个中文标签的批量翻译。
手动给8000多个标签一个个加翻译?这显然不现实,点完这8000个按钮,我怕是要直接退休。而 Polylang 官方并没有提供这种“把中文标签原样复制一份当英文标签”的批量功能,所以我只能自己动手,写个脚本搞定这件事。
而这整个过程,我踩了无数的坑,从最开始的改数据库不生效,到一步步排查 Polylang 的存储细节,最终搞定了这个通用的批量脚本,今天就把整个过程完完整整分享给大家。
一、Polylang 的标签存储原理:先搞懂它是怎么存的
在动手之前,我首先要搞懂:Polylang 是怎么给标签存语言和翻译的?
和很多插件自己建表不一样,Polylang 非常巧妙的复用了 WordPress 原生的分类系统,用三个隐藏的分类来管理语言和翻译:
1. language :文章的语言分类,用来给文章标记语言
SELECT term_taxonomy_id, term_id FROM wp_term_taxonomy WHERE taxonomy = 'language'
查询结果如图9

2. term_language :标签的语言分类,用来给标签标记语言
SELECT term_taxonomy_id, term_id FROM wp_term_taxonomy WHERE taxonomy = 'term_language'
查询结果如图8

3. term_translations :翻译组分类,用来把不同语言的同一个标签,绑定成一组
而这三个分类,都是通过 WordPress 原生的 wp_term_relationships 表来和标签做关联的——简单来说,标签本身,就像一篇文章一样,要给自己加分类,来标记自己的语言,以及自己属于哪个翻译组。
这就是我最开始踩坑的根源:我以为只要创建个标签就好了,完全忘了标签还要给自己加这些隐藏的分类!
二、最开始的踩坑:改完数据库,翻译竟然不生效?
最开始我想的很简单:不就是创建个英文标签,然后绑定翻译吗?直接改数据库不就好了?
于是我写了个最简单的脚本,往 wp_terms 和 wp_term_taxonomy 里插了数据(如图1),然后就去后台看,结果发现:

> 中文后台编辑标签的时候,English 的翻译栏是空的,英文后台的标签列表里,也看不到我刚加的标签!如图6

如图5,就是当时的中文编辑页面,明明我已经加了英文标签,但是翻译栏就是空的。

我当时就懵了,所有的字段都对了啊,为什么不生效?
三、排查:对比手动添加的标签,找到差异
既然手动添加的标签是好的,那我就把手动添加的标签的所有数据库数据导出来,和我脚本加的做对比,总能找到差异吧?
我选了我之前手动加的 验证规则 和 Validation rules 这两个标签,执行了这个SQL,把它们的关联数据查出来:
SELECT
*
FROM
wp_terms
ORDER BY
term_id DESC
LIMIT
20;
查询结果如图2

SELECT
*
FROM
wp_term_taxonomy
ORDER BY
term_taxonomy_id DESC
LIMIT
20;
查询结果如图3

SELECT * FROM wp_term_relationships WHERE object_id IN (1715, 19000)
结果出来之后,我瞬间就明白了!如图10,就是这个查询的结果:

object_id term_taxonomy_id term_order
1715 8836 0
1715 8837 0
1715 19001 0
19000 8840 0
19000 19001 0
哦!原来手动的中文标签,有三个关联!
1. 8836 : language 分类的中文ID,给标签加文章语言的分类
2. 8837 : term_language 分类的中文ID,给标签加标签语言的分类
3. 19001 : term_translations 分类的翻译组ID,把两个标签绑定成一组
而我最开始的脚本,只加了标签本身,这三个关联一个都没加!Polylang 根本不知道这个新标签的语言是什么,也不知道它和中文标签是翻译关系!
四、一步步补全关联:还是不对?
找到问题之后,我就开始一个个补关联:
1. 给英文标签加 term_language 的关联 如图12

INSERT INTO wp_term_relationships VALUES(19002,8840,0);
意思就是:给英文标签19002,贴一个 “我是英文的标签” 的标签,标签的 ID 是8840(就是我之前查到的term_language分类里的英文分类 ID)。Polylang 看到这个标签,就知道:哦,这个标签是英文的。
2. 给两个标签都加 term_translations 的关联 如图11 如图12


INSERT INTO wp_term_relationships VALUES (8835,19003,0);
INSERT INTO wp_term_relationships VALUES(19002,19003,0);
意思就是:给中文标签8835和英文标签19002,都贴同一个 “我俩是翻译组的,是同一个东西的不同语言” 的标签,标签的 ID 是19003(就是我创建的翻译组的 ID)。Polylang 看到两个标签都贴了同一个翻译组的标签,就知道:哦,这俩是翻译关系!
3. 给中文标签加 language 的关联 如图13

INSERT INTO wp_term_relationships VALUES (8835,8836,0);
意思就是:给中文标签8835,贴一个 “我是中文的文章语言” 的标签,标签的 ID 是8836(就是我之前查到的language分类里的中文分类 ID)。Polylang 看到这个标签,就知道:哦,这个标签是中文的,要在中文后台显示。
补完之后,我以为好了,结果去后台看,还是不对!中文编辑的时候,还是看不到英文的翻译!
明明所有的关联都加了,但是还是不生效。
我当时就懵了,所有的字段都和手动的一模一样了啊,为什么还是不对?
五、放弃手动改库:用 Polylang 的官方函数!
我突然反应过来:我干嘛要自己一个个改数据库啊?手动添加的时候,Polylang 自己有函数来做这件事啊!我直接调用它的函数不就好了?不管有什么隐藏的细节,它自己的函数肯定能处理啊!
Polylang 的核心 Model 里,有两个专门处理标签的函数:
1. $polylang->model->term->set_language :给标签设置语言
2. $polylang->model->term->save_translations :给标签绑定翻译,就是你后台点击“添加翻译”的时候,调用的同一个函数!
哦!对啊!我直接用这两个函数不就好了?不管有什么隐藏的数据库细节,它自己的函数肯定能处理所有的事情,就和我手动点击后台的按钮一模一样!
于是我写了个测试脚本,用这两个函数来绑定翻译。
test-mouse-tag.php
<?php
if (php_sapi_name() !== 'cli') {
die("❌ 请在命令行运行\n");
}
require __DIR__ . '/wp-config.php';
global $wpdb, $polylang;
// 要处理的新标签:鼠标,ID=2450
$zh_id = 2450;
$name = '鼠标';
$slug = '%e9%bc%a0%e6%a0%87';
echo "🔰 开始处理新标签:{$name},ID: {$zh_id}\n\n";
// 1. 先清理这个标签可能存在的旧英文数据
$old_en_id = pll_get_term($zh_id, 'en');
if ($old_en_id) {
wp_delete_term($old_en_id, 'post_tag');
echo "🗑️ 已清理旧的英文标签,ID: {$old_en_id}\n";
}
echo "🗑️ 已清理所有旧数据\n\n";
// 2. 直接插数据库创建英文标签,绕过WordPress的slug重复检查
$wpdb->insert($wpdb->terms, [
'name' => $name,
'slug' => $slug,
'term_group' => 0
]);
$en_id = $wpdb->insert_id;
echo "ℹ️ 已创建英文标签,ID: {$en_id}\n";
// 3. 英文标签的 post_tag
$wpdb->insert($wpdb->term_taxonomy, [
'term_id' => $en_id,
'taxonomy' => 'post_tag',
'description' => '',
'parent' => 0,
'count' => 0
]);
// 4. 给英文标签设置语言,用Polylang的内部方法
$polylang->model->term->set_language($en_id, 'en');
echo "ℹ️ 已给英文标签设置语言为英文\n";
// 5. 绑定翻译!用Polylang的官方函数!和你手动点击一模一样!
$polylang->model->term->save_translations($zh_id, [
'en' => $en_id
]);
echo "ℹ️ 已用Polylang官方函数绑定翻译\n";
echo "\n✅ 全部完成!\n";
echo " 中文ID: {$zh_id}\n";
echo " 英文ID: {$en_id}\n";
echo " 已经用Polylang官方函数绑定了翻译,和手动添加完全一样!\n\n";
echo "🎉 现在刷新后台,所有的关联都已经正常了!\n";
然后我用 鼠标 这个标签来测试,跑完之后,我去后台看:
> 中文后台编辑 鼠标 的时候,English 的翻译栏自动出现了(如图15)!

英文后台的标签列表里,也能看到 鼠标 这个标签了!(如图16)

终于!成了!
六、批量处理的坑:内存不够了?
测试单个标签没问题了,那我就想批量处理所有的标签,于是我写了个脚本,一次把所有的中文标签都拿出来,然后循环处理。
polylang-batch-zh-to-en-tags.php
<?php
if (php_sapi_name() !== 'cli') {
die("❌ 请在命令行运行\n");
}
require __DIR__ . '/wp-config.php';
global $wpdb, $polylang;
echo "🚀 开始批量处理所有中文标签...\n\n";
// 1. 直接拿到所有的中文post_tag标签,Polylang自动帮我过滤!
$zh_terms = get_terms([
'taxonomy' => 'post_tag',
'lang' => 'zh',
'hide_empty' => false,
'number' => 0, // 拿所有的
]);
if (is_wp_error($zh_terms) || empty($zh_terms)) {
die("❌ 没有找到中文标签\n");
}
$total = count($zh_terms);
$processed = 0;
$skipped = 0;
echo "📊 共找到 {$total} 个中文标签,开始处理...\n\n";
// 2. 循环处理每个标签
foreach ($zh_terms as $term) {
$zh_id = $term->term_id;
$name = $term->name;
$slug = $term->slug;
// 检查是不是已经有英文翻译了,有的话直接跳过
$en_id = pll_get_term($zh_id, 'en');
if ($en_id) {
$skipped++;
continue;
}
echo "🔰 正在处理: {$name} (ID: {$zh_id})\n";
// 3. 创建英文标签,绕过slug重复检查
$wpdb->insert($wpdb->terms, [
'name' => $name,
'slug' => $slug,
'term_group' => 0
]);
$new_en_id = $wpdb->insert_id;
// 4. 英文标签的post_taxonomy
$wpdb->insert($wpdb->term_taxonomy, [
'term_id' => $new_en_id,
'taxonomy' => 'post_tag',
'description' => '',
'parent' => 0,
'count' => 0
]);
// 5. 设置英文标签的语言
$polylang->model->term->set_language($new_en_id, 'en');
// 6. 绑定翻译
$polylang->model->term->save_translations($zh_id, [
'en' => $new_en_id
]);
$processed++;
}
echo "\n🎉 全部处理完成!\n";
echo "📊 统计:\n";
echo " 总中文标签: {$total}\n";
echo " 已处理新标签: {$processed}\n";
echo " 已跳过已有翻译: {$skipped}\n";
echo "\n✅ 所有标签都已经处理完毕,和手动添加的完全一样!现在刷新后台,所有的翻译都已经正常了!\n";
结果刚跑了一半,就报错了:
PHP Fatal error: Allowed memory size of 134217728 bytes exhausted
这是一个内存报错,8000个标签一次加载,直接把内存爆了。
哦,对,我的服务器只有128M的PHP内存,一次加载8000个标签,确实扛不住。那怎么办?
七、分页处理:解决内存问题
很简单,我改成分页处理,一次只拿1000个标签,处理完就释放内存,这样就不会爆内存了!
分页逻辑,每次拿1000个,处理完,然后把这1000个的内存释放掉,再拿下一页,这样不管有多少个标签,内存都够。
polylang-batch-zh-to-en-tags.php
<?php
if (php_sapi_name() !== 'cli') {
die("❌ 请在命令行运行\n");
}
require __DIR__ . '/wp-config.php';
global $wpdb, $polylang;
// 配置:一次处理多少个,你可以根据自己的服务器调整
$per_page = 1000;
// 配置:源语言和目标语言,以后其他语言直接改这里就好
$source_lang = 'zh';
$target_lang = 'en';
echo "🚀 开始批量处理所有{$source_lang}标签,自动添加{$target_lang}翻译(分页模式)...\n\n";
$processed = 0;
$skipped = 0;
$offset = 0;
while (true) {
// 分页拿源语言标签
$terms = get_terms([
'taxonomy' => 'post_tag',
'lang' => $source_lang,
'hide_empty' => false,
'number' => $per_page,
'offset' => $offset,
]);
if (is_wp_error($terms) || empty($terms)) {
break;
}
$current_count = count($terms);
echo "📊 正在处理第 " . ($offset + 1) . " - " . ($offset + $current_count) . " 个标签...\n";
foreach ($terms as $term) {
$source_id = $term->term_id;
$name = $term->name;
$slug = $term->slug;
// 检查是不是已经有目标语言的翻译了
$target_id = pll_get_term($source_id, $target_lang);
if ($target_id) {
$skipped++;
continue;
}
echo "🔰 正在处理: {$name} (ID: {$source_id})\n";
// 创建目标语言标签,绕过slug重复检查
$wpdb->insert($wpdb->terms, [
'name' => $name,
'slug' => $slug,
'term_group' => 0
]);
$new_target_id = $wpdb->insert_id;
// 目标语言标签的taxonomy
$wpdb->insert($wpdb->term_taxonomy, [
'term_id' => $new_target_id,
'taxonomy' => 'post_tag',
'description' => '',
'parent' => 0,
'count' => 0
]);
// 设置目标语言
$polylang->model->term->set_language($new_target_id, $target_lang);
// 绑定翻译
$polylang->model->term->save_translations($source_id, [
$target_lang => $new_target_id
]);
$processed++;
}
$offset += $per_page;
unset($terms);
gc_collect_cycles();
}
echo "\n🎉 全部处理完成!\n";
echo "📊 统计:\n";
echo " 已处理新标签: {$processed}\n";
echo " 已跳过已有翻译: {$skipped}\n";
echo "\n✅ 所有标签都已经处理完毕,和手动添加的完全一样!\n";
🔰 正在处理: 黑屏 (ID: 2172)
🔰 正在处理: 默认 USB 配置 (ID: 8071)
🔰 正在处理: 默认信任 (ID: 8214)
🔰 正在处理: 默认值 (ID: 1342)
🔰 正在处理: 默认时区 (ID: 5445)
🔰 正在处理: 默认浏览器 (ID: 2288)
🔰 正在处理: 默认角色 (ID: 8240)
🔰 正在处理: 默认输入法 (ID: 1666)
🔰 正在处理: 鼠标光标 (ID: 7423)
🔰 正在处理: ,;; (ID: 8233)
🎉 全部处理完成!
📊 统计:
已处理新标签: 2941
已跳过已有翻译: 5119
✅ 所有标签都已经处理完毕,和手动添加的完全一样!
最后分别查看中文与英文后台下的标签统计,符合预期。如图17

八、通用化:以后其他语言也能用
搞定了中文转英文之后,我想,以后我要是要加其他语言,比如法语、德语,是不是还要重新写脚本?
当然不用!我把脚本改成了通用的,只要改最上面的两个参数,就能处理任意语言的翻译:
// 配置:一次处理多少个,你可以根据自己的服务器调整
$per_page = 1000;
// 配置:源语言和目标语言,以后其他语言直接改这里就好
$source_lang = 'zh';
$target_lang = 'en';
这个通用的配置,以后不管你要把什么语言的标签,翻译成什么语言,只要改这两个参数,直接运行就好了,完全不用改其他的!
九、全流程的表结构分析:搞懂每个表的作用
到这里,我把整个流程的所有表都搞清楚了,今天就把所有的表的作用都分享给大家,以后你自己改的时候,就不会搞错了:
1. wp_terms:标签的基础信息
这个表存的是标签的最基础的信息,不管是中文还是英文的标签,都存在这里:
term_id | name | slug | term_group
– name :标签的名称,我这里直接用中文的
– slug :标签的别名,我也直接用中文的url编码后的结果
如图2,就是这个表的示例数据。

2. wp_term_taxonomy:标签的分类信息
这个表存的是标签的分类信息,用来标记这个标签是 post_tag ,还是 term_language ,还是 term_translations :
term_taxonomy_id | term_id | taxonomy | description | parent | count
– taxonomy :分类类型,我的标签就是 post_tag ,翻译组就是 term_translations
– description :翻译组的话,这里存的是序列化的翻译关系,比如 a:2:{s:2:”en”;i:19002;s:2:”zh”;i:8835;}
如图3,就是这个表的示例数据。

3. wp_term_relationships:关联信息
这个表是最核心的,所有的关联都存在这里:
object_id | term_taxonomy_id | term_order
– 中文标签,要关联三个: language 、 term_language 、 term_translations
– 英文标签,要关联两个: term_language 、 term_translations
如图10,就是这个表的示例数据,和手动的完全一模一样。

十、排查过程的SQL分析:我是怎么找到问题的
整个排查过程,我用了很多SQL来分析数据,今天也把这些SQL分享给大家,以后你自己排查的时候,也能用到:
1. 查term_language的ID
SELECT term_taxonomy_id, term_id FROM wp_term_taxonomy WHERE taxonomy = 'term_language'
这个SQL用来查你的语言对应的term_language的ID,我的中文是8837,英文是8840。
2. 查标签的所有关联
SELECT * FROM wp_term_relationships WHERE object_id = 你的标签ID
这个SQL用来查你的标签的所有关联,看看有没有漏掉的
3. 查标签有没有翻译
SELECT pll_get_term(你的中文标签ID, 'en')
这个SQL用来查你的中文标签有没有英文翻译,有的话就跳过
4. 对比手动和脚本的关联
-- 手动的
SELECT * FROM wp_term_relationships WHERE object_id IN (1715, 19000)
-- 脚本的
SELECT * FROM wp_term_relationships WHERE object_id IN (8835, 19002)
这个就是我最开始排查的时候用的,对比两个的差异
十一、踩坑总结:我踩过的那些坑
整个过程,我踩了无数的坑,今天也分享给大家,避免你再踩:
1. 漏掉了 wp_term_relationships 的关联
最开始我只创建了标签,忘了给标签加隐藏的分类关联,导致 Polylang 识别不了标签的语言。
2. 内存不够,一次加载所有标签
一次加载8000个标签,直接把内存爆了,后来改成分页就好了。
3. WordPress 的 slug 重复检查
WordPress 默认同一个分类下的 slug 不能重复,所以我要绕过这个检查,直接插数据库,才能创建两个一样的 slug 的标签。
十二、最终效果:8060个标签,几分钟搞定
最终,我跑完了整个脚本,8060个中文标签,全部自动生成了对应的英文标签,所有的翻译关联都正常,中英文后台都能正常显示,编辑的时候翻译也都正常。
最终的统计结果:
🔰 正在处理: 鼠标光标 (ID: 7423)
🔰 正在处理: ,;; (ID: 8233)
🎉 全部处理完成!
📊 统计:
已处理新标签: 2941
已跳过已有翻译: 5119
✅ 所有标签都已经处理完毕,和手动添加的完全一样!
8000多个标签,只用了不到5分钟就跑完了,比手动加快了不知道多少倍!
十三、针对英文语言下的中文标签:为什么我选择原样复制?
很多朋友可能会好奇:为什么你不把中文标签翻译成真正的英文?比如把`鼠标`翻译成`Mouse`,把`管道`翻译成`Channel`?其实最开始我也考虑过自动翻译标签名称,但是仔细权衡之后,我最终决定:直接把中文标签原样复制一份,作为英文语言下的标签,不做翻译,原因有两个:
1. 避免重复标签,搞乱我的标签体系
我之前已经手动给一些常用的技术标签做了真正的英文翻译,比如`管道`这个标签,我已经手动把它翻译成了`Channel`,如果我用自动翻译把所有中文标签都翻译成英文,就会在英文后台再生成一个全新的`Channel`标签,导致英文后台出现两个一模一样的`Channel`标签,一个是我手动翻译的,一个是自动生成的,直接把我整个标签体系搞乱了。
而如果我原样复制中文标签,就不会有这个问题,自动生成的英文标签的slug和name都是中文的,和我手动翻译的英文标签完全不会冲突。
2. 匹配博客的截图标签,避免用户混淆
我的博客里有大量的技术实操截图,这些截图都是我中文环境下的操作界面,里面的按钮、标签、菜单都是中文的,我写文章的时候,给这些文章加的标签,也都是对应的中文标签。
如果我把标签翻译成英文,那英文用户看文章的时候,文章的标签是英文的,但是截图里的标签是中文的,用户会完全搞混:“为什么文章标签是Channel,截图里的是管道?这俩是同一个东西吗?”
而原样保留中文标签的话,就完全不会有这个问题,标签和截图里的内容完全对应,用户一眼就能对上。
所以最终,我选择了这种最适合我博客的方案:把中文标签原样复制一份,作为英文语言下的标签,既不会搞乱我的标签体系,也能和截图完美匹配。
最后:一点小遗憾
其实到最后,我还是没能完全搞明白:Polylang 的 save_translations 函数,到底在我手动补完所有关联之外,还偷偷加了什么隐藏的细节——明明我手动加完了所有的表、所有的关联,所有的字段都和手动的一模一样,但是就是不生效,而调用它的函数,就一切正常了。
这算是整个过程里的一点小遗憾,由于时间关系,我没能把 Polylang 的所有隐藏细节100%扒出来,但是没关系(既然问题已经解决,那么细节全部挖掘的必要性也不大了),这也给了我一个教训:永远不要试图自己模拟插件的数据库操作,直接用它自己的函数,才是最稳的。
不管插件有什么隐藏的细节,它自己的函数肯定能处理所有的事情,就和你手动点击后台的按钮一模一样,再也不用自己一个个踩坑了。
不过不管怎么说,最终我还是搞定了这8060个标签的批量翻译,整个过程虽然踩了很多坑,但是最终得到了这个通用的工具,以后不管加什么语言的标签,只要改两个参数就搞定了,这就够了~