Skip to content

ユースケース — i18n / ロケールネゴシエーション

ui_locales とは

OIDC Core 1.0 §3.1.2.1 は /authorize 要求に ui_locales パラメータを定めています。RP が空白区切りの BCP 47 言語タグ(RFC 5646 — 例: ja en-US)を送ると、OP は自分が翻訳を持っている範囲で最優先言語を選んでログイン / 同意 UI を描画します。

RP が ui_locales を送らない場合でも、OP 自身が発行するロケール cookie や Accept-Language HTTP ヘッダ(RFC 9110)からユーザの希望を拾うことができ、最終的にはあらかじめ登録したデフォルトロケールへフォールバックします。

このページで触れる仕様
  • OpenID Connect Core 1.0 — §3.1.2.1(ui_locales リクエストパラメータ)
  • RFC 5646 — Tags for Identifying Languages(BCP 47)
  • RFC 9110 — HTTP Semantics, §12.5.4(Accept-Language
用語の補足
  • BCP 47 — IETF の言語タグ規格(RFC 5646)。enen-USjazh-Hant-TW のように「言語 - 任意のスクリプト - 任意の地域」で記述します。OIDC の ui_locales パラメータは、優先順位順に並べた BCP 47 タグを空白区切りで載せます。
  • Accept-Language — ブラウザがユーザの言語選好を q= quality factor 付きで通知する標準 HTTP リクエストヘッダ(ja-JP, ja;q=0.9, en;q=0.5)。RP が ui_locales で言語を固定しない場合、OP はこちらにフォールバックできます。
  • message bundle — 文字列 key だけを持つ flat JSON object。bundle values は単純な {name} placeholder に対応します。plural / gender formatting は v0.x では組み込み側 UI の責務です。

優先順序

OP は組み込みの同意 / ログイン画面の言語を、5 層の優先順序に従って決定します:

PreferredLocaleStore(ユーザ単位の上書き)
  → ui_locales リクエストパラメータ
    → __Host-oidc_locale cookie
      → Accept-Language ヘッダ
        → 登録済みのデフォルトロケール

最初に登録済みロケールへマッチしたシグナルが採用されます。bundle が 1 つでも登録されていれば最終的に必ずデフォルトロケールへ落ちるので、resolver が空文字を返すことはありません。

cookie の書き込み口

OP は /authorize__Host-oidc_locale を読みますが、ユーザ自身が値を保存するための専用書き込みエンドポイントは提供していません。組み込み側の UI から(__Host- 接頭辞が要求する Domain / Secure / HttpOnly=false 制約のもとで)直接 cookie を発行するか、後述の WithPreferredLocaleStore を使ってください。

ソース: examples/15-i18n-locale:8080 の listener を起動する前に、in-process の httptest サーバで優先順序の各行を検証する self-verify probe を実行します。ブラウザを使わずとも example の出力からリゾルバの挙動が確認できます。

実装

go
import "github.com/libraz/go-oidc-provider/op"

frBundle, _ := op.LocaleBundleFromMap("fr", map[string]string{
  "login.identifier": "Identifiant",
  "login.password":   "Mot de passe",
  /* ... */
})

brandJa, _ := op.LocaleBundleFromMap("ja", map[string]string{
  "consent.title": "Acme へのアクセス許可",  // 1 キーだけ上書き
})

op.New(
  /* 必須オプション */
  op.WithDefaultLocale("fr"),
  op.WithLocale(frBundle),  // 新規ロケールを登録
  op.WithLocale(brandJa),   // seed の ja bundle に 1 キーだけ重ねる
)

ライブラリは 英語(en日本語(ja の seed bundle を同梱しています。op.WithLocale は置き換えではなく 層を重ねる 形で動作するため、組み込み側は変更したいキーだけを供給すれば足り、それ以外は seed の翻訳がそのまま使われます。

マージのルール

  • キー単位でマージ — 各 op.WithLocale 呼び出しは、同じロケールに対する以前の bundle の上にキー単位で重ねられます。供給されなかったキーは下層(seed カタログまたは以前の呼び出し)の値が採用されます。
  • 最後の呼び出しが勝つ — 同じロケールに対して複数回呼び出した場合、衝突したキーは最後の呼び出しの値で上書きされます。
  • 未登録ロケールも安全frde のように seed に無いロケールは、最初の呼び出しで新規登録されます。bundle が一度も供給しないキーは設定済みのデフォルトロケール(op.WithDefaultLocale)にフォールバックするので、カタログ全体を翻訳し直さずに部分的なオーバレイだけを出荷しても安全です。op.New は seed または WithLocale で登録されていない default locale を拒否するため、fallback が空のカタログを指すことはありません。

bundle 形式は simple placeholder substitution 付きの flat key-value catalogue です(LocaleBundleFromMap を通すことで構築時の検証が入ります)。

op.Locale 型は内部実装に依存しない薄い文字列エイリアスです。組み込み側はここから定数を宣言できます。シードロケールは op.LocaleEnglish"en")と op.LocaleJapanese"ja")として公開されています。

ユーザ単位の preferred locale store

ログイン済みユーザの保存ロケールは、他のすべてのシグナルより優先されます。op.PreferredLocaleStore を実装して op.WithPreferredLocaleStore で渡してください:

go
type pgLocaleStore struct{ /* db ハンドルキャッシュ等 */ }

func (s *pgLocaleStore) PreferredLocale(ctx context.Context, sub string) (op.Locale, error) {
  loc, err := s.fetch(ctx, sub)
  if err != nil {
    return "", err  // err nil 以外 は「設定なし」として扱われ、次の層へ進む
  }
  return op.Locale(loc), nil  // 空 Locale も同様に次の層へフォールスルー
}

op.New(
  /* 必須オプション */
  op.WithPreferredLocaleStore(&pgLocaleStore{...}),
  op.WithLocale(frBundle),
  op.WithDefaultLocale(op.LocaleEnglish),
)

PreferredLocale/authorize のたびに呼ばれるので、ルックアップは安価でなければなりません。バックエンドがリモートの場合はローカルでキャッシュしてください。store の応答が遅いと毎回のログイン画面描画にレイテンシが乗ります。エラーを返しても空 Locale を返してもどちらも「設定なし」として扱われ、優先順序は次の層に進みます。

解決済みロケールの読み取り

OP は /authorize ごとに優先順序の解決を 1 度実行し、結果を 2 つの面で公開します。

SPA / カスタム interaction Driver から

各プロンプトのエンベロープ(GET LoginMount/state/{uid})には、orchestrator が Driver.Render の前に書き込む 3 つのロケールフィールドが含まれます:

フィールド用途
locale解決済みの BCP 47 タグ。<html lang> に乗せて翻訳 bundle の選択に使う。
ui_locales_hintRP の ui_locales 生リスト(空白区切り)。リゾルバを上書きしたいときだけ参照する。多くの SPA は無視して locale を信用する。
locales_available登録済みロケール一覧(discovery の ui_locales_supported と同じ)。discovery を再取得せずに言語ピッカを構築できる。
js
function applyLocale(prompt) {
  if (prompt.locale) {
    document.documentElement.lang = prompt.locale;
  }
  // bundles[prompt.locale] で SPA 自身のメッセージカタログを引く。
}

動く SPA の実装例は examples/10-react-login/web/static/assets/main.js にあります。applyLocale がプロンプト取得ループから呼ばれ、上のスニペットと同じ形で <html lang> を書き換えます。

同じ値は Go 側でも prompt.Locale として読めるので、カスタム interaction.Driver から Content-Language ヘッダにコピーしたり、レンダリングを分岐したりできます。examples/16-custom-interactionJSONDriver をラップしてこのパターンを示しています。

サーバ側のコード(メール、管理画面など)から

Provider.LocaleResolver()*op.Resolver を返します。/authorize の外で同じロケールを使いたい面(トランザクションメール、管理画面、プロンプトエンベロープが届かないあらゆる場所)はここから取得します:

go
loc := provider.LocaleResolver().Resolve(ctx, op.ResolveRequest{
  Subject:        userSub,                          // PreferredLocaleStore のルックアップに使われる
  AcceptLanguage: r.Header.Get("Accept-Language"),
})

renderEmail(loc, body)

Resolver はほかに Default()(フォールバックロケール)と Available()(登録順のロケール一覧 — シードが先、WithLocale 追加分が後)も公開します。Provider が起動時に 1 度だけ構築し、以後は差し替えません。並行使用は安全です。

確認

sh
# discovery が登録済みロケールを公開
curl -s http://localhost:8080/.well-known/openid-configuration | jq .ui_locales_supported
# ["en", "ja", "fr"]

優先順序を手動で確認:

リクエスト描画されるロケール
シグナルなしfr(登録済みデフォルト)
Accept-Language: esfr(es 未登録 → フォールバック)
Accept-Language: ja-JP, ja;q=0.9ja
Cookie __Host-oidc_locale=jaja
?ui_locales=ja en(RFC 5646 リスト)最初のマッチ = ja
?ui_locales=ja + cookie が frja(パラメータが cookie より優先)
PreferredLocaleja を返却 + ?ui_locales=frja(store がパラメータより優先)

翻訳対象

表示面bundle キー例
ログインフォームのラベルlogin.identifierlogin.passwordlogin.submit
同意プロンプトconsent.titleconsent.scope.profile.description
エラーページerror.invalid_request_urierror.unsupported_response_type
ログアウト確認logout.confirm

キーは表示面に追従します。新しいキーは minor リリースで追加され、bundle の構造が変わるリリースではリリースノートに記載されます。

カスタム UI が引き継ぐ場合

JSON ドライバ(op.WithInteractionDriver(interaction.JSONDriver{}))に切り替えると、描画される文字列は SPA / カスタム Driver 側が持ちます。ただしロケールネゴシエーションは引き続きライブラリが行い、結果は各プロンプトに prompt.locale として書き込まれます(上の「解決済みロケールの読み取り」を参照)。SPA やカスタム Driver から優先順序を再実行する必要はありません。seed の en / ja bundle はビルトインの HTML テンプレートにのみ使われます。

UI オプションとロケール

WithSPAUI / WithConsentUI / WithChooserUI は、同梱 HTML ドライバと同じロケール解決チェーンを使います。解決済みの prompt.locale はどの driver でも書き込まれます。詳細は Options 索引 §UI を参照してください。

続きはこちら