頁面建構器製作的頁面如何翻譯:短碼、區塊與動態內容的常見問題及處理方法
页面构建器做的页面怎么翻译:短代码、区块、动态内容各自的坑和绕法
頁面建構器網站的多語翻譯應先處理渲染前的短碼與區塊資料,再於渲染後補譯動態 DOM。短碼可用佔位符保護不可翻譯片段,區塊屬性按白名單翻譯;動態內容則用 MutationObserver 分批處理,避免寫回 DOM 後重複觸發。
2026-10-30 1
版权
版权声明:
本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《 阿里云开发者社区用户服务协议》和 《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写 侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。
简介: 切到中文之后,按钮上显示的是 [cta text="Get a quote" /],页脚多出一段没解析的 JSON,几个本该是「加入购物车」的地方只剩空白。用页面构建器搭出来的站点,做多语言时经常出现这类情况,共同点是:文案并没有老老实实待在 HTML 文本节点里。 构建器把一句话拆成好几份:一部分在文本节点,一部分在标签属性里,还有一部分存在自己的数据结构里(区块属性、构建器的 JSON 配置)。翻译工具在哪个环节动手,决定了它会不会把页面翻坏。
切到中文之后,按钮上显示的是 [cta text="Get a quote" /],页脚多出一段没解析的 JSON,几个本该是「加入购物车」的地方只剩空白。用页面构建器搭出来的站点,做多语言时经常出现这类情况,共同点是:文案并没有老老实实待在 HTML 文本节点里。
构建器把一句话拆成好几份:一部分在文本节点,一部分在标签属性里,还有一部分存在自己的数据结构里(区块属性、构建器的 JSON 配置)。翻译工具在哪个环节动手,决定了它会不会把页面翻坏。
渲染前改,还是渲染后改
渲染前:拿到的是 post_content 里的原文,短代码还是 [cta text="Get a quote" /] 这个样子,区块还是属性数组。在这个环节翻译,短代码是完整的一整段,不容易被拆坏;代价是要自己判断哪些字段是给人看的。
渲染后:拿到的是浏览器里的 DOM,短代码已经被解析成 <a class="btn">Get a quote</a>。在这个环节翻译只能看到文本节点和属性,好处是能覆盖构建器动态生成的内容,坏处是留下的线索变少了,而且很容易把类名、字段名一起翻掉。
渲染前和渲染后都得做,顺序是先渲染前、再渲染后补漏。下面按内容存放的位置分开说。
短代码:先挖走,再翻译,最后填回
短代码被翻坏的典型方式,不是整段被翻,而是被拆开。[cta text="Get a quote" /] 里的 Get a quote 是可翻译文本,翻译接口只返回这一小段,回填时括号和属性名对不上,页面就渲染成纯文本了。
做法是把不该动的片段先替换成占位符,翻译完再原样换回来。
/**
* 翻译前:把不可翻译片段替换为占位符。
* @return array{0:string,1:array<string,string>} [处理后的文本, 占位符 => 原文]
*/
function i18n_extract_protected( string $html ): array {
$store = [];
$i = 0;
$rules = [
'/<(pre|code)\b[^>]*>.*?<\/\1>/is', // 代码块:里面的东西一律不动
'/\[([a-zA-Z0-9_-]+)(?:\s[^\]]*)?\].*?\[\/\1\]/s', // 成对短代码
'/\[[a-zA-Z0-9_-]+(?:\s[^\]]*)?\/?\]/s', // 自闭合短代码
'/https?:\/\/[^\s"\'<>]+/i', // 裸链接
'/[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}/i', // 邮箱
];
foreach ( $rules as $re ) {
$html = preg_replace_callback( $re, function ( $m ) use ( &$store, &$i ) {
$token = '{
{PROT' . $i++ . '}}';
$store[ $token ] = $m[0];
return $token;
}, $html );
}
return [ $html, $store ];
}
function i18n_restore_protected( string $translated, array $store ): string {
return strtr( $translated, $store );
}
占位符的形式要挑翻译接口会原样返回的那种。{
{PROT0}} 在多数接口下能活下来,但有的接口会改空格、加分词,所以上线前一定要断言数量一致,不一致就整段回退原文,不要发出去半截。另外成对短代码的正则要放在自闭合之前,否则 [cta ...]...[/cta] 会先被自闭合规则咬掉开头。
function i18n_tokens_intact( string $before, string $after ): bool {
preg_match_all( '/\{\{PROT\d+\}\}/', $before, $a );
preg_match_all( '/\{\{PROT\d+\}\}/', $after, $b );
return $a[0] === $b[0];
}
区块与构建器 JSON:文案藏在属性里
只遍历文本节点,按钮文字、输入框提示、图片 alt、aria-label 全会漏。这些值存在区块属性或构建器自己的配置里,翻译必须发生在它们被渲染成 HTML 之前。
用区块的站点可以在渲染前的数据层拦一道,只翻白名单里的字段:
add_filter( 'render_block_data', function ( array $block ): array {
// 只翻明确是给人看的字段,其余一概不碰
$whitelist = [
'core/image' => [ 'alt' ],
'core/search' => [ 'label', 'placeholder', 'buttonText' ],
];
$name = $block['blockName'] ?? '';
if ( ! isset( $whitelist[ $name ], $block['attrs'] ) ) {
return $block;
}
foreach ( $whitelist[ $name ] as $key ) {
if ( isset( $block['attrs'][ $key ] ) && is_string( $block['attrs'][ $key ] ) ) {
$block['attrs'][ $key ] = translate_text( $block['attrs'][ $key ] );
}
}
return $block;
}, 10, 1 );
render_block_data 在区块渲染之前触发,这时改属性不会影响结构。表里这两个区块只是举例,实际字段以你站点上保存的属性为准——把导出的一份区块数据打开看一眼,比照抄别人的清单靠谱。
构建器不用区块、文案存在自己的表里,思路一样:找到它读取配置的那个入口,在读取之后、拼 HTML 之前翻译。在输出后的 HTML 上用正则替换属性值,遇到转义和单双引号混用就开始出问题,而且每次构建器改版都要跟着改。
表单控件还要单独处理:value、placeholder、<option> 的文本、aria-label,这几项在切换语言时要跟着变,不能只在首次渲染时翻一次。
动态内容:补翻要防自己触发自己
构建器常在滚动、点击、切 tab 之后才把内容插进 DOM。用 MutationObserver 补翻是对的,但直接把翻译结果写回 DOM,会再次触发监听,运气不好就是无限循环加重复请求。
写回时先断开监听、给已翻节点打标记、把变更合并成批次,少一样就会绕回自己。
(function () {
const done = new WeakSet();
const queue = new Map(); // 文本节点 -> 原文
const MAX_BATCH = 50;
let timer = null;
let observer;
const opts = {
childList: true, subtree: true, characterData: true };
function collect(root) {
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
acceptNode(node) {
const p = node.parentElement;
if (!p) return NodeFilter.FILTER_REJECT;
if (/^(SCRIPT|STYLE|CODE|PRE|TEXTAREA)$/.test(p.tagName)) return NodeFilter.FILTER_REJECT;
if (done.has(p)) return NodeFilter.FILTER_REJECT;
if (!node.nodeValue.trim()) return NodeFilter.FILTER_REJECT;
return NodeFilter.FILTER_ACCEPT;
}
});
let n;
while ((n = walker.nextNode()) && queue.size < MAX_BATCH) {
queue.set(n, n.nodeValue);
}
}
function flush() {
timer = null;
if (!queue.size || document.documentElement.lang === 'en') return;
const nodes = [...queue.keys()];
const items = nodes.map(n => queue.get(n));
queue.clear();
fetch('/api/i18n/translate', {
method: 'POST',
headers: {
'Content-Type': 'application/json' },
body: JSON.stringify({
locale: document.documentElement.lang, items })
})
.then(r => r.ok ? r.json() : Promise.reject(r.status))
.then(({
items: out }) => {
observer.disconnect(); // 关键:写回期间不监听
nodes.forEach((node, i) => {
const t = out[i];
if (typeof t === 'string' && t.trim() !== '') {
node.nodeValue = t;
done.add(node.parentElement);
}
});
observer.observe(document.body, opts);
})
.catch(() => observer.observe(document.body, opts));
}
observer = new MutationObserver(muts => {
for (const m of muts) {
if (m.type === 'characterData') {
if (m.target.parentElement && done.has(m.target.parentElement)) continue;
if (queue.size < MAX_BATCH) queue.set(m.target, m.target.nodeValue);
} else {
m.addedNodes.forEach(n => {
if (n.nodeType === 1) collect(n); });
}
}
if (queue.size && !timer) timer = setTimeout(flush, 300);
});
collect(document.body);
observer.observe(document.body, opts);
})();
批次上限和 300ms 合并的意义不只在于性能:不设上限的一次性补翻,会把整页几百个文本节点塞进一个请求,接口超时后你拿不到结果,节点也没打标记,下一轮又重来。
译文本身也要缓存:以 sha1(原文 . 目标语言 . 词典版本) 做键,命中就不再请求。
语言切换:URL、cookie、html lang、hreflang 要对得上
这几处只要有一个对不上,切换后就会出现「页面是中文、日期格式是英文」,或者「刷新又跳回英文」。
解析顺序建议固定成:URL 前缀 → cookie → Accept-Language → 站点默认。
function resolve_locale(): string {
$supported = [ 'en', 'zh-CN', 'ja' ];
$default = 'en';
if ( preg_match( '#^/([a-z]{2}(?:-[A-Z]{2})?)(/|$)#', $_SERVER['REQUEST_URI'] ?? '', $m )
&& in_array( $m[1], $supported, true ) ) {
return $m[1];
}
if ( isset( $_COOKIE['site_locale'] ) && in_array( $_COOKIE['site_locale'], $supported, true ) ) {
return $_COOKIE['site_locale'];
}
foreach ( explode( ',', $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '' ) as $part ) {
$tag = trim( explode( ';', $part )[0] );
if ( in_array( $tag, $supported, true ) ) return $tag;
}
return $default;
}
内容页用 URL 前缀,因为它要被搜索引擎收录;界面字符串、表单提示、按钮这些不需要被索引的,cookie 就够了,不必为此改 URL。写 cookie 的时候带上 Path=/; Max-Age=31536000; SameSite=Lax; Secure。
上线前跑一遍这几项
- 短代码数量:译文里
[和]的配对数量与原文一致 - 占位符 token 序列完全一致,不一致回退原文
- 用
DOMDocument加载译文,libxml_get_errors()不为空就说明标签被翻断了 - 保护名单(短代码名、字段名、类名、型号、品牌名)在译文里原样存在
- 抽 10 个页面,人工核对按钮、placeholder、下拉选项这三处
这套「挖走—翻译—填回—校验」的流程自己写一遍不算难,难的是把白名单维护住。AumLang 处理的是 AI 翻译和 hreflang 这一段,短代码保护和字段白名单仍然要按上面的方式落在你的主题里:aumcreate.com/plugins/aumlang。
來源:阿里雲開發者社區 AI · developer.aliyun.com