Skip to main content

Keep Hosted OAuth Login in Your App's Selected Language

2026-10-12

Keep Hosted OAuth Login in Your App's Selected Language

OAuth localization often breaks at the first screen outside the native app. A user selects French in the app, taps Sign in, and gets an English login or consent page because the hosted browser follows a different language preference. The redirect succeeds, so ordinary authentication tests miss the defect. Fix it by making the effective app locale an explicit input to the authorization request, mapping it to each identity provider's supported locales, and testing the full browser-to-app transaction. This guide covers the request contract, fallback rules, provider adapters, failure handling, and release checks needed to keep login, signup, consent, and errors in the expected language.

Why hosted login drifts from the app language

A native screen and a hosted authorization page do not share one localization runtime. The app may resolve language from an in-app preference. The system browser can send its own Accept-Language header. The identity provider can restrict the enabled locale set and apply a tenant default. A social or enterprise identity connection can introduce another page with its own policy.

RFC 9110 defines Accept-Language as a request field for the user's preferred response languages. That preference belongs to the browser request. It does not prove which language the native app rendered before opening the authorization session.

The mismatch appears in several forms:

  • The app omits a locale hint, so the provider chooses from the browser header.
  • The app sends an internal code such as pt_BR while the provider expects a BCP 47 tag such as pt-BR.
  • The requested locale exists in the app but is not enabled for hosted login.
  • Login uses the requested language, while signup, consent, password recovery, or error pages fall back.
  • A resumed authorization session reuses provider state from an earlier language.
  • A downstream identity connection ignores the upstream provider's locale hint.

A practitioner asking how to localize an OAuth dialog reported that a locale parameter was not preserved through the authorization flow. That question describes one provider implementation, not a universal OAuth failure. It shows why a successful redirect is insufficient evidence that locale propagation works.

Define the authentication locale contract

Choose one field that represents the effective app locale at the moment authentication starts. Compute it through the same precedence policy used by the rest of the app, then freeze it for that authorization transaction. Do not re-read device settings halfway through the callback.

A useful request model is small:

type HostedAuthRequest = {
  transactionId: string
  effectiveAppLocale: string
  providerId: string
  redirectUri: string
  flow: "login" | "signup" | "reauthenticate" | "link-account"
}

type ProviderLocaleResolution = {
  requestedLocale: string
  supportedLocale: string
  uiLocales: string[]
  fallbackReason?: "base-language" | "tenant-default" | "unsupported"
}

effectiveAppLocale is the language and regional preference already selected by the app. supportedLocale is the value the provider adapter can request after validation. uiLocales preserves an ordered preference list when the protocol supports one. Keep redirectUri stable and language-neutral. Translated path segments create callback registration and verification problems without improving the hosted page language.

Store the requested and resolved locale beside the transaction ID in short-lived diagnostic state. Do not put access tokens, authorization codes, passwords, or personally identifying form values into locale logs. The record should answer which locale the app requested, which locale the adapter sent, and why fallback occurred.

Send ui_locales as an ordered preference

OpenID Connect Core defines ui_locales as an optional, space-separated list of BCP 47 language tags ordered by preference. A Canadian French app could send:

ui_locales=fr-CA fr en

The first value expresses the exact app preference. The base language gives the provider a controlled fallback. The final product default is useful only when that fallback matches the product policy. Do not append every enabled language. The list represents preference, not capability.

The OpenID Connect specification also says unsupported values should not cause an error. That behavior makes explicit post-render verification necessary. A provider can accept the request and still render its default language. Treat a successful authorization response as authentication success, not proof of correct localization.

OAuth 2.0 alone does not define ui_locales. Use it when the authorization server implements OpenID Connect or documents equivalent support. For a provider-specific locale parameter, keep the mapping inside that provider's adapter. Do not scatter query-string construction across iOS, Android, and web clients.

An adapter can enforce the contract:

function buildLocaleParameters(
  provider: "oidc" | "provider-specific",
  resolution: ProviderLocaleResolution
): Record<string, string> {
  if (provider === "oidc") {
    return { ui_locales: resolution.uiLocales.join(" ") }
  }

  return mapProviderLocaleParameter(resolution.supportedLocale)
}

Generate the authorization URL through the identity SDK when possible, passing documented extra parameters through its supported interface. Avoid string replacement on an already encoded URL. It can duplicate parameters, break signatures, or encode the locale list incorrectly.

Map app locales to provider capability

A provider adapter needs an allowlist built from current provider configuration, not assumptions based on the app's locale inventory. The app may support es-419, while hosted login supports only es. Another provider may distinguish pt-BR and pt-PT. A third may require the locale to be enabled at the tenant before a request hint has any effect.

