ボタン、ナビゲーション、空の状態表示はドイツ語になっています。しかし商品名、カテゴリ、説明文は依然として英語のままです。これはi18n設定の不具合ではなく、その仕組みの限界です。globalize.nowはAI駆動のローカライズ基盤です: コードベースからハードコードされた文字列を抽出し、キーとロケールカタログを生成し、gitへのプッシュのたびにそれらを同期します。読み取るのはコードであってレコードではなく、ユーザーが本来求めているコンテンツはそのレコードの中にあります。

これを解決するのはツールの選定ではなく、スキーマ設計の判断です。ここでは、実際に通用する構成を紹介します。

LovableアプリのUIは翻訳されているのに、なぜデータベースのコンテンツは依然として英語のままなのですか?

それは、文字列抽出ツールがソースファイルに書かれているものしか見ることができないからです。

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' のように、そのまま画面表示する enum 的な値
  • ユーザー生成コンテンツ。通常は意図的に原文の言語のまま残しておくもの

最後の項目が特に重要です。ユーザー生成コンテンツはデフォルトでは翻訳しないでください。スペイン語で書かれたレビューはスペイン語のままにしておくべきです。実際にローカライズすべき対象は、あなた自身が作成し配信するコンテンツです。

それ以外——そのコンテンツを取り囲む「枠」の部分——は、すべてカタログ側に置いておきます。この2つの集合は決して重複してはいけません。あるラベルがルックアップテーブルとハードコードされた文字列の両方に存在している場合は、どちらか一方に置き場所を決めて、もう片方は削除してください。そうしないと、同じ概念に対して異なる2つのドイツ語訳をリリースしてしまうことになります。

Supabase でデータベースコンテンツの翻訳をどう保存するか?

行の id とロケール用のカラムをキーにした、専用の翻訳テーブルを使います。

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

これにより、日本語を追加する作業はマイグレーションではなく単なる INSERT になります。行レベルセキュリティのポリシーも、増え続けるカラム群ではなく1つのテーブルに紐づくだけになりますし、翻訳が欠けている状態は「行が欠けている」状態として扱えるので、クエリで検出したりアラートを設定したりするのも簡単です。

もう一つの選択肢は、翻訳可能なフィールドごとに JSONB カラムを持たせる方法です。

alter table products
  add column name_i18n jsonb not null default '{}'::jsonb;

-- { "en": "Trail Runner", "de": "Trail Runner", "fr": "Coureur de sentier" }

この方法はプロトタイピングが速く、小規模でほぼ静的なコンテンツには向いています。ただし、言語ごとの制約を課すコストがかかり、部分的な翻訳の監査がやりにくくなり、ローカライズされた値でフィルタやソートを行うようになるとインデックス化のコストが高くつきます。

3つ目のパターン、つまり言語ごとにカラムを分ける方式(name_en、name_de、name_fr)は避けるべきです。新しい言語を追加するたびに、すべてのテーブルに対してスキーマのマイグレーションが必要になりますし、Lovable プロジェクトの場合、それはエージェントに本番テーブルの変更を繰り返し依頼することを意味します。このパターンをより深く解説した資料が欲しければ、公開されている 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;
$$;

そうすれば、呼び出し側が持つのはロケールだけになります。

const { data: products } = await supabase
  .rpc('products_for_locale', { p_locale: locale });

これを実際のデータに耐えるものにするには、2つのルールが必要です。まず、coalesce はソース言語にフォールバックさせること——空文字列にフォールバックさせてはいけません。半分だけ翻訳されたカタログは、空欄のカードではなく英語を表示すべきです。次に、渡す locale は、ルートセグメントで使っている値、UI カタログのキーとして使っている値と、まったく同じ値でなければなりません。ロケールリストは1つ、それを使う側は3つ——ルーター、カタログ、データベースです。

多くの Lovable プロジェクトがずれてしまうのは、まさにこの最後の点です。ルーターは de しか知らず、カタログは de-DE 用に作られていて、データベースの行には german というタグが付いている、といった具合です。ロケールルートで使う BCP 47 タグの正確な集合を1つ決めて、それ以外のすべてをそれに合わせてください。まだそうしたルートを整備していない場合は、Lovable が生成する2つのスタック——Vite SPA ビルド と TanStack Start の SSR デフォルト——それぞれ向けのウォークスルーで手順を解説しています。

なぜランタイムのウィジェットにデータベースのテキスト翻訳を任せてはいけないのか?

それは、描画後のページに対して働くものであって、あなたのデータに対して働くものではないからです。そしてその違いは3つの場面で表面化します。

JavaScript の翻訳ウィジェットは、DOM に現れたテキストを読み取って事後的に差し替えます。これは確かにデータベースの行も捕捉します——カタログ方式では実現できない、ウィジェットならではの利点であり、だからこそ最初は「完全な解決策」に見えるのです。

