本文约 5,028 字,预计阅读 13 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:装了语言包,主题里一半文字还是中文
站点装了 WPML,文章、菜单、分类都翻成了英文,唯独主题模板里的”阅读全文””相关推荐””上一篇”还是中文。检查语言包文件确实存在,翻译条目也都填了,就是前台不生效。
原因通常出在两个地方:模板里输出硬编码中文根本没有经过翻译函数,或者翻译函数里的文本域(text domain)字符串与 style.css 里声明的不一致——多一个连字符就全部失效。
原理分析:文本域是翻译的”命名空间”
WordPress 的 i18n 基于 GNU gettext,核心机制是:
源码里的 __('原文', '文本域')
→ gettext 查表:{文本域}-{语言环境}.mo
→ 命中则返回译文,未命中则返回原文
文本域的作用是隔离:同一个词”Read More”在主题 A 和插件 B 里可以有不同的译法。它由 style.css 头部声明:
/*
Theme Name: 迷忙飞基础主题
Text Domain: mimangfei-basic
Domain Path: /languages
*/
规则很硬:文本域必须全小写、用连字符、与主题目录名一致(WordPress.org 的强制要求),且模板里写的字符串必须与它逐字符相同。
load_theme_textdomain() 负责把 .mo 文件挂到文本域上:
add_action( 'after_setup_theme', function () {
load_theme_textdomain( 'mimangfei-basic', get_template_directory() . '/languages' );
} );
不过从 WordPress 4.6 起,核心支持 just-in-time(JIT)翻译加载:只要主题声明了 Text Domain,并且语言文件放在 wp-content/languages/themes/{文本域}-{locale}.mo,核心会在第一次用到该文本域时自动加载,不必手写 load_theme_textdomain()。自带 /languages 目录的主题才需要显式调用。
函数家族的选择是第二层难点:
| 函数 | 行为 | 使用场景 |
|---|---|---|
__() |
返回翻译结果 | 需要拼接、赋值时 |
_e() |
直接 echo 翻译结果 | 模板里直接输出 |
_x() / _ex() |
带上下文消歧(返回 / 输出) | 同一个词在不同位置含义不同 |
_n() |
单复数(返回) | 数量变化 |
esc_html__() / esc_html_e() |
翻译 + HTML 转义 | 输出到 HTML 文本位置 |
esc_attr__() / esc_attr_e() |
翻译 + 属性转义 | 输出到 HTML 属性里 |
_n_noop() |
延迟翻译的复数 | 数量稍后才知道 |
保姆级实操
第 1 步:声明文本域并统一命名
/* style.css 头部(目录名为 mimangfei-basic) */
Text Domain: mimangfei-basic
Domain Path: /languages
检查全项目是否混用了其他文本域:
# 列出代码里出现过的所有文本域
grep -rhoE "'(__|_e|_x|_n|esc_html__|esc_html_e|esc_attr__|esc_attr_e)\([^,]+,\s*'[^']+'" . | grep -oE "'[^']+'$" | sort | uniq -c | sort -rn
第 2 步:模板里全部改用翻译函数
<!-- 错误:硬编码,无法翻译 -->
<a href="<?php the_permalink(); ?>">阅读全文</a>
<!-- 正确 -->
<a href="<?php the_permalink(); ?>"><?php esc_html_e( '阅读全文', 'mimangfei-basic' ); ?></a>
需要拼接时必须用带占位符的写法,不能把字符串切断:
// 错误:把句子拆开,翻译者看不到完整语义
echo esc_html__( '发布于 ', 'mimangfei-basic' ) . get_the_date();
// 正确:用占位符 + sprintf
printf(
/* translators: %s 是文章发布日期 */
esc_html__( '发布于 %s', 'mimangfei-basic' ),
esc_html( get_the_date() )
);
/* translators: ... */ 注释会被 wp i18n make-pot 提取到 .pot 文件里,翻译者能看到上下文,这是专业做法。
第 3 步:属性位置用 esc_attr 系列
printf(
'<input type="search" class="search-field" placeholder="%s" value="%s" name="s">',
esc_attr__( '搜索…', 'mimangfei-basic' ),
esc_attr( get_search_query() )
);
printf(
'<a class="more-link" href="%1$s" aria-label="%2$s">%3$s</a>',
esc_url( get_permalink() ),
/* translators: %s 是文章标题 */
esc_attr( sprintf( __( '继续阅读 %s', 'mimangfei-basic' ), get_the_title() ) ),
esc_html__( '阅读全文', 'mimangfei-basic' )
);
第 4 步:歧义词用 _x()、数量用 _n()
// "Post" 既可指"文章"也可指"发布",用上下文区分
$label = _x( 'Post', 'noun: a blog article', 'mimangfei-basic' );
$verb = _x( 'Post', 'verb: to publish', 'mimangfei-basic' );
// 评论数量:单复数由第一个参数决定
printf(
/* translators: %s 是评论数量 */
esc_html( _n( '%s 条评论', '%s 条评论', (int) get_comments_number(), 'mimangfei-basic' ) ),
esc_html( number_format_i18n( get_comments_number() ) )
);
number_format_i18n() 会按站点语言输出千分位,别用 PHP 原生 number_format()。
第 5 步:生成 pot 并编译 mo
# 安装 WP-CLI 的 i18n 命令包
wp package install wp-cli/i18n-command
# 扫描主题生成模板文件
wp i18n make-pot . languages/mimangfei-basic.pot --domain=mimangfei-basic --exclude=node_modules,vendor
# 用 Poedit 打开 pot 生成 zh_CN.po / en_US.po,编译出同名 .mo
文件命名规则是 {文本域}-{语言环境}.mo,例如 mimangfei-basic-zh_CN.mo、mimangfei-basic-en_US.mo。语言环境代码要与 wp option get WPLANG 的返回值匹配。
第 6 步:JS 里的翻译(可选但推荐)
wp_enqueue_script( 'mmf-main', get_stylesheet_directory_uri() . '/assets/js/main.js', array(), '1.0.0', true );
wp_set_script_translations( 'mmf-main', 'mimangfei-basic', get_template_directory() . '/languages' );
JS 侧用 wp.i18n:
import { __, _n, sprintf } from '@wordpress/i18n';
const label = __( '加载更多', 'mimangfei-basic' );
const tip = sprintf( _n( '%d 条结果', '%d 条结果', count, 'mimangfei-basic' ), count );
JS 的 .json 翻译文件同样用 wp i18n make-pot 相关流程生成(配合 make-json 子命令)。
常见踩坑
- 文本域不一致。
style.css写mimangfei-basic,代码里写mimangfei_basic或Mimangfei-Basic,翻译静默失效,无任何报错。 - 用变量当文本域或原文。
__($text, $domain)这种写法 gettext 扫描工具完全提取不到,make-pot扫不出来。 - 把 HTML 标签写进待翻译字符串。
__('<strong>注意</strong>', '...')让翻译者被迫处理标记,应改成__('注意', '...')外面套<strong>。 - 翻译结果没转义。
_e()只翻译不转义,用户能控制的内容必须走esc_html_e(),转义规范详见输出转义规范 esc_html esc_attr esc_url。 .mo文件命名错了。zh_CN.mo不带文本域前缀,核心找不到;必须是{文本域}-zh_CN.mo。- 案例: 某主题开发者把
Text Domain写成了主题显示名”迷忙飞基础主题”,模板里统一用'mimangfei-basic'。本地测试一切正常(因为本地从来没加载过语言包,全部返回原文),上线英文站后所有模板文字依旧是中文,而语言包文件检查了一遍又一遍。最后用wp i18n make-pot重新生成才发现pot文件的X-Domain头是空的。教训是:国际化一定要”先生成 pot 再验证”,不能靠肉眼看代码。 load_theme_textdomain()挂在init上。 太晚,after_setup_theme之后到init之间若有字符串被翻译,就会漏掉。钩子时机的选择与add_theme_support 该声明哪些能力里讲的是同一套逻辑。
总结
国际化的三条铁律:一、文本域全小写、与目录名一致、全项目统一;二、模板里不留任何硬编码字符串,输出用 _e 系列、拼接用 __ + sprintf 占位符、属性用 esc_attr__、数量用 _n;三、用 wp i18n make-pot 生成 pot 并确认 X-Domain 头正确,再交给 Poedit 编译 mo。验证方式不是看代码,而是切一次站点语言看前台——切换后仍有中文,说明某处漏了翻译函数或文本域写错。
