开学特惠 会员低至4.4折 限时加赠 10000 AI积分 立即前往 arrow

AI翻译多语言文案:键名、变量与复数校验

文章目录
免费使用墨刀
更新时间: 2026年10月09日

AI翻译多语言文案后,要同时检查译文含义和资源文件结构。键名必须能被程序找到,变量必须保留,数量句子则要按目标语言选择复数形式。中英文资源的键集合未必完全相同,逐字翻译或机械对齐键名都可能产生问题。

以购物车为例,可以把中文资源、使用场景和变量约定交给墨刀AI客户端整理英文资源与检查项,再把结果交给项目自己的校验程序。下面提供一个可在Node.js运行的完整例子:两份词典、变量规则和六类故意改坏的样本。

中英文欢迎文案与同名变量的对应校验
翻译改变文字表达,程序仍依赖稳定的键名与变量。

给译文上下文,也给它不能改的部分

只给“结算”两个字,AI可能译成Checkout、Settlement或其他词。购物车按钮与财务结算单的语境不同。准备资源时,将调用位置、对象和格式约束放在同一张表里,让译文服务于实际动作。

消息出现位置与含义固定约束
cart.checkout购物车底部,进入结算流程的按钮不得改键名;没有变量
cart.owner标题,显示某位用户的购物车必须保留name变量,不翻译变量名
cart.items商品数量摘要必须保留count,并按语言提供数量分支

本例使用扁平键名和双花括号变量,例如{{name}}。点号只是键名的一部分,检查程序不会把它当成嵌套对象路径。参考的是i18next默认插值语法,但下面的脚本是独立检查器,没有安装或调用i18next。

可以这样交代任务:“请根据购物车上下文补全英文词典。保留普通消息键和变量名,数量句采用目标语言所需的分支。只输出资源对象和你仍无法判断的语境,不擅自增加按钮、不把变量替换成某个人名。”如果产品采用ICU MessageFormat,应提供真实格式与解析器规则,不能混用本例语法。

中文一条数量句,英文可能需要两条

本例中文商品数使用other分支,英文使用one和other。因而中文只有cart.items_other,而英文还有cart.items_one。如果校验只要求“英文键与中文键完全相同”,反而会把英文正确的单数分支当成多余内容。

i18next复数文档说明了按语言划分的复数类别及count参数。本文使用JavaScript的Intl.PluralRules取得目标语言的类别,并根据数量选择后缀。其他语言可能需要更多分支,不能把one、other写死成所有语言的规则。

下面的校验限定为购物车三类消息:普通键名固定,数量分支根据语言生成;变量集合必须相同;空白译文、额外键和不支持的花括号语法会报错。它不处理嵌套资源、格式化器、HTML标签、ICU嵌套表达式或翻译质量判断。

复制词典与检查器,先看一轮真实输出

将下面代码保存为i18n_demo.mjs,执行node i18n_demo.mjs。脚本内置中英文字典,随后逐项制造缺键、改变量等错误。运行时无需额外安装依赖。

import assert from 'node:assert/strict';

const dictionaries = {
  zh: { 'cart.checkout': '结算', 'cart.owner': '{{name}}的购物车',
        'cart.items_other': '{{count}}件商品' },
  en: { 'cart.checkout': 'Checkout', 'cart.owner': "{{name}}'s cart",
        'cart.items_one': '{{count}} item', 'cart.items_other': '{{count}} items' }
};
const variables = { 'cart.checkout': [], 'cart.owner': ['name'], 'cart.items': ['count'] };
const token = /\{\{([A-Za-z_][A-Za-z0-9_]*)\}\}/g;

function validate(locale, dictionary) {
  const categories = new Intl.PluralRules(locale).resolvedOptions().pluralCategories;
  const expected = ['cart.checkout', 'cart.owner', ...categories.map(c => `cart.items_${c}`)];
  const errors = [];
  for (const key of expected) {
    if (!Object.hasOwn(dictionary, key)) { errors.push(`missing:${key}`); continue; }
    const value = dictionary[key];
    if (typeof value !== 'string' || !value.trim()) { errors.push(`empty:${key}`); continue; }
    const used = [...new Set([...value.matchAll(token)].map(m => m[1]))].sort();
    const base = key.startsWith('cart.items_') ? 'cart.items' : key;
    if (JSON.stringify(used) !== JSON.stringify([...variables[base]].sort())) {
      errors.push(`variables:${key}`);
    }
    if (/[{}]/.test(value.replace(token, ''))) errors.push(`unsupported_syntax:${key}`);
  }
  for (const key of Object.keys(dictionary)) {
    if (!expected.includes(key)) errors.push(`extra:${key}`);
  }
  return errors;
}

