各言語に専用のURLを与え、そのURLに相互参照のhreflangタグを付与しましょう。Reactのstateで文字列を切り替えるだけのドロップダウンは、すでにサイトを訪れているユーザーに対してはインターフェースを翻訳しますが、検索エンジンがインデックスできるものは何も生み出さないため、ドイツ語版やスペイン語版は見えないままです。globalize.nowはAIを活用したローカライゼーション基盤で、ハードコードされた文字列を抽出してGitにコミットされたLingui POカタログに変換し、Git pushごとに同期を保ちます。これがあるからこそ、ロケールごとのルートを一度きりの雑務ではなく、現実的に維持できるようになります。

これはLovableビルダーの多くが見落としているステップです。アプリは翻訳され、スイッチャーもブラウザ上では機能しているのに、トラフィックが一切来ないのです。


なぜGoogleはLovableアプリの他言語をインデックスしないのですか?

それらの言語にはほぼ確実に専用のアドレスが存在しないからです。

AIビルダーが生成する標準的なパターンは、useStateというロケール値と、それを設定するだけのドロップダウンです。どの言語もすべて同じURLでレンダリングされます。クローラーがそのURLをリクエストすると、返ってくるのは1つの言語による1つのレスポンスであり、インデックスされるのは1ページだけです。クローラーが発見できるものは何もなく、他の言語版が存在することを示すシグナルもありません。

必要な修正は構造的なものであり、見た目だけの問題ではありません。Google自身のローカライズ版に関するガイダンスでも、代替言語ページはURLによって識別されること、そしてそれらをつなぐアノテーションはセット内の各ページから返される必要があることが明記されています。/de/pricingが取得可能なアドレスとして存在しなければ、どれだけマークアップを追加しても表面化しません。


本物の言語切り替えに必要なことは何ですか?

順番に3つあります:URLを変える、ユーザーを同じページに留める、そして選択を記憶することです。

URLを変えることが、検索において重要な部分です。ユーザーを同じページに留めることはユーザーにとって重要な部分であり、多くの実装が失敗しがちなポイントです。/en/pricingから切り替えたら/de/pricingに着地するべきで、/de/に着地してはいけません。選択を記憶するのはあくまで便利機能であり、URLに明示されたロケールを上書きしてはいけません。

名前を挙げておくべきアンチパターンが1つあります:ブラウザの言語設定に基づく自動リダイレクトです。米国のクローラーが/de/pricingをリクエストして/en/pricingに転送されてしまうと、ドイツ語ページは実質的に存在しないことになります。切り替えを提供することと、強制することは別物です。


LovableのTanStack Startアプリにロケールルーティングを追加するにはどうすればいいですか?

ルートツリーの最上部にロケールセグメントを追加し、そこからカタログを有効化します。

Lovableは2026年5月に新規プロジェクトをTanStack Startによるサーバーサイドレンダリングへ移行しました。そのため、ロケールをプレフィックスとするルートは最初のレスポンスで完全に翻訳されたHTMLを返します。これはまさにクローラーに受け取ってほしい形です。

残りのルート全体をラップする動的セグメントを作成します:

src/routes/
  $locale/
    route.tsx
    index.tsx
    pricing.tsx

次に、子要素がレンダリングされる前にそのロケール用の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>
  );
}

ここでは2つの詳細が実際に効いています。未知のロケールをnotFound()で拒否することで、クローラーが試すあらゆる無効なパスに対して/xx/pricingがソフト200を返すのを防げます。ルートローダー内でカタログを読み込むことは、HTMLがシリアライズされる前、サーバー側で有効化が行われることを意味します。

従来のスタックを使っている場合、URLの形は同じですがレンダリングの経路が異なります。Lovable Vite i18nの解説ではその場合を扱っており、TanStack StartセットアップガイドではSSR側をより詳しく解説しています。


言語切り替えコンポーネントはどのように作ればいいですか?

異なるロケールパラメーターで同じルートに遷移させ、各オプションを本物のリンクとしてレンダリングします。

// 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>
  );
}

オプションはルーター呼び出しに紐付いたボタンではなく、アンカーとしてレンダリングしてください。アンカーはクロール可能なので、サイトマップに加えてロケールルートへの発見経路を検索エンジンにもう一つ提供できます。各オプションは国旗ではなく、その言語自身でラベル付けしましょう。国旗は国を表すものであり、スペイン語は国ではありません。


hreflangタグを正しく出力するにはどうすればいいですか?

