做跨境电商独立站的人,几乎早晚都会撞上一个问题:WooCommerce 结账页面上的英文文案,看着哪儿都不顺眼。“Billing details”“Place order”“Apply coupon”这些词,放在国外用户面前可能没问题,但如果你做的是一站卖多国的生意,或者你的目标客户本来就说中文,那结账页面的翻译和自定义就不是“锦上添花”,而是直接影响下单转化率的事。我见过不少店铺,产品页、购物车都处理得不错,一到结账这一关就露馅——语言混杂、术语不一致、按钮文字过长把布局挤乱,用户填到一半直接关掉页面。这篇东西,就是想把“自定义 WooCommerce 结账页面翻译”这件事从头到尾讲透:文字从哪来、有哪些改法、实操时怎么写代码、以及我踩过的那些坑。
1. 先定位:结账页面的文字到底从哪来
1.1 结账页面的文本来源清单
很多人一上来就搜“怎么改 WooCommerce 结账页面文字”,然后找到一个 function 塞进子主题,结果发现有的文字改了,有的纹丝不动。原因很简单:结账页面从来不是一个单一文件渲染出来的,而是由多个来源拼装而成,每个来源的改法都不一样。
最常见的来源有四个:
- WooCommerce 核心插件自带的字符串。比如“Billing details”“Shipping details”“Place order”“Apply coupon”这类,来自 WooCommerce 插件的语言文件,里面的文本通过
__()、_e()、_x()这类 WordPress 翻译函数输出,所以理论上可以用 gettext 机制统一拦截。 - 当前主题输出的文案。比如主题可能在结账区域加了自定义标题、提示文字、图标旁边的说明,这些文本属于主题的 text domain,和 WooCommerce 的 text domain 不是一个域,拦截的时候要区分。
- 第三方插件输出的文案。支付网关、物流插件、优惠券插件、地址自动补全插件都会在结账页插入自己的文本。有的插件规范地走翻译函数,有的则直接在模板里写死,甚至通过 JS 动态插入,处理难度差别很大。
- JavaScript 渲染的文本。新版 WooCommerce 的结账区块(Checkout Block)大量使用 React 在前端渲染界面,很多字符串根本没经过 PHP 翻译函数,而是打包在 JS 文件里。这部分用传统的 gettext filter 基本拦不到。
这还只是静态部分。动态内容也要考虑进去,比如订单金额、运费选项、优惠券剩余次数、库存提示,这些文本通常由变量拼装,翻译时还要保留 %s、%d 占位符的位置。
1.2 为什么“直接改模板”是一个大坑
我见过不少开发者,包括刚入行的自由职业者,遇到“结账页文案不对”第一反应是去改主题目录下的 form-checkout.php、review-order.php,或者直接编辑 WooCommerce 插件里的模板文件。短期看确实快,文本立刻变成你想要的中文、日文、西班牙文,但长期看几乎必然出事。
第一是升级覆盖问题。WooCommerce 每个版本都在微调结账逻辑,主题也可能更新模板文件,你手工改动过的文件在下一次更新时要么被直接覆盖,要么出现冲突提示,最后要么保留一个旧版本错过修复,要么丢失所有自定义内容。第二是改动分散。十几个模板文件里各藏一句硬编码文本,后面想统一改术语,得满目录搜,改漏一句就是一处 bug。第三是逻辑和展示耦合。你只是在“展示层”替换了文字,但字段校验信息、错误提示、无障碍标签里的文案还是英文,用户填错了邮箱,看到的依然是英文报错,体验依然断裂。
所以我现在的原则是:模板文件尽量一个不动,能走语言文件就走语言文件,能走过滤器就走过滤器,实在走不通才用前端替换兜底。这也是这篇文章后面所有内容的大前提。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 选路线:四种自定义翻译方案对比
2.1 用 Loco Translate / Poedit 维护语言文件
如果你的需求是把一套完整的语言包变成自己的双语版本,最主流的方式是维护 .po / .mo 翻译文件。.po 是给人类编辑的文本文件,记录英文原文和对应译文的配对;.mo 是编译后的二进制文件,PHP 运行时读取速度更快。
实际操作中,我推荐在后台安装 Loco Translate 插件,它可以图形化管理主题和插件的翻译文件。选择 WooCommerce 后,点“新建语言”,选择简体中文,Loco 会在 wp-content/languages/loco/plugins/ 下生成一套覆盖用的翻译文件。这套文件不会被官方升级覆盖,因为 WordPress 加载语言文件的顺序里,loco 目录下的翻译优先级高于默认语言目录,所以即使 WooCommerce 更新了官方中文包,你的自定义译文依然能压住它。
Poedit 是另一个选择,适合本地开发流程。你先通过 FTP 把 WooCommerce 源码拉到本地,用 Poedit 的“从源代码提取字符串”功能扫描出全部待翻译文本,然后一条条翻译,最后把 .po、.mo 传到服务器。Poedit 的优势是批量操作顺手,能看到字符串出现的上下文,适合翻译量大的项目;劣势是每次升级都要重新扫描,新字符串要手工筛查,更新流程比 Loco Translate 慢。
提示:用 Loco Translate 时,一定要确认服务器上
wp-content/languages/loco目录有写权限。很多虚拟主机的默认权限是只读,导致你保存翻译时报错,还以为插件坏了。
2.2 用多语言插件的字符串翻译功能
如果你的目标不止是把结账页改成中文,而是要做一个真正的多语言独立站,那 WPML、Polylang、TranslatePress 这类多语言插件会是更完整的方案。它们能做的不只是翻译结账字段,还包括文章、商品、分类页、导航菜单、小工具,以及全站所有 gettext 字符串的统一管理。
WPML 的 String Translation 模块适合覆盖 WooCommerce 这种动态字符串较多的场景。它会扫描主题和插件里通过翻译函数输出的文本,然后在后台给你一个列表,你可以在“字符串翻译”面板里逐条覆盖。Polylang 免费版在这块能力比较弱,通常只能翻译固定内容,字符串翻译放在付费版里。TranslatePress 的思路不同,它是可视化翻译:你切换到目标语言,直接在页面上点击任意文字进行修改。对结账页面这种布局敏感的区域,可视化编辑确实直观,不用和代码打交道。
但要注意,多语言插件不是万能的。它解决的是“同一套逻辑产生不同语言”的问题,如果你的需求是“保留英文原文本,只在特定条件下换一种表达方式”,或者“某个硬编码在 JS 里的文本没有接口”,多语言插件同样会卡住。而且多语言插件本身会给结账页面增加脚本和样式,对性能有轻微影响,需要开缓存时特别注意。
2.3 用代码过滤器做精准翻译
第三种路线,也是我最喜欢用的路线,是用 WordPress 提供的过滤器(filter)在运行时改写字符串。它的最大优势是精准:可以只改结账页,只改某一个域名下的某一句文本,其他页面一概不动;可以判断语言、判断用户角色、判断商品类型,按需输出不同文案。
核心是几个钩子:
gettext:拦截__()的输出,参数是$translated、$original、$domain。gettext_with_context:拦截_x()的输出,多了$context参数,适合区分同单词在不同场景下的不同译法。ngettext:拦截_n()的输出,处理单复数机制,参数是$translated、$single、$plural、$number、$domain。ngettext_with_context:处理带上下文的单复数。woocommerce_checkout_fields:直接修改结账字段数组里的 label、placeholder、class、priority 等属性,改表单文案最顺手。woocommerce_order_button_text:单独修改订单提交按钮的文字,算是一个专用钩子。
代码放在子主题的 functions.php 里,或者用 Code Snippets 插件管理。推荐 Code Snippets,因为它可以在后台开启/关闭代码片段,出问题不用急着动主题文件,调试效率高。
2.4 三种路线的对比与选型
我自己面对一个新项目时,会先按这个思路快速判断该用哪条路线:
| 需求场景 | 推荐方案 | 理由 |
|---|---|---|
| 大量字符串要翻成另一种语言 | Loco Translate / Poedit | 集中管理,批量处理,符合 WordPress 翻译机制 |
| 完整多语言站,需要内容级切换 | WPML / Polylang / TranslatePress | 集成度高,能处理全站多语言 |
| 只改结账页几处特定文案 | gettext 过滤器 + 字段过滤器 | 改动最小,只影响目标位置 |
| JS 渲染的文本,服务端钩子无效 | 前端 DOM 替换或迁移到经典结账短代码 | 覆盖最后一段“翻译盲区” |
| 不想写代码,运营人员也想自己改 | TranslatePress 可视化编辑 | 对非技术用户最友好 |
这里要提醒一句,不要把方案定死。成熟的项目往往是三四种方案叠加:官方语言包打底,Loco 覆盖个别错译,代码过滤器处理动态逻辑文本,前端替换兜底 JS 内容。翻译这件事没有银弹,组合拳才是常态。
3. 动手改:三个高频结账翻译场景实录
3.1 修改“Place order”按钮文字
这是出现频率最高的需求。很多店主觉得默认的“Place order”太生硬,想改成“确认并提交订单”“提交订单并付款”“完成购买”之类更贴合语境的文案。
最简单的实现是用 WooCommerce 专用过滤器:
php复制add_filter( 'woocommerce_order_button_text', 'custom_order_button_text' );
function custom_order_button_text() {
return '确认并提交订单';
}
这段代码会把所有结账页面的订单按钮文字统一替换。优点是简单,缺点是不区分语言和场景。如果你的结算页同时服务中英文用户,或者你想在某个促销活动期间换成“立即抢购”,这个写法就不够了。
我的做法是配一个语言判断。如果你在用 WPML,可以这样写:
php复制add_filter( 'woocommerce_order_button_text', 'custom_order_button_text' );
function custom_order_button_text() {
$current_language = apply_filters( 'wpml_current_language', 'en' );
if ( 'zh-hans' === $current_language ) {
return '确认并提交订单';
}
if ( 'ja' === $current_language ) {
return '注文を確定する';
}
return 'Place Order';
}
如果是普通单语言站,或者你只是想在英文原文基础上覆盖,也可以用 gettext 过滤器做一次更“翻译”的处理。因为“Place order”本身是通过 __() 输出的,所以这两个钩子的效果会叠加,写的时候注意优先级,避免被别的地方二次覆盖。
心得:按钮文字尽量控制在 4 到 8 个汉字之间。中文信息密度比英文高,如果不控制长度,按钮会被撑得很宽,和输入框、金额摘要区的排版对不上。我见过一个项目把按钮改成“亲,确认订单信息并提交订单”,结果手机端按钮变成两行,点击区域反而变小了。
3.2 重写“Billing details”等区块标题
按钮之外,结账页的区块标题也是重灾区。“Billing details”“Shipping details”“Your order”“Additional information”这些标题,默认英文看久了非常出戏。
这类文本走的是 WooCommerce 的 text domain,用 gettext 过滤器就能精准拦截。下面这段代码是一个完整的替换映射,我一般放在 Code Snippets 里:
php复制add_filter( 'gettext', 'custom_checkout_gettext_translation', 20, 3 );
function custom_checkout_gettext_translation( $translated, $original, $domain ) {
if ( 'woocommerce' !== $domain ) {
return $translated;
}
$map = array(
'Billing details' => '账单信息',
'Shipping details' => '收货信息',
'Your order' => '订单确认',
'Additional information' => '订单备注',
'Apply coupon' => '使用优惠券',
'Coupon code' => '优惠券代码',
'Update cart' => '更新购物车',
);
if ( isset( $map[ $original ] ) ) {
return $map[ $original ];
}
return $translated;
}
这里的 $domain 判断很重要。如果不判断 woocommerce,你会拦截到全站所有含相同英文单词的字符串,比如主题里的“Your order”也可能跟着变。加 domain 能把影响范围缩到 WooCommerce 的字符串域内。
需要注意,gettext 过滤器只拦截 PHP 端通过翻译函数输出的文本。如果某句话来自首页构建器的标题模块,或者来自某个自定义的 WP Query 输出,它可能属于其他 text domain,你需要继续扩展数组去覆盖对应 domain。这也是为什么我建议先把“文本来源清单”做出来的原因,漫无目的地替换迟早会出乱子。
3.3 别把占位符和变量翻没了
很多动态文本不是一句完整的静态句,而是包含变量和格式占位符。比如 __('Price: %s', 'woocommerce'),%s 会在运行时被替换成具体金额。如果你在 gettext 过滤器里直接返回“价格”,那金额就丢了,用户会看到一个孤零零的“价格”。
正确的做法是保留 %s,并调整它在译文中的位置。比如:
php复制// 原始:Price: %s
// 正确译文:价格:%s
如果是复数文本,比如购物车里商品件数的提示,WooCommerce 可能调用 _n() 来区分单复数和数量,比如 _n('item', 'items', $count, 'woocommerce')。这时你需要挂 ngettext 过滤器:
php复制add_filter( 'ngettext', 'custom_checkout_ngettext_translation', 20, 5 );
function custom_checkout_ngettext_translation( $translated, $single, $plural, $number, $domain ) {
if ( 'woocommerce' === $domain && 'item' === $single && 'items' === $plural ) {
if ( 1 === $number ) {
return '件商品';
}
return '件商品';
}
return $translated;
}
你可能会问:“单数和复数都是‘件商品’,那还判断什么?”中文里单复数常常没有形态变化,这是中文的语法特性。但如果你的站点是日语、韩语这种同样没有强复数变化的高级语言,写法也类似。关键是保留 $number 参数,因为有些语言在数量不同时需要完全不同的形式,比如俄语、波兰语,到时候你就能体会到 WooCommerce 为什么要把这套复数机制做得这么细。
还有一种更隐蔽的坑是“带上下文的翻译”。英文里同一个词在不同语境下译法不同,比如“Address”在结账页表示地址,在后台设置里可能指“处理”。WooCommerce 代码里可能用 _x('Address', 'checkout', 'woocommerce') 给翻译者提供上下文提示。这种字符串要用 gettext_with_context 过滤:
php复制add_filter( 'gettext_with_context', 'custom_checkout_gettext_with_context', 20, 4 );
function custom_checkout_gettext_with_context( $translated, $original, $context, $domain ) {
if ( 'woocommerce' === $domain && 'Address' === $original && 'checkout' === $context ) {
return '收货地址';
}
return $translated;
}
这条钩子的签名是 $translated、$original、$context、$domain,参数顺序和 gettext 不同,网上抄代码时特别容易搞错。我本身也栽过这个跟头。
3.4 处理 JS 渲染的文本和支付网关文案
如果你是最近两年新建的 WooCommerce 站点,用的还是默认的结账区块,那你会遇到一个让人抓狂的现象:上面那些 gettext 过滤器,换了半天,页面上却一点反应都没有。这是因为结账区块是 React 渲染的,文字打包在前端 JS 里,根本不经过 PHP 翻译函数。
面对这种情况,我的建议分三步走。
第一步,如果你不需要结账区块的高级定制,可以直接在页面里切换回经典短代码结账。把结账页面内容的区块改成经典短代码块,插入 [woocommerce_checkout],这样页面会走 PHP 模板渲染路线,服务端翻译钩子全部恢复作用。这是目前最省事的“还原”方案。
第二步,如果你确实想保留结账区块的现代体验,那就只能在前端做兜底替换。比如加载一段自定义 JS,在 DOM ready 时改写页面文本:
javascript复制jQuery(document).ready(function($) {
if (! $('form.checkout').length) {
return;
}
$('.woocommerce-billing-fields h3').text('账单信息');
$('.woocommerce-shipping-fields h3').text('收货信息');
$('label[for="billing_email"]').text('电子邮箱');
});
这种写法的坏处很明显:依赖固定的 DOM 结构,主题一改选择器可能失效;对屏幕阅读器不友好;文本替换后可能和页面里其他逻辑拿到的原始字符串不一致。所以只能作为临时兜底,不适合做成长久方案。
第三步,支付网关的文案往往比 WooCommerce 核心更难啃。像 PayPal、Stripe 这类服务,按钮文案一部分由店铺站点输出,一部分由服务商的 SDK 在弹窗或跳转页里渲染,你没法用 PHP 钩子覆盖所有位置。我的经验是:先去支付插件设置页找语言选项,再去插件文档查有没有专门的文案过滤器,实在不行就用前端 JS 对按钮做文本替换。这类问题没有统一解,一条条排查时心态要放平。
4. 踩坑录:翻译不生效与升级覆盖的排查清单
4.1 翻译不生效的常见原因
“我改了很久的翻译,页面一点没变”,这是我被问过最多的问题。排查时我会按以下顺序过一遍,几乎 80% 的情况能定位到原因。
第一,确认字符串确实走的是 gettext 翻译函数。有些主题作者会把文本直接写死在模板里,比如 <h3>Billing details</h3>,这种硬编码文本和翻译机制毫无关系。判断方法:在后台切到一个内置英文主题,看同样的文本是否也出现在默认主题里。如果默认主题下文本正常翻译,说明问题出在你的主题;如果默认主题下也不翻译,那可能是 WooCommerce 核心字符串的问题。
第二,确认 text domain 匹配。WooCommerce 核心字符串的 domain 通常是 woocommerce,主题字符串则各不相同。同一个英文文本可能在不同 domain 下出现多次,你拦截的是 A domain,实际页面输出的是 B domain,自然不生效。调试时可以临时在代码里加一句日志,打印出当前 $original 和 $domain 的值,一目了然。
第三,确认过滤器是否被后执行的高优先级钩子覆盖。我写过比较早的插件会优先用 gettext 做全站翻译,如果你的自定义代码优先级不够高,就会被别人的翻译先接管。用 Code Snippets 时,可以给过滤器加上一个较大的 $priority,比如 30 或 40,确保后执行。
第四,检查页面缓存。翻译文件的修改并不一定会被完整页面缓存感知,尤其是用了 LiteSpeed Cache、W3 Total Cache 这类插件时,HTML 可能已经静态化了。记得清一遍页面缓存,最好在结账页面 URL 后面加个 ?nocache=1 之类的参数做单页验证。
4.2 缓存机制到底缓存了什么
这里有个容易混淆的概念:WordPress 的翻译缓存和页面缓存是两回事。
页面缓存是整段 HTML 的输出缓存,服务器直接把之前生成的页面返回给访客,PHP 都没执行,翻译过滤器自然也没机会跑。所以改了翻译后,第一件事是清页面缓存。如果你用了 CDN,还要在 CDN 后台强制刷新 URL,否则边缘节点上旧的 HTML 还能“存活”几个小时。
翻译文件本身的缓存主要来自 PHP 的 opcache。.mo 文件被读取后会进入 opcache,修改后如果 PHP-FPM 没有 reload,可能出现旧译文残留。很多服务器面板里清 PHP 缓存和重启 PHP-FPM 是同一个操作,遇到“代码改了但没反应”,顺手重启一下并不亏。
还有一个经常被忽略的是对象缓存。如果你站点开了 Redis 或 Memcached,并且 WooCommerce 会把某些翻译相关的查询结果缓存起来,那就涉及专门的清理。不过 WooCommerce 本身不会缓存翻译字符串,对象缓存影响的主要是结账字段配置,所以这个问题更多出现在你用了额外字段管理插件时。