しかし、そのコストは構造的なものです。翻訳された行はブラウザのセッション内にしか存在しないため、ページをクロールする検索エンジンにはソース言語のまま見えます。実際、Weglot 自身のヘルプセンターでも、その JavaScript 統合は SEO 上のメリットを提供しないと明言されています。翻訳がクライアントサイドで描画されるためです。修正内容はデータベースではなくベンダーのダッシュボードに保存されるため、あるフィールドの「本当の値」が2つのシステムに分断されてしまいます。そして、この差し替えは描画後に発生するため、この方式で作られた翻訳アプリはナビゲーションのたびに一瞬英語がちらつくことになります。

だからといって、社内向けツールでこのアプローチをまるごと却下する理由にはなりません。ただし、公開され、検索エンジンにインデックスされる多言語プロダクトをこの上に構築すべきではない理由にはなります。

この2つの層はどうやって同期を保つのか?

1つのロケールリストと1つのトリガーを共有することによってです。

コード層は自動化されています。Lovable ワークスペースに lovable-i18n スキルを追加して、Lovable に i18n をセットアップするよう指示するか、あるいはローカルに次のようにインストールしてください。

npx skills add globalize-now/globalize-skills

それ以降は、抽出処理が実行され、プッシュのたびに翻訳のプルリクエストが自動で開かれます——ダッシュボードでの作業も、ロケールファイルを手作業で確認する工程も不要です。これは Lovable の翻訳がプッシュのたびに壊れる理由 で説明されているのと同じループです。

データ層はあなた自身が管理するものであり、そこには1つ、意図的なフックが必要です。ロケールを追加するたびに、ルートやカタログを駆動しているのと同じリストが、翻訳テーブルへのバックフィルも駆動すべきだということです。Lovable プロジェクトにおける実践的な形は、ロケールリストを読み取り、ソース言語の値を使って欠けている行を挿入する、エージェントに一度だけ書いてもらうシードファンクションです。欠けている行はそうして埋められるのであって、でっち上げられるわけではなく、その間のギャップは coalesce がカバーします。

価格設定もここで足を引っ張ることはありません。Starter プランはワークスペースあたり月額20ユーロで、月20ユーロ分(約20万語相当)の翻訳クレジットが含まれています。それを超える利用分は、公開されている文字単価で課金されます。シート課金も言語数課金も一切ないため、5つ目の言語を追加してもかかるコストは実際の文字数分だけです。サインアップ時には5ユーロ分の翻訳クレジットが付与され、クレジットカードの登録も不要です。エージェントが構築するスタックにこれがどう組み込まれるかについては、vibe coders ページと developers ページで詳しく解説しています。

ローカライズベンダーが事業を終了すると、何が壊れるのか?

彼らのサーバーに保存されていたものはすべて失われますが、あなた自身のサーバーに保存されていたものは何一つ失われません。

これは今月の仮定の話ではありません。多くの Lovable ビルダーが採用したランタイム翻訳レイヤーである Lovalingo は、2026年8月31日 23時59分(中央ヨーロッパ夏時間)をもってサービスを終了します。これは同社自身のサービス終了ページで確認されており、新規プロジェクトおよび新規契約はすでに停止され、既存の利用中の顧客向けにはデータ保全のためのエクスポート機能が準備されています。興味深いのは、同社自身のガイダンスの中身です。プロジェクト自身が所有する国際化対応をこのレイヤーと並行して実装するよう顧客に指示し、本番環境で実際に稼働確認が取れた後にのみ、このレイヤーを外すよう勧めているのです。

これは、その撤退自体が証拠となっているベンダー自身の口から出た、一文に凝縮された議論そのものです。コミット済みのカタログと product_translations テーブルは、どちらもあなた自身が所有するものであり、「サービスステータス」というものが存在しません。マネージドなランタイムレイヤーは、あなたのプロダクトのテキストを描画するための依存関係であり、依存関係はいつか終わるものなのです。

もし今まさにウィジェットからの移行を進めているなら、まずデータ層から手を付けてください。UI の文字列は、あなたのコードから数分で再抽出できます。しかし、データベースの翻訳は、それがベンダーのダッシュボードの中にしか存在したことがなかった場合、二度と戻ってきません。何かを取り除く前に確認すべきことは、私たちの Lovalingo 比較記事 にまとめてあります。

コード層は、本来あなたが手作業で管理すべきではない部分です。globalize.now はあなたの文字列を抽出し、カタログを生成し、プッシュのたびに翻訳のプルリクエストを開いてくれるので、あなたはデータ層を一度設計してしまえば、あとは気にせずに済みます。

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

globalize.nowを無料で試す