アラビア語版は壊れて見えるのに、翻訳自体は問題ありません。サイドバーは左側に居座ったまま、戻る矢印は左を向いたまま、アバターはどのカードでも左端にぴったりくっついている——単語はすべてアラビア語なのに、ボックスの位置は英語版のときのままなのです。テキストの方向は HTML の属性と、コンポーネント内に存在する一連の CSS の判断で決まるものであり、どんなカタログファイルにもその情報は載っていません。globalize.now は AI を活用したローカライゼーション基盤で、文字列を抽出し、Git への push のたびにカタログを同期し続けます。
アラビア語にしても、なぜ Lovable アプリは相変わらず左から右に読める見た目のままなのでしょうか?
方向は文言(メッセージ)ではなく、ドキュメントとスタイルシートのプロパティだからです。
カタログは元の文字列と翻訳済み文字列のペアです。ar向けにコンパイルされ、コンポーネントがそれをレンダリングすれば、変わるのは言葉だけで他は何も変わりません。そのカタログが globalize.now が書き込む Lingui であっても、react-i18next であっても、ここでは違いはありません。どちらのフォーマットにも「サイドバーを反対側に配置する」という情報を置く場所がないのです。
変えるべきことは2つです。html要素のdir属性は、インライン軸がどちら向きに流れるかをブラウザに伝えます。そして CSS の側は、辺(サイド)を指定するのをやめて、その軸の開始と終了を指定するように変える必要があります。他のすべては、この2点から自然に導かれます。
Lovable アプリにアラビア語を追加すると、実際には何が壊れるのでしょうか?
不具合は決まった場所に集中して発生し、そのすべてがコピー(文言)ではなくスタイリングの問題です。
- ナビゲーションドロワーやサイドバーが物理的な左側に留まったままになる。
- シェブロン、戻る矢印、進捗インジケーターが間違った向きを指す。
- 非対称なパディングによってコンテンツが誤った端に固定され、ラベルが片側に寄って窮屈になる。
- 見出しに
text-leftが指定されていると、アラビア語のテキストがコンテナの左側に固定されたままになる。 - 絶対配置されたバッジや閉じるボタンが、読み手が想定するのとは反対の隅に着地してしまう。
- アクティブなリスト項目を示す左ボーダーが、先頭側ではなく末尾側に表示されてしまう。
- アラビア語の文中にある英語の商品コードは、方向のヒントがないと予測不能な順序で並び替わってしまう。
これらはどれも翻訳の不具合ではありません。すべてクラス名に焼き付けられた物理的な方向指定に起因するものです。
ロケールタグからテキストの方向をどう導出すればいいですか?
ロケールにどちらの方向を使うかを尋ね、その答えをドキュメントに反映させます。
Intl.Locale.prototype.getTextInfo()は、ロケールタグに対してltrかrtlのどちらかの方向を返します。これは2026年7月に Baseline newly available ステータスに達しました。つまり主要ブラウザの最新版には対応しているものの、実際にユーザーが使っているほとんどのブラウザにはまだ対応していないということです。以下に示すフォールバックを現時点での実運用パスとして扱い、この API はいずれフォールバックを不要にする将来の手段として捉えておきましょう。
// getTextInfo() is not yet in TypeScript's bundled Intl.Locale definitions.
declare global {
namespace Intl {
interface Locale {
getTextInfo?(): { direction: "ltr" | "rtl" };
}
}
}
// Stopgap for runtimes without getTextInfo(). This set is the one place where
// adding a new right-to-left language still costs you a code change.
const RTL_FALLBACK = new Set([
"ar", "he", "fa", "ur", "ps", "sd", "yi", "dv", "ckb", "ug", "nqo", "syr",
]);
export function directionFor(locale: string): "ltr" | "rtl" {
let tag: Intl.Locale | undefined;
try {
tag = new Intl.Locale(locale);
} catch {
// Malformed tag. Fall through to the string path below.
}
const direction = tag?.getTextInfo?.().direction;
if (direction === "ltr" || direction === "rtl") return direction;
const language = tag?.language ?? locale.split(/[-_]/)[0].toLowerCase();
return RTL_FALLBACK.has(language) ? "rtl" : "ltr";
}
重要なのはtryが使われる2つの箇所です。new Intl.Locale()は不正な形式のタグに対してRangeErrorをスローし、getTextInfo()は古いランタイムでは単純に存在しません。よくあるバグは後者だけを処理してしまうケースです。たとえばクエリ文字列から届いたar_EGのようなロケールがあると、関数は自身のリカバリ処理に入り、そこから抜け出せずに未捕捉のエラーとして返ってきてしまいます。
すでにアプリがアクティブなロケールを把握している箇所であれば、どこでも両方の属性を設定してください。Lovable は現在、Vite のシングルページアプリと、サーバーレンダリングの TanStack Start アプリという2つの構成をスキャフォールドするので、自分のプロジェクトが実際にどちらを生成したか確認しましょう。TanStack Start ならルートのドキュメントコンポーネントが該当箇所です。Vite の場合は i18n プロバイダーをマウントしている箇所になります。
<html lang={locale} dir={directionFor(locale)}>
dirはラッパーのdivではなくhtmlに設定してください。ポータル、モーダル、トーストはコンポーネントツリーの外側にレンダリングされますが、それでもこの設定を継承する必要があるからです。
右横書き対応のために変更が必要な Tailwind のクラスはどれですか?
物理的な方向を指定しているクラスです。それらに対応する論理的なクラスはインライン軸に沿って動作し、dirが変わると自動的に反転します。
| 物理的なユーティリティ | 論理的な置き換え | 設定される内容 |
|---|---|---|
| ml-4 / mr-4 | ms-4 / me-4 | margin-inline-start / margin-inline-end |
| pl-6 / pr-6 | ps-6 / pe-6 | padding-inline-start / padding-inline-end |
| left-0 / right-0 | start-0 / end-0 | inset-inline-start / inset-inline-end |
| text-left / text-right | text-start / text-end | インライン軸方向のテキスト配置 |
| border-l / border-r | border-s / border-e | 先頭側/末尾側のボーダー |
| rounded-l-lg / rounded-r-lg | rounded-s-lg / rounded-e-lg | 先頭側/末尾側の角丸 |
| float-left / float-right | float-start / float-end | インライン軸方向のフロート |
| clear-left / clear-right | clear-start / clear-end | インライン軸方向のクリア |
| scroll-ml-4 / scroll-mr-4 | scroll-ms-4 / scroll-me-4 | スクロールスナップのマージン |
Tailwind のマージンに関するドキュメントを見れば、この効果が一目瞭然です。dir="ltr"コンテナとdir="rtl"コンテナの中でレンダリングされたms-8とme-8は、それぞれ反対側に位置します。論理プロパティのユーティリティは Tailwind v3.3 で導入されたため、長く運用されているプロジェクトにはまだ含まれていないことがあります。
リネームだけでは済まず、あらためて検討が必要なユーティリティが2つあります。space-x-*とdivide-x-*は兄弟要素間に間隔やボーダーを追加するものなので、行が反転していると各子要素の間違った側に配置されてしまいます。Tailwind はまさにこのケース向けにspace-x-reverseとdivide-x-reverseを用意しています。mt-*やpb-*のような垂直方向のユーティリティは、方向がインライン軸にしか作用しないため、何も変更する必要がありません。
論理プロパティで解決できないことは何ですか?
ボックスモデルの外側で方向をエンコードするものすべてです。まさにここでrtl:とltr:のバリアントが役立ちます。
<ChevronRight className="rtl:rotate-180" />
<div className="bg-[url(/hero.svg)] bg-left rtl:bg-right" />
<div className="shadow-[4px_0_8px_rgba(0,0,0,.15)] rtl:shadow-[-4px_0_8px_rgba(0,0,0,.15)]" />
変形(transform)、背景の位置、シャドウのオフセット、グラデーションの角度、カルーセルのスクロール計算、そして「前進」を意味するアイコン全般がこれに該当します。ただしこのリストは短く保ちましょう。ほぼすべての要素にrtl:を書いているようであれば、それは元々のスタイルがまだ物理的なままであり、本来は変換すべきだという合図です。
ユーザー生成コンテンツには専用の対応が必要で、2つのケースは性質が異なります。言語を制御できないブロック全体に対しては、dir="auto"を使うことでブラウザがブロックの先頭にある強い方向性を持つ文字から基本方向を決定してくれます。
<p dir="auto">{review.body}</p>
すでに方向が決まっている文の中に紛れ込む外国語スクリプトの一部分、たとえばアラビア語の本文中にある英語の SKU のようなケースでは、dir="auto"は何の効果もありません。というのもこれは段落全体の基本方向を設定するものであり、インライン部分だけを分離するものではないからです。この場合は、その部分を別途囲んで対応してください。
<bdi>{product.sku}</bdi>
なぜ翻訳ウィジェットではレイアウトを自動的に反転させられないのでしょうか?
ウィジェットはレンダリング済みのページ上のテキストノードに対して動作するものであり、方向の問題はそのページを生み出したスタイルの側にあるからです。
ランタイムサービスはドキュメントにdir="rtl"を注入し、英語版が描画された後に表示されている文字列を翻訳することはできます。しかしコンポーネントを開いてml-4をms-4に書き換えることはできません。なぜならml-4は、そのサービスが所有していないスタイルシートの中で、すでにmargin-leftとしてコンパイル済みだからです。結果として得られるのは、左横書きの器の中にアラビア語のテキストが入っている状態です。
もう一つコストがあります。一部の Lovable アプリで使われているランタイム翻訳サービス Lovalingo は、2026年8月31日23時59分(中央ヨーロッパ夏時間)をもってサービスを終了すると発表しました。同社の移行ガイダンスでは、顧客に対しプロジェクト側で国際化対応を持つ体制へ移行すること、そしてサービスを外す前にロケールルート、正規URL、hreflang を改めて確認するよう案内しています。ベンダーの製品内で行ったレイアウト作業は、そのベンダーとともに失われます。しかし、自分のリポジトリに置いた論理プロパティのユーティリティとdir属性は失われません。私たちのLovalingo 比較記事でも、文字列レイヤーについて同じ主張をしています。
次の Lovable プロンプトによって、この対応が元に戻されてしまうのをどう防げばいいですか?
一度変換したら、あとは物理的なユーティリティがレビューで目に見えるようにしておきましょう。
- ワークフローをインストールします。Lovable ワークスペースで
lovable-i18nスキルを追加し、Lovable に i18n のセットアップを依頼するプロンプトを送ります。エディタの外でインストールする場合はnpx skills add globalize-now/globalize-skillsを使い、すべてのエージェント向けにインストールするには--allを付けます。 - 上記の
directionForを使い、html要素にロケールから方向を設定します。 - 物理的なユーティリティを、コンポーネント全体にわたって論理的なものへ変換します。まずはページ全体をラップしている部分から始めましょう。
rtl:バリアントは、変形(transform)、背景、シャドウ、方向性のあるアイコンにのみ追加し、インライン内の外国語スクリプト部分には<bdi>を使いましょう。- レビューに grep によるゲートを追加し、新しく生成された物理的なユーティリティがマージされる前に失敗するようにします。
rg -n '\b(ml|mr|pl|pr|scroll-m[lr])-|\b(float|clear|text)-(left|right)\b|\bborder-[lr]\b|\brounded-[lr]-|\b(left|right)-[0-9]' src/
5番目のステップこそが、実際に効果を発揮し続けるものです。Lovable はプロンプトのたびにコンポーネントを再生成しますし、設定パネルの追加を頼まれたモデルはml-4に手を伸ばしがちです。これらのモデルが学習した React コードでは、物理的なユーティリティが圧倒的に多いからです。プルリクエストを失敗させるチェックのほうが、3週間後にアラビア語のスクリーンショットで問題を見つけるよりもずっと安上がりです。
文字列側のループは、それ自体で自動的に回っていきます。globalize.now は Lovable が生成した新しいハードコード文字列を抽出し、カタログに書き込み、Git への push のたびに同期を行います。つまり、あなたがプロンプトを送り続ける間もアラビア語版は最新の状態を保ちます。これはVite ガイドやTanStack Start ガイドで説明しているのと同じ仕組みであり、だからこそpush のたびに翻訳が壊れることがなくなるのです。
アラビア語を追加しても、プラン上の追加費用は一切かかりません。Starter プランはワークスペースごとに月額20ユーロで、20ユーロ分の翻訳クレジット(約20万語相当)が含まれており、それを超えた分は同じ文字単価で利用を継続できます。シート課金も言語ごとの課金もなく、右横書き言語も含まれます。設定に関する詳細はLovable 連携ページ、エンジニアリングの詳細は開発者向けページ、そして非エンジニアの制作者向けにはより簡単な手順をvibe coder 向けページで紹介しています。
レイアウトの変更は一度きり、あとはそのまま維持される
方向は、あなたのリポジトリが一度だけ下す決定です。プロンプトを送るたびに変わるのは文字列の部分であり、その部分は自動的に回り続けます。
globalize.nowは、ハードコードされたアプリ内テキストを翻訳可能なロケールファイルに変換し、リリースのたびに自動で最新の状態を保ちます。
globalize.nowを無料で試す