你的 Lovable 应用可能已经全部翻译完成,却仍然向德国用户显示“5 элемент”,或把价格显示成“1,234.50”。原因在于,为英语编写的复数和格式化逻辑还留在组件里。globalize.now 是 AI 驱动的本地化基础设施,把字符串保存在已提交的语言目录中,下面的修复方案会把复数规则放进这些目录,而不是留在代码里。

Lovable 翻译应用时,复数为什么会出错?

因为复数规则写在组件里,而不是翻译里。常见的生成代码如下:

<p>{count} {count === 1 ? "item" : "items"} in your cart</p>

这行代码把英语语法硬编码了。翻译人员可以改词,却改不了 === 1 这个判断条件,语序也是固定的,数字永远在最前面。英语只有两种形式,俄语有四种,所以“1 элемент”“2 элемента”“5 элементов”和“1,5 элемента”需要不同的词尾。

如果你先配置了路由(参见如何让 Lovable 应用支持多语言),复数就是用户接下来会发现的 bug,因为它不会引发任何崩溃,只是句子不对。

第 1 步:如何写出翻译人员可以修改的复数?

使用 ICU plural 消息,把规则移入翻译。在 Lingui 中,Plural 组件和 plural 宏都能生成这种消息。

import { Plural } from "@lingui/react/macro";

<p>
  <Plural value={count} one="# item in your cart" other="# items in your cart" />
</p>

对于 JSX 之外的字符串,使用核心包中的宏:

import { plural } from "@lingui/core/macro";

const label = plural(count, { one: "# file", other: "# files" });

# 会被替换为数字。根据 Lingui 文档,你只需写出源语言需要的形式,其中 other 为必填。像 _0="Your cart is empty" 这样的精确匹配,可以在英语中处理零的情况,无需单独写分支。

Lovable 的默认技术栈在配置上有差异,但这套 API 完全相同,所以同一个组件在 Vite SPA 和 TanStack Start 项目中都能使用。

提取复数后,语言目录长什么样?

每个字符串对应一条标准 ICU 语法的消息,以 PO 文件的形式存放在仓库中。翻译者(人工或 AI)负责补全所在语言需要的各种形式。

msgid "{count, plural, one {# item in your cart} other {# items in your cart}}"
msgstr "{count, plural, one {# товар в корзине} few {# товара в корзине} many {# товаров в корзине} other {# товара в корзине}}"

俄语的 other 形式也涵盖 1,5 这样的小数,这正是它与单数属格形式相同的原因。由于语言目录是已提交的文件,你可以像审查其他代码一样,在 PR 中审查复数的改动。语法本身请参见什么是 ICU MessageFormat。

哪些语言需要 one 和 other 之外的类别?

Unicode CLDR 定义了六个复数类别:zero、one、two、few、many 和 other。每种语言只使用其中一部分,而 other 在所有语言中都是必需的。

语言使用的类别
日语、中文other
英语one, other
俄语one, few, many, other
阿拉伯语zero, one, two, few, many, other

CLDR 规范指出,威尔士语同样使用全部六个类别。你不必死记这些,只需要一种能让各语言填写自身形式的目录格式,而带 ICU 消息的 PO 正是如此。

如何阻止 Lovable 再次生成三元表达式写的复数?

把规则写到 Lovable 读取指令的地方。你提示 Lovable 开发新功能时,它会重新生成组件,而没有遵守规则的新功能会把三元表达式带回来。

把下面这段加入 Knowledge 或 AGENTS.md:

Never hand-build plurals or number formats. Use the Lingui Plural component
or plural() macro for every count. Use Intl with the active locale for every
date, number and price.

lovable-i18n Skill 会替你写入这类规则,具体做法见Lovable i18n Skill 的作用。下次生成新功能后,在 diff 中搜索 === 1 和 !== 1,确认规则是否生效。

第 2 步:如何按 locale 格式化日期和货币?

配合当前 locale 使用 Intl。Lingui 的 i18n 对象也提供 i18n.date() 和 i18n.number(),但直接用 i18n.locale 调用 Intl,可以让格式化选项在你自己的代码中一目了然。

import { useLingui } from "@lingui/react";

export function Price({ amount, currency }: { amount: number; currency: string }) {
  const { i18n } = useLingui();
  return (
    <span>
      {new Intl.NumberFormat(i18n.locale, { style: "currency", currency }).format(amount)}
    </span>
  );
}

export function OrderDate({ date }: { date: Date }) {
  const { i18n } = useLingui();
  return <time>{new Intl.DateTimeFormat(i18n.locale, { dateStyle: "long" }).format(date)}</time>;
}

同样的 1234.5 欧元,在各个 locale 下会渲染成不同的字符串,分隔符和符号位置各不相同。格式化从不换算金额。以欧元存储的价格,在所有语言中都还是欧元,所以请在数据库中为每个价格保存对应的货币代码。

第 3 步:上线前如何测试所有复数类别?

让浏览器告诉你某个数字属于哪个类别,再检查每个类别是否都有对应的形式。

const ru = new Intl.PluralRules("ru");
[1, 2, 5, 21, 1.5].map((n) => ru.select(n));
// ["one", "few", "many", "one", "other"]

const ar = new Intl.PluralRules("ar");
[0, 1, 2, 3, 11, 100].map((n) => ar.select(n));
// ["zero", "one", "two", "few", "many", "other"]

为每种上线的语言添加一个测试:用各类别的数字渲染组件,断言没有任何一个回退到缺失的形式。然后在浏览器中查看一遍俄语和阿拉伯语页面。目录中缺少 few 不会报错,只会表现为词尾错误。

如果之前用的是运行时插件,会有什么变化?

运行时插件在页面加载后翻译已渲染的文本,因此看不到你代码中的复数规则。这正是要把翻译移入仓库的原因之一。Lovalingo 已于 2026-08-31 停止服务;当语言目录存放在你的仓库中,会发生哪些变化,参见迁移指南和对比页面。

使用 globalize.now,你只需连接一次仓库,代码库就会在应用内完成转换。之后,每次推送都会触发任务翻译新增的目录条目,结果以 PR 的形式送达。Lovable 从 GitHub 同步,复数形式会像其他改动一样进入你的应用。配置方法见 Lovable 集成页面,无需控制台的工作流见无需控制台,本地化 Lovable 应用。

globalize.now 把硬编码的应用文案转换为可直接翻译的语言文件,并在你持续发布时保持同步更新。

免费试用 globalize.now