先给每种语言分配独立 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