Skip to content

Use case — i18n / locale negotiation

What is ui_locales?

OIDC Core 1.0 §3.1.2.1 defines a ui_locales parameter on the /authorize request. The RP sends a space-separated list of BCP 47 language tags (RFC 5646 — e.g. ja en-US) and the OP renders the login / consent UI in the highest-priority language it actually has translations for.

If the RP does not send ui_locales, you can still pick up the user's preference from a locale cookie (set by the OP itself) or the Accept-Language HTTP header (RFC 9110), falling back to a registered default locale.

Specs referenced on this page
Vocabulary refresher
  • BCP 47 — The IETF language-tag scheme (RFC 5646). Tags look like en, en-US, ja, zh-Hant-TW — language, optional script, optional region. The OIDC ui_locales parameter is a space-separated list of BCP 47 tags in priority order.
  • Accept-Language — A standard HTTP request header where the browser publishes the user's preferred languages with q= quality factors (ja-JP, ja;q=0.9, en;q=0.5). When the RP does not pin a language with ui_locales, the OP can fall back to this signal.
  • ICU MessageFormat — A locale-aware string-formatting language used for the bundle values. It handles plurals, gender, number/date formatting in a way concatenation can't ({count, plural, one {1 device} other {# devices}}).

Resolution chain

The OP picks the language for built-in consent / login screens from a priority chain:

ui_locales request parameter
  → locale cookie
    → Accept-Language header
      → registered default locale

The first signal that resolves to a registered locale wins.

Source: examples/16-i18n-locale

Wiring

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 へのアクセス許可",  // override one key only
})

op.New(
  /* required options */
  op.WithDefaultLocale("fr"),
  op.WithLocale(frBundle),  // register a new locale
  op.WithLocale(brandJa),   // overlay one key on top of the seed ja bundle
)

The library ships seed bundles for English (en) and Japanese (ja) out of the box. op.WithLocale is layered, not replacement: each call merges on top of any earlier bundle for the same locale at key granularity. Embedders supply only the keys they want to change, and every unsupplied key falls through to the previous layer (the seed catalogue, or an earlier op.WithLocale call). Repeated calls for the same locale compose by repeated merge, so the last call wins per key.

Unknown locales (e.g. fr, de) register fresh on first call and merge on subsequent calls. The resolver still falls back to the configured default locale (see op.WithDefaultLocale) for any key the new locale's bundles never supply, so it is safe to ship a partial overlay without re-translating the whole catalogue.

The bundle format is ICU MessageFormat (passed through LocaleBundleFromMap for compile-time validation).

Verifying

sh
# Discovery advertises the registered locales
curl -s http://localhost:8080/.well-known/openid-configuration | jq .ui_locales_supported
# ["en", "ja", "fr"]

Drive the priority chain manually:

RequestRendered locale
No signalfr (registered default)
Accept-Language: esfr (es not registered → fallback)
Accept-Language: ja-JP, ja;q=0.9ja
?ui_locales=ja en (RFC 5646 list)first match = ja
?ui_locales=ja + cookie set to frja (parameter outranks cookie)

What gets translated

SurfaceBundle key example
Login form labelslogin.identifier, login.password, login.submit
Consent promptconsent.title, consent.scope.profile.description
Error pageserror.invalid_request_uri, error.unsupported_response_type
Logout confirmationlogout.confirm

The keys follow the surface they render. New keys are added in minor releases — listing keys is part of the release notes when bundles change shape.

Custom UI takes over

If you ship op.WithSPAUI(...) or op.WithConsentUI(...), your templates are responsible for i18n. The library still negotiates the locale and exposes it through the interaction context, but the seed bundles only feed the built-in templates.