diff --git a/.changeset/password-confirmation-attempts.md b/.changeset/password-confirmation-attempts.md new file mode 100644 index 00000000000..489bad323b7 --- /dev/null +++ b/.changeset/password-confirmation-attempts.md @@ -0,0 +1,11 @@ +--- +'@clerk/clerk-js': minor +'@clerk/localizations': minor +'@clerk/shared': minor +'@clerk/ui': minor +--- + +Show how many password attempts remain when a user enters the wrong current password while changing their password or during reverification, and explain that the session has ended once the attempts run out. + +- `ClerkAPIError.meta.remainingAttempts` holds the attempts left in the current session. `0` means the session has ended, and Clerk signs the user out of the current tab and notifies other tabs. +- Customize the messages with the `unstable__errors.password_confirmation_attempt_remaining`, `unstable__errors.password_confirmation_attempts_remaining` (receives `{{remainingAttempts}}`), and `unstable__errors.password_confirmation_session_ended` localization keys. diff --git a/packages/clerk-js/src/core/__tests__/clerk.test.ts b/packages/clerk-js/src/core/__tests__/clerk.test.ts index 0a2a7f40aaa..05c1a264574 100644 --- a/packages/clerk-js/src/core/__tests__/clerk.test.ts +++ b/packages/clerk-js/src/core/__tests__/clerk.test.ts @@ -3815,6 +3815,36 @@ describe('Clerk singleton', () => { }); }); + describe('__internal_handleSessionEnded', () => { + afterEach(() => { + (window as any).__internal_onAfterSetActive = null; + }); + + it('signs out and notifies other tabs after the server ended the session', async () => { + const mockOnAfterSetActive = vi.fn().mockReturnValue(Promise.resolve()); + (window as any).__internal_onAfterSetActive = mockOnAfterSetActive; + const eventBusSpy = vi.spyOn(eventBus, 'emit'); + const mockSession = { + id: 'session_1', + status: 'active', + user: { id: 'user_1' }, + lastActiveToken: { getRawString: () => 'token_1' }, + }; + + const sut = new Clerk(productionPublishableKey); + sut.updateClient({ sessions: [mockSession], signedInSessions: [mockSession] } as any); + // The failed confirmation's response carries the client without the ended session. + sut.updateClient({ sessions: [], signedInSessions: [] } as any); + + await sut.__internal_handleSessionEnded(); + + expect(sut.session).toBeNull(); + expect(eventBusSpy).toHaveBeenCalledWith(events.UserSignOut, null); + expect(mockOnAfterSetActive).toHaveBeenCalled(); + eventBusSpy.mockRestore(); + }); + }); + describe('__internal_attemptToEnableEnvironmentSetting', () => { afterEach(() => { mockEnvironmentFetch.mockReset(); diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index f5eaa05aa92..6a41b3b2b9a 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -2992,10 +2992,7 @@ export class Clerk implements ClerkInterface { if (this.session) { return; } - if (opts.broadcast) { - eventBus.emit(events.UserSignOut, null); - } - return this.setActive({ session: null }); + return this.#completeSignOut(opts); } catch (err) { // `/client` can fail with either a 401, a 403, 500 or network errors. // 401 is already handled internally in our fetcher. @@ -3009,6 +3006,23 @@ export class Clerk implements ClerkInterface { } }; + /** + * Completes a sign-out that FAPI already performed, such as when a failed password + * confirmation ended the session and the piggybacked client no longer contains it. + * + * @internal + */ + public __internal_handleSessionEnded = (): Promise => { + return this.#completeSignOut({ broadcast: true }); + }; + + #completeSignOut = (opts: { broadcast: boolean }): Promise => { + if (opts.broadcast) { + eventBus.emit(events.UserSignOut, null); + } + return this.setActive({ session: null }); + }; + public __internal_handleUnauthenticatedDevBrowser = async (): Promise => { if (this.#authService) { await this.#authService.handleUnauthenticatedDevBrowser(); diff --git a/packages/clerk-js/src/core/resources/Base.ts b/packages/clerk-js/src/core/resources/Base.ts index 4ad63d5d01c..6e5baffb446 100644 --- a/packages/clerk-js/src/core/resources/Base.ts +++ b/packages/clerk-js/src/core/resources/Base.ts @@ -157,6 +157,9 @@ export abstract class BaseResource { await BaseResource.clerk.__internal_handleUnauthenticatedDevBrowser(); } else if (status === 401 && code !== 'requires_captcha') { await BaseResource.clerk.handleUnauthenticated(); + } else if (status === 422 && errors?.[0]?.meta?.remaining_attempts === 0) { + // The password confirmation limit ended the session and invalidated this client. + await BaseResource.clerk.__internal_handleSessionEnded(); } assertProductionKeysOnDev(status, errors); diff --git a/packages/clerk-js/src/core/resources/__tests__/Base.test.ts b/packages/clerk-js/src/core/resources/__tests__/Base.test.ts index 0eb8022b2a2..299116c47c8 100644 --- a/packages/clerk-js/src/core/resources/__tests__/Base.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/Base.test.ts @@ -11,6 +11,10 @@ class TestResource extends BaseResource { return this._baseGet(); } + mutate() { + return BaseResource._fetch({ method: 'PATCH', path: '/test' }); + } + fromJSON() { return this; } @@ -58,4 +62,82 @@ describe('BaseResource', () => { console.dir(errResponse); expect(errResponse.retryAfter).toBe(undefined); }); + + it('applies a piggybacked client update before throwing a failed mutation response', async () => { + const requestingSession = { id: 'sess_requesting', status: 'ended' }; + const otherSession = { id: 'sess_other', status: 'active' }; + const client = { id: 'client_1', sessions: [requestingSession, otherSession] }; + const updateClient = vi.fn(); + + BaseResource.clerk = { + getFapiClient: () => ({ + request: vi.fn().mockResolvedValue({ + payload: { + client, + errors: [{ code: 'form_password_validation_failed', message: 'Password is incorrect' }], + }, + status: 422, + statusText: 'Unprocessable Entity', + headers: new Headers(), + }), + }), + __internal_setCountry: vi.fn(), + updateClient, + } as any; + + const resource = new TestResource(); + await expect(resource.mutate()).rejects.toMatchObject({ + errors: [expect.objectContaining({ code: 'form_password_validation_failed' })], + }); + + expect(updateClient).toHaveBeenCalledWith( + expect.objectContaining({ + sessions: expect.arrayContaining([ + expect.objectContaining({ id: requestingSession.id, status: requestingSession.status }), + expect.objectContaining({ id: otherSession.id, status: otherSession.status }), + ]), + }), + ); + }); + it.each([ + [0, 1], + [1, 0], + ])( + 'completes the sign-out when a password confirmation ends the session (remaining attempts: %i)', + async (remainingAttempts, expectedCalls) => { + const updateClient = vi.fn(); + const handleSessionEnded = vi.fn().mockResolvedValue(undefined); + + BaseResource.clerk = { + getFapiClient: () => ({ + request: vi.fn().mockResolvedValue({ + payload: { + client: { id: 'client_1', sessions: [] }, + errors: [ + { + code: 'form_password_validation_failed', + message: 'Password is incorrect', + meta: { remaining_attempts: remainingAttempts }, + }, + ], + }, + status: 422, + statusText: 'Unprocessable Entity', + headers: new Headers(), + }), + }), + __internal_setCountry: vi.fn(), + __internal_handleSessionEnded: handleSessionEnded, + updateClient, + } as any; + + const resource = new TestResource(); + await expect(resource.mutate()).rejects.toMatchObject({ + errors: [expect.objectContaining({ meta: expect.objectContaining({ remainingAttempts }) })], + }); + + expect(updateClient).toHaveBeenCalledTimes(1); + expect(handleSessionEnded).toHaveBeenCalledTimes(expectedCalls); + }, + ); }); diff --git a/packages/localizations/src/ar-SA.ts b/packages/localizations/src/ar-SA.ts index 0665f4b68d0..c7b7537a37e 100644 --- a/packages/localizations/src/ar-SA.ts +++ b/packages/localizations/src/ar-SA.ts @@ -2101,6 +2101,10 @@ export const arSA: LocalizationResource = { requireUppercase: 'حرف كبير', sentencePrefix: 'كلمة السر يجب أن تحتوي على', }, + password_confirmation_attempt_remaining: 'كلمة المرور غير صحيحة. لديك محاولة واحدة متبقية.', + password_confirmation_attempts_remaining: 'كلمة المرور غير صحيحة. عدد المحاولات المتبقية: {{remainingAttempts}}.', + password_confirmation_session_ended: + 'انتهت جلستك لأنك وصلت إلى الحد الأقصى لمحاولات تأكيد كلمة المرور. سجّل الدخول مرة أخرى للمتابعة.', phone_number_exists: 'هذا الرقم مأخوذ الرجاء أختيار رقم آخر', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/be-BY.ts b/packages/localizations/src/be-BY.ts index e4ad0e6c004..68a7598a4f5 100644 --- a/packages/localizations/src/be-BY.ts +++ b/packages/localizations/src/be-BY.ts @@ -2112,6 +2112,10 @@ export const beBY: LocalizationResource = { requireUppercase: 'літару ў верхнім рэгістры', sentencePrefix: 'Ваш пароль павінен утрымліваць', }, + password_confirmation_attempt_remaining: 'Няправільны пароль. У вас засталася 1 спроба.', + password_confirmation_attempts_remaining: 'Няправільны пароль. Засталося спроб: {{remainingAttempts}}.', + password_confirmation_session_ended: + 'Ваш сеанс завершаны, бо вы дасягнулі ліміту спроб пацвярджэння пароля. Увайдзіце зноў, каб працягнуць.', phone_number_exists: 'Гэты нумар тэлефона ўжо заняты. Калі ласка, паспрабуйце іншы.', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/bg-BG.ts b/packages/localizations/src/bg-BG.ts index 717ee166f6e..7ba04c82119 100644 --- a/packages/localizations/src/bg-BG.ts +++ b/packages/localizations/src/bg-BG.ts @@ -2105,6 +2105,10 @@ export const bgBG: LocalizationResource = { requireUppercase: 'главна буква', sentencePrefix: 'Вашата парола трябва да съдържа', }, + password_confirmation_attempt_remaining: 'Грешна парола. Остава ви 1 опит.', + password_confirmation_attempts_remaining: 'Грешна парола. Остават ви {{remainingAttempts}} опита.', + password_confirmation_session_ended: + 'Сесията ви приключи, защото достигнахте лимита на опитите за потвърждение на паролата. Влезте отново, за да продължите.', phone_number_exists: 'Този телефонен номер е зает. Моля, опитайте с друг.', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/bn-IN.ts b/packages/localizations/src/bn-IN.ts index 022665b1f17..551ada0bd2c 100644 --- a/packages/localizations/src/bn-IN.ts +++ b/packages/localizations/src/bn-IN.ts @@ -2130,6 +2130,10 @@ export const bnIN: LocalizationResource = { requireUppercase: 'একটি বড় হাতের অক্ষর', sentencePrefix: 'আপনার পাসওয়ার্ডে অবশ্যই থাকতে হবে', }, + password_confirmation_attempt_remaining: 'ভুল পাসওয়ার্ড। আপনার আর ১টি প্রচেষ্টা বাকি আছে।', + password_confirmation_attempts_remaining: 'ভুল পাসওয়ার্ড। আপনার আর {{remainingAttempts}}টি প্রচেষ্টা বাকি আছে।', + password_confirmation_session_ended: + 'পাসওয়ার্ড নিশ্চিতকরণের প্রচেষ্টার সীমায় পৌঁছানোর কারণে আপনার সেশন শেষ হয়েছে। চালিয়ে যেতে আবার সাইন ইন করুন।', phone_number_exists: 'এই ফোন নম্বর ব্যবহৃত হয়েছে। দয়া করে অন্য একটি ব্যবহার করুন।', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/ca-ES.ts b/packages/localizations/src/ca-ES.ts index 6fecadbf40b..861ae37b5a5 100644 --- a/packages/localizations/src/ca-ES.ts +++ b/packages/localizations/src/ca-ES.ts @@ -2118,6 +2118,10 @@ export const caES: LocalizationResource = { requireUppercase: 'una lletra majúscula', sentencePrefix: 'La teva contrasenya ha de contenir', }, + password_confirmation_attempt_remaining: 'Contrasenya incorrecta. Et queda 1 intent.', + password_confirmation_attempts_remaining: 'Contrasenya incorrecta. Et queden {{remainingAttempts}} intents.', + password_confirmation_session_ended: + "La teva sessió ha finalitzat perquè has arribat al límit d'intents de confirmació de la contrasenya. Torna a iniciar sessió per continuar.", phone_number_exists: "Aquest número de telèfon ja està en ús. Si us plau, prova'n un altre.", protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/cs-CZ.ts b/packages/localizations/src/cs-CZ.ts index 2050a63cde1..caaf61fd279 100644 --- a/packages/localizations/src/cs-CZ.ts +++ b/packages/localizations/src/cs-CZ.ts @@ -2117,6 +2117,10 @@ export const csCZ: LocalizationResource = { requireUppercase: 'velké písmeno', sentencePrefix: 'Vaše heslo musí obsahovat', }, + password_confirmation_attempt_remaining: 'Nesprávné heslo. Zbývá vám 1 pokus.', + password_confirmation_attempts_remaining: 'Nesprávné heslo. Počet zbývajících pokusů: {{remainingAttempts}}.', + password_confirmation_session_ended: + 'Vaše relace byla ukončena, protože jste dosáhli limitu pokusů o potvrzení hesla. Pro pokračování se znovu přihlaste.', phone_number_exists: 'Toto telefonní číslo se používá. Zkuste prosím jiný.', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/da-DK.ts b/packages/localizations/src/da-DK.ts index 863123bccb2..bf9973e36a1 100644 --- a/packages/localizations/src/da-DK.ts +++ b/packages/localizations/src/da-DK.ts @@ -2102,6 +2102,10 @@ export const daDK: LocalizationResource = { requireUppercase: 'et stort bogstav', sentencePrefix: 'Din adgangskode skal indeholde', }, + password_confirmation_attempt_remaining: 'Forkert adgangskode. Du har 1 forsøg tilbage.', + password_confirmation_attempts_remaining: 'Forkert adgangskode. Du har {{remainingAttempts}} forsøg tilbage.', + password_confirmation_session_ended: + 'Din session er afsluttet, fordi du har nået grænsen for forsøg på at bekræfte adgangskoden. Log ind igen for at fortsætte.', phone_number_exists: 'Dette telefonnummer er allerede taget. Prøv et andet.', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/de-DE.ts b/packages/localizations/src/de-DE.ts index 3dae5675b44..02e4163df88 100644 --- a/packages/localizations/src/de-DE.ts +++ b/packages/localizations/src/de-DE.ts @@ -2135,6 +2135,10 @@ export const deDE: LocalizationResource = { requireUppercase: 'einen Großbuchstaben enthalten', sentencePrefix: 'Das Passwort muss', }, + password_confirmation_attempt_remaining: 'Falsches Passwort. Sie haben noch 1 Versuch.', + password_confirmation_attempts_remaining: 'Falsches Passwort. Sie haben noch {{remainingAttempts}} Versuche.', + password_confirmation_session_ended: + 'Ihre Sitzung wurde beendet, da Sie die maximale Anzahl an Versuchen zur Passwortbestätigung erreicht haben. Melden Sie sich erneut an, um fortzufahren.', phone_number_exists: 'Diese Telefonnummer ist bereits vergeben. Bitte wählen Sie eine Andere.', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/el-GR.ts b/packages/localizations/src/el-GR.ts index cbaa83a14eb..a963a258f64 100644 --- a/packages/localizations/src/el-GR.ts +++ b/packages/localizations/src/el-GR.ts @@ -2126,6 +2126,11 @@ export const elGR: LocalizationResource = { requireUppercase: 'ένα κεφαλαίο γράμμα', sentencePrefix: 'Ο κωδικός πρόσβασής σας πρέπει να περιέχει', }, + password_confirmation_attempt_remaining: 'Λανθασμένος κωδικός πρόσβασης. Σας απομένει 1 προσπάθεια.', + password_confirmation_attempts_remaining: + 'Λανθασμένος κωδικός πρόσβασης. Σας απομένουν {{remainingAttempts}} προσπάθειες.', + password_confirmation_session_ended: + 'Η συνεδρία σας έληξε επειδή φτάσατε το όριο προσπαθειών επιβεβαίωσης κωδικού πρόσβασης. Συνδεθείτε ξανά για να συνεχίσετε.', phone_number_exists: 'Αυτός ο αριθμός τηλεφώνου χρησιμοποιείται ήδη. Δοκιμάστε έναν άλλο.', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/en-GB.ts b/packages/localizations/src/en-GB.ts index 8cf3fa19f17..3020c1511af 100644 --- a/packages/localizations/src/en-GB.ts +++ b/packages/localizations/src/en-GB.ts @@ -2109,6 +2109,10 @@ export const enGB: LocalizationResource = { requireUppercase: 'an uppercase letter', sentencePrefix: 'Your password must contain', }, + password_confirmation_attempt_remaining: 'Incorrect password. You have 1 attempt remaining.', + password_confirmation_attempts_remaining: 'Incorrect password. You have {{remainingAttempts}} attempts remaining.', + password_confirmation_session_ended: + 'Your session has ended because you reached the password confirmation attempt limit. Sign in again to continue.', phone_number_exists: 'This phone number is taken. Please try another.', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index 34ba72cb85c..50f8c4d9a66 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -2161,6 +2161,10 @@ export const enUS: LocalizationResource = { requireUppercase: 'an uppercase letter', sentencePrefix: 'Your password must contain', }, + password_confirmation_attempt_remaining: 'Incorrect password. You have 1 attempt remaining.', + password_confirmation_attempts_remaining: 'Incorrect password. You have {{remainingAttempts}} attempts remaining.', + password_confirmation_session_ended: + 'Your session has ended because you reached the password confirmation attempt limit. Sign in again to continue.', phone_number_exists: undefined, protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/es-CR.ts b/packages/localizations/src/es-CR.ts index 881651ad605..9b817edcf6f 100644 --- a/packages/localizations/src/es-CR.ts +++ b/packages/localizations/src/es-CR.ts @@ -2117,6 +2117,10 @@ export const esCR: LocalizationResource = { requireUppercase: 'al menos una letra mayúscula', sentencePrefix: 'Tu contraseña debe contener', }, + password_confirmation_attempt_remaining: 'Contraseña incorrecta. Te queda 1 intento.', + password_confirmation_attempts_remaining: 'Contraseña incorrecta. Te quedan {{remainingAttempts}} intentos.', + password_confirmation_session_ended: + 'Tu sesión finalizó porque alcanzaste el límite de intentos de confirmación de contraseña. Vuelve a iniciar sesión para continuar.', phone_number_exists: 'Este número de telefónico ya está en uso.', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/es-ES.ts b/packages/localizations/src/es-ES.ts index 352b491767c..14f55f3da79 100644 --- a/packages/localizations/src/es-ES.ts +++ b/packages/localizations/src/es-ES.ts @@ -2118,6 +2118,10 @@ export const esES: LocalizationResource = { requireUppercase: 'Al menos una letra mayúscula', sentencePrefix: 'Tu contraseña debe contener:', }, + password_confirmation_attempt_remaining: 'Contraseña incorrecta. Te queda 1 intento.', + password_confirmation_attempts_remaining: 'Contraseña incorrecta. Te quedan {{remainingAttempts}} intentos.', + password_confirmation_session_ended: + 'Tu sesión ha finalizado porque has alcanzado el límite de intentos de confirmación de contraseña. Vuelve a iniciar sesión para continuar.', phone_number_exists: 'Este número de teléfono ya está en uso. Por favor, inténtelo con otro.', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/es-MX.ts b/packages/localizations/src/es-MX.ts index fb8f136a774..4671aa8e3d9 100644 --- a/packages/localizations/src/es-MX.ts +++ b/packages/localizations/src/es-MX.ts @@ -2118,6 +2118,10 @@ export const esMX: LocalizationResource = { requireUppercase: 'al menos una letra mayúscula', sentencePrefix: 'Tu contraseña debe contener', }, + password_confirmation_attempt_remaining: 'Contraseña incorrecta. Te queda 1 intento.', + password_confirmation_attempts_remaining: 'Contraseña incorrecta. Te quedan {{remainingAttempts}} intentos.', + password_confirmation_session_ended: + 'Tu sesión finalizó porque alcanzaste el límite de intentos de confirmación de contraseña. Vuelve a iniciar sesión para continuar.', phone_number_exists: 'Este número de telefónico ya está en uso.', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/es-UY.ts b/packages/localizations/src/es-UY.ts index 4bc80046ddc..bada58202a4 100644 --- a/packages/localizations/src/es-UY.ts +++ b/packages/localizations/src/es-UY.ts @@ -2119,6 +2119,10 @@ export const esUY: LocalizationResource = { requireUppercase: 'una letra mayúscula', sentencePrefix: 'Tu contraseña debe contener', }, + password_confirmation_attempt_remaining: 'Contraseña incorrecta. Te queda 1 intento.', + password_confirmation_attempts_remaining: 'Contraseña incorrecta. Te quedan {{remainingAttempts}} intentos.', + password_confirmation_session_ended: + 'Tu sesión finalizó porque alcanzaste el límite de intentos de confirmación de contraseña. Volvé a iniciar sesión para continuar.', phone_number_exists: 'Este número de teléfono ya está en uso. Por favor, probá con otro.', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/fa-IR.ts b/packages/localizations/src/fa-IR.ts index 1cec93c532a..c3077f00221 100644 --- a/packages/localizations/src/fa-IR.ts +++ b/packages/localizations/src/fa-IR.ts @@ -2116,6 +2116,10 @@ export const faIR: LocalizationResource = { requireUppercase: 'یک حرف بزرگ', sentencePrefix: 'رمز عبور شما باید حاوی', }, + password_confirmation_attempt_remaining: 'رمز عبور نادرست است. ۱ تلاش دیگر باقی مانده است.', + password_confirmation_attempts_remaining: 'رمز عبور نادرست است. {{remainingAttempts}} تلاش دیگر باقی مانده است.', + password_confirmation_session_ended: + 'جلسه شما به پایان رسید زیرا به حد مجاز تلاش‌های تأیید رمز عبور رسیدید. برای ادامه دوباره وارد شوید.', phone_number_exists: 'این شماره تلفن قبلاً استفاده شده است.', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/fi-FI.ts b/packages/localizations/src/fi-FI.ts index af968a9348d..66e1f27b9b8 100644 --- a/packages/localizations/src/fi-FI.ts +++ b/packages/localizations/src/fi-FI.ts @@ -2132,6 +2132,10 @@ export const fiFI: LocalizationResource = { requireUppercase: 'iso kirjain', sentencePrefix: 'Salasanan on sisällettävä', }, + password_confirmation_attempt_remaining: 'Väärä salasana. Sinulla on 1 yritys jäljellä.', + password_confirmation_attempts_remaining: 'Väärä salasana. Sinulla on {{remainingAttempts}} yritystä jäljellä.', + password_confirmation_session_ended: + 'Istuntosi päättyi, koska saavutit salasanan vahvistusyritysten rajan. Kirjaudu uudelleen sisään jatkaaksesi.', phone_number_exists: 'Tämä puhelinnumero on jo käytössä. Kokeile toista.', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/fr-FR.ts b/packages/localizations/src/fr-FR.ts index f10b2f0e17e..35f4a1b4779 100644 --- a/packages/localizations/src/fr-FR.ts +++ b/packages/localizations/src/fr-FR.ts @@ -2127,6 +2127,10 @@ export const frFR: LocalizationResource = { requireUppercase: 'une lettre majuscule', sentencePrefix: 'Votre mot de passe doit contenir', }, + password_confirmation_attempt_remaining: 'Mot de passe incorrect. Il vous reste 1 tentative.', + password_confirmation_attempts_remaining: 'Mot de passe incorrect. Il vous reste {{remainingAttempts}} tentatives.', + password_confirmation_session_ended: + 'Votre session a pris fin, car vous avez atteint la limite de tentatives de confirmation du mot de passe. Reconnectez-vous pour continuer.', phone_number_exists: 'Ce numéro de téléphone est déjà utilisé. Veuillez essayer un autre.', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/he-IL.ts b/packages/localizations/src/he-IL.ts index 9238c575468..df023ffa7bb 100644 --- a/packages/localizations/src/he-IL.ts +++ b/packages/localizations/src/he-IL.ts @@ -2093,6 +2093,10 @@ export const heIL: LocalizationResource = { requireUppercase: 'אות גדולה', sentencePrefix: 'הסיסמה שלך חייבת להכיל', }, + password_confirmation_attempt_remaining: 'סיסמה שגויה. נותר לך ניסיון אחד.', + password_confirmation_attempts_remaining: 'סיסמה שגויה. נותרו לך {{remainingAttempts}} ניסיונות.', + password_confirmation_session_ended: + 'הסשן שלך הסתיים מכיוון שהגעת למגבלת ניסיונות אימות הסיסמה. התחבר שוב כדי להמשיך.', phone_number_exists: 'מספר הטלפון הזה כבר בשימוש. אנא נסה מספר אחר.', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/hi-IN.ts b/packages/localizations/src/hi-IN.ts index 50faad7f25b..42a504b7b99 100644 --- a/packages/localizations/src/hi-IN.ts +++ b/packages/localizations/src/hi-IN.ts @@ -2131,6 +2131,10 @@ export const hiIN: LocalizationResource = { requireUppercase: 'एक अपरकेस अक्षर', sentencePrefix: 'आपके पासवर्ड में होना चाहिए', }, + password_confirmation_attempt_remaining: 'गलत पासवर्ड। आपके पास 1 प्रयास शेष है।', + password_confirmation_attempts_remaining: 'गलत पासवर्ड। आपके पास {{remainingAttempts}} प्रयास शेष हैं।', + password_confirmation_session_ended: + 'पासवर्ड पुष्टि प्रयासों की सीमा पूरी होने के कारण आपका सत्र समाप्त हो गया है। जारी रखने के लिए फिर से साइन इन करें।', phone_number_exists: 'यह फोन नंबर पहले से लिया गया है। कृपया दूसरा प्रयास करें।', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/hr-HR.ts b/packages/localizations/src/hr-HR.ts index d4f315fc89c..e769a96b233 100644 --- a/packages/localizations/src/hr-HR.ts +++ b/packages/localizations/src/hr-HR.ts @@ -2136,6 +2136,10 @@ export const hrHR: LocalizationResource = { requireUppercase: 'veliko slovo', sentencePrefix: 'Vaša lozinka mora sadržavati', }, + password_confirmation_attempt_remaining: 'Neispravna lozinka. Preostao vam je 1 pokušaj.', + password_confirmation_attempts_remaining: 'Neispravna lozinka. Preostalo pokušaja: {{remainingAttempts}}.', + password_confirmation_session_ended: + 'Vaša sesija je završena jer ste dosegnuli ograničenje pokušaja potvrde lozinke. Ponovno se prijavite za nastavak.', phone_number_exists: 'Ovaj telefonski broj je zauzet. Molimo pokušajte s drugim.', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/hu-HU.ts b/packages/localizations/src/hu-HU.ts index 87f03437062..0cb008bcca9 100644 --- a/packages/localizations/src/hu-HU.ts +++ b/packages/localizations/src/hu-HU.ts @@ -2135,6 +2135,10 @@ export const huHU: LocalizationResource = { requireUppercase: 'egy nagybetű', sentencePrefix: 'A jelszavadnak tartalmaznia kell', }, + password_confirmation_attempt_remaining: 'Helytelen jelszó. 1 próbálkozásod maradt.', + password_confirmation_attempts_remaining: 'Helytelen jelszó. {{remainingAttempts}} próbálkozásod maradt.', + password_confirmation_session_ended: + 'A munkameneted véget ért, mert elérted a jelszó-megerősítési kísérletek korlátját. A folytatáshoz jelentkezz be újra.', phone_number_exists: 'Ez a telefonszám már foglalt. Kérlek próbálj meg egy másikat.', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/id-ID.ts b/packages/localizations/src/id-ID.ts index 4a56e4e5711..bb2efb2ed1d 100644 --- a/packages/localizations/src/id-ID.ts +++ b/packages/localizations/src/id-ID.ts @@ -2115,6 +2115,11 @@ export const idID: LocalizationResource = { requireUppercase: 'huruf besar', sentencePrefix: 'Kata sandi Anda harus mengandung', }, + password_confirmation_attempt_remaining: 'Kata sandi salah. Anda memiliki 1 percobaan tersisa.', + password_confirmation_attempts_remaining: + 'Kata sandi salah. Anda memiliki {{remainingAttempts}} percobaan tersisa.', + password_confirmation_session_ended: + 'Sesi Anda telah berakhir karena Anda mencapai batas percobaan konfirmasi kata sandi. Masuk kembali untuk melanjutkan.', phone_number_exists: 'Nomor telepon ini sudah digunakan. Silakan coba yang lain.', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/is-IS.ts b/packages/localizations/src/is-IS.ts index 78dc41a170e..f772dea8f4f 100644 --- a/packages/localizations/src/is-IS.ts +++ b/packages/localizations/src/is-IS.ts @@ -2133,6 +2133,10 @@ export const isIS: LocalizationResource = { requireUppercase: 'hástaf', sentencePrefix: 'Lykilorðið þitt verður að innihalda', }, + password_confirmation_attempt_remaining: 'Rangt lykilorð. Þú átt 1 tilraun eftir.', + password_confirmation_attempts_remaining: 'Rangt lykilorð. Þú átt {{remainingAttempts}} tilraunir eftir.', + password_confirmation_session_ended: + 'Lotunni þinni lauk vegna þess að þú náðir hámarksfjölda tilrauna til að staðfesta lykilorð. Skráðu þig inn aftur til að halda áfram.', phone_number_exists: 'Þetta símanúmer er þegar í notkun. Vinsamlegast reyndu annað.', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/it-IT.ts b/packages/localizations/src/it-IT.ts index bb419bfd479..8edc0278903 100644 --- a/packages/localizations/src/it-IT.ts +++ b/packages/localizations/src/it-IT.ts @@ -2114,6 +2114,10 @@ export const itIT: LocalizationResource = { requireUppercase: undefined, sentencePrefix: undefined, }, + password_confirmation_attempt_remaining: 'Password errata. Hai ancora 1 tentativo.', + password_confirmation_attempts_remaining: 'Password errata. Hai ancora {{remainingAttempts}} tentativi.', + password_confirmation_session_ended: + 'La sessione è terminata perché hai raggiunto il limite di tentativi di conferma della password. Accedi di nuovo per continuare.', phone_number_exists: 'Questo numero di telefono è già in uso. Per favore, prova con un altro.', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/ja-JP.ts b/packages/localizations/src/ja-JP.ts index 4f1fe4d90ce..18a7f6c5b29 100644 --- a/packages/localizations/src/ja-JP.ts +++ b/packages/localizations/src/ja-JP.ts @@ -2129,6 +2129,10 @@ export const jaJP: LocalizationResource = { requireUppercase: '大文字を含む', sentencePrefix: 'パスワードは次の条件を満たす必要があります:', }, + password_confirmation_attempt_remaining: 'パスワードが正しくありません。残り1回試行できます。', + password_confirmation_attempts_remaining: 'パスワードが正しくありません。残り{{remainingAttempts}}回試行できます。', + password_confirmation_session_ended: + 'パスワード確認の試行回数の上限に達したため、セッションが終了しました。続行するには再度サインインしてください。', phone_number_exists: 'この電話番号は既に使用されています。別のものをお試しください。', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/kk-KZ.ts b/packages/localizations/src/kk-KZ.ts index f669637237f..00d2d9d4590 100644 --- a/packages/localizations/src/kk-KZ.ts +++ b/packages/localizations/src/kk-KZ.ts @@ -2095,6 +2095,10 @@ export const kkKZ: LocalizationResource = { requireUppercase: 'бас әріп', sentencePrefix: 'Құпия сөзде болуы керек:', }, + password_confirmation_attempt_remaining: 'Құпиясөз қате. Сізде 1 әрекет қалды.', + password_confirmation_attempts_remaining: 'Құпиясөз қате. Сізде {{remainingAttempts}} әрекет қалды.', + password_confirmation_session_ended: + 'Құпиясөзді растау әрекеттерінің шегіне жеткендіктен, сеансыңыз аяқталды. Жалғастыру үшін қайта кіріңіз.', phone_number_exists: 'Бұл телефон нөмірі тіркелген. Басқасын қолданыңыз.', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/ko-KR.ts b/packages/localizations/src/ko-KR.ts index 88e554eaefa..9fe4be36a77 100644 --- a/packages/localizations/src/ko-KR.ts +++ b/packages/localizations/src/ko-KR.ts @@ -2102,6 +2102,10 @@ export const koKR: LocalizationResource = { requireUppercase: '대문자', sentencePrefix: '비밀번호에는 다음이 꼭 포함돼야 해요', }, + password_confirmation_attempt_remaining: '비밀번호가 올바르지 않습니다. 남은 시도 횟수: 1회', + password_confirmation_attempts_remaining: '비밀번호가 올바르지 않습니다. 남은 시도 횟수: {{remainingAttempts}}회', + password_confirmation_session_ended: + '비밀번호 확인 시도 횟수 한도에 도달하여 세션이 종료되었습니다. 계속하려면 다시 로그인하세요.', phone_number_exists: '이 전화번호는 이미 사용 중이에요. 다른 번호를 시도해 주세요.', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/mn-MN.ts b/packages/localizations/src/mn-MN.ts index 6aa4840b5a8..0d4630b3de6 100644 --- a/packages/localizations/src/mn-MN.ts +++ b/packages/localizations/src/mn-MN.ts @@ -2107,6 +2107,10 @@ export const mnMN: LocalizationResource = { requireUppercase: 'том үсэг', sentencePrefix: 'таны нууц үг агуулсан байх ёстой', }, + password_confirmation_attempt_remaining: 'Нууц үг буруу байна. Танд 1 оролдлого үлдлээ.', + password_confirmation_attempts_remaining: 'Нууц үг буруу байна. Танд {{remainingAttempts}} оролдлого үлдлээ.', + password_confirmation_session_ended: + 'Нууц үг баталгаажуулах оролдлогын хязгаарт хүрсэн тул таны сесс дууслаа. Үргэлжлүүлэхийн тулд дахин нэвтэрнэ үү.', phone_number_exists: 'Энэ утасны дугаарыг авсан. Өөр оролдоно уу.', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/ms-MY.ts b/packages/localizations/src/ms-MY.ts index 7c53a9ae9c3..3d73aaa6708 100644 --- a/packages/localizations/src/ms-MY.ts +++ b/packages/localizations/src/ms-MY.ts @@ -2140,6 +2140,10 @@ export const msMY: LocalizationResource = { requireUppercase: 'huruf besar', sentencePrefix: 'Kata laluan anda mesti mengandungi', }, + password_confirmation_attempt_remaining: 'Kata laluan salah. Anda mempunyai 1 percubaan lagi.', + password_confirmation_attempts_remaining: 'Kata laluan salah. Anda mempunyai {{remainingAttempts}} percubaan lagi.', + password_confirmation_session_ended: + 'Sesi anda telah tamat kerana anda telah mencapai had percubaan pengesahan kata laluan. Log masuk semula untuk meneruskan.', phone_number_exists: 'Nombor telefon ini telah diambil. Sila cuba yang lain.', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/nb-NO.ts b/packages/localizations/src/nb-NO.ts index c31ca1ffa75..0be6281614e 100644 --- a/packages/localizations/src/nb-NO.ts +++ b/packages/localizations/src/nb-NO.ts @@ -2133,6 +2133,10 @@ export const nbNO: LocalizationResource = { requireUppercase: 'en stor bokstav', sentencePrefix: 'Passordet ditt må inneholde', }, + password_confirmation_attempt_remaining: 'Feil passord. Du har 1 forsøk igjen.', + password_confirmation_attempts_remaining: 'Feil passord. Du har {{remainingAttempts}} forsøk igjen.', + password_confirmation_session_ended: + 'Økten din er avsluttet fordi du nådde grensen for forsøk på passordbekreftelse. Logg inn på nytt for å fortsette.', phone_number_exists: 'Dette telefonnummeret er allerede i bruk. Vennligst bruk et annet telefonnummer.', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/nl-BE.ts b/packages/localizations/src/nl-BE.ts index 69a5f136b18..ad1e2633326 100644 --- a/packages/localizations/src/nl-BE.ts +++ b/packages/localizations/src/nl-BE.ts @@ -2106,6 +2106,10 @@ export const nlBE: LocalizationResource = { requireUppercase: 'Wachtwoord moet minstens 1 hoofdletter bevatten.', sentencePrefix: 'Wachtwoord moet minstens 1 speciaal teken bevatten.', }, + password_confirmation_attempt_remaining: 'Onjuist wachtwoord. Je hebt nog 1 poging.', + password_confirmation_attempts_remaining: 'Onjuist wachtwoord. Je hebt nog {{remainingAttempts}} pogingen.', + password_confirmation_session_ended: + 'Je sessie is beëindigd omdat je het maximale aantal pogingen voor wachtwoordbevestiging hebt bereikt. Log opnieuw in om door te gaan.', phone_number_exists: 'Dit telefoonnummer is al in gebruik. Probeer een ander nummer.', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/nl-NL.ts b/packages/localizations/src/nl-NL.ts index 27da0499ce9..45d8f9ebba9 100644 --- a/packages/localizations/src/nl-NL.ts +++ b/packages/localizations/src/nl-NL.ts @@ -2106,6 +2106,10 @@ export const nlNL: LocalizationResource = { requireUppercase: 'Wachtwoord moet minstens 1 hoofdletter bevatten.', sentencePrefix: 'Wachtwoord moet minstens 1 speciaal teken bevatten.', }, + password_confirmation_attempt_remaining: 'Onjuist wachtwoord. Je hebt nog 1 poging.', + password_confirmation_attempts_remaining: 'Onjuist wachtwoord. Je hebt nog {{remainingAttempts}} pogingen.', + password_confirmation_session_ended: + 'Je sessie is beëindigd omdat je het maximale aantal pogingen voor wachtwoordbevestiging hebt bereikt. Log opnieuw in om door te gaan.', phone_number_exists: 'Dit telefoonnummer is al in gebruik. Probeer een ander nummer.', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/pl-PL.ts b/packages/localizations/src/pl-PL.ts index 04af979284e..eb73dd1838d 100644 --- a/packages/localizations/src/pl-PL.ts +++ b/packages/localizations/src/pl-PL.ts @@ -2114,6 +2114,10 @@ export const plPL: LocalizationResource = { requireUppercase: 'wielką literę', sentencePrefix: 'Twoje hasło musi zawierać', }, + password_confirmation_attempt_remaining: 'Nieprawidłowe hasło. Pozostała 1 próba.', + password_confirmation_attempts_remaining: 'Nieprawidłowe hasło. Pozostałe próby: {{remainingAttempts}}.', + password_confirmation_session_ended: + 'Twoja sesja została zakończona, ponieważ osiągnięto limit prób potwierdzenia hasła. Zaloguj się ponownie, aby kontynuować.', phone_number_exists: 'Numer telefonu jest już zajęty. Proszę spróbować innego.', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/pt-BR.ts b/packages/localizations/src/pt-BR.ts index 99b368031c2..b99c8567b9e 100644 --- a/packages/localizations/src/pt-BR.ts +++ b/packages/localizations/src/pt-BR.ts @@ -2124,6 +2124,10 @@ export const ptBR: LocalizationResource = { requireUppercase: 'uma letra maiúscula', sentencePrefix: 'Sua senha deve conter', }, + password_confirmation_attempt_remaining: 'Senha incorreta. Você tem mais 1 tentativa.', + password_confirmation_attempts_remaining: 'Senha incorreta. Você tem mais {{remainingAttempts}} tentativas.', + password_confirmation_session_ended: + 'Sua sessão foi encerrada porque você atingiu o limite de tentativas de confirmação de senha. Faça login novamente para continuar.', phone_number_exists: 'Este número de telefone já está em uso. Por favor, tente outro.', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/pt-PT.ts b/packages/localizations/src/pt-PT.ts index 48d6a572489..dcf1843086b 100644 --- a/packages/localizations/src/pt-PT.ts +++ b/packages/localizations/src/pt-PT.ts @@ -2130,6 +2130,10 @@ export const ptPT: LocalizationResource = { requireUppercase: 'uma letra maiúscula', sentencePrefix: 'A sua palavra-passe deve conter', }, + password_confirmation_attempt_remaining: 'Palavra-passe incorreta. Tem mais 1 tentativa.', + password_confirmation_attempts_remaining: 'Palavra-passe incorreta. Tem mais {{remainingAttempts}} tentativas.', + password_confirmation_session_ended: + 'A sua sessão terminou porque atingiu o limite de tentativas de confirmação da palavra-passe. Inicie sessão novamente para continuar.', phone_number_exists: 'Este número de telemóvel já está em uso. Por favor, tente outro.', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/ro-RO.ts b/packages/localizations/src/ro-RO.ts index be1e270ce09..6b4fc156d31 100644 --- a/packages/localizations/src/ro-RO.ts +++ b/packages/localizations/src/ro-RO.ts @@ -2125,6 +2125,10 @@ export const roRO: LocalizationResource = { requireUppercase: 'o literă mare', sentencePrefix: 'Parola ta trebuie să conțină', }, + password_confirmation_attempt_remaining: 'Parolă incorectă. Mai aveți 1 încercare.', + password_confirmation_attempts_remaining: 'Parolă incorectă. Încercări rămase: {{remainingAttempts}}.', + password_confirmation_session_ended: + 'Sesiunea dvs. s-a încheiat deoarece ați atins limita de încercări de confirmare a parolei. Autentificați-vă din nou pentru a continua.', phone_number_exists: undefined, protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/ru-RU.ts b/packages/localizations/src/ru-RU.ts index 4403998a770..94eaeffb0bd 100644 --- a/packages/localizations/src/ru-RU.ts +++ b/packages/localizations/src/ru-RU.ts @@ -2122,6 +2122,10 @@ export const ruRU: LocalizationResource = { requireUppercase: 'букву в верхнем регистре', sentencePrefix: 'Ваш пароль должен содержать', }, + password_confirmation_attempt_remaining: 'Неверный пароль. У вас осталась 1 попытка.', + password_confirmation_attempts_remaining: 'Неверный пароль. Осталось попыток: {{remainingAttempts}}.', + password_confirmation_session_ended: + 'Ваш сеанс завершён, так как вы достигли лимита попыток подтверждения пароля. Войдите снова, чтобы продолжить.', phone_number_exists: 'Этот номер телефона уже занят. Пожалуйста, попробуйте другой.', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/sk-SK.ts b/packages/localizations/src/sk-SK.ts index c73b35e563c..d48f9ea2236 100644 --- a/packages/localizations/src/sk-SK.ts +++ b/packages/localizations/src/sk-SK.ts @@ -2113,6 +2113,10 @@ export const skSK: LocalizationResource = { requireUppercase: 'veľké písmeno', sentencePrefix: 'Vaše heslo musí obsahovať', }, + password_confirmation_attempt_remaining: 'Nesprávne heslo. Zostáva vám 1 pokus.', + password_confirmation_attempts_remaining: 'Nesprávne heslo. Počet zostávajúcich pokusov: {{remainingAttempts}}.', + password_confirmation_session_ended: + 'Vaša relácia bola ukončená, pretože ste dosiahli limit pokusov o potvrdenie hesla. Ak chcete pokračovať, znova sa prihláste.', phone_number_exists: 'Toto telefónne číslo je už obsadené. Skúste prosím iné.', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/sr-RS.ts b/packages/localizations/src/sr-RS.ts index e43def61292..14a2f20e0ab 100644 --- a/packages/localizations/src/sr-RS.ts +++ b/packages/localizations/src/sr-RS.ts @@ -2105,6 +2105,10 @@ export const srRS: LocalizationResource = { requireUppercase: 'veliko slovo', sentencePrefix: 'Tvoja lozinka mora sadržati', }, + password_confirmation_attempt_remaining: 'Pogrešna lozinka. Preostao vam je 1 pokušaj.', + password_confirmation_attempts_remaining: 'Pogrešna lozinka. Preostalo pokušaja: {{remainingAttempts}}.', + password_confirmation_session_ended: + 'Vaša sesija je završena jer ste dostigli ograničenje pokušaja potvrde lozinke. Ponovo se prijavite da biste nastavili.', phone_number_exists: 'Ovaj telefonski broj je zauzet. Molimo pokušaj sa drugim.', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/sv-SE.ts b/packages/localizations/src/sv-SE.ts index 31dfc362a9e..37800657369 100644 --- a/packages/localizations/src/sv-SE.ts +++ b/packages/localizations/src/sv-SE.ts @@ -2108,6 +2108,10 @@ export const svSE: LocalizationResource = { requireUppercase: 'Kräver stora bokstäver', sentencePrefix: 'Lösenordet måste innehålla', }, + password_confirmation_attempt_remaining: 'Felaktigt lösenord. Du har 1 försök kvar.', + password_confirmation_attempts_remaining: 'Felaktigt lösenord. Du har {{remainingAttempts}} försök kvar.', + password_confirmation_session_ended: + 'Din session har avslutats eftersom du nådde gränsen för försök att bekräfta lösenordet. Logga in igen för att fortsätta.', phone_number_exists: 'Detta telefonnummer är taget. Vänligen prova ett annat.', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/ta-IN.ts b/packages/localizations/src/ta-IN.ts index 71ba727aadc..8ab5196e7df 100644 --- a/packages/localizations/src/ta-IN.ts +++ b/packages/localizations/src/ta-IN.ts @@ -2141,6 +2141,11 @@ export const taIN: LocalizationResource = { requireUppercase: 'ஒரு பெரிய எழுத்து', sentencePrefix: 'உங்கள் கடவுச்சொல்லில் இருக்க வேண்டும்', }, + password_confirmation_attempt_remaining: 'தவறான கடவுச்சொல். உங்களுக்கு 1 முயற்சி மீதமுள்ளது.', + password_confirmation_attempts_remaining: + 'தவறான கடவுச்சொல். உங்களுக்கு {{remainingAttempts}} முயற்சிகள் மீதமுள்ளன.', + password_confirmation_session_ended: + 'கடவுச்சொல் உறுதிப்படுத்தல் முயற்சிகளின் வரம்பை அடைந்ததால் உங்கள் அமர்வு முடிந்தது. தொடர மீண்டும் உள்நுழையவும்.', phone_number_exists: 'இந்த தொலைபேசி எண் எடுக்கப்பட்டுள்ளது. வேறொன்றை முயற்சிக்கவும்.', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/te-IN.ts b/packages/localizations/src/te-IN.ts index e5bf22ca4cd..4b728fd4bbe 100644 --- a/packages/localizations/src/te-IN.ts +++ b/packages/localizations/src/te-IN.ts @@ -2135,6 +2135,10 @@ export const teIN: LocalizationResource = { requireUppercase: 'ఒక పెద్ద అక్షరం', sentencePrefix: 'మీ పాస్‌వర్డ్‌లో ఉండాలి', }, + password_confirmation_attempt_remaining: 'తప్పు పాస్‌వర్డ్. మీకు 1 ప్రయత్నం మిగిలి ఉంది.', + password_confirmation_attempts_remaining: 'తప్పు పాస్‌వర్డ్. మీకు {{remainingAttempts}} ప్రయత్నాలు మిగిలి ఉన్నాయి.', + password_confirmation_session_ended: + 'పాస్‌వర్డ్ నిర్ధారణ ప్రయత్నాల పరిమితిని చేరుకున్నందున మీ సెషన్ ముగిసింది. కొనసాగించడానికి మళ్లీ సైన్ ఇన్ చేయండి.', phone_number_exists: 'ఈ ఫోన్ నంబర్ తీసుకోబడింది. దయచేసి మరొకదాన్ని ప్రయత్నించండి.', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/th-TH.ts b/packages/localizations/src/th-TH.ts index 25185bfbb26..1906112ceb7 100644 --- a/packages/localizations/src/th-TH.ts +++ b/packages/localizations/src/th-TH.ts @@ -2103,6 +2103,10 @@ export const thTH: LocalizationResource = { requireUppercase: 'ตัวอักษรพิมพ์ใหญ่', sentencePrefix: 'รหัสผ่านของคุณต้องมี', }, + password_confirmation_attempt_remaining: 'รหัสผ่านไม่ถูกต้อง คุณเหลือโอกาสอีก 1 ครั้ง', + password_confirmation_attempts_remaining: 'รหัสผ่านไม่ถูกต้อง คุณเหลือโอกาสอีก {{remainingAttempts}} ครั้ง', + password_confirmation_session_ended: + 'เซสชันของคุณสิ้นสุดลงเนื่องจากคุณพยายามยืนยันรหัสผ่านครบตามจำนวนครั้งที่อนุญาตแล้ว โปรดลงชื่อเข้าใช้อีกครั้งเพื่อดำเนินการต่อ', phone_number_exists: undefined, protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/tr-TR.ts b/packages/localizations/src/tr-TR.ts index addbef60fef..598298a5e7d 100644 --- a/packages/localizations/src/tr-TR.ts +++ b/packages/localizations/src/tr-TR.ts @@ -2109,6 +2109,10 @@ export const trTR: LocalizationResource = { requireUppercase: 'bir büyük harf içermeli', sentencePrefix: 'Şifreniz;', }, + password_confirmation_attempt_remaining: 'Hatalı şifre. 1 deneme hakkınız kaldı.', + password_confirmation_attempts_remaining: 'Hatalı şifre. {{remainingAttempts}} deneme hakkınız kaldı.', + password_confirmation_session_ended: + 'Şifre doğrulama deneme sınırına ulaştığınız için oturumunuz sona erdi. Devam etmek için tekrar giriş yapın.', phone_number_exists: 'Bu telefon numarası zaten kullanılıyor. Lütfen başka bir numara deneyin.', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/uk-UA.ts b/packages/localizations/src/uk-UA.ts index f714296df0c..c6174ee108f 100644 --- a/packages/localizations/src/uk-UA.ts +++ b/packages/localizations/src/uk-UA.ts @@ -2103,6 +2103,10 @@ export const ukUA: LocalizationResource = { requireUppercase: 'букву у верхньому регістрі', sentencePrefix: 'Ваш пароль повинен містити', }, + password_confirmation_attempt_remaining: 'Неправильний пароль. У вас залишилася 1 спроба.', + password_confirmation_attempts_remaining: 'Неправильний пароль. Залишилося спроб: {{remainingAttempts}}.', + password_confirmation_session_ended: + 'Ваш сеанс завершено, оскільки ви досягли ліміту спроб підтвердження пароля. Увійдіть знову, щоб продовжити.', phone_number_exists: 'Цей номер телефону вже використовується. Спробуйте інший.', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/vi-VN.ts b/packages/localizations/src/vi-VN.ts index f4af25fadb9..cc216ec209f 100644 --- a/packages/localizations/src/vi-VN.ts +++ b/packages/localizations/src/vi-VN.ts @@ -2126,6 +2126,10 @@ export const viVN: LocalizationResource = { requireUppercase: 'một chữ cái viết hoa', sentencePrefix: 'Mật khẩu của bạn phải chứa', }, + password_confirmation_attempt_remaining: 'Mật khẩu không đúng. Bạn còn 1 lần thử.', + password_confirmation_attempts_remaining: 'Mật khẩu không đúng. Bạn còn {{remainingAttempts}} lần thử.', + password_confirmation_session_ended: + 'Phiên của bạn đã kết thúc vì bạn đã đạt đến giới hạn số lần xác nhận mật khẩu. Hãy đăng nhập lại để tiếp tục.', phone_number_exists: undefined, protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/zh-CN.ts b/packages/localizations/src/zh-CN.ts index 429fba2d4d1..76dd0670066 100644 --- a/packages/localizations/src/zh-CN.ts +++ b/packages/localizations/src/zh-CN.ts @@ -2086,6 +2086,9 @@ export const zhCN: LocalizationResource = { requireUppercase: '一个大写字母', sentencePrefix: '您的密码必须包含', }, + password_confirmation_attempt_remaining: '密码错误。您还有 1 次尝试机会。', + password_confirmation_attempts_remaining: '密码错误。您还有 {{remainingAttempts}} 次尝试机会。', + password_confirmation_session_ended: '由于密码确认尝试次数已达上限,您的会话已结束。请重新登录以继续。', phone_number_exists: '该电话号码已被使用,请尝试其他号码。', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/localizations/src/zh-TW.ts b/packages/localizations/src/zh-TW.ts index 0a59569f066..f6fcc1f3879 100644 --- a/packages/localizations/src/zh-TW.ts +++ b/packages/localizations/src/zh-TW.ts @@ -2091,6 +2091,9 @@ export const zhTW: LocalizationResource = { requireUppercase: '一個大寫字母', sentencePrefix: '您的密碼必須包含:', }, + password_confirmation_attempt_remaining: '密碼錯誤。您還有 1 次嘗試機會。', + password_confirmation_attempts_remaining: '密碼錯誤。您還有 {{remainingAttempts}} 次嘗試機會。', + password_confirmation_session_ended: '由於密碼確認嘗試次數已達上限,您的工作階段已結束。請重新登入以繼續。', phone_number_exists: '此電話號碼已被使用,請嘗試其他號碼。', protect_check_aborted: undefined, protect_check_already_resolved: undefined, diff --git a/packages/shared/src/errors/__tests__/clerkApiError.test.ts b/packages/shared/src/errors/__tests__/clerkApiError.test.ts new file mode 100644 index 00000000000..6ba2047adb0 --- /dev/null +++ b/packages/shared/src/errors/__tests__/clerkApiError.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest'; + +import { ClerkAPIError } from '../clerkApiError'; +import { errorToJSON } from '../parseError'; + +describe('ClerkAPIError remainingAttempts metadata', () => { + it('converts remaining_attempts to remainingAttempts, including zero', () => { + const error = new ClerkAPIError({ + code: 'form_password_validation_failed', + message: 'Password is incorrect', + meta: { remaining_attempts: 0 }, + }); + + expect(error.meta.remainingAttempts).toBe(0); + }); + + it('serializes remainingAttempts as remaining_attempts', () => { + const error = new ClerkAPIError({ + code: 'form_password_incorrect', + message: 'Password is incorrect', + meta: { remaining_attempts: 2 }, + }); + + expect(errorToJSON(error).meta?.remaining_attempts).toBe(2); + }); +}); diff --git a/packages/shared/src/errors/clerkApiError.ts b/packages/shared/src/errors/clerkApiError.ts index adaeb3e5b9d..102c87c09f4 100644 --- a/packages/shared/src/errors/clerkApiError.ts +++ b/packages/shared/src/errors/clerkApiError.ts @@ -35,6 +35,7 @@ export class ClerkAPIError implements Cler linkUrl: json.meta?.link_url, linkText: json.meta?.link_text, data: json.meta?.data, + remainingAttempts: json.meta?.remaining_attempts, } as unknown as Meta, }; this.code = parsedError.code; diff --git a/packages/shared/src/errors/parseError.ts b/packages/shared/src/errors/parseError.ts index a13945d8f10..a3e0620a2f0 100644 --- a/packages/shared/src/errors/parseError.ts +++ b/packages/shared/src/errors/parseError.ts @@ -48,6 +48,7 @@ export function errorToJSON(error: ClerkAPIError | null): ClerkAPIErrorJSON { link_url: error?.meta?.linkUrl, link_text: error?.meta?.linkText, data: error?.meta?.data, + remaining_attempts: error?.meta?.remainingAttempts, }, }; } diff --git a/packages/shared/src/types/errors.ts b/packages/shared/src/types/errors.ts index f54e6982ea0..1192fb43ddb 100644 --- a/packages/shared/src/types/errors.ts +++ b/packages/shared/src/types/errors.ts @@ -30,6 +30,7 @@ export interface ClerkAPIErrorJSON { link_url?: string; link_text?: string; data?: Record; + remaining_attempts?: number; }; } @@ -122,6 +123,8 @@ export interface ClerkAPIError { * sensitive. */ data?: Record; + /** Remaining password confirmation attempts in the current session; zero means the session has ended. */ + remainingAttempts?: number; }; } diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts index 912ac54e607..99a12004e0a 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -2506,6 +2506,9 @@ type UnstableErrors = WithParamName<{ form_password_incorrect: LocalizationValue; form_password_or_identifier_incorrect: LocalizationValue; form_password_validation_failed: LocalizationValue; + password_confirmation_attempt_remaining: LocalizationValue; + password_confirmation_attempts_remaining: LocalizationValue<'remainingAttempts'>; + password_confirmation_session_ended: LocalizationValue; not_allowed_access: LocalizationValue; oauth_access_denied: LocalizationValue; form_identifier_exists: LocalizationValue; diff --git a/packages/ui/src/components/UserProfile/PasswordForm.tsx b/packages/ui/src/components/UserProfile/PasswordForm.tsx index c9038469e86..6821a449af4 100644 --- a/packages/ui/src/components/UserProfile/PasswordForm.tsx +++ b/packages/ui/src/components/UserProfile/PasswordForm.tsx @@ -10,7 +10,7 @@ import { FormContainer } from '@/ui/elements/FormContainer'; import { InformationBox } from '@/ui/elements/InformationBox'; import type { SuccessPage } from '@/ui/elements/SuccessPage'; import { handleError } from '@/ui/utils/errorHandler'; -import { createPasswordError } from '@/ui/utils/passwordUtils'; +import { createPasswordConfirmationError, createPasswordError } from '@/ui/utils/passwordUtils'; import { useFormControl } from '@/ui/utils/useFormControl'; import { useEnvironment } from '../../contexts'; @@ -69,6 +69,8 @@ export const PasswordForm = withCardStateProvider((props: PasswordFormProps) => type: 'password', label: localizationKeys('formFieldLabel__currentPassword'), isRequired: true, + validatePassword: false, + buildErrorMessage: errors => createPasswordConfirmationError(errors, { t }), }); const passwordField = useFormControl('newPassword', '', { diff --git a/packages/ui/src/components/UserVerification/UserVerificationFactorOnePassword.tsx b/packages/ui/src/components/UserVerification/UserVerificationFactorOnePassword.tsx index a8f62c47a04..f34b751e07a 100644 --- a/packages/ui/src/components/UserVerification/UserVerificationFactorOnePassword.tsx +++ b/packages/ui/src/components/UserVerification/UserVerificationFactorOnePassword.tsx @@ -6,9 +6,10 @@ import { useCardState } from '@/ui/elements/contexts'; import { Form } from '@/ui/elements/Form'; import { Header } from '@/ui/elements/Header'; import { handleError } from '@/ui/utils/errorHandler'; +import { createPasswordConfirmationError } from '@/ui/utils/passwordUtils'; import { useFormControl } from '@/ui/utils/useFormControl'; -import { Col, descriptors, Flow, localizationKeys } from '../../customizables'; +import { Col, descriptors, Flow, localizationKeys, useLocalizations } from '../../customizables'; import { HavingTrouble } from '../SignIn/HavingTrouble'; import { useAfterVerification } from './use-after-verification'; @@ -22,6 +23,7 @@ export function UserVerificationFactorOnePasswordCard(props: UserVerificationFac const { handleVerificationResponse } = useAfterVerification(); const card = useCardState(); + const { t } = useLocalizations(); const [showHavingTrouble, setShowHavingTrouble] = React.useState(false); const toggleHavingTrouble = React.useCallback(() => setShowHavingTrouble(s => !s), [setShowHavingTrouble]); @@ -30,6 +32,8 @@ export function UserVerificationFactorOnePasswordCard(props: UserVerificationFac type: 'password', label: localizationKeys('formFieldLabel__password'), placeholder: localizationKeys('formFieldInputPlaceholder__password'), + validatePassword: false, + buildErrorMessage: errors => createPasswordConfirmationError(errors, { t }), }); const handlePasswordSubmit: React.FormEventHandler = async e => { diff --git a/packages/ui/src/utils/__tests__/passwordUtils.test.tsx b/packages/ui/src/utils/__tests__/passwordUtils.test.tsx index d1e596d93ac..5d6b443e4b0 100644 --- a/packages/ui/src/utils/__tests__/passwordUtils.test.tsx +++ b/packages/ui/src/utils/__tests__/passwordUtils.test.tsx @@ -4,7 +4,7 @@ import { bindCreateFixtures } from '@/test/create-fixtures'; import { renderHook } from '@/test/utils'; import { OptionsProvider } from '@/ui/contexts'; import { useLocalizations } from '@/ui/customizables'; -import { createPasswordError } from '@/ui/utils/passwordUtils'; +import { createPasswordConfirmationError, createPasswordError } from '@/ui/utils/passwordUtils'; const { createFixtures } = bindCreateFixtures('SignIn'); @@ -286,3 +286,53 @@ describe('createPasswordError() constructs error that password', () => { ); }); }); + +describe('createPasswordConfirmationError()', () => { + const renderLocalizations = async (localization = {}) => { + const { wrapper: Wrapper } = await createFixtures(); + const wrapper = ({ children }) => ( + + {children} + + ); + return renderHook(() => useLocalizations(), { wrapper }).result; + }; + + it.each([ + [2, 'Incorrect password. You have 2 attempts remaining.'], + [1, 'Incorrect password. You have 1 attempt remaining.'], + [ + 0, + 'Your session has ended because you reached the password confirmation attempt limit. Sign in again to continue.', + ], + ])('shows the countdown with %i attempts remaining', async (remainingAttempts, expected) => { + const result = await renderLocalizations(); + + const res = createPasswordConfirmationError( + [{ code: 'form_password_validation_failed', message: 'Incorrect password', meta: { remainingAttempts } }], + { t: result.current.t }, + ); + expect(res).toBe(expected); + }); + + it('uses customer translations for the countdown', async () => { + const result = await renderLocalizations({ + unstable__errors: { + password_confirmation_attempts_remaining: 'Mot de passe incorrect. {{remainingAttempts}} tentatives restantes.', + }, + }); + + const res = createPasswordConfirmationError( + [{ code: 'form_password_incorrect', message: 'Incorrect password', meta: { remainingAttempts: 3 } }], + { t: result.current.t }, + ); + expect(res).toBe('Mot de passe incorrect. 3 tentatives restantes.'); + }); + + it('returns other errors unchanged', async () => { + const result = await renderLocalizations(); + const error = { code: 'form_password_validation_failed', message: 'Incorrect password' }; + + expect(createPasswordConfirmationError([error], { t: result.current.t })).toBe(error); + }); +}); diff --git a/packages/ui/src/utils/passwordUtils.ts b/packages/ui/src/utils/passwordUtils.ts index 55447f7937d..e36fa108469 100644 --- a/packages/ui/src/utils/passwordUtils.ts +++ b/packages/ui/src/utils/passwordUtils.ts @@ -92,6 +92,27 @@ export const createPasswordError = ( return passwordErrorMessage; }; +// A password confirmation failure carries the remaining attempts; zero means the session has ended. +// Other errors are returned unchanged so they keep the default translation. +export const createPasswordConfirmationError = ( + errors: ClerkAPIError[], + localizationConfig: Pick, +) => { + const remainingAttempts = errors[0]?.meta?.remainingAttempts; + if (typeof remainingAttempts !== 'number') { + return errors[0]; + } + + const { t } = localizationConfig; + if (remainingAttempts === 0) { + return t(localizationKeys('unstable__errors.password_confirmation_session_ended')); + } + if (remainingAttempts === 1) { + return t(localizationKeys('unstable__errors.password_confirmation_attempt_remaining')); + } + return t(localizationKeys('unstable__errors.password_confirmation_attempts_remaining', { remainingAttempts })); +}; + export const addFullStop = (string: string | undefined) => { return !string ? '' : string.endsWith('.') ? string : `${string}.`; };