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(),
};

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,
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