ロケールルーティング
ページの翻訳を導入すると、次は「このリクエストはどの言語で 返すのか」を誰かが決める必要が出てきます。この判断はハンドラではなくルートのもの です。同じ判断が「その応答をどうキャッシュしてよいか」も決めていて、ここを間違えた ページはある読者の言語を次の読者に返します。
1言語で運用しているプロジェクトにはこれは要りません。[i18n] ブロックがなければ
方式も交渉も Vary も存在しない。2つ目の言語ができた瞬間から効いてくる話です。
方式は3つ、パス接頭辞ごとに宣言します。
[i18n]locales = ["ja", "en"]default_locale = "ja"path_routes = ["/"]cookie_routes = ["/admin/"]header_routes = ["/api/"]最長一致が勝つので、上の /api/ は / に覆われていてもヘッダー交渉になります。
どのルートがどれを選ぶか
Section titled “どのルートがどれを選ぶか”公開ページは path_routes。 言語がパスセグメントなので、2言語は2つの URL です。
Vary が付かず、共有キャッシュが普通に効き、hreflang に指す先ができます。検索エンジンが
言語ごとにインデックスできるのはこの方式だけです。
認証後のアプリケーションは cookie_routes。 読者が設定画面で選んだ言語は残るべき
なので、保存された選択がブラウザのヘッダーより優先されます。代償は本物です。応答が
Cookie に Vary し、HTTP は「この Cookie にだけ Vary する」を表現できないので、
共有キャッシュがセッションごとに割れます。認証済みページはもともとそういうものなので、
このコストが公開ツリーではなくここに落ちるのは筋が通っています。
API は header_routes。 ネイティブクライアントは端末が報告する言語を送り、Cookie を
管理しません。そして本文が Cookie の状態で変わる API は、クライアントが言っていない
理由で同じリクエストに2通りの答えを返していることになります。
宣言したどの接頭辞にも当たらないパスは、URL に何も出さずに既定言語で返します。 だから一部のサブツリーにだけ i18n を入れても、残りは今まで通りです。
方式ごとの Vary
Section titled “方式ごとの Vary”| 方式 | 決め方 | Vary |
|---|---|---|
path |
URL の接頭辞 | なし |
cookie |
ロケール Cookie、次に Accept-Language |
Cookie, Accept-Language |
header |
Accept-Language |
Accept-Language |
交渉するルートは、そのシグナルを実際に受け取った応答だけでなく全応答に Vary を 付けます。ユーザー設定の扱いとはここが違っていて、 理由を書いておく価値があります。カラースキームには床があります — CSS がサーバー抜きで 正しく答えるので、シグナルなしで組み立てた応答は誰にとっても正しい。言語には床が ありません。 Cookie を持たない読者が来ると、Vary なしの既定言語が共有キャッシュに 入り、それを Cookie で別の言語を選んでいる次の読者が受け取ります。
どの方式でも Content-Language は付きます。
パス方式のルート
Section titled “パス方式のルート”path_routes では /about はどの言語も名乗っていません。交渉して /ja/about へ
リダイレクトします。301 ではなく 302 です。行き先は誰が聞いたかで変わるので、
恒久ステータスにすると同じプロキシの後ろにいる全員に、ある1人の交渉結果がキャッシュ
されてしまいます。
接頭辞の位置は常に言語として読みます。ドイツ語を宣言していないときの /de/about は
壊れたロケールではなく普通のパスです。これが、ドイツ語を追加した日に /de/ という
名前のルートが壊れるのを防いでいます。
既定言語から接頭辞を外す
Section titled “既定言語から接頭辞を外す”prefix_default = false/about が日本語になり、/ja/about はそこへ恒久リダイレクトします。ルートで交渉
リダイレクトが要らなくなるので、最も重要な訪問で往復が1回減ります。
既定は true です。理由は後から効いてきます。false のもとで既定言語を変えると、
サイトの全 URL が移動します。true ならルートのリダイレクト先が変わるだけです。
主要言語がこの先変わらないと確信できるときに false を取ってください。
Go からメッセージを呼ぶ
Section titled “Go からメッセージを呼ぶ”生成されたメッセージは第1引数にロケールを取ります。だから同じ関数がハンドラでも、 バッチでも、メール生成でも、プッシュ通知でも動きます。「ライブラリ用の別モード」は ありません — 生成される surface がすでにそれです。
package handlers
import ( "net/http"
"github.com/shibukawa/popcornweb/pw" "example.com/app/messages")
func Welcome(w http.ResponseWriter, r *http.Request) { greeting := messages.ShopGreeting(pw.LocaleContext(r.Context()), "Ada") pw.WriteHTML(w, r, Page(greeting))}リクエストの外には読むべきコンテキストがないので、記録されている値から解決します。
func SendReceipt(ctx context.Context, account Account) error { locale, ok := pw.ParseLocale(account.Language) if !ok { locale = pw.DefaultLocale() } return mail.Send(account.Address, messages.MailReceipt(locale, account.Name))}ParseLocale は既定値で埋めずに「無い」と報告します。保存された設定が宣言済み言語で
なくなっていることは、知る価値のある事実だからです。マッチングは RFC 4647 に従うので、
ja-JP は ja に当たります。
テンプレートの外で URL を組むとき — リダイレクト先、メール本文中のリンク、プッシュの
ディープリンク — は pw.LocalePath がテンプレートのバインディングと同じ接頭辞規則を
適用するので、呼び出し側が方式で分岐する必要はありません。
API の応答をサーバーで翻訳すべきか
Section titled “API の応答をサーバーで翻訳すべきか”たいていは不要です。Problem レスポンスはすでに機械 可読なコードを持っていて、それを自分で訳すネイティブクライアントは、端末の言語を正確に 知り、オフラインでも動き、アプリのバージョンと揃った文言を持っています。サーバーが 選んだ文字列はこの3点すべてで劣ります。
サーバーが訳すのは、クライアントにできない場合です。メール、プッシュ通知、帳票、 管理者が設定した文言、そして自前のカタログを持てないクライアント。フレームワークが 自分の判断で訳すことはありません — 訳文が欲しいときは、組み立てた文字列を Problem の コンストラクタに渡します。
つまずきやすいところ
Section titled “つまずきやすいところ”ロケールは必ず宣言済みのもので、リクエストから反射した値は使えません。
ParseLocale がそれを保証します。任意のタグが URL に届くと、マッチしなかった
Accept-Language が片端からオリジンで別アドレスになり、これは表示崩れではなく
上限のないキャッシュ面です。
Cookie 方式はパス方式の安価版ではありません。 共有キャッシュを完全に手放します。 ページが公開向けなら、コストの向きが逆です。
キャッシュされたコンポーネントは読んだ言語をキーに含めます。 これは自動で、 メッセージを描画するコンポーネントは言語間で共有されなくなります。メッセージを 読まないコンポーネントのキーは以前のままです。
カタログの形式、複数形、言語切り替えは ページの翻訳にあります。
