本文约 4,175 字,预计阅读 11 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:你写的插件,中文站长装上去一句看不懂
很多开发者把插件提交到 WordPress.org 之后才发现:后台设置页、按钮文案、错误提示全是英文,中文站长装完一脸茫然,评论区第一条就是”有没有中文版”。更尴尬的是另一种情况——插件里塞了一份中文语言包,结果英文站点的用户也被强制显示中文,因为代码里直接写了中文字面量。
这两个问题分属两件事:国际化(i18n)是让代码”可被翻译”,本地化(l10n)是”真的翻译了并加载”。国内很多插件只做了后半段:把界面文案硬编码成中文,短期内中文用户爽了,但只要站点切到英文、或者要做外贸站多语言版本,就必须推倒重来。正确顺序是先 i18n 后 l10n,代价极低,收益却贯穿插件整个生命周期。
原理分析:WordPress 是怎么找到那句中文的
WordPress 的翻译机制本质是 GNU gettext 的一套封装,理解三个概念就够了:
- 文本域(Text Domain):每条待翻译字符串归属的”命名空间”。它必须是插件目录名的 slug 形式(小写、连字符),且要和插件头部声明完全一致。文本域写错是最常见的”翻译文件明明在却不生效”的原因。
- 语言包查找路径:从 WordPress 4.6 起引入了”即时加载(just-in-time translation)”,核心会根据插件头部的
Text Domain和Domain Path,自动去wp-content/languages/plugins/{text-domain}-{locale}.mo找文件。也就是说,显式调用load_plugin_textdomain()已经不是强制项,但为了兼容老版本和自定义目录,多数插件仍会保留。 - locale 标识:
get_locale()返回的形如zh_CN、en_US的字符串,决定加载哪一个.mo。后台”设置 → 常规 → 站点语言”改的就是它。
翻译函数不是一个,而是一族,选错就会带来转义或转义过度的问题:
| 场景 | 返回 | 直接输出 |
|---|---|---|
| 普通字符串 | __() |
_e() |
| 需要 HTML 转义后输出 | esc_html__() |
esc_html_e() |
| 放到 HTML 属性里 | esc_attr__() |
esc_attr_e() |
| 单复数 | _n() |
— |
| 带歧义上下文 | _x() |
_ex() |
规则很简单:只要结果会进 HTML,就用带 esc_ 的版本,别让翻译文件成为 XSS 入口。
保姆级实操
第一步:在插件头部声明文本域与路径
<?php
/**
* Plugin Name: 迷茫飞订单导出
* Plugin URI: https://mimangfei.com
* Description: 把 WooCommerce 订单导出为 CSV。
* Version: 1.0.0
* Author: mimangfei
* Text Domain: mimangfei-order-export
* Domain Path: /languages
* License: GPL-2.0-or-later
*/
Domain Path 指插件自带语言包的相对目录,通常叫 /languages。
第二步:所有面向用户的字符串包一层翻译函数
<?php
// 后台菜单
add_action( 'admin_menu', function () {
add_menu_page(
__( '订单导出', 'mimangfei-order-export' ),
__( '订单导出', 'mimangfei-order-export' ),
'manage_options',
'mfoe-export',
'mfoe_render_page'
);
} );
// 带占位符,务必用 %s / %d,不要在翻译里拼接变量
function mfoe_notice( $count ) {
printf(
/* translators: %d 是导出的订单数量 */
esc_html__( '本次共导出 %d 条订单。', 'mimangfei-order-export' ),
(int) $count
);
}
// 单复数
printf(
esc_html( _n( '%s 个商品', '%s 个商品', $total, 'mimangfei-order-export' ) ),
number_format_i18n( $total )
);
注意 /* translators: ... */ 注释,它会被提取工具带进 POT,是给译者看的上下文说明。
第三步:兼容旧版本,显式加载文本域
<?php
add_action( 'plugins_loaded', function () {
load_plugin_textdomain(
'mimangfei-order-export',
false,
dirname( plugin_basename( __FILE__ ) ) . '/languages'
);
} );
第四步:用 WP-CLI 提取 POT 模板
cd wp-content/plugins/mimangfei-order-export
wp i18n make-pot . languages/mimangfei-order-export.pot \
--slug=mimangfei-order-export \
--domain=mimangfei-order-export \
--exclude=vendor,node_modules,tests
第五步:从 POT 生成中文 po,再编译成 mo
# 复制模板为简体中文语言包
cp languages/mimangfei-order-export.pot languages/mimangfei-order-export-zh_CN.po
# 翻译完成后编译(需要系统装了 gettext)
msgfmt -o languages/mimangfei-order-export-zh_CN.mo \
languages/mimangfei-order-export-zh_CN.po
# WordPress 6.5+ 还可生成更快的 PHP 翻译文件
wp i18n make-php languages/
# 确认 locale 与文件名匹配
wp eval "echo get_locale();"
常见踩坑
- 文本域和插件目录名不一致。比如目录叫
mimangfei-order-export,文本域写成mfoe,即时加载就找不到文件。WordPress.org 的插件检查也会报这个错。 - 把变量拼进翻译字符串。
__( '共 ' . $n . ' 条' )在英语里语序一变就废。必须用%s/%d占位符。 - 忘了
esc_系列。翻译文件来自第三方时,__()直接 echo 等于给译者开了 HTML 权限。 - 只放了
.po没放.mo。WordPress 只读.mo(或 6.5+ 的.l10n.php),.po是源文件,放上去没用。 - 改了源码忘了重新提取。新增字符串后 POT 不更新,pot 里根本没有这条,译者无从下手。
- 案例:某插件作者把
Text Domain写成Mimangfei_Order_Export(带下划线大写),同时在代码里用小写连字符调用。本地测试时因为显式调用了load_plugin_textdomain()指向自己的/languages目录而正常显示中文;发布到官方目录后,官方语言包走的是即时加载路径wp-content/languages/plugins/mimangfei_order_export-zh_CN.mo,大小写与 slug 规范化后对不上,结果线上所有中文站点全部回落英文。修复只需统一为mimangfei-order-export,但已经积累的翻译文件命名也要一并改名。 - JS 里的字符串漏翻。块编辑器或前台脚本里的文案要用
wp_set_script_translations()注册,否则界面一半中文一半英文。
<?php
wp_enqueue_script(
'mfoe-block-editor',
plugins_url( 'build/index.js', __FILE__ ),
array( 'wp-i18n', 'wp-blocks' ),
'1.0.0',
true
);
wp_set_script_translations( 'mfoe-block-editor', 'mimangfei-order-export', plugin_dir_path( __FILE__ ) . 'languages' );
总结
判断一个插件国际化是否合格,只看三点:插件头部的 Text Domain 是否与目录 slug 完全一致;代码里是否所有用户可见字符串都走了翻译函数且带了正确的转义;languages 目录里是否有随源码同步更新的 .pot 以及编译好的 .mo。做到这三点,中文包可以由官方社区翻译,也可以自己用 Poedit 与 Loco Translate 汉化 维护,插件就具备了面向多语言站点的基础能力。
如果你只是想快速把别人的插件翻成中文,可以接着看 用 Loco Translate 汉化未翻译的插件;而插件要真正适配多语言站点,还需要配合 WordPress 多语言站点搭建方案 的整体规划。
