Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/funny-memes-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/clerk-js': minor
'@clerk/clerk-react': minor
'@clerk/types': minor
---

Add support for sign up `locale`
17 changes: 17 additions & 0 deletions packages/clerk-js/src/core/resources/SignUp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
generateSignatureWithMetamask,
generateSignatureWithOKXWallet,
getBaseIdentifier,
getBrowserLocale,
getClerkQueryParam,
getCoinbaseWalletIdentifier,
getMetamaskIdentifier,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -154,6 +156,14 @@ export class SignUp extends BaseResource implements SignUpResource {

let finalParams = { ...params };

// Inject browser locale if not already provided
if (!finalParams.locale) {
const browserLocale = getBrowserLocale();
if (browserLocale) {
finalParams.locale = browserLocale;
}
}

if (!__BUILD_DISABLE_RHC__ && !this.clientBypass() && !this.shouldBypassCaptchaForAttempt(params)) {
const captchaChallenge = new CaptchaChallenge(SignUp.clerk);
const captchaParams = await captchaChallenge.managedOrInvisible({ action: 'signup' });
Expand Down Expand Up @@ -477,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 });
Expand Down Expand Up @@ -505,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,
};
Expand Down Expand Up @@ -620,6 +632,10 @@ class SignUpFuture implements SignUpFutureResource {
return this.resource.legalAcceptedAt;
}

get locale() {
return this.resource.locale;
}

get unverifiedFields() {
return this.resource.unverifiedFields;
}
Expand Down Expand Up @@ -670,6 +686,7 @@ class SignUpFuture implements SignUpFutureResource {
captchaError,
...params,
unsafeMetadata: params.unsafeMetadata ? normalizeUnsafeMetadata(params.unsafeMetadata) : undefined,
locale: params.locale ?? getBrowserLocale(),
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Locale injection in _create is correct.

The nullish coalescing operator properly handles the fallback: user-provided locale takes precedence, then browser locale, and finally null (when browser detection fails).

However, ensure that the password, sendPhoneCode (when !id), and sso methods also inject locale into their request bodies to maintain consistency across all sign-up creation flows. Previous review comments flagged this gap.


🏁 Script executed:

#!/bin/bash
# Verify locale injection in all sign-up creation POST requests
rg -nC3 "__internal_basePost.*pathRoot" packages/clerk-js/src/core/resources/SignUp.ts | grep -A10 "body:"

Length of output: 0


🏁 Script executed:

#!/bin/bash
rg -n "__internal_basePost" -C5 packages/clerk-js/src/core/resources/SignUp.ts

Length of output: 4285


Inject locale into sendPhoneCode and SSO flows

sendPhoneCode’s initial POST (when !id) and its prepare-verification branch, as well as the SSO prepare POST, currently omit locale: params.locale ?? getBrowserLocale(). Add this fallback to their request bodies.

🤖 Prompt for AI Agents
In packages/clerk-js/src/core/resources/SignUp.ts around line 689, the
sendPhoneCode initial POST when !id, its prepare-verification branch, and the
SSO prepare POST are missing the locale field; update each request body to
include locale: params.locale ?? getBrowserLocale() so the locale fallback is
sent with the request, adding that expression into the respective body objects
for those three branches.

✅ Addressed in commits 65b7cc7 to e07029d

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need that now especially because that's for the FutureSignUp logic with signal. That's experimental and I'm unsure we need to add more surface and update it now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@guilherme6191, understood! That makes sense—since SignUpFuture is experimental, it's reasonable to keep the scope focused for now and avoid expanding the surface area unnecessarily. The current implementation in _create covers the main use case. 👍

};

await this.resource.__internal_basePost({ path: this.resource.pathRoot, body });
Expand Down
2 changes: 2 additions & 0 deletions packages/clerk-js/src/test/core-fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,8 @@ export const createSignUp = (signUpParams: Partial<SignUpJSON> = {}) => {
first_name: signUpParams.first_name,
has_password: signUpParams.has_password,
last_name: signUpParams.last_name,
legal_accepted_at: signUpParams.legal_accepted_at,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch!

locale: signUpParams.locale,
missing_fields: signUpParams.missing_fields,
object: 'sign_up',
optional_fields: signUpParams.optional_fields,
Expand Down
33 changes: 33 additions & 0 deletions packages/clerk-js/src/utils/__tests__/locale.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { afterEach, describe, expect, it, vi } from 'vitest';

import { getBrowserLocale } from '../locale';

describe('getBrowserLocale()', () => {
afterEach(() => {
vi.unstubAllGlobals();
});

it('returns the browser locale when available', () => {
vi.stubGlobal('navigator', { language: 'es-ES' });

expect(getBrowserLocale()).toBe('es-ES');
});

it('returns null as default when navigator.language is not available', () => {
vi.stubGlobal('navigator', { language: undefined });

expect(getBrowserLocale()).toBeNull();
});

it('returns null as default when navigator.language is empty string', () => {
vi.stubGlobal('navigator', { language: '' });

expect(getBrowserLocale()).toBeNull();
});

it('returns null as default when navigator object is not defined', () => {
vi.stubGlobal('navigator', undefined);

expect(getBrowserLocale()).toBeNull();
});
});
1 change: 1 addition & 0 deletions packages/clerk-js/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
28 changes: 28 additions & 0 deletions packages/clerk-js/src/utils/locale.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { inBrowser } from '@clerk/shared/browser';

const DEFAULT_LOCALE = null;

/**
* Detects the user's preferred locale from the browser.
* Falls back to 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()) {
return DEFAULT_LOCALE;
}

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() === '') {
return DEFAULT_LOCALE;
}
return locale;
} catch {
return DEFAULT_LOCALE;
}
}
3 changes: 3 additions & 0 deletions packages/react/src/stateProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
},
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
1 change: 1 addition & 0 deletions packages/types/src/signUp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<SignUpResource>;

Expand Down
1 change: 1 addition & 0 deletions packages/types/src/signUpCommon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export type SignUpCreateParams = Partial<
oidcPrompt: string;
oidcLoginHint: string;
channel: PhoneCodeChannel;
locale?: string;
} & Omit<SnakeToCamel<Record<SignUpAttributeField | SignUpVerifiableField, string>>, 'legalAccepted'>
>;

Expand Down
3 changes: 3 additions & 0 deletions packages/types/src/signUpFuture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ interface SignUpFutureAdditionalParams {
lastName?: string;
unsafeMetadata?: SignUpUnsafeMetadata;
legalAccepted?: boolean;
locale?: string;
}

export interface SignUpFutureCreateParams extends SignUpFutureAdditionalParams {
Expand Down Expand Up @@ -136,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 }>;
Expand Down
5 changes: 4 additions & 1 deletion playground/app-router/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,14 @@ yarn-debug.log*
yarn-error.log*

# local env files
.env*.local
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts

# clerk configuration (can include secrets)
/.clerk/
4 changes: 2 additions & 2 deletions playground/app-router/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Loading