先给每种语言分配独立 URL,再用双向 hreflang 标签标注这些 URL。在 React 状态里替换字符串的下拉菜单,只能为已经在你站点上的用户翻译界面,却不会产生任何可供搜索引擎索引的内容,德语和西班牙语版本依然不可见。globalize.now 是 AI 驱动的本地化基础设施,把硬编码字符串提取到已提交的 Lingui PO 目录,并在每次 Git 推送时保持同步,让按 locale 划分的路由能长期维护,而不是一次性的苦差事。
这一步,大多数 Lovable 开发者都会跳过。应用翻译好了,切换器在浏览器里也能用,流量却始终没来。
为什么 Google 不收录我的 Lovable 应用的其他语言?
因为这些语言几乎可以肯定没有自己的地址。
AI 构建工具默认生成的模式,是一个 useState locale 值,加一个设置它的下拉菜单。所有语言都在同一个 URL 渲染。爬虫请求该 URL,只得到一种语言的一份响应,只索引一个页面。它无从发现其他内容,也没有任何信号告诉它还存在其他语言版本。
解决办法在结构,而非表面。Google 关于本地化版本的官方指南明确指出,备用语言页面通过 URL 识别,且连接它们的标注必须由集合中的每个页面互相返回。如果 /de/pricing 不是可访问的地址,再多标记也无法让它被发现。
真正的语言切换器需要做到什么?
按顺序做到三件事:改变 URL,让用户停留在同一页面,记住用户的选择。
改变 URL 对搜索至关重要。让用户停留在同一页面对用户至关重要,也是多数实现出问题的地方:从 /en/pricing 切换后应该落在 /de/pricing,而不是 /de/。记住选择只是锦上添花,且不应覆盖 URL 中显式指定的 locale。
有个反模式值得点名:根据浏览器语言自动重定向。如果美国的爬虫请求 /de/pricing 却被跳转到 /en/pricing,德语页面实际上就不存在了。提供切换选项即可,不要强制切换。
如何为 Lovable 的 TanStack Start 应用添加 locale 路由?
在路由树顶层添加 locale 段,并由它激活目录。
2026 年 5 月,Lovable 把新项目迁移到 TanStack Start 的服务端渲染,带 locale 前缀的路由因此在首次响应中就返回完整翻译的 HTML,正是你希望爬虫拿到的内容。
创建一个包裹其余路由的动态段:
src/routes/
$locale/
route.tsx
index.tsx
pricing.tsx
然后在子路由渲染之前,激活该 locale 的 Lingui 目录:
// src/routes/$locale/route.tsx
import { createFileRoute, Outlet, notFound } from "@tanstack/react-router";
import { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
const LOCALES = ["en", "de", "es", "fr"] as const;
export const Route = createFileRoute("/$locale")({
loader: async ({ params }) => {
if (!LOCALES.includes(params.locale as (typeof LOCALES)[number])) {
throw notFound();
}
const { messages } = await import(
`../../locales/${params.locale}/messages.po`
);
i18n.loadAndActivate({ locale: params.locale, messages });
return { locale: params.locale };
},
component: LocaleLayout,
});
function LocaleLayout() {
return (
<I18nProvider i18n={i18n}>
<Outlet />
</I18nProvider>
);
}
有两个细节很关键。用 notFound() 拒绝未知 locale,可以避免 /xx/pricing 对爬虫尝试的每个无效路径都返回软 200。在路由 loader 中加载目录,意味着激活发生在服务端,早于 HTML 序列化。
如果你用的是旧技术栈,URL 结构相同,渲染路径不同。Lovable Vite i18n 实操指南 覆盖了这种情况,TanStack Start 配置指南 则更深入地讲解了 SSR 部分。
如何构建语言切换器组件?
跳转到同一路由、不同的 locale 参数,并把每个选项渲染为真正的链接。
// src/components/LanguageSwitcher.tsx
import { Link, useLocation, useParams } from "@tanstack/react-router";
const LOCALES = {
en: "English",
de: "Deutsch",
es: "Español",
fr: "Français",
} as const;
export function LanguageSwitcher() {
const { locale } = useParams({ from: "/$locale" });
const { pathname } = useLocation();
const rest = pathname.replace(`/${locale}`, "") || "/";
return (
<nav aria-label="Language">
<ul>
{Object.entries(LOCALES).map(([code, label]) => (
<li key={code}>
<Link
to={`/${code}${rest}`}
hrefLang={code}
aria-current={code === locale ? "true" : undefined}
>
{label}
</Link>
</li>
))}
</ul>
</nav>
);
}
把选项渲染成锚点,而不是绑定路由调用的按钮。锚点可被抓取,除了 sitemap,又为搜索引擎提供了发现 locale 路由的第二条路径。每个选项用该语言自身的名称标注,不要用旗帜:旗帜代表国家,而西班牙语不是国家。
如何正确输出 hreflang 标签?
在集合中的每个页面为每个 locale 输出一条标注,包含自引用,再加一个 x-default。
Google 会校验备用 URL 是否互相回链,并忽略没有回链、或与页面自身 canonical 不一致的标注。因此,用程序生成是唯一靠谱的做法:4 种语言、20 个页面,就是 320 条需要手动保持一致的标注。
在 TanStack Start 中,在路由的 head 里构建它们:
// src/routes/$locale/route.tsx (excerpt)
const SITE = "https://example.com";
const LOCALES = ["en", "de", "es", "fr"] as const;
export const Route = createFileRoute("/$locale")({
head: ({ params, location }) => {
const rest = location.pathname.replace(`/${params.locale}`, "") || "/";
return {
links: [
{ rel: "canonical", href: `${SITE}/${params.locale}${rest}` },
...LOCALES.map((code) => ({
rel: "alternate",
hrefLang: code,
href: `${SITE}/${code}${rest}`,
})),
{ rel: "alternate", hrefLang: "x-default", href: `${SITE}/en${rest}` },
],
};
},
});
由于标注与路由器使用同一个 LOCALES 数组,新增一种语言时,路由、切换器和 hreflang 集合会同时更新。这是需要守住的特性:一份列表,三处消费。
把同样的 URL 连同 xhtml:link 备用项加入 sitemap,并确保 <html lang> 属性与当前 locale 一致。lang 属性不影响 hreflang,但会影响屏幕阅读器和浏览器的翻译提示。
为什么运行时翻译插件做不到?
因为插件是在页面已经以英文返回之后才翻译的。
运行时插件在脚本加载后,才在浏览器中替换文本。爬虫收到的第一份响应是英文 DOM,译文只有在客户端 JavaScript 运行之后才出现。部分插件确实提供子域名或子目录模式,并生成 hreflang,但此时 URL 结构和标注都属于服务商的基础设施,而不在你的仓库里。
这种依赖的代价已不再是假设。Lovalingo 是 Lovable 开发者常用的一款翻译插件,将于 2026 年 8 月 31 日关闭,其官方指引也建议用户迁移到项目自有的国际化方案。插件一旦下线,它生成的语言 URL 随之消失,站点退回纯英文。已提交的目录和写在自己代码里的路由,没有这样的开关。我们在 Lovalingo 替代方案对比 中整理了这次迁移的实操版本。
经典的 Vite SPA 技术栈呢?
URL 结构完全一致,渲染方式不同。
在切换到 TanStack Start 之前创建的 Lovable 项目,是 Vite 加 React 的单页应用,以静态文件形式提供,在浏览器中渲染。你依然可以基于 /:locale 做路由,依然可以输出 hreflang,依然可以把目录放在仓库里。失去的是一项保证:爬虫收到的第一个字节里不一定包含译文。
如果你做本地化就是为了 SEO,请为 locale 路由添加预渲染,让每个页面直接输出真实 HTML,或者规划迁移到服务端渲染的技术栈。不要在客户端渲染的应用上再叠加运行时插件:那会叠加两轮客户端渲染,让每种语言的首次有效绘制都变得更慢。
如何避免四个 locale 逐渐不同步?
自动化提取,并把目录当作源代码对待。
按 locale 路由最终被放弃,原因不在路由本身,而在于每个新功能都会带来没人提取的英文字符串,/de/ 里慢慢塞满英文片段,看起来比没有德语还糟。globalize.now 负责这一层:在 Lovable 工作区添加 lovable-i18n Skill 并提示 Lovable 配置 i18n,或在本地安装 Skill:
npx skills add globalize-now/globalize-skills
加上 --all,即可为所有 Agent 安装。新字符串被提取,键自动生成,目录完成翻译,更新后的 PO 文件在每次 Git 推送时提交回仓库。
定价为每个工作区每月 €20,包含每月 €20 的翻译额度(约 20 万词),超出部分按相同的单字符费率按 token 计费。不按席位收费,也不按语言收费,所以新增第五个 locale 只需改路由配置,账单不会变。注册即送 €5 翻译额度,无需绑卡。更多细节见面向开发者和面向交付 AI 构建应用的人的页面。
路由和 hreflang 一个下午就能搞定。随着应用迭代,让四份目录始终保持完整,才是没有尽头的部分,而这正是 globalize.now 自动化的部分。
globalize.now 把硬编码的应用文案转换为可直接翻译的语言文件,并在你持续发布时保持同步更新。
免费试用 globalize.now