From 7abb603ce5c2d22aea20bd3749bcef55af4177ff Mon Sep 17 00:00:00 2001 From: guilherme6191 Date: Fri, 3 Oct 2025 17:49:07 -0300 Subject: [PATCH 1/7] feat: Add optional locale frowm browser to signup flow --- .../clerk-js/src/core/resources/SignUp.ts | 11 ++++- .../src/utils/__tests__/locale.test.ts | 41 +++++++++++++++++++ packages/clerk-js/src/utils/index.ts | 1 + packages/clerk-js/src/utils/locale.ts | 25 +++++++++++ packages/types/src/signUp.ts | 1 + packages/types/src/signUpCommon.ts | 1 + packages/types/src/signUpFuture.ts | 1 + playground/app-router/.gitignore | 5 ++- playground/app-router/README.md | 4 +- 9 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 packages/clerk-js/src/utils/__tests__/locale.test.ts create mode 100644 packages/clerk-js/src/utils/locale.ts diff --git a/packages/clerk-js/src/core/resources/SignUp.ts b/packages/clerk-js/src/core/resources/SignUp.ts index bac10783b6e..6bd5f721f6a 100644 --- a/packages/clerk-js/src/core/resources/SignUp.ts +++ b/packages/clerk-js/src/core/resources/SignUp.ts @@ -46,6 +46,7 @@ import { generateSignatureWithMetamask, generateSignatureWithOKXWallet, getBaseIdentifier, + getBrowserLocale, getClerkQueryParam, getCoinbaseWalletIdentifier, getMetamaskIdentifier, @@ -95,6 +96,7 @@ export class SignUp extends BaseResource implements SignUpResource { createdUserId: string | null = null; abandonAt: number | null = null; legalAcceptedAt: number | null = null; + locale: string | null = null; /** * The current status of the sign-up process. @@ -154,6 +156,11 @@ export class SignUp extends BaseResource implements SignUpResource { let finalParams = { ...params }; + // Inject browser locale if not already provided + if (!finalParams.locale) { + finalParams.locale = getBrowserLocale(); + } + if (!__BUILD_DISABLE_RHC__ && !this.clientBypass() && !this.shouldBypassCaptchaForAttempt(params)) { const captchaChallenge = new CaptchaChallenge(SignUp.clerk); const captchaParams = await captchaChallenge.managedOrInvisible({ action: 'signup' }); @@ -677,7 +684,9 @@ class SignUpFuture implements SignUpFutureResource { async create(params: SignUpFutureCreateParams): Promise<{ error: unknown }> { return runAsyncResourceTask(this.resource, async () => { - await this._create(params); + // Inject browser locale if not already provided + const locale = params.locale || getBrowserLocale(); + await this._create({ ...params, locale }); }); } diff --git a/packages/clerk-js/src/utils/__tests__/locale.test.ts b/packages/clerk-js/src/utils/__tests__/locale.test.ts new file mode 100644 index 00000000000..2ee0c0d5868 --- /dev/null +++ b/packages/clerk-js/src/utils/__tests__/locale.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from 'vitest'; + +import { getBrowserLocale } from '../locale'; + +describe('getBrowserLocale()', () => { + it('returns the browser locale when available', () => { + Object.defineProperty(window.navigator, 'language', { + value: 'es-ES', + configurable: true, + }); + + expect(getBrowserLocale()).toBe('es-ES'); + }); + + it('returns en-US as default when navigator.language is not available', () => { + Object.defineProperty(window.navigator, 'language', { + value: undefined, + configurable: true, + }); + + expect(getBrowserLocale()).toBe('en-US'); + }); + + it('returns en-US when navigator.language is empty string', () => { + Object.defineProperty(window.navigator, 'language', { + value: '', + configurable: true, + }); + + expect(getBrowserLocale()).toBe('en-US'); + }); + + it('returns en-US when navigator object is not defined', () => { + Object.defineProperty(window, 'navigator', { + value: undefined, + configurable: true, + }); + + expect(getBrowserLocale()).toBe('en-US'); + }); +}); diff --git a/packages/clerk-js/src/utils/index.ts b/packages/clerk-js/src/utils/index.ts index 9794c1c3b1f..37bd8d49ff6 100644 --- a/packages/clerk-js/src/utils/index.ts +++ b/packages/clerk-js/src/utils/index.ts @@ -15,6 +15,7 @@ export * from './ignoreEventValue'; export * from './image'; export * from './instance'; export * from './jwt'; +export * from './locale'; export * from './normalizeRoutingOptions'; export * from './organization'; export * from './pageLifecycle'; diff --git a/packages/clerk-js/src/utils/locale.ts b/packages/clerk-js/src/utils/locale.ts new file mode 100644 index 00000000000..187a954197f --- /dev/null +++ b/packages/clerk-js/src/utils/locale.ts @@ -0,0 +1,25 @@ +import { inBrowser } from '@clerk/shared/browser'; + +const DEFAULT_LOCALE = 'en-US'; + +/** + * Detects the user's preferred locale from the browser. + * Falls back to 'en-US' if locale cannot be determined. + * + * @returns The detected locale string in BCP 47 format (e.g., 'en-US', 'es-ES') + */ +export function getBrowserLocale(): string { + if (!inBrowser()) { + return DEFAULT_LOCALE; + } + + // Get locale from the browser + const locale = navigator?.language; + + // Validate that we got a non-empty string + if (!locale || typeof locale !== 'string' || locale.trim() === '') { + return DEFAULT_LOCALE; + } + + return locale; +} diff --git a/packages/types/src/signUp.ts b/packages/types/src/signUp.ts index 63976a78fa1..f6c9e9bd48b 100644 --- a/packages/types/src/signUp.ts +++ b/packages/types/src/signUp.ts @@ -60,6 +60,7 @@ export interface SignUpResource extends ClerkResource { createdUserId: string | null; abandonAt: number | null; legalAcceptedAt: number | null; + locale: string | null; create: (params: SignUpCreateParams) => Promise; diff --git a/packages/types/src/signUpCommon.ts b/packages/types/src/signUpCommon.ts index 573b38d6341..62b81715a2a 100644 --- a/packages/types/src/signUpCommon.ts +++ b/packages/types/src/signUpCommon.ts @@ -100,6 +100,7 @@ export type SignUpCreateParams = Partial< oidcPrompt: string; oidcLoginHint: string; channel: PhoneCodeChannel; + locale?: string; } & Omit>, 'legalAccepted'> >; diff --git a/packages/types/src/signUpFuture.ts b/packages/types/src/signUpFuture.ts index 660d5622f5f..7ff1ae5ffdd 100644 --- a/packages/types/src/signUpFuture.ts +++ b/packages/types/src/signUpFuture.ts @@ -8,6 +8,7 @@ interface SignUpFutureAdditionalParams { lastName?: string; unsafeMetadata?: SignUpUnsafeMetadata; legalAccepted?: boolean; + locale?: string; } export interface SignUpFutureCreateParams extends SignUpFutureAdditionalParams { diff --git a/playground/app-router/.gitignore b/playground/app-router/.gitignore index 8f322f0d8f4..927a115dba8 100644 --- a/playground/app-router/.gitignore +++ b/playground/app-router/.gitignore @@ -25,7 +25,7 @@ yarn-debug.log* yarn-error.log* # local env files -.env*.local +.env* # vercel .vercel @@ -33,3 +33,6 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# clerk configuration (can include secrets) +/.clerk/ diff --git a/playground/app-router/README.md b/playground/app-router/README.md index cde9c0cde80..293d5ade579 100644 --- a/playground/app-router/README.md +++ b/playground/app-router/README.md @@ -12,11 +12,11 @@ yarn dev pnpm dev ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +Open [http://localhost:4011](http://localhost:4011) with your browser to see the result. You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. -[http://localhost:3000/api/hello](http://localhost:3000/api/hello) is an endpoint that uses [Route Handlers](https://nextjs.org/docs/routing/route-handlers). This endpoint can be edited in `app/api/hello/route.ts`. +[http://localhost:4011/api/hello](http://localhost:4011/api/hello) is an endpoint that uses [Route Handlers](https://nextjs.org/docs/routing/route-handlers). This endpoint can be edited in `app/api/hello/route.ts`. This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. From 9bac8f86fdc593807077453f2eccc2f1ff76ed3e Mon Sep 17 00:00:00 2001 From: guilherme6191 Date: Wed, 8 Oct 2025 17:13:50 -0300 Subject: [PATCH 2/7] feat(i18n): add locale to SignUp fixtures, class, and improve testing --- .../clerk-js/src/core/resources/SignUp.ts | 13 ++++++- packages/clerk-js/src/test/core-fixtures.ts | 2 + .../src/utils/__tests__/locale.test.ts | 38 ++++++++----------- packages/clerk-js/src/utils/locale.ts | 8 ++-- packages/types/src/json.ts | 1 + packages/types/src/signUpFuture.ts | 2 + 6 files changed, 35 insertions(+), 29 deletions(-) diff --git a/packages/clerk-js/src/core/resources/SignUp.ts b/packages/clerk-js/src/core/resources/SignUp.ts index 6bd5f721f6a..164a8955340 100644 --- a/packages/clerk-js/src/core/resources/SignUp.ts +++ b/packages/clerk-js/src/core/resources/SignUp.ts @@ -158,7 +158,10 @@ export class SignUp extends BaseResource implements SignUpResource { // Inject browser locale if not already provided if (!finalParams.locale) { - finalParams.locale = getBrowserLocale(); + const browserLocale = getBrowserLocale(); + if (browserLocale) { + finalParams.locale = browserLocale; + } } if (!__BUILD_DISABLE_RHC__ && !this.clientBypass() && !this.shouldBypassCaptchaForAttempt(params)) { @@ -484,6 +487,7 @@ export class SignUp extends BaseResource implements SignUpResource { this.abandonAt = data.abandon_at; this.web3wallet = data.web3_wallet; this.legalAcceptedAt = data.legal_accepted_at; + this.locale = data.locale; } eventBus.emit('resource:update', { resource: this }); @@ -512,6 +516,7 @@ export class SignUp extends BaseResource implements SignUpResource { abandon_at: this.abandonAt, web3_wallet: this.web3wallet, legal_accepted_at: this.legalAcceptedAt, + locale: this.locale, external_account: this.externalAccount, external_account_strategy: this.externalAccount?.strategy, }; @@ -627,6 +632,10 @@ class SignUpFuture implements SignUpFutureResource { return this.resource.legalAcceptedAt; } + get locale() { + return this.resource.locale; + } + get unverifiedFields() { return this.resource.unverifiedFields; } @@ -685,7 +694,7 @@ class SignUpFuture implements SignUpFutureResource { async create(params: SignUpFutureCreateParams): Promise<{ error: unknown }> { return runAsyncResourceTask(this.resource, async () => { // Inject browser locale if not already provided - const locale = params.locale || getBrowserLocale(); + const locale = params.locale || getBrowserLocale() || undefined; await this._create({ ...params, locale }); }); } diff --git a/packages/clerk-js/src/test/core-fixtures.ts b/packages/clerk-js/src/test/core-fixtures.ts index f7d1587fc97..4f7b5ad90d8 100644 --- a/packages/clerk-js/src/test/core-fixtures.ts +++ b/packages/clerk-js/src/test/core-fixtures.ts @@ -231,6 +231,8 @@ export const createSignUp = (signUpParams: Partial = {}) => { first_name: signUpParams.first_name, has_password: signUpParams.has_password, last_name: signUpParams.last_name, + legal_accepted_at: signUpParams.legal_accepted_at, + locale: signUpParams.locale, missing_fields: signUpParams.missing_fields, object: 'sign_up', optional_fields: signUpParams.optional_fields, diff --git a/packages/clerk-js/src/utils/__tests__/locale.test.ts b/packages/clerk-js/src/utils/__tests__/locale.test.ts index 2ee0c0d5868..e486aa3f854 100644 --- a/packages/clerk-js/src/utils/__tests__/locale.test.ts +++ b/packages/clerk-js/src/utils/__tests__/locale.test.ts @@ -1,41 +1,33 @@ -import { describe, expect, it } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { getBrowserLocale } from '../locale'; describe('getBrowserLocale()', () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + it('returns the browser locale when available', () => { - Object.defineProperty(window.navigator, 'language', { - value: 'es-ES', - configurable: true, - }); + vi.stubGlobal('navigator', { language: 'es-ES' }); expect(getBrowserLocale()).toBe('es-ES'); }); - it('returns en-US as default when navigator.language is not available', () => { - Object.defineProperty(window.navigator, 'language', { - value: undefined, - configurable: true, - }); + it('returns null as default when navigator.language is not available', () => { + vi.stubGlobal('navigator', { language: undefined }); - expect(getBrowserLocale()).toBe('en-US'); + expect(getBrowserLocale()).toBeNull(); }); - it('returns en-US when navigator.language is empty string', () => { - Object.defineProperty(window.navigator, 'language', { - value: '', - configurable: true, - }); + it('returns null as default when navigator.language is empty string', () => { + vi.stubGlobal('navigator', { language: '' }); - expect(getBrowserLocale()).toBe('en-US'); + expect(getBrowserLocale()).toBeNull(); }); - it('returns en-US when navigator object is not defined', () => { - Object.defineProperty(window, 'navigator', { - value: undefined, - configurable: true, - }); + it('returns null as default when navigator object is not defined', () => { + vi.stubGlobal('navigator', undefined); - expect(getBrowserLocale()).toBe('en-US'); + expect(getBrowserLocale()).toBeNull(); }); }); diff --git a/packages/clerk-js/src/utils/locale.ts b/packages/clerk-js/src/utils/locale.ts index 187a954197f..7469b4162ea 100644 --- a/packages/clerk-js/src/utils/locale.ts +++ b/packages/clerk-js/src/utils/locale.ts @@ -1,14 +1,14 @@ import { inBrowser } from '@clerk/shared/browser'; -const DEFAULT_LOCALE = 'en-US'; +const DEFAULT_LOCALE = null; /** * Detects the user's preferred locale from the browser. - * Falls back to 'en-US' if locale cannot be determined. + * Falls back to null if locale cannot be determined. * - * @returns The detected locale string in BCP 47 format (e.g., 'en-US', 'es-ES') + * @returns The detected locale string in BCP 47 format (e.g., 'en-US', 'es-ES') or null if locale cannot be determined. */ -export function getBrowserLocale(): string { +export function getBrowserLocale(): string | null { if (!inBrowser()) { return DEFAULT_LOCALE; } diff --git a/packages/types/src/json.ts b/packages/types/src/json.ts index fb8a8eb3b9c..3714242dc53 100644 --- a/packages/types/src/json.ts +++ b/packages/types/src/json.ts @@ -136,6 +136,7 @@ export interface SignUpJSON extends ClerkResourceJSON { created_user_id: string | null; abandon_at: number | null; legal_accepted_at: number | null; + locale: string | null; verifications: SignUpVerificationsJSON | null; } diff --git a/packages/types/src/signUpFuture.ts b/packages/types/src/signUpFuture.ts index 7ff1ae5ffdd..a9c180245b5 100644 --- a/packages/types/src/signUpFuture.ts +++ b/packages/types/src/signUpFuture.ts @@ -137,6 +137,8 @@ export interface SignUpFutureResource { readonly legalAcceptedAt: number | null; + readonly locale: string | null; + create: (params: SignUpFutureCreateParams) => Promise<{ error: unknown }>; update: (params: SignUpFutureUpdateParams) => Promise<{ error: unknown }>; From 55028fa9fe2d19d57c666b066d6fb6f44f554079 Mon Sep 17 00:00:00 2001 From: guilherme6191 Date: Wed, 8 Oct 2025 17:40:02 -0300 Subject: [PATCH 3/7] chore: Update missing locale in state that implements it --- packages/react/src/stateProxy.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/react/src/stateProxy.ts b/packages/react/src/stateProxy.ts index 995c78dc647..ab65bcf8912 100644 --- a/packages/react/src/stateProxy.ts +++ b/packages/react/src/stateProxy.ts @@ -194,6 +194,9 @@ export class StateProxy implements State { get legalAcceptedAt() { return gateProperty(target, 'legalAcceptedAt', null); }, + get locale() { + return gateProperty(target, 'locale', null); + }, get status() { return gateProperty(target, 'status', 'missing_requirements'); }, From 4541abcf5708fb56eda2140c7f77b4e8499bc0fc Mon Sep 17 00:00:00 2001 From: guilherme6191 Date: Wed, 8 Oct 2025 17:48:03 -0300 Subject: [PATCH 4/7] refactor(i18n): only add locale key if any value is present --- packages/clerk-js/src/core/resources/SignUp.ts | 10 ++++++++-- packages/clerk-js/src/utils/locale.ts | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/clerk-js/src/core/resources/SignUp.ts b/packages/clerk-js/src/core/resources/SignUp.ts index 164a8955340..9c817defffe 100644 --- a/packages/clerk-js/src/core/resources/SignUp.ts +++ b/packages/clerk-js/src/core/resources/SignUp.ts @@ -694,8 +694,14 @@ class SignUpFuture implements SignUpFutureResource { async create(params: SignUpFutureCreateParams): Promise<{ error: unknown }> { return runAsyncResourceTask(this.resource, async () => { // Inject browser locale if not already provided - const locale = params.locale || getBrowserLocale() || undefined; - await this._create({ ...params, locale }); + const createParams = { ...params }; + if (!createParams.locale) { + const browserLocale = getBrowserLocale(); + if (browserLocale) { + createParams.locale = browserLocale; + } + } + await this._create(createParams); }); } diff --git a/packages/clerk-js/src/utils/locale.ts b/packages/clerk-js/src/utils/locale.ts index 7469b4162ea..63be37cd018 100644 --- a/packages/clerk-js/src/utils/locale.ts +++ b/packages/clerk-js/src/utils/locale.ts @@ -6,7 +6,7 @@ const DEFAULT_LOCALE = null; * Detects the user's preferred locale from the browser. * Falls back to null if locale cannot be determined. * - * @returns The detected locale string in BCP 47 format (e.g., 'en-US', 'es-ES') or null if locale cannot be determined. + * @returns The browser's reported locale string (typically BCP 47 format like 'en-US', 'es-ES') or null if locale cannot be determined. */ export function getBrowserLocale(): string | null { if (!inBrowser()) { From e4478a035a503b40fdcff796179e41f158d65a4a Mon Sep 17 00:00:00 2001 From: guilherme6191 Date: Thu, 9 Oct 2025 11:10:27 -0300 Subject: [PATCH 5/7] feat: Add try catch to be extra safe --- packages/clerk-js/src/utils/locale.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/clerk-js/src/utils/locale.ts b/packages/clerk-js/src/utils/locale.ts index 63be37cd018..94bdbce3515 100644 --- a/packages/clerk-js/src/utils/locale.ts +++ b/packages/clerk-js/src/utils/locale.ts @@ -13,13 +13,16 @@ export function getBrowserLocale(): string | null { return DEFAULT_LOCALE; } - // Get locale from the browser - const locale = navigator?.language; + try { + // Get locale from the browser + const locale = navigator?.language; - // Validate that we got a non-empty string - if (!locale || typeof locale !== 'string' || locale.trim() === '') { + // Validate that we got a non-empty string + if (!locale || typeof locale !== 'string' || locale.trim() === '') { + return DEFAULT_LOCALE; + } + return locale; + } catch { return DEFAULT_LOCALE; } - - return locale; } From 1096c51699e84eb80cf8ad9b1a770d31fd977020 Mon Sep 17 00:00:00 2001 From: guilherme6191 Date: Thu, 9 Oct 2025 11:14:33 -0300 Subject: [PATCH 6/7] chore: generate changeset --- .changeset/funny-memes-crash.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/funny-memes-crash.md diff --git a/.changeset/funny-memes-crash.md b/.changeset/funny-memes-crash.md new file mode 100644 index 00000000000..cfea551ea5b --- /dev/null +++ b/.changeset/funny-memes-crash.md @@ -0,0 +1,7 @@ +--- +'@clerk/clerk-js': minor +'@clerk/clerk-react': minor +'@clerk/types': minor +--- + +Add support for sign up `locale` From e07029d86ac1e488452fa102944eab1670c9aa38 Mon Sep 17 00:00:00 2001 From: guilherme6191 Date: Thu, 9 Oct 2025 13:43:47 -0300 Subject: [PATCH 7/7] refactor: Move locale util down to _create for consistency --- packages/clerk-js/src/core/resources/SignUp.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/clerk-js/src/core/resources/SignUp.ts b/packages/clerk-js/src/core/resources/SignUp.ts index 9c817defffe..02e23aae5f4 100644 --- a/packages/clerk-js/src/core/resources/SignUp.ts +++ b/packages/clerk-js/src/core/resources/SignUp.ts @@ -686,6 +686,7 @@ class SignUpFuture implements SignUpFutureResource { captchaError, ...params, unsafeMetadata: params.unsafeMetadata ? normalizeUnsafeMetadata(params.unsafeMetadata) : undefined, + locale: params.locale ?? getBrowserLocale(), }; await this.resource.__internal_basePost({ path: this.resource.pathRoot, body }); @@ -693,15 +694,7 @@ class SignUpFuture implements SignUpFutureResource { async create(params: SignUpFutureCreateParams): Promise<{ error: unknown }> { return runAsyncResourceTask(this.resource, async () => { - // Inject browser locale if not already provided - const createParams = { ...params }; - if (!createParams.locale) { - const browserLocale = getBrowserLocale(); - if (browserLocale) { - createParams.locale = browserLocale; - } - } - await this._create(createParams); + await this._create(params); }); }