コンテンツへスキップ
製品アップデート

Astroでサイトを9言語にローカライズした話

Alex Rivera1 分の読書

今回のスプリントで、9言語分のロケールルーティングをリリースしました。ここでは、それがどのように実現したかを正直に振り返ります。私たちが気づく前に、静かに本番環境を傷つけていたバグの話も含めて。

発見

Sprint 1の時点で、想定通りのi18n用の骨組みはすでに作られていました。9言語(英語、スペイン語、アラビア語、ドイツ語、フランス語、日本語、ポルトガル語、ロシア語、中国語)分のロケールJSONファイル、getTranslation() ヘルパー、ロケール対応URLを構築する getLocalePath()、そして9つのロケールバリアントすべてを指すhreflangタグを、すべてのページに設置していました。

作っていなかったもの、それはそれらのロケールURLに実際に存在するページそのものでした。

src/pages/ 内のすべてのページで const locale = "en" as const がハードコードされていました。翻訳の仕組みは存在し、正しく動作していましたが、その先には何もありませんでした。つまり BaseLayout.astro は、すべてのページで https://proman365.com/es/pricing/ のようなhreflangタグを出力していましたが、そのURLは本番環境では404を返していたのです。

これは見た目だけの問題ではありません。検索エンジンはhreflangタグを使って、どのURLが互いの言語バリアントであるかを判断します。大規模に404を宣伝することは、実質的なネガティブSEOシグナルです。クローラーに対して「このサイトのメタデータは実態と一致していない」と伝えることになり、壊れている箇所だけでなく、メタデータ全体への信頼を損ないます。

修正: 複製ではなくテンプレート抽出

安易な修正方法は、非英語ロケールごとに各ページを8回コピーし、それぞれに異なるロケール定数をハードコードすることです。これは即座に却下しました。ファイル数が8倍になるということは、誰かがページに手を加えるたびに、残り7つのコピーを忘れるリスクも8倍になるということだからです。

代わりに採用したアプローチは、各ページの内容を locale をpropとして受け取るテンプレートコンポーネントに抽出し、そのテンプレートを2種類のルートからレンダリングするというものです。

12のTier-1ページ(ホーム、料金、お問い合わせ、パートナー、ヘルプ、そして6つの機能ページ)について、それぞれ src/pages/*.astro から src/page-templates/*.astro に移動し、ハードコードされていたロケール定数を次のように置き換えました。

interface Props {
  locale: Locale;
}
const { locale } = Astro.props;

元の英語ルートは薄いラッパーになります。テンプレートをインポートして locale="en" でレンダリングする、数行だけのコードです。src/pages/[locale]/ 配下の新しいミラールートは、8つの非英語ロケール向けに同じテンプレートをレンダリングし、getStaticPaths() を使ってロケールごとに1ページずつ生成します。

これがきれいに機能したのは、各Tier-1ページのロジック — getTranslation(locale)、メタタグビルダー、スキーママークアップ — が、Sprint 1の骨組みの時点ですでにその1つの locale 定数でパラメータ化されていたからです。私たちはこれらのページにi18nを後付けしたのではなく、使われずに眠っていたi18nをようやく配線し直しただけでした。

その結果、Tier-1の展開だけで96の新しい静的ページ(12ページ×8ロケール)が生まれ、続いて同じパターンを64の比較ページ(8競合×8ロケール)にも拡張しました。

localized propによるhreflangのゲーティング

ルーティングの穴を塞いだだけでは、hreflangの問題は自動的には解決しません。タグが実際に真実であることだけを主張するようにする必要があります。そこで BaseLayoutPageLayout に、デフォルトを false とする localized?: boolean propを追加しました。

localizedfalse の場合、ページは enx-default の2つのhreflangタグだけを出力します。true の場合は、9ロケールすべてと x-default を合わせた10個をフルセットで出力します。localized={true} を渡すのは、Tier-1のテンプレートと比較ページだけです。

デフォルトを false にしたのは、意図的な安全策です。ページは「9つの言語バリアントを持つ」と主張するために、明示的にオプトインしなければなりません。もし誰かが明日新しいページを追加し、ロケールについて考えるのを忘れたとしても、安全側に倒れます。英語のみのメタデータになるだけで、幻のURLがまた増えることはありません。

意図的にローカライズしなかったもの

すべてをロケール対応にしたわけではなく、それは見落としではなく意思決定です。

  • ブログ は当面、英語のみのままにします。これはSaaSマーケティングブログとしては標準的な運用であり、長文コンテンツを機械翻訳すると、翻訳しないよりも質の低い文章になってしまいます。もしアナリティクスが専用のロケール別コンテンツを正当化するようになれば、そのときに見直します。
  • 法務ページ(プライバシー、利用規約、Cookieポリシー)は英語のままです。この文章はTermlyで生成された法的テキストであり、それを翻訳するかどうかは私たちが単独で判断できる話ではありません。
  • 404ページ は英語のままです。静的ホスティングはリクエストされたパスに関係なく単一の404を返すため、別のホスティングモデルなしにこれをローカライズするきれいな方法がありません。
  • RSS はもともと英語のみで、hreflangから正しく除外されていました。ここに変更は必要ありませんでした。

教訓チェックリスト

静的なAstroサイトで同様のことをするなら、次の点を確認してください。

  1. 構築する前に監査する。 hreflangタグが実際に存在するページを指しているかを確認しましょう。i18nの骨組みがルーティングより先に作られている場合、すでにこのバグを抱えている可能性があります。
  2. ページを複製せず、テンプレートを抽出する。 ページごとに1つのテンプレートを用意し、その上にロケール固有の薄いラッパールートを置きます。腐敗が起きるとすればラッパー層であり、ラッパーは同期を保つコストが低いものです。
  3. メタデータの主張は、成立する最も狭い範囲をデフォルトにする。 デフォルトが「いいえ」である localized フラグがあれば、新しいページが誤って過大に主張することはありません。
  4. 何を意図的に翻訳しないままにするかを決め、記録する。 ブログ、法務、エラーページは除外の妥当な候補ですが、それは沈黙ではなく文書化された決定にすべきです。
  5. 翻訳とルーティングは別の問題である。 ルートを出荷するために翻訳を完成させる必要はありません。実在するロケールルートにある英語のフォールバックテキストのほうが、ルートの存在しない動作する翻訳キーよりもまだ正直です。

手作業での翻訳作業はまだこれからです。非英語ロケールファイルには、人間の翻訳者を待っている英語のプレースホルダー文字列がまだ多く残っています。しかしURLは今や実在し、メタデータは嘘をつかなくなりました。それがより緊急な問題でした。

シェアXLinkedIn

Alex Rivera

プロジェクトマネージャー兼ワークフローコンサルタント。チームが実際の働き方に合ったツールを見つける手助けをしています。

最新情報を受け取る

プロジェクト管理のヒントや製品アップデート情報をお届けします(スパムなし、いつでも購読解除可能)。

ツールをシンプルにしませんか?

Promanを無料で試してみましょう(クレジットカード不要)。

無料で始める →