Skip to content

feat: default the checkout locale to the buyer's browser locale#962

Open
vkartaviy wants to merge 3 commits into
mainfrom
vk/web-checkout-locale-default
Open

feat: default the checkout locale to the buyer's browser locale#962
vkartaviy wants to merge 3 commits into
mainfrom
vk/web-checkout-locale-default

Conversation

@vkartaviy

@vkartaviy vkartaviy commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

What

The four SDK checkout entry points now default selectedLocale to the buyer's browser locale (navigator.language) instead of hardcoded English, matching what the paywall flow (presentPaywall) already does. Passing selectedLocale explicitly still overrides, and English stays the final fallback.

Why

Today the paywall flow and the custom-checkout flows resolve the default locale differently:

Entry point Default when selectedLocale is omitted
presentPaywall navigator.language (auto-detected)
purchase() -> Web Billing englishLocale ("en")
purchase() -> Stripe englishLocale ("en")
presentExpressPurchaseButton (Stripe Express) englishLocale ("en")
purchase() -> Paddle englishLocale ("en")

So a developer who renders a RevenueCat paywall gets a localized checkout for free, while a developer who builds their own UI and calls purchase() silently gets an English checkout for every buyer unless they discover and pass selectedLocale. Same SDK, opposite behavior, no warning.

It also affects emails: the locale captured at checkout is persisted on the subscription and drives the language of the lifecycle/transactional emails. With the English default, a buyer on a Japanese browser sees an English checkout and receives English emails, with nothing in the integration to explain why.

Change

  • New getBrowserLocale() helper in src/helpers/locale-helper.ts: returns navigator.language, or undefined outside a browser (SSR-safe via getNullableWindow).
  • The four entry points in main.ts now default selectedLocale = getBrowserLocale() ?? englishLocale.
  • defaultLocale and the explicit selectedLocale override are unchanged. English remains the final fallback when the browser locale is unavailable.

It localizes correctly for real regional tags

Browsers report regional tags (ja-JP, not bare ja). Verified the checkout renders in the buyer's language across realistic tags, with no selectedLocale passed (the SDK normalizes region -> language internally):

navigator.language Checkout renders in
ja-JP Japanese
en-US English
pt-BR Portuguese
fr-FR French
de-DE German

Rollout / compatibility

  • This changes the default for existing custom-checkout integrations: buyers on non-English browsers will start seeing a localized checkout and localized emails. That is the intended improvement, but it is a behavior change, so it should land in a versioned release with a changelog note, not silently.
  • Developers who pin a language keep passing selectedLocale (and optionally defaultLocale); nothing changes for them.
  • Possible follow-ups: negotiate the full navigator.languages preference list against the supported-locale set (instead of only the top navigator.language), and route the paywall path through the same getBrowserLocale() resolver so there is a single source of the default.

Test plan

  • Unit test for getBrowserLocale() (returns navigator.language; undefined outside a browser).
  • svelte-check clean; existing purchase-flow and locale suites pass.
  • Manually verified checkout rendering across ja-JP / en-US / pt-BR / fr-FR / de-DE with no selectedLocale passed.

Opening as a draft for feedback on the default change and the rollout approach.


Note

Medium Risk
Behavior change for integrations that omit selectedLocale: non-English buyers get localized checkout and persisted locale-driven emails; low code risk but meaningful product/compatibility impact for existing apps.

Overview
Custom checkout flows now default selectedLocale to the buyer's browser locale (navigator.language via new SSR-safe getBrowserLocale()), with English as fallback when the browser locale is unavailable. Explicit selectedLocale still overrides; defaultLocale is unchanged.

This applies to presentExpressPurchaseButton, Stripe, Web Billing, and Paddle purchase paths in main.ts, aligning them with presentPaywall behavior instead of silently defaulting to English.

Unit tests cover getBrowserLocale() in-browser and when window is missing.

Reviewed by Cursor Bugbot for commit 87eefda. Bugbot is set up for automated code reviews on this repo. Configure here.

The custom-checkout entry points (Web Billing, Stripe, Stripe Express,
Paddle) defaulted selectedLocale to hardcoded English, while the paywall
flow already defaults to navigator.language. This aligns all four checkout
flows with the paywall path so the checkout UI and the lifecycle emails
follow the buyer's browser locale by default; passing selectedLocale still
overrides and English remains the final fallback.
The rationale (default for checkout, mirrors the paywall path) lives in the
PR description; the function name and signature carry the rest.
@vkartaviy vkartaviy marked this pull request as ready for review July 6, 2026 22:54
@vkartaviy vkartaviy requested a review from a team as a code owner July 6, 2026 22:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant