Skip to main content

Keep Third-Party Payment Sheets in the App's Selected Language

2026-10-21

Keep Third-Party Payment Sheets in the App's Selected Language

Payment sheet localization often fails at the last step of checkout. The app shows French product copy, the user taps Pay, and an SDK-owned form opens in English because it follows the device locale, browser locale, or an incomplete SDK translation bundle. Mismatched labels, validation errors, decline messages, authentication, and wallet handoffs can stop a user from finishing payment. Treat the payment SDK as a separate localization surface with an explicit locale adapter, a documented fallback, and release tests that exercise failures as well as the happy path.

Why payment UI drifts from the app language

A payment screen combines text owned by several systems. Your app owns the paywall heading, plan description, coupon explanation, and button that launches checkout. The payment provider owns card-field labels, address prompts, validation messages, and many decline errors. Apple Pay, Google Pay, or a bank authentication page may add another system-owned layer. Each layer can resolve language independently.

That ownership split explains why adding translations to the app catalog does not fix every checkout string. Stripe's mobile PaymentSheet integration describes a prebuilt payment interface and notes that it displays localized errors in its own UI. Your ordinary string lookup never sees that provider-owned copy.

Automatic locale detection also has a specific meaning. Stripe's browser documentation lists a bounded set of supported payment UI locales and says the default auto mode detects the browser locale. An app-specific language can differ from that signal. A user may keep the phone in English while choosing French inside the app, or choose pt-BR while an SDK supports only a broader or differently named locale.

Wrapper libraries can hide or delay native locale controls. A React Native developer asked for PaymentSheet localization support after finding no way to select a locale. A Flutter wrapper user separately requested translated or configurable PaymentSheet labels. Those reports concern particular library versions, but they show the integration question your design must answer: which component owns the language, and can the installed SDK accept it explicitly?

Define one payment locale contract

Start with the app's effective locale, not the device region, SIM country, billing country, or payment currency. Those values answer different questions. Currency controls monetary display. Billing country affects address fields and payment methods. Effective locale controls the language the user selected for the current interface.

Create one adapter between your app locale registry and each payment surface. The adapter should return a supported provider locale, the fallback reason, and the original app locale for diagnostics. Do not scatter string replacements or locale guesses across checkout screens.

type PaymentLocaleResolution = {
  appLocale: string;
  providerLocale: string | "auto";
  fallback: "exact" | "language" | "product-default" | "provider-auto";
};

const stripeLocales = new Set([
  "ar", "bg", "cs", "da", "de", "el", "en", "es", "et", "fi",
  "fr", "he", "hr", "hu", "id", "it", "ja", "ko", "lt", "lv",
  "ms", "mt", "nb", "nl", "pl", "pt", "ro", "ru", "sk", "sl",
  "sv", "th", "tr", "vi", "zh"
]);

function resolvePaymentLocale(appLocale: string): PaymentLocaleResolution {
  const normalized = Intl.getCanonicalLocales(appLocale)[0];
  const language = normalized.split("-")[0];

  if (stripeLocales.has(normalized)) {
    return { appLocale, providerLocale: normalized, fallback: "exact" };
  }
  if (stripeLocales.has(language)) {
    return { appLocale, providerLocale: language, fallback: "language" };
  }
  if (stripeLocales.has("en")) {
    return { appLocale, providerLocale: "en", fallback: "product-default" };
  }
  return { appLocale, providerLocale: "auto", fallback: "provider-auto" };
}

Treat the set above as an example shape, not a permanent copy of any provider's current support. Generate or review your mapping against the SDK version you ship. Locale support can differ between web Elements, native PaymentSheet, and cross-platform wrappers. Pin the provider and wrapper versions in the release record so a translation regression can be tied to a concrete build.

The fallback must be visible to engineering and QA. Record an event such as payment_locale_resolved with the app locale, provider locale, fallback class, SDK version, and checkout surface. Do not attach card details, names, addresses, client secrets, or provider error text. The event should explain language selection without collecting payment data.

Separate merchant copy from provider copy

Inventory checkout text by owner before changing code.

Copy surface Typical owner Translation path Release check
Plan name and benefits App App string catalog Paywall screenshot by locale
Card and address labels Payment SDK Provider bundle or locale option Payment sheet on device
Merchant display name App configuration Reviewed configuration value Wallet and sheet header
Validation and decline text Provider plus app SDK localization and app error mapping Forced error scenarios
Bank authentication page Issuer or provider External policy Test account and fallback note
Receipt or confirmation copy App or backend App and email catalogs Completed payment flow