自己参照とx-defaultを含め、セット内のすべてのページ上でロケールごとに1つのアノテーションを出力します。

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配列から生成されているため、言語を1つ追加すればルート、スイッチャー、hreflangセットが一度に更新されます。これこそ守るべき特性です:1つのリストに3つの利用者。

同じURLをxhtml:link代替版としてサイトマップに追加し、<html lang>属性がアクティブなロケールと一致していることを確認してください。lang属性はhreflangには影響しませんが、スクリーンリーダーやブラウザの翻訳提案には影響します。


ランタイム翻訳ウィジェットではこれができないのはなぜですか?

ウィジェットは、ページが既に英語で配信された後に翻訳を行うからです。

ランタイムウィジェットは、そのスクリプトが読み込まれた時点でブラウザ上のテキストを切り替えます。クローラーが最初に受け取るレスポンスは英語のDOMであり、翻訳版が存在するのはクライアント側のJavaScriptが実行された後だけです。一部のウィジェットはサブドメインやサブディレクトリのモードでhreflangを生成する機能を提供していますが、その場合URL構造とアノテーションはあなたのリポジトリではなく、ベンダーのインフラに属することになります。

その依存関係にはコストがあり、それはもう仮定の話ではなくなりました。Lovableのビルダーに人気の翻訳ウィジェットLovalingoは2026年8月31日にサービスを終了し、公式のガイダンスでもユーザーにプロジェクト自前の国際化対応への移行を案内しています。ウィジェットがなくなれば、それが生成していた言語URLも一緒に消え、サイトは英語に戻ってしまいます。自分のコード内でコミットされたカタログとルートには、そのような切り替えスイッチはありません。この移行の実践的な内容はLovalingoの代替比較にまとめています。


従来のVite SPAスタックの場合はどうなりますか?

URL構造は同じですが、レンダリングは異なります。

TanStack Startへの移行前に作成されたLovableプロジェクトは、ViteとReactによるシングルページアプリで、静的ファイルとして配信されブラウザ上でレンダリングされます。それでも/:localeに基づいたルーティングやhreflangの出力、リポジトリ内でのカタログ管理は可能です。失われるのは、クローラーが受け取る最初のバイトに翻訳済みコンテンツが含まれているという保証です。

SEOがそもそもローカライズを行う理由であるなら、各ロケールルートが本物のHTMLを配信するようにプリレンダリングを追加するか、サーバーレンダリングされるスタックへの移行を計画してください。クライアントレンダリングのアプリの上にランタイムウィジェットを重ねて解決しようとしないでください。それではクライアント側のレンダリングパスが2回重なり、どの言語でも最初の有意義な描画がさらに遅くなってしまいます。


4つのロケールが食い違わないようにするにはどうすればいいですか?

抽出を自動化し、カタログをソースコードとして扱いましょう。

ロケールごとのルーティングが放棄される理由はルーティング自体にあるのではありません。新機能を追加するたびに誰も抽出していない英語の文字列が増え、/de/が徐々に英語の断片で埋まっていき、ドイツ語版がないほうがまだ良かったように見えてしまうのです。globalize.nowはその層を扱います:Lovableのワークスペースにlovable-i18nスキルを追加してLovableにi18nの設定を指示するか、スキルをローカルにインストールしてください:

npx skills add globalize-now/globalize-skills

すべてのエージェントにインストールするために--allを追加してください。新しい文字列が抽出され、キーが生成され、カタログが翻訳され、更新されたPOファイルがGit pushごとにコミットされます。

料金はワークスペースごとに月額20ユーロで、月あたり20ユーロ分(約20万語相当)の翻訳クレジットを含みます。それを超える分は同じ文字単価でのトークン利用課金になります。ユーザー単位や言語単位の追加料金は一切ないため、5つ目のロケールを追加してもルーティング設定が変わるだけで、料金は変わりません。サインアップには5ユーロ分の翻訳クレジットが付与され、カード登録も不要です。詳しくは開発者向けおよびAI構築アプリを公開する人向けの情報をご覧ください。

ルーティングとhreflangの設定は半日で終わる作業です。しかし、アプリが変化していく中で4つのカタログを常に完全な状態に保つことは、終わりのない作業です。その部分を自動化するのがglobalize.nowです。

globalize.nowは、ハードコードされたアプリ内テキストを翻訳可能なロケールファイルに変換し、リリースのたびに自動で最新の状態を保ちます。

globalize.nowを無料で試す