按钮、导航和空状态都是德语,产品名称、分类和描述却还是英文。这不是 i18n 配置出了 bug,而是它本来的能力边界。globalize.now 是 AI 驱动的本地化基础设施:它从代码库中提取硬编码字符串,生成翻译键和语言目录,并在每次 Git 推送时同步。它读的是代码,不是数据行,而用户真正关心的内容恰恰存放在数据行里。
解决这个问题靠的是 schema 设计,而不是换工具。下面是经得起实践检验的做法。
为什么 Lovable 应用的界面已翻译,数据库内容却还是英文?
因为字符串提取工具只能看到源文件里写下的内容。
本地化 Lovable 应用时,扫描器会遍历组件,提取它发现的每一个字面量:"Add to cart"、"No results"、"Sign in"。这些字面量会成为语言目录中的消息 ID,目录经过翻译后提交。这条流水线是确定性的,对界面的覆盖也很完整。
但它覆盖不了数据行。products.name = 'Trail Runner' 从来不在 .tsx 文件里。它来自表单、种子脚本或导入,存放在 Postgres 中:可能是 Lovable Cloud 内置的后端(基于 Supabase),也可能是你自己连接的 Supabase 项目。任何代码扫描器都不会读取它,重复运行提取也改变不了这一点。
结果就是翻译过的外壳包着英文内容。导航写着 Warenkorb,里面的商品却是 Trail Runner, lightweight trail shoe for wet conditions。用户一眼就能看出来,这比完全没翻译的应用更糟,因为它看起来像做到一半就被放弃了。
在 Lovable 应用中,哪些算数据库内容?
凡是用户或管理员无需部署就能修改的内容都算。在典型的 Lovable 项目中,包括:
- 产品、套餐或商品的名称和描述
- 由查找表渲染的分类、标签和状态文案
- 存放在
pages或sections表中的营销文案 - 邮件和通知模板
- 直接展示的类枚举值,例如
status = 'pending_review' - 用户生成内容,通常会有意保留原始语言
最后一项很重要。默认不要翻译用户生成内容。用西班牙语写的评论就应该保持西班牙语。真正需要本地化的,是由你撰写并发布的内容。
其余的一切,也就是内容周围的界面框架,都放在语言目录里。这两部分不应该有重叠。如果同一个文案既出现在查找表里,又出现在硬编码字符串中,选定一处保留,把另一处删掉,否则同一个概念会出现两个不同的德语译法。
在 Supabase 中,如何存储数据库内容的翻译?
单独建一张翻译表,以行 id 和 locale 列作为键。
create table product_translations (
product_id uuid not null references products(id) on delete cascade,
locale text not null,
name text not null,
description text,
primary key (product_id, locale)
);
create index product_translations_locale_idx on product_translations (locale);
现在新增日语只需插入数据,不用做迁移。行级安全策略只需挂在一张表上,而不是一堆不断增加的列上;缺失翻译就是缺一行记录,既容易查询,也容易告警。
另一种方案是为每个可翻译字段使用一个 JSONB 列:
alter table products
add column name_i18n jsonb not null default '{}'::jsonb;
-- { "en": "Trail Runner", "de": "Trail Runner", "fr": "Coureur de sentier" }
这种方式原型开发更快,适合内容少且基本不变的场景。代价是无法做按语言的约束,部分翻译也难以审计,而且一旦要按本地化后的值过滤或排序,建索引的成本会很高。
第三种模式是每种语言一列(name_en、name_de、name_fr),应该避免。每新增一种语言,所有表都要做一次 schema 迁移,而在 Lovable 项目里,这意味着要反复让 Agent 去修改生产表。如果想深入了解这种模式,公开的 pg_i18n Postgres 扩展以视图的形式实现了翻译表方案,并内置回退,值得在确定方案前读一读。
该选哪种模式?
选翻译表,除非内容很少且基本不变。
只有在可翻译字段不超过寥寥几个、无需按它们过滤或排序、也不打算让别人编辑的情况下,才选 JSONB。其余情况,比如有管理后台的、要持续新增语言的、超过几百行的,都应该用翻译表。
渲染时如何查询正确的语言?
在数据库里显式写好回退,应用端就不用操心了。
create or replace function products_for_locale(p_locale text)
returns table (id uuid, slug text, name text, description text)
language sql stable as $$
select
p.id,
p.slug,
coalesce(t.name, base.name) as name,
coalesce(t.description, base.description) as description
from products p
left join product_translations base
on base.product_id = p.id and base.locale = 'en'
left join product_translations t
on t.product_id = p.id and t.locale = p_locale;
$$;
这样调用方只需要传一个 locale,别的都不用管:
const { data: products } = await supabase
.rpc('products_for_locale', { p_locale: locale });
Two rules make this survive contact with real data. First, coalesce to the source language, never to an empty string: a half-translated catalogue should show English, not blank cards. Second, the locale you pass must be the same value your route segment uses and the same value your UI catalogs are keyed on. One locale list, three consumers: router, catalogs, database.
最后这一点,正是大多数 Lovable 项目容易走样的地方。路由认的是 de,语言目录是按 de-DE 构建的,数据库里的行却标成了 german。选定 locale 路由所用的那组 BCP 47 标签,让其他一切都向它看齐。如果还没配置这些路由,我们针对 Lovable 两种生成技术栈的分栈教程都有讲解:Vite SPA 构建和 TanStack Start SSR 默认方案。
为什么不直接让运行时插件翻译数据库文本?
因为它作用于渲染后的页面,而不是你的数据,这个差别会在三个地方显现出来。
JavaScript 翻译插件会读取所有进入 DOM 的文本,并事后替换。这确实能覆盖数据库数据行:这是插件唯一比语言目录多出的能力,也是它们起初看起来像完整方案的原因。
代价是结构性的。翻译后的数据行只存在于浏览器会话中,所以搜索引擎抓取页面时看到的是源语言;Weglot 自己的帮助中心就写明,其 JavaScript 集成不带来 SEO 收益,因为这些翻译是在客户端渲染的。修正内容保存在服务商的后台而不是你的数据库里,所以一个字段的真实值被拆分在两个系统中。而且替换发生在首次绘制之后,这就是用这种方式构建的翻译应用每次跳转页面都会闪一下英文的原因。
这些问题并不意味着内部工具就不能用这种方案,但它们足以说明,不该用它来构建面向公众、需要被搜索引擎索引的多语言产品。
两层如何保持同步?
共用一份 locale 列表和一个触发器。
代码层是自动的。在 Lovable 工作区中添加 lovable-i18n Skill,然后提示 Lovable 配置 i18n,或者在本地安装:
npx skills add globalize-now/globalize-skills
此后每次推送都会运行提取,并自动开一个翻译 PR:不用进后台操作,也不用手动过一遍语言文件。这就是为什么 Lovable 翻译每次推送都会失效一文中描述的同一套流程。
数据层归你自己管,需要一个刻意设计的钩子:每当新增 locale,驱动路由和语言目录的那份列表,也应该驱动对翻译表的回填。在 Lovable 项目里,比较实用的做法是让 Agent 写一次种子函数,读取 locale 列表,并基于源语言的值为缺失的数据行补插记录。这样缺失的行是补全而非凭空编造,在此期间 coalesce 会兜住空缺。
这里的定价同样不会为难你。Starter 套餐每个工作区每月 €20,包含每月 €20 的翻译额度(约 20 万词),超出部分按公开的单字符费率计费。不按席位收费,也不按语言收费,所以新增第五种语言,只需要为实际翻译的字符付费,没有其他开销。注册即送 €5 翻译额度,无需绑定银行卡。关于它如何适配由 Agent 构建的技术栈,详见 vibe coder 和开发者页面。
本地化服务商停运时,哪些东西会失效?
存放在他们服务器上的一切都会失效,存放在你自己这边的则毫发无损。
这并不是纸上谈兵,本月就在发生。Lovalingo 是许多 Lovable 开发者采用的运行时翻译层,将于 2026 年 8 月 31 日 23:59(CEST)停止服务,其服务终止页面已确认这一点,新项目和新订阅已停止受理,并正在为现有用户准备留存导出。最有意思的是它自己的指引:建议用户同步实现项目自有的国际化方案,待替代方案在线上环境通过生产检查后,再移除 Lovalingo。
服务商自己的这句话,恰好把整个论点说完了,而它的退出正是证明。已提交的语言目录和 product_translations 表都归你所有,不存在什么服务状态。托管的运行时层是渲染产品文案的依赖项,而依赖项总有终结的一天。
如果你现在正要从插件迁移出来,先处理数据层。UI 字符串几分钟就能从代码中重新提取。数据库翻译如果只存在于服务商的后台里,就再也找不回来了。我们的 Lovalingo 对比列出了移除前需要检查的事项。
代码层是你不该手动维护的部分。globalize.now 负责提取字符串、生成语言目录,并在每次推送时自动开一个翻译 PR,让你只需设计一次数据层,之后就不用再管它。
globalize.now 把硬编码的应用文案转换为可直接翻译的语言文件,并在你持续发布时保持同步更新。
免费试用 globalize.now