Skip to main content

Keep CAPTCHA Challenges in the App's Selected Language

2026-10-26

Keep CAPTCHA Challenges in the App's Selected Language

A multilingual sign-in flow can stay in Spanish until the CAPTCHA opens in English. The user has not changed languages. The challenge has read a device, browser, web view, or SDK default that differs from the locale selected inside the app. The mixed-language handoff appears during registration, password recovery, payment protection, or another high-friction step where users must understand instructions and errors.

A reliable CAPTCHA localization design passes the effective app locale into the challenge, maps that locale to the provider's supported codes, and defines what happens when no exact code exists. It also keeps the verification token independent from language. This guide builds that contract for reCAPTCHA, hCaptcha, and similar provider-owned challenges.

Why CAPTCHA language drifts from the app

The app and the challenge often resolve language from different inputs. Your app may store an explicit choice such as pt-BR. A web challenge may inspect browser preferences. A native wrapper may start with the device locale. A hosted verification page may receive neither value and fall back to English.

That split is visible in a Stack Overflow report about reCAPTCHA on Android. The author describes a multilingual app whose CAPTCHA follows the phone language instead of the app-level locale. Treat this as one implementation report, not a guarantee about every current reCAPTCHA product or SDK.

Providers expose different controls. Google's reCAPTCHA language reference lists language codes for visible challenge UI. The hCaptcha configuration reference documents an hl option that forces a specific localization; without it, hCaptcha auto-detects the user's locale. A merged hCaptcha iOS SDK change made the current locale the default locale value, which shows why an integration must know whether locale selection belongs to app code, a wrapper, or provider JavaScript.

Automatic detection is useful when an app has no language setting. It is the wrong source of truth after the user has made an explicit choice. The integration must distinguish these two cases instead of sending a guessed locale on every request.

Define the locale contract before configuring a provider

Start with one function that returns the app's effective locale. Do not let each CAPTCHA call site read a different preference, framework object, or device property. The value should be the same locale used to render the screen containing the challenge.

The resolver needs four outputs:

  1. requestedLocale: the user's explicit app choice, if one exists.
  2. effectiveLocale: the supported app locale used on the current screen.
  3. providerLocale: the code accepted by the selected CAPTCHA provider.
  4. fallbackReason: an optional reason explaining why the provider locale differs.

A small mapping layer makes the decisions reviewable:

resolveCaptchaLocale(appLocale, provider):
  normalized = canonicalizeBcp47(appLocale)
  supported = provider.supportedLanguageCodes()

  if supported contains normalized:
    return { locale: normalized, fallbackReason: null }

  languageOnly = normalized.language
  if supported contains languageOnly:
    return { locale: languageOnly, fallbackReason: "region-not-supported" }

  configuredFallback = providerFallbackFor(normalized)
  if configuredFallback is not null:
    return { locale: configuredFallback, fallbackReason: "mapped-fallback" }

  return { locale: null, fallbackReason: "provider-auto-detection" }

Keep this map next to other locale adapters, not inside a sign-in view controller. Provider codes and support can change independently from the app's translation catalog. A central adapter gives the team one place to update mappings and one place to log fallbacks.

Do not assume that a general BCP 47 tag will be accepted unchanged. Compare the app locale with the provider's current documented list. Script and region subtags need deliberate handling. For example, collapsing every Chinese locale to zh can select the wrong script, while dropping a region from Portuguese may produce copy that differs from the rest of the app. When the provider supports only a broader language, record that downgrade rather than hiding it.

Pass language only to the visible challenge layer

CAPTCHA language belongs to presentation. It should not alter the semantic action being protected, the risk score, the token format, or the server verification policy.

For a web challenge that documents hl, pass the mapped provider code when constructing or rendering the widget. The hCaptcha configuration documentation permits the language override through configuration or the render method. Build the configuration from the locale adapter instead of concatenating an unchecked value into a URL.

providerLocale = resolveCaptchaLocale(effectiveLocale, hcaptcha)
config = {
  sitekey: publicSiteKey,
  size: "invisible"
}

if providerLocale.locale is not null:
  config.hl = providerLocale.locale

challenge.render(container, config)

For reCAPTCHA products that display a visual challenge, use only a code listed in Google's supported language table. Different reCAPTCHA products and integration paths expose different user interfaces, so confirm that the specific mobile or web flow has a visible challenge before promising localized copy.

Google's current mobile documentation separates token creation in the app from assessment on the backend. The iOS integration guide explains client integration and notes that the visual checkbox challenge is not available for iOS mobile apps. The Android integration guide likewise sends response tokens to the backend for assessment. That separation gives the implementation a clean rule: locale configures any visible provider UI, while the backend verifies the token against the protected action and security policy.

Never localize action names, token fields, API parameters, or server error identifiers. If the app changes login to connexion because the screen is French, analytics and verification rules can split across languages. Keep machine identifiers stable and translate only app-owned messages shown after the server returns a structured result.