function itemText(locale, count) {
  const suffix = new Intl.PluralRules(locale).select(count);
  return dictionaries[locale][`cart.items_${suffix}`].replace('{{count}}', String(count));
}

assert.deepEqual(validate('zh', dictionaries.zh), []);
assert.deepEqual(validate('en', dictionaries.en), []);
const cases = [
  ['missing_key', d => { delete d['cart.checkout']; }, 'missing:cart.checkout'],
  ['extra_key', d => { d['cart.pay'] = 'Pay'; }, 'extra:cart.pay'],
  ['renamed_variable', d => { d['cart.owner'] = '{{user}} cart'; }, 'variables:cart.owner'],
  ['empty_translation', d => { d['cart.checkout'] = ' '; }, 'empty:cart.checkout'],
  ['missing_plural', d => { delete d['cart.items_one']; }, 'missing:cart.items_one'],
  ['unsupported_icu', d => { d['cart.owner'] = '{name} cart'; }, 'unsupported_syntax:cart.owner'],
];
for (const [label, change, expected] of cases) {
  const changed = structuredClone(dictionaries.en);
  change(changed);
  assert.ok(validate('en', changed).includes(expected), label);
}
assert.deepEqual([0, 1, 2].map(n => itemText('en', n)), ['0 items', '1 item', '2 items']);
assert.deepEqual([0, 1, 2].map(n => itemText('zh', n)), ['0件商品', '1件商品', '2件商品']);
console.log(JSON.stringify({ node: process.version, validDictionaries: 2,
  rejectedMutations: cases.map(c => c[0]), english: [0, 1, 2].map(n => itemText('en', n)),
  chinese: [0, 1, 2].map(n => itemText('zh', n)) }, null, 2));

本地Node.js v24.19.0运行结果:两份词典通过,六类修改后的词典都被对应规则识别。数量0、1、2在英文分别输出“0 items”“1 item”“2 items”,中文分别为“0件商品”“1件商品”“2件商品”。这验证了此例的结构和分支选择,没有验证真实页面布局或所有语言的译文。

故意造成的问题检查结果实际项目中的意义
删除cart.checkoutmissing避免按钮找不到资源
新增未约定的cart.payextra提示AI可能擅自改键或新增动作
将name改成uservariables避免运行时数据对不上
把按钮译文写成空格empty避免有键却没有可见文字
删除英文one分支missing避免单数调用缺少资源
将双花括号改成单花括号unsupported_syntax提示消息格式发生了变化

通过脚本后,还要把文案放回页面

结构正确的英文也可能不合适。例如“Checkout”是否准确,取决于点击后进入结算还是直接提交付款;“某人的购物车”是否应出现所有者,则取决于产品是否支持代选购。业务含义需要结合界面,由熟悉目标语言与场景的人复核。

回填时至少检查数量为0、1、2的页面,较长的用户名称,以及从中文切换到英文后的按钮和摘要。变量不是固定长度,不能只用“Tom”验证标题。布局处理可继续参考长文本与多语言UI的换行和省略方法。

若项目直接读取JSON文件,还应在解析前发现重复键;普通JSON解析后的对象通常只剩后出现的值,本例基于已创建的对象,无法替你发现源码里曾写过两个同名键。同样,变量名集合一致不代表文字顺序、礼貌程度和语气正确,不能把所有人工复核都压缩成一个正则。

新增一种语言时,沿用规则而不是复制英文分支

把新语言加入时,先确认它的消息格式和数量类别,再扩充校验器的消息范围。对每条消息保留上下文、允许的变量和实际调用位置;对每次AI改译记录资源差异和运行结果,出现问题才知道是词句修改还是程序约定被破坏。

如果中文原稿本身还含糊,应先完成按钮与错误提示的业务文案整理,再进入翻译。一个可靠的多语言交付包,需要译文、资源规则和页面复核相互对得上;单独一份流畅的英文JSON还不足以证明可以上线。

免费在线原型设计工具

内容丰富组件拖拽即用

多人在线编辑实时协作

海量模板素材快速复用

一键分享交付在线评论互动