搜索全站
主题 13 分钟阅读

WordPress 主题国际化:文本域与 __() _e() 正确用法

主题里的中文为什么翻不成英文?讲清文本域声明规则、__/_e/_x/_n/esc 系列函数的选择、变量与复数处理,以及用 WP-CLI 生成 pot 与编译 mo 的完整流程。

U
UFO_PF 2025-11-28 · 阅读 13 分钟
Wo 主题
阅读提示
本文约 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 子命令)。

常见踩坑

  1. 文本域不一致。 style.css 写 mimangfei-basic,代码里写 mimangfei_basic 或 Mimangfei-Basic,翻译静默失效,无任何报错。
  2. 用变量当文本域或原文。 __($text, $domain) 这种写法 gettext 扫描工具完全提取不到,make-pot 扫不出来。
  3. 把 HTML 标签写进待翻译字符串。 __('<strong>注意</strong>', '...') 让翻译者被迫处理标记,应改成 __('注意', '...') 外面套 <strong>。
  4. 翻译结果没转义。 _e() 只翻译不转义,用户能控制的内容必须走 esc_html_e(),转义规范详见输出转义规范 esc_html esc_attr esc_url。
  5. .mo 文件命名错了。 zh_CN.mo 不带文本域前缀,核心找不到;必须是 {文本域}-zh_CN.mo。
  6. 案例: 某主题开发者把 Text Domain 写成了主题显示名”迷忙飞基础主题”,模板里统一用 'mimangfei-basic'。本地测试一切正常(因为本地从来没加载过语言包,全部返回原文),上线英文站后所有模板文字依旧是中文,而语言包文件检查了一遍又一遍。最后用 wp i18n make-pot 重新生成才发现 pot 文件的 X-Domain 头是空的。教训是:国际化一定要”先生成 pot 再验证”,不能靠肉眼看代码。
  7. 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。验证方式不是看代码,而是切一次站点语言看前台——切换后仍有中文,说明某处漏了翻译函数或文本域写错。

U
UFO_PF作者

专注 WordPress 生态的实战作者,内容以「可复制、可验证」为第一原则。

留下你的想法

邮箱地址不会被公开。带 * 的为必填项。