Webアプリの多言語対応 — lang属性、ロケール判定、そして書式のローカライズ
Webアプリの多言語対応は、モバイルアプリよりも多くのレイヤーに関わります。一つのページはブラウザ、検索エンジンのクローラー、スクリーンリーダーという、それぞれロケールのシグナルを別の方法で読み取る三者に同時に対応しなければならないからです。作業の多くは、これらのシグナルを互いに、そして実際にユーザーが見ている内容と一致させることです。
lang属性は飾りではない
html要素のlang属性は、ページのコンテンツが何語かをブラウザ、スクリーンリーダー、検索エンジンに伝えます。これはスペルチェック辞書の選択、スクリーンリーダーがどう発音するか、ハイフネーションのルールなど、意外なところに影響します。lang属性はページに実際に表示されている言語と一致し、ページの言語が切り替わればそれに応じて更新されるべきで、アプリを最初に作った言語のまま固定されるべきではありません。
よくある失敗は、ルートのレイアウトでlangを一度だけ設定してそのままにしてしまうことです。その結果、完全に別の言語に翻訳されたページが、宣言上はソース言語のままになります。この属性を読むツールや支援技術は、目に見えるテキストよりもこの属性の方を信頼します。
<html lang="ja"> <!-- ここから下はすべて日本語として発音・処理される --> </html>
なぜロケールはURLに入れるべきか
どの言語を表示するかを決める方法として一般的なのは、URLの一部やサブドメイン、過去の訪問から設定されたクッキー、ブラウザが送るAccept-Languageヘッダーの三つです。この三つはすべて、実際にブラウザの前にいる人間には機能しますが、検索エンジンが実際にインデックスできるものを提供できるのはURLベースの方法だけです。
クローラーは通常、訪問間でクッキーを保持しませんし、実際のユーザーのブラウザのような意味のあるAccept-Languageの希望も送りません。そのためクッキーやヘッダーだけで言語を分岐させているサイトは、サイトが実際に何言語対応していても、クローラーには常に一つの言語 — たいていはデフォルト言語 — しか見せられません。/ja/pricingと/en/pricingのようにロケールをURLに入れれば、各言語版は独立した、リンク可能でインデックス可能なページになります。
クッキーやヘッダーによる判定にも役割はあります。初めて訪れたユーザーをどの言語のページにリダイレクトするかを決めるのには妥当な方法です。ただしその判定をした後は、その選択を暗黙に記憶するだけでなく、選択をエンコードしたURLにユーザーを着地させるべきです。
hreflangで検索エンジンに代替ページを伝える
言語ごとに別々のURLを用意したら、hreflangのlinkタグを使って、それらのURLが同じページの別言語版であることを検索エンジンに伝えます。これにより検索エンジンは、それらを無関係な重複コンテンツとして扱うのではなく、検索者の言語や地域に合ったバージョンを表示できるようになります。
<link rel="alternate" hreflang="en" href="https://example.com/en/pricing" /> <link rel="alternate" hreflang="ja" href="https://example.com/ja/pricing" /> <link rel="alternate" hreflang="x-default" href="https://example.com/pricing" />
ロケールに応じたAPIで日付や数値を書式化する
日付や数値の書式は言語圏によって異なります。日・月・年の並び順、小数点や桁区切りに使う記号、通貨額の書き方などです。フォーマット文字列を決め打ちすると、一つの言語圏の慣習をすべてのロケールに押し付けることになり、それはほとんどのロケールにとって不自然な結果になります。
標準的な対処は、ロケールを意識したフォーマット用APIを使い、現在のロケールをパラメータとして渡すことです。フォーマット文字列を手で組み立てるのではなく、プラットフォーム側に正しい慣習を適用させます。
- 日付・数値・通貨の書式化は、決め打ちではなく必ず現在のロケールを渡して行う
- 内部的には曖昧さのない形式で日付やタイムスタンプを保持し、表示時にのみローカライズする
- カンマが小数点か桁区切りかを決め打ちせず、ロケールごとにフォーマッタに判断させる
翻訳対象のテキストはテンプレートやコードから切り離す
モバイルアプリと同じ規律がここでも当てはまります。翻訳対象の文字列は、テンプレートに直接書いたりコンポーネントのコードに散らばらせたりせず、識別子をキーとした専用のファイルに置きます。「Welcome, 」+ name + 「!」のように変数の周りに文の断片を連結するテンプレートは、元の言語の語順を前提にしており、語順の異なる言語に翻訳した途端に崩れることがよくあります。
より安全なパターンは、変数をプレースホルダーとして含む文全体を渡すことです。こうすれば翻訳者は、その言語が必要とする語順に合わせて文全体を組み替えられます。
// 壊れやすい: 語順を前提にしている
"Welcome, " + name + "!"
// 安全: 翻訳者が語順を決められる
t("welcome_message", { name })まとめ
ここまで挙げたものはどれも「動くアプリへのおまけ」ではありません。それぞれ別の読み手が読んでおり、一つでも省くとその読み手には言語の不一致が見えてしまいます — クローラーが誤ったページをインデックスする、スクリーンリーダーが誤った発音をする、その地域では見慣れない書式で日付が表示される、といった具合です。