Recreate challenges after a live language change

A challenge view can retain the locale captured at initialization. Updating the surrounding screen does not prove that provider JavaScript, a web view, or a native wrapper has re-read language state.

When the user changes the app language while a challenge is active:

  1. Cancel or invalidate the visible challenge according to the provider API.
  2. Discard any unsubmitted token associated with the old view.
  3. Resolve the new effective locale through the shared adapter.
  4. Create a fresh challenge instance with the new provider locale.
  5. Restore only safe app-owned form state, such as the email address.

Do not reuse a completed token merely to avoid making the user repeat a challenge. Token lifetime, action binding, and single-use behavior belong to the provider's security contract. Submit a token promptly through the normal verification path, and request a new one when the challenge context has been replaced.

The same rule applies after an operating-system configuration change, account switch, or restored session changes the app locale. Rebuilding on a clear locale transition is easier to reason about than trying to mutate undocumented internals inside a loaded provider view.

Keep fallback behavior explicit and recoverable

A provider may not support one of the app's locales. A script may fail to load. Audio may be unavailable. Network filtering may block the challenge domain. Localization code must not turn those conditions into an endless spinner.

Define a fallback table with three separate decisions:

Condition Language decision Product response
Exact provider locale exists Pass the exact code Render the challenge
Only a broader language exists Pass the approved broader code Record the downgrade and render
No approved provider code exists Allow documented automatic detection or a configured fallback Tell the user which language may appear
Challenge cannot load No language fallback can fix transport Offer retry or an alternative verification path
User cannot complete the challenge Preserve app locale Offer a supported recovery route

A fallback language must remain understandable to the target audience. Product and localization teams should approve it. Engineering should not pick the first code that shares two letters.

Error ownership also needs a boundary. Provider-owned instructions should come from the provider localization. App-owned messages such as "Verification expired" should come from the normal app catalog and use stable error codes. Avoid scraping visible challenge text or matching translated sentences to infer failure state.

Test the complete handoff, not one screenshot

A single English test cannot verify CAPTCHA localization. Build a matrix around the sources that can disagree:

  • App language differs from device language.
  • App language differs from browser or web-view preferences.
  • Exact locale is supported by the app and provider.
  • The app supports a regional or script variant that the provider does not.
  • The user changes app language before opening the challenge.
  • The user changes app language while the challenge is visible.
  • The app returns from background with an old challenge instance.
  • Network loss occurs before loading, during completion, and before token submission.
  • The token expires or the backend rejects it.
  • Audio and keyboard-accessible alternatives are available in the expected language.

For each case, capture the effective app locale, provider locale, fallback reason, challenge lifecycle event, and structured verification outcome. Do not log the token. These fields let QA distinguish a mapping defect from a provider fallback, stale view, network failure, or backend rejection.

Run the matrix on release builds. Debug builds may use a different web-view cache, test key, provider environment, or device-language setup. Check registration, sign-in, account recovery, and any high-risk action separately because teams often create independent challenge instances for each route.

Accessibility testing belongs in the same pass. Navigate the challenge with the platform screen reader, verify focus returns to the app after completion or cancellation, and check that an audio alternative does not switch to an unrelated language. If provider-owned accessibility text cannot be controlled for a supported app locale, document the limitation and provide a recovery route rather than claiming full localization.

Common implementation mistakes

Passing the device locale unconditionally

This recreates the original defect for anyone who deliberately chose another app language. Use the device locale only when the product has no explicit app choice and its documented locale precedence allows that fallback.

Sending unsupported locale tags

An app may support a finer regional or script distinction than the challenge provider. Validate against the provider list and use a reviewed map. Silent string truncation can select the wrong writing system.

Changing only the surrounding screen

A loaded provider view can keep old state. Destroy and recreate the challenge after an effective-locale transition, then verify the rendered result.

Translating server identifiers

Localized action names and error codes fragment security rules and observability. Keep identifiers stable. Map structured outcomes to translated app messages at the UI boundary.

Treating an unavailable challenge as a translation error

Language fallback does not repair blocked scripts, network failures, inaccessible controls, or rejected tokens. Give each condition its own recovery path and telemetry.

Ship one locale adapter and one verification matrix

Inventory every CAPTCHA entry point, then route all of them through the same effective-locale resolver and provider map. Add the app-versus-device mismatch case first because it exposes automatic-detection bugs quickly. Next, test one unsupported regional or script variant and one live language change that forces challenge recreation.

Do not ship until the logs show the effective app locale, mapped provider locale, and fallback reason without recording challenge tokens. That evidence proves the integration is using the app's language contract rather than whatever locale the provider happened to detect.

Hosted sign-in pages have the same language handoff problem; see how to keep hosted OAuth login in the app's selected language.

References