Do not overwrite provider labels by scraping views or reaching into private SDK classes. That approach breaks accessibility, can disappear after an SDK upgrade, and may leave error states untranslated. Use a documented locale or customization option when one exists. When a wrapper does not expose the native option, upgrade, contribute the missing bridge, or document the automatic behavior rather than patching rendered text.

Keep merchant-authored copy in your own catalog. The payment amount, recurrence terms, cancellation wording, and merchant label need the same review controls as the rest of checkout. Passing an English merchant display name into a French sheet still creates mixed language even when every provider label is correct.

Handle localized payment errors without losing control flow

Payment failures have two parts: a stable machine condition and user-facing copy. Branch on the provider's typed code or status, never on translated text. Display provider-localized text only where the SDK contract says it is safe for end users. Map application-specific recovery instructions through your own catalog.

function paymentFailureView(error: PaymentError, t: Translate) {
  switch (error.code) {
    case "authentication_required":
      return {
        title: t("checkout.authenticationRequired.title"),
        action: t("checkout.authenticationRequired.retry")
      };
    case "network_error":
      return {
        title: t("checkout.networkError.title"),
        action: t("checkout.networkError.retry")
      };
    default:
      return {
        title: error.localizedMessage ?? t("checkout.genericError.title"),
        action: t("checkout.genericError.dismiss")
      };
  }
}

A Stripe Android report documented missing translations for several PaymentSheet errors in a specific SDK release. Never parse the English sentence to handle that class of defect. Reproduce it against the exact version, check whether a maintained release fixes it, keep your generic localized recovery message available, and verify that analytics still records the stable error code.

Do not promise that every issuer or authentication screen will match the app locale. Some screens belong to a wallet, bank, browser, or operating system. Document where control ends. Keep the transition understandable with localized setup copy before the handoff and a localized result screen after it.

Apply language changes before presenting checkout

Resolve the payment locale when the user starts checkout, not once at app startup. A retained PaymentSheet configuration may outlive an in-app language change. Reusing it can reopen the old language even though the paywall has refreshed.

Use this sequence:

  1. Read the effective app locale from the same source used by the current screen.
  2. Resolve it through the payment locale adapter.
  3. Build or update the SDK configuration with the resolved locale where supported.
  4. Recreate locale-sensitive payment UI after a language change.
  5. Present checkout only after configuration succeeds.
  6. Record the resolution outcome without payment data.

If the provider does not support runtime locale selection, state that limitation in code and tests. Reinitialization may be necessary, or the surface may remain tied to device settings. Do not imply that changing the billing country will translate the interface. Country, currency, and language must remain separate fields even when one checkout setup needs all three.

Test the failure paths that expose mixed language

A single successful card payment will miss most localization defects. Build a matrix around ownership boundaries and state changes.

For every launch locale, check an exact provider locale, a regional locale that falls back by language, and an unsupported locale that reaches the product default. Open checkout after a cold launch and after changing the app language without restarting. Verify card labels, saved-payment-method labels, billing address fields, wallet buttons, loading states, validation errors, decline errors, authentication return, cancellation, and the final receipt screen.

Run the matrix on release builds. Debug builds can use different resource packaging or SDK settings. Include at least one right-to-left locale if the app supports one, and inspect focus order and field alignment rather than checking translated words alone. Capture the app locale, resolved provider locale, provider version, wrapper version, platform version, and expected fallback in the test record.

Use synthetic provider test methods to trigger failures. Never create real declines or store live payment data for localization QA. Compare screenshots only after masking names, email addresses, billing addresses, and payment identifiers.

Guard payment localization during SDK upgrades

Payment dependencies change independently from your translation catalog. Put their localization surface into the upgrade checklist.

Before merging an SDK or wrapper update, compare supported locales, locale-option behavior, resource packaging, default detection, and public error contracts. Run a small golden flow in the top app locales plus one unsupported locale. Confirm that your adapter still maps only values accepted by the installed API. Search the provider's release notes and issue tracker for localization fixes, but reproduce the behavior in your build rather than assuming an issue title describes the current release.

Add assertions around the adapter itself. It should canonicalize valid BCP 47 tags, preserve the original app locale for diagnostics, select language-only fallback deliberately, and never use billing country as a hidden language fallback. A change in supported locales should produce a reviewed mapping diff, not silently alter checkout behavior.

Ship with a concrete payment localization gate

Before the next release, list every checkout surface and assign its text owner. Add one locale adapter at the SDK boundary, log only the resolution metadata, and build test cases for one exact locale, one regional fallback, one unsupported locale, and one live language change. Then force validation, decline, cancellation, and authentication paths on a release build. Payment sheet localization is complete only when the user can understand both the form and the recovery path without the checkout switching languages unexpectedly.

Hosted sign-in has the same provider-versus-app split; see how to keep hosted OAuth login in the app's selected language.

References