Auth0's Universal Login internationalization documentation describes three distinct inputs: enabled tenant locales, the ui_locales request parameter, and browser Accept-Language, followed by the tenant default. That model explains a common failure. Sending a valid language tag cannot select a locale the tenant has not enabled.

Keep a source-controlled mapping such as:

{
  "fr-CA": { "provider": "fr-CA", "fallbacks": ["fr", "en"] },
  "es-MX": { "provider": "es-MX", "fallbacks": ["es", "en"] },
  "zh-Hant-HK": { "provider": "zh-Hant", "fallbacks": ["en"] }
}

This is an example policy, not a universal provider table. Build the actual values from the provider's documented locale set and the tenant configuration. Preserve script subtags when they change written content. Do not collapse Chinese variants to a region-free zh unless the provider documents what that value renders.

Fail configuration checks when an app launch locale has no declared hosted-login mapping. Runtime can then use an approved fallback, but the release should not discover the gap from customer reports.

Cover every page in the hosted transaction

Testing the first login page leaves most of the flow unverified. Build a matrix for each launch locale and identity connection:

  1. Start login from a clean browser session.
  2. Start signup from the app-selected language.
  3. Trigger invalid credentials and account-not-found errors.
  4. Open consent and permission pages when the provider shows them.
  5. Use password recovery and return to the authorization session.
  6. Switch accounts when the browser already has identity state.
  7. Cancel in the browser and verify the localized native error.
  8. Complete login and confirm the callback resumes the same app locale.
  9. Repeat after changing the app language without changing the device language.
  10. Test an unsupported app locale and confirm the declared fallback.

Capture the page title, rendered language, requested locale, resolved provider locale, and callback result. Screenshots help reviewers inspect copy, but assertions should also check machine-readable language metadata when the provider exposes it. Do not automate credential entry into production accounts for this test. Use a test tenant and dedicated accounts.

The downstream connection matters. An OpenID Connect broker can localize its own login page, then redirect to a social provider that chooses another language. Record the boundary where control changed. If the downstream provider offers no locale control, document that limitation rather than claiming end-to-end enforcement.

Handle fallback and failure explicitly

Locale fallback should not block authentication unless product or regulatory requirements demand an exact language. A missing hosted translation usually needs a usable default plus diagnostics. A malformed authorization request, invalid redirect URI, or failed state check must still stop the flow.

Separate these outcomes:

  • requested: the hosted page used the exact app locale;
  • base-language: the page used an approved base-language fallback;
  • provider-default: the provider did not support the requested preference;
  • unknown: the integration could not verify the rendered language;
  • auth-failed: the authorization transaction itself failed.

Do not infer the rendered language from the callback alone. The callback proves that the provider returned control. It does not report what the user saw. Where direct verification is unavailable, mark the result unknown and rely on bounded release checks for each configured provider.

Keep native error handling localized through the app's resource system. Browser cancellation, network loss, state mismatch, and provider errors should map to stable error codes. Do not display raw provider prose as the app's final error message because that copy can use another language and can change without an app release.

Common implementation mistakes

Do not set the HTTP Accept-Language header on unrelated API requests and assume the system authorization browser inherits it. The browser owns its request headers. Pass the documented authorization parameter instead.

Do not translate the redirect URI. Identity providers normally compare registered callback URIs as protocol values. Keep locale in transaction state and request parameters, not in a translated callback route.

Do not treat locale and country as interchangeable. A French-speaking user can authenticate from Canada, France, or elsewhere. Country can influence policy, but it does not replace the language preference.

Do not let each client invent locale aliases. Put provider mappings in one versioned configuration consumed by all clients or by the authorization backend. Log the mapping version with the transaction so a regression can be traced.

Do not verify only a provider's default connection. Enterprise connections, social login, account recovery, and consent can render separate pages owned by different systems.

Verify OAuth localization before release

Add a locale check to the authentication release gate. For every supported app locale, assert that the adapter emits valid ordered BCP 47 values, the tenant enables the resolved locale, the authorization URL contains one correctly encoded locale parameter, and the callback preserves the original transaction locale.

Run visual checks on login, signup, consent, error, cancellation, and recovery pages. Repeat with the device language set differently from the app language because that fixture exposes browser fallback. Test warm sessions as well as clean sessions because provider cookies can affect which page appears.

Monitor fallback reasons without collecting sensitive authentication data. A sudden increase in provider-default after enabling a locale can reveal a missing tenant setting or a provider mapping regression. An increase in unknown means verification coverage disappeared and needs investigation.

Start with one provider and two deliberately conflicting settings: set the app to French and the device browser to English. Send ui_locales=fr en, verify every hosted page, complete the callback, and confirm the app resumes in French. Then encode that case as a release fixture before expanding the mapping to the rest of the locale catalog.

References