Skip to content

Commit aa7210c

Browse files
feat(clerk-js, types): Add optional locale from browser to signup flow (#6915)
1 parent 9822e62 commit aa7210c

File tree

13 files changed

+103
-3
lines changed

13 files changed

+103
-3
lines changed

.changeset/funny-memes-crash.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@clerk/clerk-js': minor
3+
'@clerk/clerk-react': minor
4+
'@clerk/types': minor
5+
---
6+
7+
Add support for sign up `locale`

packages/clerk-js/src/core/resources/SignUp.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
generateSignatureWithMetamask,
4747
generateSignatureWithOKXWallet,
4848
getBaseIdentifier,
49+
getBrowserLocale,
4950
getClerkQueryParam,
5051
getCoinbaseWalletIdentifier,
5152
getMetamaskIdentifier,
@@ -95,6 +96,7 @@ export class SignUp extends BaseResource implements SignUpResource {
9596
createdUserId: string | null = null;
9697
abandonAt: number | null = null;
9798
legalAcceptedAt: number | null = null;
99+
locale: string | null = null;
98100

99101
/**
100102
* The current status of the sign-up process.
@@ -154,6 +156,14 @@ export class SignUp extends BaseResource implements SignUpResource {
154156

155157
let finalParams = { ...params };
156158

159+
// Inject browser locale if not already provided
160+
if (!finalParams.locale) {
161+
const browserLocale = getBrowserLocale();
162+
if (browserLocale) {
163+
finalParams.locale = browserLocale;
164+
}
165+
}
166+
157167
if (!__BUILD_DISABLE_RHC__ && !this.clientBypass() && !this.shouldBypassCaptchaForAttempt(params)) {
158168
const captchaChallenge = new CaptchaChallenge(SignUp.clerk);
159169
const captchaParams = await captchaChallenge.managedOrInvisible({ action: 'signup' });
@@ -477,6 +487,7 @@ export class SignUp extends BaseResource implements SignUpResource {
477487
this.abandonAt = data.abandon_at;
478488
this.web3wallet = data.web3_wallet;
479489
this.legalAcceptedAt = data.legal_accepted_at;
490+
this.locale = data.locale;
480491
}
481492

482493
eventBus.emit('resource:update', { resource: this });
@@ -505,6 +516,7 @@ export class SignUp extends BaseResource implements SignUpResource {
505516
abandon_at: this.abandonAt,
506517
web3_wallet: this.web3wallet,
507518
legal_accepted_at: this.legalAcceptedAt,
519+
locale: this.locale,
508520
external_account: this.externalAccount,
509521
external_account_strategy: this.externalAccount?.strategy,
510522
};
@@ -620,6 +632,10 @@ class SignUpFuture implements SignUpFutureResource {
620632
return this.resource.legalAcceptedAt;
621633
}
622634

635+
get locale() {
636+
return this.resource.locale;
637+
}
638+
623639
get unverifiedFields() {
624640
return this.resource.unverifiedFields;
625641
}
@@ -670,6 +686,7 @@ class SignUpFuture implements SignUpFutureResource {
670686
captchaError,
671687
...params,
672688
unsafeMetadata: params.unsafeMetadata ? normalizeUnsafeMetadata(params.unsafeMetadata) : undefined,
689+
locale: params.locale ?? getBrowserLocale(),
673690
};
674691

675692
await this.resource.__internal_basePost({ path: this.resource.pathRoot, body });

packages/clerk-js/src/test/core-fixtures.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,8 @@ export const createSignUp = (signUpParams: Partial<SignUpJSON> = {}) => {
231231
first_name: signUpParams.first_name,
232232
has_password: signUpParams.has_password,
233233
last_name: signUpParams.last_name,
234+
legal_accepted_at: signUpParams.legal_accepted_at,
235+
locale: signUpParams.locale,
234236
missing_fields: signUpParams.missing_fields,
235237
object: 'sign_up',
236238
optional_fields: signUpParams.optional_fields,
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { afterEach, describe, expect, it, vi } from 'vitest';
2+
3+
import { getBrowserLocale } from '../locale';
4+
5+
describe('getBrowserLocale()', () => {
6+
afterEach(() => {
7+
vi.unstubAllGlobals();
8+
});
9+
10+
it('returns the browser locale when available', () => {
11+
vi.stubGlobal('navigator', { language: 'es-ES' });
12+
13+
expect(getBrowserLocale()).toBe('es-ES');
14+
});
15+
16+
it('returns null as default when navigator.language is not available', () => {
17+
vi.stubGlobal('navigator', { language: undefined });
18+
19+
expect(getBrowserLocale()).toBeNull();
20+
});
21+
22+
it('returns null as default when navigator.language is empty string', () => {
23+
vi.stubGlobal('navigator', { language: '' });
24+
25+
expect(getBrowserLocale()).toBeNull();
26+
});
27+
28+
it('returns null as default when navigator object is not defined', () => {
29+
vi.stubGlobal('navigator', undefined);
30+
31+
expect(getBrowserLocale()).toBeNull();
32+
});
33+
});

packages/clerk-js/src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export * from './ignoreEventValue';
1515
export * from './image';
1616
export * from './instance';
1717
export * from './jwt';
18+
export * from './locale';
1819
export * from './normalizeRoutingOptions';
1920
export * from './organization';
2021
export * from './pageLifecycle';
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { inBrowser } from '@clerk/shared/browser';
2+
3+
const DEFAULT_LOCALE = null;
4+
5+
/**
6+
* Detects the user's preferred locale from the browser.
7+
* Falls back to null if locale cannot be determined.
8+
*
9+
* @returns The browser's reported locale string (typically BCP 47 format like 'en-US', 'es-ES') or null if locale cannot be determined.
10+
*/
11+
export function getBrowserLocale(): string | null {
12+
if (!inBrowser()) {
13+
return DEFAULT_LOCALE;
14+
}
15+
16+
try {
17+
// Get locale from the browser
18+
const locale = navigator?.language;
19+
20+
// Validate that we got a non-empty string
21+
if (!locale || typeof locale !== 'string' || locale.trim() === '') {
22+
return DEFAULT_LOCALE;
23+
}
24+
return locale;
25+
} catch {
26+
return DEFAULT_LOCALE;
27+
}
28+
}

packages/react/src/stateProxy.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,9 @@ export class StateProxy implements State {
194194
get legalAcceptedAt() {
195195
return gateProperty(target, 'legalAcceptedAt', null);
196196
},
197+
get locale() {
198+
return gateProperty(target, 'locale', null);
199+
},
197200
get status() {
198201
return gateProperty(target, 'status', 'missing_requirements');
199202
},

packages/types/src/json.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ export interface SignUpJSON extends ClerkResourceJSON {
136136
created_user_id: string | null;
137137
abandon_at: number | null;
138138
legal_accepted_at: number | null;
139+
locale: string | null;
139140
verifications: SignUpVerificationsJSON | null;
140141
}
141142

packages/types/src/signUp.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export interface SignUpResource extends ClerkResource {
6060
createdUserId: string | null;
6161
abandonAt: number | null;
6262
legalAcceptedAt: number | null;
63+
locale: string | null;
6364

6465
create: (params: SignUpCreateParams) => Promise<SignUpResource>;
6566

packages/types/src/signUpCommon.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ export type SignUpCreateParams = Partial<
100100
oidcPrompt: string;
101101
oidcLoginHint: string;
102102
channel: PhoneCodeChannel;
103+
locale?: string;
103104
} & Omit<SnakeToCamel<Record<SignUpAttributeField | SignUpVerifiableField, string>>, 'legalAccepted'>
104105
>;
105106

0 commit comments

Comments
 (0)