From 216056c60b7d5598cb1166576c7120e269e0231a Mon Sep 17 00:00:00 2001 From: guilherme6191 Date: Thu, 16 Oct 2025 16:12:37 -0300 Subject: [PATCH 1/6] feat(i18n): detect locale from browser and send it to FAPI if exists at sign-in --- .../clerk-js/src/core/resources/SignIn.ts | 14 ++- .../core/resources/__tests__/SignIn.test.ts | 113 +++++++++++++----- packages/clerk-js/vitest.setup.mts | 7 ++ 3 files changed, 102 insertions(+), 32 deletions(-) diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts index e98538e1ba5..c084e008f20 100644 --- a/packages/clerk-js/src/core/resources/SignIn.ts +++ b/packages/clerk-js/src/core/resources/SignIn.ts @@ -69,6 +69,7 @@ import { generateSignatureWithMetamask, generateSignatureWithOKXWallet, getBaseIdentifier, + getBrowserLocale, getClerkQueryParam, getCoinbaseWalletIdentifier, getMetamaskIdentifier, @@ -164,9 +165,10 @@ export class SignIn extends BaseResource implements SignInResource { create = (params: SignInCreateParams): Promise => { debugLogger.debug('SignIn.create', { id: this.id, strategy: 'strategy' in params ? params.strategy : undefined }); + const locale = getBrowserLocale(); return this._basePost({ path: this.pathRoot, - body: params, + body: { ...params, ...(locale ? { locale } : {}) }, }); }; @@ -708,9 +710,10 @@ class SignInFuture implements SignInFutureResource { } private async _create(params: SignInFutureCreateParams): Promise { + const locale = getBrowserLocale(); await this.resource.__internal_basePost({ path: this.resource.pathRoot, - body: params, + body: { ...params, ...(locale ? { locale } : {}) }, }); } @@ -729,9 +732,14 @@ class SignInFuture implements SignInFutureResource { // TODO @userland-errors: const identifier = params.identifier || params.emailAddress || params.phoneNumber; const previousIdentifier = this.resource.identifier; + const locale = getBrowserLocale(); await this.resource.__internal_basePost({ path: this.resource.pathRoot, - body: { identifier: identifier || previousIdentifier, password: params.password }, + body: { + identifier: identifier || previousIdentifier, + password: params.password, + ...(locale ? { locale } : {}), + }, }); }); } diff --git a/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts b/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts index 824e8845460..67b015ec0c5 100644 --- a/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts @@ -1,39 +1,94 @@ -import { describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; import { BaseResource } from '../internal'; import { SignIn } from '../SignIn'; describe('SignIn', () => { + describe('signIn.create', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it('includes locale in request body when navigator.language is available', async () => { + vi.stubGlobal('navigator', { language: 'fr-FR' }); + + const mockFetch = vi.fn().mockResolvedValue({ + client: null, + response: { id: 'signin_123', status: 'needs_first_factor' }, + }); + BaseResource._fetch = mockFetch; + + const signIn = new SignIn(); + await signIn.create({ identifier: 'user@example.com' }); + + expect(mockFetch).toHaveBeenCalledWith( + expect.objectContaining({ + method: 'POST', + path: '/client/sign_ins', + body: { + identifier: 'user@example.com', + locale: 'fr-FR', + }, + }), + ); + }); + + it('excludes locale from request body when navigator.language is empty', async () => { + vi.stubGlobal('navigator', { language: '' }); + + const mockFetch = vi.fn().mockResolvedValue({ + client: null, + response: { id: 'signin_123', status: 'needs_first_factor' }, + }); + BaseResource._fetch = mockFetch; + + const signIn = new SignIn(); + await signIn.create({ identifier: 'user@example.com' }); + + expect(mockFetch).toHaveBeenCalledWith( + expect.objectContaining({ + method: 'POST', + path: '/client/sign_ins', + body: { + identifier: 'user@example.com', + }, + }), + ); + }); + }); + describe('SignInFuture', () => { describe('selectFirstFactor', () => { - const signInCreatedJSON = { - id: 'test_id', - - supported_first_factors: [ - { strategy: 'email_code', emailAddressId: 'email_address_0', safe_identifier: 'test+abc@clerk.com' }, - { strategy: 'email_code', emailAddressId: 'email_address_1', safe_identifier: 'test@clerk.com' }, - { strategy: 'phone_code', phoneNumberId: 'phone_number_1', safe_identifier: '+301234567890' }, - ], - }; - - const firstFactorPreparedJSON = {}; - - BaseResource._fetch = vi.fn().mockImplementation(({ method, path, body }) => { - if (method === 'POST' && path === '/client/sign_ins') { - return Promise.resolve({ - client: null, - response: { ...signInCreatedJSON, identifier: body.identifier }, - }); - } - - if (method === 'POST' && path === '/client/sign_ins/test_id/prepare_first_factor') { - return Promise.resolve({ - client: null, - response: firstFactorPreparedJSON, - }); - } - - throw new Error('Unexpected call to BaseResource._fetch'); + beforeAll(() => { + const signInCreatedJSON = { + id: 'test_id', + + supported_first_factors: [ + { strategy: 'email_code', emailAddressId: 'email_address_0', safe_identifier: 'test+abc@clerk.com' }, + { strategy: 'email_code', emailAddressId: 'email_address_1', safe_identifier: 'test@clerk.com' }, + { strategy: 'phone_code', phoneNumberId: 'phone_number_1', safe_identifier: '+301234567890' }, + ], + }; + + const firstFactorPreparedJSON = {}; + + BaseResource._fetch = vi.fn().mockImplementation(({ method, path, body }) => { + if (method === 'POST' && path === '/client/sign_ins') { + return Promise.resolve({ + client: null, + response: { ...signInCreatedJSON, identifier: body.identifier }, + }); + } + + if (method === 'POST' && path === '/client/sign_ins/test_id/prepare_first_factor') { + return Promise.resolve({ + client: null, + response: firstFactorPreparedJSON, + }); + } + + throw new Error('Unexpected call to BaseResource._fetch'); + }); }); it('should select correct first factor by email address', async () => { diff --git a/packages/clerk-js/vitest.setup.mts b/packages/clerk-js/vitest.setup.mts index 2e81392e900..c685560d307 100644 --- a/packages/clerk-js/vitest.setup.mts +++ b/packages/clerk-js/vitest.setup.mts @@ -103,6 +103,13 @@ if (typeof window !== 'undefined') { writable: true, }); + // Set default navigator.language to empty to prevent auto-locale injection in tests + Object.defineProperty(window.navigator, 'language', { + writable: true, + configurable: true, + value: undefined, + }); + // Mock IntersectionObserver //@ts-expect-error - Mocking class globalThis.IntersectionObserver = class IntersectionObserver { From bbc986ab767e23356aee49223ee6dc35635db286 Mon Sep 17 00:00:00 2001 From: guilherme6191 Date: Thu, 16 Oct 2025 16:15:18 -0300 Subject: [PATCH 2/6] chore: generate changeset --- .changeset/quick-kings-rescue.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/quick-kings-rescue.md diff --git a/.changeset/quick-kings-rescue.md b/.changeset/quick-kings-rescue.md new file mode 100644 index 00000000000..87b88c2f0c2 --- /dev/null +++ b/.changeset/quick-kings-rescue.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': patch +--- + +Add browser locale to sign-in flow From b5a35869ed42bd25e4c1fc10b79c6c322af9fce2 Mon Sep 17 00:00:00 2001 From: guilherme6191 Date: Thu, 16 Oct 2025 16:38:45 -0300 Subject: [PATCH 3/6] tests: add global unstub to be extra safe --- packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts b/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts index 67b015ec0c5..c18e20aeb56 100644 --- a/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts @@ -7,6 +7,7 @@ describe('SignIn', () => { describe('signIn.create', () => { afterEach(() => { vi.clearAllMocks(); + vi.unstubAllGlobals(); }); it('includes locale in request body when navigator.language is available', async () => { From 35b45c2b9892e8d7a34a2c497e8a79aeb34039a9 Mon Sep 17 00:00:00 2001 From: guilherme6191 Date: Thu, 16 Oct 2025 16:43:53 -0300 Subject: [PATCH 4/6] fix: default value to empty string --- packages/clerk-js/vitest.setup.mts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/clerk-js/vitest.setup.mts b/packages/clerk-js/vitest.setup.mts index c685560d307..a9acc96325b 100644 --- a/packages/clerk-js/vitest.setup.mts +++ b/packages/clerk-js/vitest.setup.mts @@ -103,11 +103,10 @@ if (typeof window !== 'undefined') { writable: true, }); - // Set default navigator.language to empty to prevent auto-locale injection in tests Object.defineProperty(window.navigator, 'language', { writable: true, configurable: true, - value: undefined, + value: '', }); // Mock IntersectionObserver From f5000173d2ee6f00e74627d8ec9054721c77aa5f Mon Sep 17 00:00:00 2001 From: guilherme6191 Date: Thu, 16 Oct 2025 18:03:16 -0300 Subject: [PATCH 5/6] feat: Address review comments --- .changeset/quick-kings-rescue.md | 2 +- packages/clerk-js/src/core/resources/SignIn.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/quick-kings-rescue.md b/.changeset/quick-kings-rescue.md index 87b88c2f0c2..fb9c37d9c32 100644 --- a/.changeset/quick-kings-rescue.md +++ b/.changeset/quick-kings-rescue.md @@ -2,4 +2,4 @@ '@clerk/clerk-js': patch --- -Add browser locale to sign-in flow +Add support for automatically sending the browser locale during the sign-in flow diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts index c084e008f20..17a00d15221 100644 --- a/packages/clerk-js/src/core/resources/SignIn.ts +++ b/packages/clerk-js/src/core/resources/SignIn.ts @@ -168,7 +168,7 @@ export class SignIn extends BaseResource implements SignInResource { const locale = getBrowserLocale(); return this._basePost({ path: this.pathRoot, - body: { ...params, ...(locale ? { locale } : {}) }, + body: { ...(locale ? { locale } : {}), ...params }, }); }; @@ -713,7 +713,7 @@ class SignInFuture implements SignInFutureResource { const locale = getBrowserLocale(); await this.resource.__internal_basePost({ path: this.resource.pathRoot, - body: { ...params, ...(locale ? { locale } : {}) }, + body: { ...(locale ? { locale } : {}), ...params }, }); } From c78137247124c2383a778d2117337891baf0d697 Mon Sep 17 00:00:00 2001 From: guilherme6191 Date: Fri, 17 Oct 2025 13:19:49 -0300 Subject: [PATCH 6/6] refactor: Reduce spreads --- packages/clerk-js/src/core/resources/SignIn.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts index 17a00d15221..d2bb1dee2fb 100644 --- a/packages/clerk-js/src/core/resources/SignIn.ts +++ b/packages/clerk-js/src/core/resources/SignIn.ts @@ -168,7 +168,7 @@ export class SignIn extends BaseResource implements SignInResource { const locale = getBrowserLocale(); return this._basePost({ path: this.pathRoot, - body: { ...(locale ? { locale } : {}), ...params }, + body: locale ? { locale, ...params } : params, }); }; @@ -713,7 +713,7 @@ class SignInFuture implements SignInFutureResource { const locale = getBrowserLocale(); await this.resource.__internal_basePost({ path: this.resource.pathRoot, - body: { ...(locale ? { locale } : {}), ...params }, + body: locale ? { locale, ...params } : params, }); }