From 32a22aa8cfbd6ffc1ef2a10cbb5102ef41ee6fce Mon Sep 17 00:00:00 2001 From: Brad <44413459+lastbestdev@users.noreply.github.com> Date: Wed, 11 Feb 2026 09:40:39 -0800 Subject: [PATCH 001/134] [PM-32060] Access Intelligence: Disable select all checkbox when table is empty (#18914) --- .../shared/app-table-row-scrollable-m11.component.html | 1 + 1 file changed, 1 insertion(+) diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.html index 29da8a7a818..05dec048328 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.html @@ -10,6 +10,7 @@ [bitTooltip]="allAppsSelected() ? ('deselectAll' | i18n) : ('selectAll' | i18n)" (change)="selectAllChanged($event.target)" [attr.aria-label]="allAppsSelected() ? ('deselectAll' | i18n) : ('selectAll' | i18n)" + [disabled]="dataSource().filteredData?.length === 0" /> From 4b7e3eae41a421c48e2266f921d1179ed5e0df22 Mon Sep 17 00:00:00 2001 From: Bryan Cunningham Date: Wed, 11 Feb 2026 12:58:59 -0500 Subject: [PATCH 002/134] show underline on focus (#18916) --- libs/components/src/link/link.component.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/libs/components/src/link/link.component.ts b/libs/components/src/link/link.component.ts index d826a4633a9..79cf55da637 100644 --- a/libs/components/src/link/link.component.ts +++ b/libs/components/src/link/link.component.ts @@ -58,13 +58,12 @@ const commonStyles = [ "[&.tw-test-hover_span]:tw-underline", "[&:hover_span]:tw-decoration-[.125em]", "[&.tw-test-hover_span]:tw-decoration-[.125em]", - "disabled:tw-no-underline", - "disabled:tw-cursor-not-allowed", - "disabled:!tw-text-fg-disabled", - "disabled:hover:!tw-text-fg-disabled", - "disabled:hover:tw-no-underline", "focus-visible:tw-outline-none", "focus-visible:before:tw-ring-border-focus", + "[&:focus-visible_span]:tw-underline", + "[&:focus-visible_span]:tw-decoration-[.125em]", + "[&.tw-test-focus-visible_span]:tw-underline", + "[&.tw-test-focus-visible_span]:tw-decoration-[.125em]", // Workaround for html button tag not being able to be set to `display: inline` // and at the same time not being able to use `tw-ring-offset` because of box-shadow issue. @@ -93,6 +92,7 @@ const commonStyles = [ "aria-disabled:!tw-text-fg-disabled", "aria-disabled:hover:!tw-text-fg-disabled", "aria-disabled:hover:tw-no-underline", + "[&[aria-disabled]:focus-visible_span]:!tw-no-underline", ]; @Component({ From 975c8fb6f81435b0ac047d997d945ca4f2326b0f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 13:05:48 -0500 Subject: [PATCH 003/134] [deps] Autofill: Update tldts to v7.0.22 (#18881) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- apps/cli/package.json | 2 +- package-lock.json | 18 +++++++++--------- package.json | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index 40058bed16e..6c27267054f 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -88,7 +88,7 @@ "proper-lockfile": "4.1.2", "rxjs": "7.8.1", "semver": "7.7.3", - "tldts": "7.0.19", + "tldts": "7.0.22", "zxcvbn": "4.4.2" } } diff --git a/package-lock.json b/package-lock.json index 680aad40adf..dbdcd6d083d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,7 +63,7 @@ "rxjs": "7.8.1", "semver": "7.7.3", "tabbable": "6.3.0", - "tldts": "7.0.19", + "tldts": "7.0.22", "ts-node": "10.9.2", "utf-8-validate": "6.0.5", "vite-tsconfig-paths": "5.1.4", @@ -223,7 +223,7 @@ "proper-lockfile": "4.1.2", "rxjs": "7.8.1", "semver": "7.7.3", - "tldts": "7.0.19", + "tldts": "7.0.22", "zxcvbn": "4.4.2" }, "bin": { @@ -41006,21 +41006,21 @@ } }, "node_modules/tldts": { - "version": "7.0.19", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.19.tgz", - "integrity": "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==", + "version": "7.0.22", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.22.tgz", + "integrity": "sha512-nqpKFC53CgopKPjT6Wfb6tpIcZXHcI6G37hesvikhx0EmUGPkZrujRyAjgnmp1SHNgpQfKVanZ+KfpANFt2Hxw==", "license": "MIT", "dependencies": { - "tldts-core": "^7.0.19" + "tldts-core": "^7.0.22" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "7.0.19", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.19.tgz", - "integrity": "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==", + "version": "7.0.23", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.23.tgz", + "integrity": "sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ==", "license": "MIT" }, "node_modules/tmp": { diff --git a/package.json b/package.json index c95d6af7437..bc1553c4622 100644 --- a/package.json +++ b/package.json @@ -201,7 +201,7 @@ "rxjs": "7.8.1", "semver": "7.7.3", "tabbable": "6.3.0", - "tldts": "7.0.19", + "tldts": "7.0.22", "ts-node": "10.9.2", "utf-8-validate": "6.0.5", "vite-tsconfig-paths": "5.1.4", From b2f8fd67ef8411d5ea4085896c92fef4a87d5553 Mon Sep 17 00:00:00 2001 From: Jonathan Prusik Date: Wed, 11 Feb 2026 13:53:26 -0500 Subject: [PATCH 004/134] consolidate excluded domains copy to allow removal of service invocation (#18610) --- apps/browser/src/_locales/en/messages.json | 3 --- .../autofill/popup/settings/excluded-domains.component.html | 6 +----- .../autofill/popup/settings/excluded-domains.component.ts | 6 +----- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 5768d336115..7944904c44a 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden will not ask to save login details for these domains. You must refresh the page for changes to take effect." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." - }, "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, diff --git a/apps/browser/src/autofill/popup/settings/excluded-domains.component.html b/apps/browser/src/autofill/popup/settings/excluded-domains.component.html index 30170820a27..74ff0de6f5c 100644 --- a/apps/browser/src/autofill/popup/settings/excluded-domains.component.html +++ b/apps/browser/src/autofill/popup/settings/excluded-domains.component.html @@ -7,11 +7,7 @@

- {{ - (accountSwitcherEnabled$ | async) - ? ("excludedDomainsDescAlt" | i18n) - : ("excludedDomainsDesc" | i18n) - }} + {{ "excludedDomainsDesc" | i18n }}

diff --git a/apps/browser/src/autofill/popup/settings/excluded-domains.component.ts b/apps/browser/src/autofill/popup/settings/excluded-domains.component.ts index 6714f749d2d..2316aef390e 100644 --- a/apps/browser/src/autofill/popup/settings/excluded-domains.component.ts +++ b/apps/browser/src/autofill/popup/settings/excluded-domains.component.ts @@ -15,7 +15,7 @@ import { FormArray, } from "@angular/forms"; import { RouterModule } from "@angular/router"; -import { Observable, Subject, takeUntil } from "rxjs"; +import { Subject, takeUntil } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; @@ -35,7 +35,6 @@ import { TypographyModule, } from "@bitwarden/components"; -import { AccountSwitcherService } from "../../../auth/popup/account-switching/services/account-switcher.service"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; import { PopupFooterComponent } from "../../../platform/popup/layout/popup-footer.component"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; @@ -74,8 +73,6 @@ export class ExcludedDomainsComponent implements AfterViewInit, OnDestroy { @ViewChildren("uriInput") uriInputElements: QueryList> = new QueryList(); - readonly accountSwitcherEnabled$: Observable = - this.accountSwitcherService.accountSwitchingEnabled$(); dataIsPristine = true; isLoading = false; excludedDomainsState: string[] = []; @@ -96,7 +93,6 @@ export class ExcludedDomainsComponent implements AfterViewInit, OnDestroy { private toastService: ToastService, private formBuilder: FormBuilder, private popupRouterCacheService: PopupRouterCacheService, - private accountSwitcherService: AccountSwitcherService, ) {} get domainForms() { From f8976f992a968b33e4b82aad9c84d97e18ebe7ba Mon Sep 17 00:00:00 2001 From: John Harrington <84741727+harr1424@users.noreply.github.com> Date: Wed, 11 Feb 2026 13:37:20 -0700 Subject: [PATCH 005/134] [PM-31611] [Defect] After entering an email, the Anyone with the link option cannot be selected anymore (#18844) * add authType to to sendDetailsForm valueChanges --- .../send-form/components/send-details/send-details.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.ts b/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.ts index 46eded5e86d..ac1453a925c 100644 --- a/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.ts +++ b/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.ts @@ -199,6 +199,7 @@ export class SendDetailsComponent implements OnInit { deletionDate: new Date(this.formattedDeletionDate), expirationDate: new Date(this.formattedDeletionDate), password: value.password, + authType: value.authType, emails: value.emails ? value.emails .split(",") From d7cca1bedf2c87f94ae3b178728af1aa45b2c07b Mon Sep 17 00:00:00 2001 From: John Harrington <84741727+harr1424@users.noreply.github.com> Date: Wed, 11 Feb 2026 14:44:49 -0700 Subject: [PATCH 006/134] [PM-23108] CLI Add Email Verification to Send Receive (#18649) --- .../service-container/service-container.ts | 13 + .../send/commands/receive.command.spec.ts | 560 ++++++++++++++++++ .../tools/send/commands/receive.command.ts | 418 +++++++++++-- apps/cli/src/tools/send/send.program.ts | 2 + 4 files changed, 943 insertions(+), 50 deletions(-) create mode 100644 apps/cli/src/tools/send/commands/receive.command.spec.ts diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index 2033a2dd064..b5a2b1b8196 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -39,6 +39,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; import { MasterPasswordApiService as MasterPasswordApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; +import { SendTokenService, DefaultSendTokenService } from "@bitwarden/common/auth/send-access"; import { AccountServiceImplementation, getUserId, @@ -91,6 +92,8 @@ import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin. import { PinService } from "@bitwarden/common/key-management/pin/pin.service.implementation"; import { SecurityStateService } from "@bitwarden/common/key-management/security-state/abstractions/security-state.service"; import { DefaultSecurityStateService } from "@bitwarden/common/key-management/security-state/services/security-state.service"; +import { SendPasswordService } from "@bitwarden/common/key-management/sends/abstractions/send-password.service"; +import { DefaultSendPasswordService } from "@bitwarden/common/key-management/sends/services/default-send-password.service"; import { DefaultVaultTimeoutService, DefaultVaultTimeoutSettingsService, @@ -306,6 +309,8 @@ export class ServiceContainer { userVerificationApiService: UserVerificationApiService; organizationApiService: OrganizationApiServiceAbstraction; sendApiService: SendApiService; + sendTokenService: SendTokenService; + sendPasswordService: SendPasswordService; devicesApiService: DevicesApiServiceAbstraction; deviceTrustService: DeviceTrustServiceAbstraction; authRequestService: AuthRequestService; @@ -629,6 +634,8 @@ export class ServiceContainer { this.sendService, ); + this.sendPasswordService = new DefaultSendPasswordService(this.cryptoFunctionService); + this.searchService = new SearchService(this.logService, this.i18nService, this.stateProvider); this.collectionService = new DefaultCollectionService( @@ -675,6 +682,12 @@ export class ServiceContainer { customUserAgent, ); + this.sendTokenService = new DefaultSendTokenService( + this.globalStateProvider, + this.sdkService, + this.sendPasswordService, + ); + this.keyConnectorService = new KeyConnectorService( this.accountService, this.masterPasswordService, diff --git a/apps/cli/src/tools/send/commands/receive.command.spec.ts b/apps/cli/src/tools/send/commands/receive.command.spec.ts new file mode 100644 index 00000000000..fe982905059 --- /dev/null +++ b/apps/cli/src/tools/send/commands/receive.command.spec.ts @@ -0,0 +1,560 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore +import { mock } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { SendTokenService, SendAccessToken } from "@bitwarden/common/auth/send-access"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; +import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SendAccessResponse } from "@bitwarden/common/tools/send/models/response/send-access.response"; +import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; +import { SendType } from "@bitwarden/common/tools/send/types/send-type"; +import { KeyService } from "@bitwarden/key-management"; + +import { Response } from "../../../models/response"; + +import { SendReceiveCommand } from "./receive.command"; + +describe("SendReceiveCommand", () => { + let command: SendReceiveCommand; + + const keyService = mock(); + const encryptService = mock(); + const cryptoFunctionService = mock(); + const platformUtilsService = mock(); + const environmentService = mock(); + const sendApiService = mock(); + const apiService = mock(); + const sendTokenService = mock(); + const configService = mock(); + + const testUrl = "https://send.bitwarden.com/#/send/abc123/key456"; + const testSendId = "abc123"; + + beforeEach(() => { + jest.clearAllMocks(); + + environmentService.environment$ = of({ + getUrls: () => ({ + api: "https://api.bitwarden.com", + webVault: "https://vault.bitwarden.com", + }), + } as any); + + platformUtilsService.isDev.mockReturnValue(false); + + keyService.makeSendKey.mockResolvedValue({} as any); + + cryptoFunctionService.pbkdf2.mockResolvedValue(new Uint8Array(32)); + + command = new SendReceiveCommand( + keyService, + encryptService, + cryptoFunctionService, + platformUtilsService, + environmentService, + sendApiService, + apiService, + sendTokenService, + configService, + ); + }); + + describe("URL parsing", () => { + it("should return error for invalid URL", async () => { + const response = await command.run("not-a-valid-url", {}); + + expect(response.success).toBe(false); + expect(response.message).toContain("Failed to parse"); + }); + + it("should return error when URL is missing send ID or key", async () => { + configService.getFeatureFlag.mockResolvedValue(false); + + const response = await command.run("https://send.bitwarden.com/#/send/", {}); + + expect(response.success).toBe(false); + expect(response.message).toContain("not a valid Send url"); + }); + }); + + describe("V1 Flow (Feature Flag Off)", () => { + beforeEach(() => { + configService.getFeatureFlag.mockResolvedValue(false); + }); + + it("should successfully access unprotected Send", async () => { + const mockSendAccess = { + id: testSendId, + type: SendType.Text, + text: { text: "secret message" }, + }; + + sendApiService.postSendAccess.mockResolvedValue({} as any); + + jest.spyOn(command as any, "sendRequest").mockResolvedValue(mockSendAccess); + + const response = await command.run(testUrl, {}); + + expect(response.success).toBe(true); + }); + + it("should successfully access password-protected Send with --password option", async () => { + const mockSendAccess = { + id: testSendId, + type: SendType.Text, + text: { text: "secret message" }, + }; + + sendApiService.postSendAccess.mockResolvedValue({} as any); + jest.spyOn(command as any, "sendRequest").mockResolvedValue(mockSendAccess); + + const response = await command.run(testUrl, { password: "test-password" }); + + expect(response.success).toBe(true); + expect(cryptoFunctionService.pbkdf2).toHaveBeenCalledWith( + "test-password", + expect.any(Uint8Array), + "sha256", + 100000, + ); + }); + + it("should return error for incorrect password in non-interactive mode", async () => { + process.env.BW_NOINTERACTION = "true"; + + const error = new ErrorResponse( + { + statusCode: 401, + message: "Unauthorized", + }, + 401, + ); + + sendApiService.postSendAccess.mockRejectedValue(error); + + const response = await command.run(testUrl, { password: "wrong-password" }); + + expect(response.success).toBe(false); + expect(response.message).toContain("Incorrect or missing password"); + + delete process.env.BW_NOINTERACTION; + }); + + it("should return 404 for non-existent Send", async () => { + const error = new ErrorResponse( + { + statusCode: 404, + message: "Not found", + }, + 404, + ); + + sendApiService.postSendAccess.mockRejectedValue(error); + + const response = await command.run(testUrl, {}); + + expect(response.success).toBe(false); + }); + }); + + describe("V2 Flow (Feature Flag On)", () => { + beforeEach(() => { + configService.getFeatureFlag.mockResolvedValue(true); + }); + + describe("Unprotected Sends", () => { + it("should successfully access Send with cached token", async () => { + const mockToken = new SendAccessToken("test-token", Date.now() + 3600000); + sendTokenService.tryGetSendAccessToken$.mockReturnValue(of(mockToken)); + sendApiService.postSendAccessV2.mockResolvedValue({} as any); + jest.spyOn(command as any, "accessSendWithToken").mockResolvedValue(Response.success()); + + const response = await command.run(testUrl, {}); + + expect(response.success).toBe(true); + expect(sendTokenService.tryGetSendAccessToken$).toHaveBeenCalledWith(testSendId); + }); + + it("should handle expired token and determine auth type", async () => { + sendTokenService.tryGetSendAccessToken$.mockReturnValue( + of({ + kind: "expected_server", + error: { + error: "invalid_request", + send_access_error_type: "password_hash_b64_required", + }, + } as any), + ); + + // Mock password auth flow + const mockToken = new SendAccessToken("test-token", Date.now() + 3600000); + sendTokenService.getSendAccessToken$.mockReturnValue(of(mockToken)); + jest.spyOn(command as any, "accessSendWithToken").mockResolvedValue(Response.success()); + + const response = await command.run(testUrl, { password: "test-password" }); + + expect(response.success).toBe(true); + }); + }); + + describe("Password Authentication (V2)", () => { + it("should successfully authenticate with password", async () => { + sendTokenService.tryGetSendAccessToken$.mockReturnValue( + of({ + kind: "expected_server", + error: { + error: "invalid_request", + send_access_error_type: "password_hash_b64_required", + }, + } as any), + ); + + const mockToken = new SendAccessToken("test-token", Date.now() + 3600000); + sendTokenService.getSendAccessToken$.mockReturnValue(of(mockToken)); + sendApiService.postSendAccessV2.mockResolvedValue({} as any); + jest.spyOn(command as any, "accessSendWithToken").mockResolvedValue(Response.success()); + + const response = await command.run(testUrl, { password: "correct-password" }); + + expect(response.success).toBe(true); + expect(sendTokenService.getSendAccessToken$).toHaveBeenCalledWith( + testSendId, + expect.objectContaining({ + kind: "password", + passwordHashB64: expect.any(String), + }), + ); + }); + + it("should return error for invalid password", async () => { + process.env.BW_NOINTERACTION = "true"; + + sendTokenService.tryGetSendAccessToken$.mockReturnValue( + of({ + kind: "expected_server", + error: { + error: "invalid_request", + send_access_error_type: "password_hash_b64_required", + }, + } as any), + ); + + sendTokenService.getSendAccessToken$.mockReturnValue( + of({ + kind: "expected_server", + error: { + error: "invalid_grant", + send_access_error_type: "password_hash_b64_invalid", + }, + } as any), + ); + + const response = await command.run(testUrl, { password: "wrong-password" }); + + expect(response.success).toBe(false); + expect(response.message).toContain("Invalid password"); + + delete process.env.BW_NOINTERACTION; + }); + + it("should work with --passwordenv option", async () => { + process.env.TEST_SEND_PASSWORD = "env-password"; + process.env.BW_NOINTERACTION = "true"; + + sendTokenService.tryGetSendAccessToken$.mockReturnValue( + of({ + kind: "expected_server", + error: { + error: "invalid_request", + send_access_error_type: "password_hash_b64_required", + }, + } as any), + ); + + const mockToken = new SendAccessToken("test-token", Date.now() + 3600000); + sendTokenService.getSendAccessToken$.mockReturnValue(of(mockToken)); + jest.spyOn(command as any, "accessSendWithToken").mockResolvedValue(Response.success()); + + const response = await command.run(testUrl, { passwordenv: "TEST_SEND_PASSWORD" }); + + expect(response.success).toBe(true); + + delete process.env.TEST_SEND_PASSWORD; + delete process.env.BW_NOINTERACTION; + }); + }); + + describe("Email OTP Authentication (V2)", () => { + it("should return error in non-interactive mode for email OTP", async () => { + process.env.BW_NOINTERACTION = "true"; + + sendTokenService.tryGetSendAccessToken$.mockReturnValue( + of({ + kind: "expected_server", + error: { + error: "invalid_request", + send_access_error_type: "email_required", + }, + } as any), + ); + + const response = await command.run(testUrl, {}); + + expect(response.success).toBe(false); + expect(response.message).toContain("Email verification required"); + expect(response.message).toContain("interactive mode"); + + delete process.env.BW_NOINTERACTION; + }); + + it("should handle email submission and OTP prompt flow", async () => { + sendTokenService.tryGetSendAccessToken$.mockReturnValue( + of({ + kind: "expected_server", + error: { + error: "invalid_request", + send_access_error_type: "email_required", + }, + } as any), + ); + + sendTokenService.getSendAccessToken$.mockReturnValueOnce( + of({ + kind: "expected_server", + error: { + error: "invalid_request", + send_access_error_type: "email_and_otp_required_otp_sent", + }, + } as any), + ); + + const mockToken = new SendAccessToken("test-token", Date.now() + 3600000); + sendTokenService.getSendAccessToken$.mockReturnValueOnce(of(mockToken)); + + // We can't easily test the interactive prompts, but we can verify the token service calls + // would be made in the right order + expect(sendTokenService.getSendAccessToken$).toBeDefined(); + }); + + it("should handle invalid email error", async () => { + sendTokenService.tryGetSendAccessToken$.mockReturnValue( + of({ + kind: "expected_server", + error: { + error: "invalid_request", + send_access_error_type: "email_required", + }, + } as any), + ); + + sendTokenService.getSendAccessToken$.mockReturnValue( + of({ + kind: "expected_server", + error: { + error: "invalid_grant", + send_access_error_type: "email_invalid", + }, + } as any), + ); + + // In a real scenario with interactive prompts, this would retry + // For unit tests, we verify the error is recognized + expect(sendTokenService.getSendAccessToken$).toBeDefined(); + }); + + it("should handle invalid OTP error", async () => { + sendTokenService.getSendAccessToken$.mockReturnValue( + of({ + kind: "expected_server", + error: { + error: "invalid_grant", + send_access_error_type: "otp_invalid", + }, + } as any), + ); + + // Verify OTP validation would be handled + expect(sendTokenService.getSendAccessToken$).toBeDefined(); + }); + }); + + describe("File Downloads (V2)", () => { + it("should successfully download file Send with V2 API", async () => { + const mockToken = new SendAccessToken("test-token", Date.now() + 3600000); + sendTokenService.tryGetSendAccessToken$.mockReturnValue(of(mockToken)); + + const mockSendResponse = { + id: testSendId, + type: SendType.File, + file: { + id: "file-123", + fileName: "test.pdf", + size: 1024, + }, + }; + + sendApiService.postSendAccessV2.mockResolvedValue(mockSendResponse as any); + sendApiService.getSendFileDownloadDataV2.mockResolvedValue({ + url: "https://example.com/download", + } as any); + + encryptService.decryptFileData.mockResolvedValue(new ArrayBuffer(1024) as any); + jest.spyOn(command as any, "saveAttachmentToFile").mockResolvedValue(Response.success()); + + await command.run(testUrl, { output: "./test.pdf" }); + + expect(sendApiService.getSendFileDownloadDataV2).toHaveBeenCalledWith( + expect.any(Object), + mockToken, + "https://api.bitwarden.com", + ); + }); + }); + + describe("Invalid Send ID", () => { + it("should return 404 for invalid Send ID", async () => { + sendTokenService.tryGetSendAccessToken$.mockReturnValue( + of({ + kind: "expected_server", + error: { + error: "invalid_grant", + send_access_error_type: "send_id_invalid", + }, + } as any), + ); + + const response = await command.run(testUrl, {}); + + expect(response.success).toBe(false); + }); + }); + + describe("Text Send Output", () => { + it("should output text to stdout for text Sends", async () => { + const mockToken = new SendAccessToken("test-token", Date.now() + 3600000); + sendTokenService.tryGetSendAccessToken$.mockReturnValue(of(mockToken)); + + const secretText = "This is a secret message"; + + sendApiService.postSendAccessV2.mockResolvedValue({} as any); + + // Mock the entire accessSendWithToken to avoid encryption issues + jest.spyOn(command as any, "accessSendWithToken").mockImplementation(async () => { + process.stdout.write(secretText); + return Response.success(); + }); + + const stdoutSpy = jest.spyOn(process.stdout, "write").mockImplementation(() => true); + + const response = await command.run(testUrl, {}); + + expect(response.success).toBe(true); + expect(stdoutSpy).toHaveBeenCalledWith(secretText); + + stdoutSpy.mockRestore(); + }); + + it("should return JSON object when --obj flag is used", async () => { + const mockToken = new SendAccessToken("test-token", Date.now() + 3600000); + sendTokenService.tryGetSendAccessToken$.mockReturnValue(of(mockToken)); + + const mockDecryptedView = { + id: testSendId, + type: SendType.Text, + text: { text: "secret message" }, + }; + + sendApiService.postSendAccessV2.mockResolvedValue({} as any); + + // Mock the entire accessSendWithToken to avoid encryption issues + jest.spyOn(command as any, "accessSendWithToken").mockImplementation(async () => { + const sendAccessResponse = new SendAccessResponse(mockDecryptedView as any); + const res = new Response(); + res.success = true; + res.data = sendAccessResponse as any; + return res; + }); + + const response = await command.run(testUrl, { obj: true }); + + expect(response.success).toBe(true); + expect(response.data).toBeDefined(); + expect(response.data.constructor.name).toBe("SendAccessResponse"); + }); + }); + }); + + describe("API URL Resolution", () => { + it("should resolve send.bitwarden.com to api.bitwarden.com", async () => { + configService.getFeatureFlag.mockResolvedValue(false); + + const sendUrl = "https://send.bitwarden.com/#/send/abc123/key456"; + sendApiService.postSendAccess.mockResolvedValue({} as any); + jest.spyOn(command as any, "sendRequest").mockResolvedValue({ + type: SendType.Text, + text: { text: "test" }, + }); + + await command.run(sendUrl, {}); + + const apiUrl = await (command as any).getApiUrl(new URL(sendUrl)); + expect(apiUrl).toBe("https://api.bitwarden.com"); + }); + + it("should handle custom domain URLs", async () => { + configService.getFeatureFlag.mockResolvedValue(false); + + const customUrl = "https://custom.example.com/#/send/abc123/key456"; + sendApiService.postSendAccess.mockResolvedValue({} as any); + jest.spyOn(command as any, "sendRequest").mockResolvedValue({ + type: SendType.Text, + text: { text: "test" }, + }); + + await command.run(customUrl, {}); + + const apiUrl = await (command as any).getApiUrl(new URL(customUrl)); + expect(apiUrl).toBe("https://custom.example.com/api"); + }); + }); + + describe("Feature Flag Routing", () => { + it("should route to V1 flow when feature flag is off", async () => { + configService.getFeatureFlag.mockResolvedValue(false); + + sendApiService.postSendAccess.mockResolvedValue({} as any); + const v1Spy = jest.spyOn(command as any, "attemptV1Access"); + jest.spyOn(command as any, "sendRequest").mockResolvedValue({ + type: SendType.Text, + text: { text: "test" }, + }); + + await command.run(testUrl, {}); + + expect(configService.getFeatureFlag).toHaveBeenCalledWith(FeatureFlag.SendEmailOTP); + expect(v1Spy).toHaveBeenCalled(); + }); + + it("should route to V2 flow when feature flag is on", async () => { + configService.getFeatureFlag.mockResolvedValue(true); + + const mockToken = new SendAccessToken("test-token", Date.now() + 3600000); + sendTokenService.tryGetSendAccessToken$.mockReturnValue(of(mockToken)); + + const v2Spy = jest.spyOn(command as any, "attemptV2Access"); + jest.spyOn(command as any, "accessSendWithToken").mockResolvedValue(Response.success()); + + await command.run(testUrl, {}); + + expect(configService.getFeatureFlag).toHaveBeenCalledWith(FeatureFlag.SendEmailOTP); + expect(v2Spy).toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/cli/src/tools/send/commands/receive.command.ts b/apps/cli/src/tools/send/commands/receive.command.ts index 5cbf458c87f..9496855a7a5 100644 --- a/apps/cli/src/tools/send/commands/receive.command.ts +++ b/apps/cli/src/tools/send/commands/receive.command.ts @@ -5,9 +5,25 @@ import * as inquirer from "inquirer"; import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { + SendTokenService, + SendAccessToken, + emailRequired, + emailAndOtpRequired, + otpInvalid, + passwordHashB64Required, + passwordHashB64Invalid, + sendIdInvalid, + SendHashedPasswordB64, + SendOtp, + GetSendAccessTokenError, + SendAccessDomainCredentials, +} from "@bitwarden/common/auth/send-access"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -17,6 +33,7 @@ import { SendAccess } from "@bitwarden/common/tools/send/models/domain/send-acce import { SendAccessRequest } from "@bitwarden/common/tools/send/models/request/send-access.request"; import { SendAccessView } from "@bitwarden/common/tools/send/models/view/send-access.view"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; +import { AuthType } from "@bitwarden/common/tools/send/types/auth-type"; import { SendType } from "@bitwarden/common/tools/send/types/send-type"; import { KeyService } from "@bitwarden/key-management"; import { NodeUtils } from "@bitwarden/node/node-utils"; @@ -38,6 +55,8 @@ export class SendReceiveCommand extends DownloadCommand { private environmentService: EnvironmentService, private sendApiService: SendApiService, apiService: ApiService, + private sendTokenService: SendTokenService, + private configService: ConfigService, ) { super(encryptService, apiService); } @@ -62,58 +81,13 @@ export class SendReceiveCommand extends DownloadCommand { } const keyArray = Utils.fromUrlB64ToArray(key); - this.sendAccessRequest = new SendAccessRequest(); - let password = options.password; - if (password == null || password === "") { - if (options.passwordfile) { - password = await NodeUtils.readFirstLine(options.passwordfile); - } else if (options.passwordenv && process.env[options.passwordenv]) { - password = process.env[options.passwordenv]; - } - } + const sendEmailOtpEnabled = await this.configService.getFeatureFlag(FeatureFlag.SendEmailOTP); - if (password != null && password !== "") { - this.sendAccessRequest.password = await this.getUnlockedPassword(password, keyArray); - } - - const response = await this.sendRequest(apiUrl, id, keyArray); - - if (response instanceof Response) { - // Error scenario - return response; - } - - if (options.obj != null) { - return Response.success(new SendAccessResponse(response)); - } - - switch (response.type) { - case SendType.Text: - // Write to stdout and response success so we get the text string only to stdout - process.stdout.write(response?.text?.text); - return Response.success(); - case SendType.File: { - const downloadData = await this.sendApiService.getSendFileDownloadData( - response, - this.sendAccessRequest, - apiUrl, - ); - - const decryptBufferFn = async (resp: globalThis.Response) => { - const encBuf = await EncArrayBuffer.fromResponse(resp); - return this.encryptService.decryptFileData(encBuf, this.decKey); - }; - - return await this.saveAttachmentToFile( - downloadData.url, - response?.file?.fileName, - decryptBufferFn, - options.output, - ); - } - default: - return Response.success(new SendAccessResponse(response)); + if (sendEmailOtpEnabled) { + return await this.attemptV2Access(apiUrl, id, keyArray, options); + } else { + return await this.attemptV1Access(apiUrl, id, keyArray, options); } } @@ -146,6 +120,350 @@ export class SendReceiveCommand extends DownloadCommand { return Utils.fromBufferToB64(passwordHash); } + private async attemptV1Access( + apiUrl: string, + id: string, + keyArray: Uint8Array, + options: OptionValues, + ): Promise { + this.sendAccessRequest = new SendAccessRequest(); + + let password = options.password; + if (password == null || password === "") { + if (options.passwordfile) { + password = await NodeUtils.readFirstLine(options.passwordfile); + } else if (options.passwordenv && process.env[options.passwordenv]) { + password = process.env[options.passwordenv]; + } + } + + if (password != null && password !== "") { + this.sendAccessRequest.password = await this.getUnlockedPassword(password, keyArray); + } + + const response = await this.sendRequest(apiUrl, id, keyArray); + + if (response instanceof Response) { + return response; + } + + if (options.obj != null) { + return Response.success(new SendAccessResponse(response)); + } + + switch (response.type) { + case SendType.Text: + process.stdout.write(response?.text?.text); + return Response.success(); + case SendType.File: { + const downloadData = await this.sendApiService.getSendFileDownloadData( + response, + this.sendAccessRequest, + apiUrl, + ); + + const decryptBufferFn = async (resp: globalThis.Response) => { + const encBuf = await EncArrayBuffer.fromResponse(resp); + return this.encryptService.decryptFileData(encBuf, this.decKey); + }; + + return await this.saveAttachmentToFile( + downloadData.url, + response?.file?.fileName, + decryptBufferFn, + options.output, + ); + } + default: + return Response.success(new SendAccessResponse(response)); + } + } + + private async attemptV2Access( + apiUrl: string, + id: string, + keyArray: Uint8Array, + options: OptionValues, + ): Promise { + let authType: AuthType = AuthType.None; + + const currentResponse = await this.getTokenWithRetry(id); + + if (currentResponse instanceof SendAccessToken) { + return await this.accessSendWithToken(currentResponse, keyArray, apiUrl, options); + } + + if (currentResponse.kind === "expected_server") { + const error = currentResponse.error; + + if (emailRequired(error)) { + authType = AuthType.Email; + } else if (passwordHashB64Required(error)) { + authType = AuthType.Password; + } else if (sendIdInvalid(error)) { + return Response.notFound(); + } + } else { + return this.handleError(currentResponse); + } + + // Handle authentication based on type + if (authType === AuthType.Email) { + if (!this.canInteract) { + return Response.badRequest("Email verification required. Run in interactive mode."); + } + return await this.handleEmailOtpAuth(id, keyArray, apiUrl, options); + } else if (authType === AuthType.Password) { + return await this.handlePasswordAuth(id, keyArray, apiUrl, options); + } + + // The auth layer will immediately return a token for Sends with AuthType.None + // If this code is reached, something has gone wrong + if (authType === AuthType.None) { + return Response.error("Could not determine authentication requirements"); + } + + return Response.error("Authentication failed"); + } + + private async getTokenWithRetry( + sendId: string, + credentials?: SendAccessDomainCredentials, + ): Promise { + let expiredAttempts = 0; + + while (expiredAttempts < 3) { + const response = credentials + ? await firstValueFrom(this.sendTokenService.getSendAccessToken$(sendId, credentials)) + : await firstValueFrom(this.sendTokenService.tryGetSendAccessToken$(sendId)); + + if (response instanceof SendAccessToken) { + return response; + } + + if (response.kind === "expired") { + expiredAttempts++; + continue; + } + + // Not expired, return the response for caller to handle + return response; + } + + // After 3 expired attempts, return an error response + return { + kind: "unknown", + error: "Send access token has expired and could not be refreshed", + }; + } + + private handleError(error: GetSendAccessTokenError): Response { + if (error.kind === "unexpected_server") { + return Response.error("Server error: " + JSON.stringify(error.error)); + } + + return Response.error("Error: " + JSON.stringify(error.error)); + } + + private async promptForOtp(sendId: string, email: string): Promise { + const otpAnswer = await inquirer.createPromptModule({ output: process.stderr })({ + type: "input", + name: "otp", + message: "Enter the verification code sent to your email:", + }); + return otpAnswer.otp; + } + + private async promptForEmail(): Promise { + const emailAnswer = await inquirer.createPromptModule({ output: process.stderr })({ + type: "input", + name: "email", + message: "Enter your email address:", + validate: (input: string) => { + if (!input || !input.includes("@")) { + return "Please enter a valid email address"; + } + return true; + }, + }); + return emailAnswer.email; + } + + private async handleEmailOtpAuth( + sendId: string, + keyArray: Uint8Array, + apiUrl: string, + options: OptionValues, + ): Promise { + const email = await this.promptForEmail(); + + const emailResponse = await this.getTokenWithRetry(sendId, { + kind: "email", + email: email, + }); + + if (emailResponse instanceof SendAccessToken) { + /* + At this point emailResponse should only be expected to be a GetSendAccessTokenError type, + but TS must have a logical branch in case it is a SendAccessToken type. If a valid token is + returned by the method above, something has gone wrong. + */ + + return Response.error("Unexpected server response"); + } + + if (emailResponse.kind === "expected_server") { + const error = emailResponse.error; + + if (emailAndOtpRequired(error)) { + const promptResponse = await this.promptForOtp(sendId, email); + + // Use retry helper for expired token handling + const otpResponse = await this.getTokenWithRetry(sendId, { + kind: "email_otp", + email: email, + otp: promptResponse, + }); + + if (otpResponse instanceof SendAccessToken) { + return await this.accessSendWithToken(otpResponse, keyArray, apiUrl, options); + } + + if (otpResponse.kind === "expected_server") { + const error = otpResponse.error; + + if (otpInvalid(error)) { + return Response.badRequest("Invalid email or verification code"); + } + + /* + If the following evaluates to true, it means that the email address provided was not + configured to be used for email OTP for this Send. + + To avoid leaking information that would allow email enumeration, instead return an + error indicating that some component of the email OTP challenge was invalid. + */ + if (emailAndOtpRequired(error)) { + return Response.badRequest("Invalid email or verification code"); + } + } + return this.handleError(otpResponse); + } + } + return this.handleError(emailResponse); + } + + private async handlePasswordAuth( + sendId: string, + keyArray: Uint8Array, + apiUrl: string, + options: OptionValues, + ): Promise { + let password = options.password; + + if (password == null || password === "") { + if (options.passwordfile) { + password = await NodeUtils.readFirstLine(options.passwordfile); + } else if (options.passwordenv && process.env[options.passwordenv]) { + password = process.env[options.passwordenv]; + } + } + + if ((password == null || password === "") && this.canInteract) { + const answer = await inquirer.createPromptModule({ output: process.stderr })({ + type: "password", + name: "password", + message: "Send password:", + }); + password = answer.password; + } + + if (!password) { + return Response.badRequest("Password required"); + } + + const passwordHashB64 = await this.getUnlockedPassword(password, keyArray); + + // Use retry helper for expired token handling + const response = await this.getTokenWithRetry(sendId, { + kind: "password", + passwordHashB64: passwordHashB64 as SendHashedPasswordB64, + }); + + if (response instanceof SendAccessToken) { + return await this.accessSendWithToken(response, keyArray, apiUrl, options); + } + + if (response.kind === "expected_server") { + const error = response.error; + + if (passwordHashB64Invalid(error)) { + return Response.badRequest("Invalid password"); + } + } else if (response.kind === "unexpected_server") { + return Response.error("Server error: " + JSON.stringify(response.error)); + } else if (response.kind === "unknown") { + return Response.error("Error: " + response.error); + } + + return Response.error("Authentication failed"); + } + + private async accessSendWithToken( + accessToken: SendAccessToken, + keyArray: Uint8Array, + apiUrl: string, + options: OptionValues, + ): Promise { + try { + const sendResponse = await this.sendApiService.postSendAccessV2(accessToken, apiUrl); + + const sendAccess = new SendAccess(sendResponse); + this.decKey = await this.keyService.makeSendKey(keyArray); + const decryptedView = await sendAccess.decrypt(this.decKey); + + if (options.obj != null) { + return Response.success(new SendAccessResponse(decryptedView)); + } + + switch (decryptedView.type) { + case SendType.Text: + process.stdout.write(decryptedView?.text?.text); + return Response.success(); + + case SendType.File: { + const downloadData = await this.sendApiService.getSendFileDownloadDataV2( + decryptedView, + accessToken, + apiUrl, + ); + + const decryptBufferFn = async (resp: globalThis.Response) => { + const encBuf = await EncArrayBuffer.fromResponse(resp); + return this.encryptService.decryptFileData(encBuf, this.decKey); + }; + + return await this.saveAttachmentToFile( + downloadData.url, + decryptedView?.file?.fileName, + decryptBufferFn, + options.output, + ); + } + + default: + return Response.success(new SendAccessResponse(decryptedView)); + } + } catch (e) { + if (e instanceof ErrorResponse) { + if (e.statusCode === 404) { + return Response.notFound(); + } + } + return Response.error(e); + } + } + private async sendRequest( url: string, id: string, diff --git a/apps/cli/src/tools/send/send.program.ts b/apps/cli/src/tools/send/send.program.ts index a84b6c15ead..e40cea4daa9 100644 --- a/apps/cli/src/tools/send/send.program.ts +++ b/apps/cli/src/tools/send/send.program.ts @@ -133,6 +133,8 @@ export class SendProgram extends BaseProgram { this.serviceContainer.environmentService, this.serviceContainer.sendApiService, this.serviceContainer.apiService, + this.serviceContainer.sendTokenService, + this.serviceContainer.configService, ); const response = await cmd.run(url, options); this.processResponse(response); From 5cf4678838c419aa2f3806de4e7118874c17f20b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Wed, 11 Feb 2026 22:10:55 +0000 Subject: [PATCH 007/134] [PM-28300] Remove BlockClaimedDomainAccountCreation feature flag and related logic from policy component (#18720) --- .../block-claimed-domain-account-creation.component.ts | 10 ---------- libs/common/src/enums/feature-flag.enum.ts | 2 -- 2 files changed, 12 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/block-claimed-domain-account-creation.component.ts b/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/block-claimed-domain-account-creation.component.ts index 75c61e3e7e3..7e6e346d3b7 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/block-claimed-domain-account-creation.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/block-claimed-domain-account-creation.component.ts @@ -1,10 +1,6 @@ import { ChangeDetectionStrategy, Component } from "@angular/core"; -import { map, Observable } from "rxjs"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { BasePolicyEditDefinition, BasePolicyEditComponent, @@ -16,12 +12,6 @@ export class BlockClaimedDomainAccountCreationPolicy extends BasePolicyEditDefin description = "blockClaimedDomainAccountCreationDesc"; type = PolicyType.BlockClaimedDomainAccountCreation; component = BlockClaimedDomainAccountCreationPolicyComponent; - - override display$(organization: Organization, configService: ConfigService): Observable { - return configService - .getFeatureFlag$(FeatureFlag.BlockClaimedDomainAccountCreation) - .pipe(map((enabled) => enabled && organization.useOrganizationDomains)); - } } @Component({ diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 40e22cfbb5a..1edbcc4e376 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -12,7 +12,6 @@ import { ServerConfig } from "../platform/abstractions/config/server-config"; export enum FeatureFlag { /* Admin Console Team */ AutoConfirm = "pm-19934-auto-confirm-organization-users", - BlockClaimedDomainAccountCreation = "pm-28297-block-uninvited-claimed-domain-registration", DefaultUserCollectionRestore = "pm-30883-my-items-restored-users", MembersComponentRefactor = "pm-29503-refactor-members-inheritance", BulkReinviteUI = "pm-28416-bulk-reinvite-ux-improvements", @@ -108,7 +107,6 @@ const FALSE = false as boolean; export const DefaultFeatureFlagValue = { /* Admin Console Team */ [FeatureFlag.AutoConfirm]: FALSE, - [FeatureFlag.BlockClaimedDomainAccountCreation]: FALSE, [FeatureFlag.DefaultUserCollectionRestore]: FALSE, [FeatureFlag.MembersComponentRefactor]: FALSE, [FeatureFlag.BulkReinviteUI]: FALSE, From 88140604c120dc8c98c8d25eaae2f9d85a28eaec Mon Sep 17 00:00:00 2001 From: Amy Galles <9685081+AmyLGalles@users.noreply.github.com> Date: Wed, 11 Feb 2026 14:32:23 -0800 Subject: [PATCH 008/134] Add missing bw-linux-arm64 release artifact (#18614) * duplicating changes made previously by @RoboMagus * organizing builds --- .github/workflows/release-cli.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 3f7b7e326d9..5d37c00c2d9 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -91,7 +91,9 @@ jobs: apps/cli/bw-macos-${{ env.PKG_VERSION }}.zip, apps/cli/bw-macos-arm64-${{ env.PKG_VERSION }}.zip, apps/cli/bw-oss-linux-${{ env.PKG_VERSION }}.zip, + apps/cli/bw-oss-linux-arm64-${{ env.PKG_VERSION }}.zip, apps/cli/bw-linux-${{ env.PKG_VERSION }}.zip, + apps/cli/bw-linux-arm64-${{ env.PKG_VERSION }}.zip, apps/cli/bitwarden-cli.${{ env.PKG_VERSION }}.nupkg, apps/cli/bw_${{ env.PKG_VERSION }}_amd64.snap, apps/cli/bitwarden-cli-${{ env.PKG_VERSION }}-npm-build.zip" From 30d3a36c7e584be138e6beab4018a2329712fc47 Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Wed, 11 Feb 2026 17:32:35 -0500 Subject: [PATCH 009/134] [PM-31938] refactor archive btn logic in web view modal (#18874) * refactor showArchiveBtn logic in web view modal --- .../vault-item-dialog.component.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts index d9eb03ea1ca..4da2d05f12b 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts @@ -10,7 +10,7 @@ import { OnInit, viewChild, } from "@angular/core"; -import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { takeUntilDestroyed, toSignal } from "@angular/core/rxjs-interop"; import { Router } from "@angular/router"; import { firstValueFrom, Observable, Subject, switchMap } from "rxjs"; import { map } from "rxjs/operators"; @@ -226,6 +226,9 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { ); protected archiveFlagEnabled$ = this.archiveService.hasArchiveFlagEnabled$; + private readonly archiveFlagEnabled = toSignal(this.archiveFlagEnabled$, { + initialValue: false, + }); protected userId$ = this.accountService.activeAccount$.pipe(getUserId); @@ -237,6 +240,8 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { switchMap((userId) => this.archiveService.userCanArchive$(userId)), ); + private readonly userCanArchive = toSignal(this.userCanArchive$, { initialValue: false }); + protected get isTrashFilter() { return this.filter?.type === "trash"; } @@ -293,14 +298,14 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { return this.cipher?.isArchived; } - private _userCanArchive = false; - protected get showArchiveOptions(): boolean { - return this._userCanArchive && !this.params.isAdminConsoleAction && this.params.mode === "view"; + return ( + this.archiveFlagEnabled() && !this.params.isAdminConsoleAction && this.params.mode === "view" + ); } protected get showArchiveBtn(): boolean { - return this.cipher?.canBeArchived; + return this.userCanArchive() && this.cipher?.canBeArchived; } protected get showUnarchiveBtn(): boolean { @@ -355,8 +360,6 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { takeUntilDestroyed(), ) .subscribe(); - - this.userCanArchive$.pipe(takeUntilDestroyed()).subscribe((v) => (this._userCanArchive = v)); } async ngOnInit() { From 11e2b25ede7276157a0feb676b740a5752cd1d74 Mon Sep 17 00:00:00 2001 From: Ben Brooks <56796209+bensbits91@users.noreply.github.com> Date: Wed, 11 Feb 2026 14:47:27 -0800 Subject: [PATCH 010/134] PM-28831 Add isTrusted checks to ignore programmatically generated events (#18627) * ignore events that do not originate from the user agent * [pm-28831] Add isTrusted checks and update tests * [pm-28831] Add isTrusted check to click events * [pm-28831] Replace in-code jest exceptions with new utils * [pm-28831] Move isTrusted checks to testable util * [pm-28831] Remove redundant check in cipher-action.ts * [pm-28831] Add isTrusted checks to click events in autofill-inine-menu-list --------- Signed-off-by: Ben Brooks Co-authored-by: Jonathan Prusik --- .../components/buttons/action-button.ts | 3 +- .../components/buttons/badge-button.ts | 3 +- .../content/components/buttons/edit-button.ts | 3 +- .../notification/confirmation/message.ts | 3 +- .../option-selection/option-item.ts | 21 +++- .../option-selection/option-items.ts | 5 + .../option-selection/option-selection.ts | 3 +- .../content/content-message-handler.spec.ts | 2 + .../content/content-message-handler.ts | 7 +- .../autofill/content/context-menu-handler.ts | 8 ++ .../list/autofill-inline-menu-list.spec.ts | 2 + .../pages/list/autofill-inline-menu-list.ts | 119 +++++++++++++++--- .../autofill-inline-menu-page-element.ts | 6 +- .../autofill-overlay-content.service.spec.ts | 6 + .../autofill-overlay-content.service.ts | 12 ++ .../src/autofill/utils/event-security.spec.ts | 26 ++++ .../src/autofill/utils/event-security.ts | 13 ++ 17 files changed, 219 insertions(+), 23 deletions(-) create mode 100644 apps/browser/src/autofill/utils/event-security.spec.ts create mode 100644 apps/browser/src/autofill/utils/event-security.ts diff --git a/apps/browser/src/autofill/content/components/buttons/action-button.ts b/apps/browser/src/autofill/content/components/buttons/action-button.ts index 73fc1e79ec5..e83f2b4b77c 100644 --- a/apps/browser/src/autofill/content/components/buttons/action-button.ts +++ b/apps/browser/src/autofill/content/components/buttons/action-button.ts @@ -3,6 +3,7 @@ import { html, TemplateResult } from "lit"; import { Theme } from "@bitwarden/common/platform/enums"; +import { EventSecurity } from "../../../utils/event-security"; import { border, themes, typography, spacing } from "../constants/styles"; import { Spinner } from "../icons"; @@ -26,7 +27,7 @@ export function ActionButton({ fullWidth = true, }: ActionButtonProps) { const handleButtonClick = (event: Event) => { - if (!disabled && !isLoading) { + if (EventSecurity.isEventTrusted(event) && !disabled && !isLoading) { handleClick(event); } }; diff --git a/apps/browser/src/autofill/content/components/buttons/badge-button.ts b/apps/browser/src/autofill/content/components/buttons/badge-button.ts index 3cdd453ee1a..98968d0b57b 100644 --- a/apps/browser/src/autofill/content/components/buttons/badge-button.ts +++ b/apps/browser/src/autofill/content/components/buttons/badge-button.ts @@ -3,6 +3,7 @@ import { html } from "lit"; import { Theme } from "@bitwarden/common/platform/enums"; +import { EventSecurity } from "../../../utils/event-security"; import { border, themes, typography, spacing } from "../constants/styles"; export type BadgeButtonProps = { @@ -23,7 +24,7 @@ export function BadgeButton({ username, }: BadgeButtonProps) { const handleButtonClick = (event: Event) => { - if (!disabled) { + if (EventSecurity.isEventTrusted(event) && !disabled) { buttonAction(event); } }; diff --git a/apps/browser/src/autofill/content/components/buttons/edit-button.ts b/apps/browser/src/autofill/content/components/buttons/edit-button.ts index ecbb736bb8e..88caae13590 100644 --- a/apps/browser/src/autofill/content/components/buttons/edit-button.ts +++ b/apps/browser/src/autofill/content/components/buttons/edit-button.ts @@ -3,6 +3,7 @@ import { html } from "lit"; import { Theme } from "@bitwarden/common/platform/enums"; +import { EventSecurity } from "../../../utils/event-security"; import { themes, typography, spacing } from "../constants/styles"; import { PencilSquare } from "../icons"; @@ -21,7 +22,7 @@ export function EditButton({ buttonAction, buttonText, disabled = false, theme } aria-label=${buttonText} class=${editButtonStyles({ disabled, theme })} @click=${(event: Event) => { - if (!disabled) { + if (EventSecurity.isEventTrusted(event) && !disabled) { buttonAction(event); } }} diff --git a/apps/browser/src/autofill/content/components/notification/confirmation/message.ts b/apps/browser/src/autofill/content/components/notification/confirmation/message.ts index 36ea9c1f9d6..480b2acd0dd 100644 --- a/apps/browser/src/autofill/content/components/notification/confirmation/message.ts +++ b/apps/browser/src/autofill/content/components/notification/confirmation/message.ts @@ -3,6 +3,7 @@ import { html, nothing } from "lit"; import { Theme } from "@bitwarden/common/platform/enums"; +import { EventSecurity } from "../../../../utils/event-security"; import { spacing, themes, typography } from "../../constants/styles"; export type NotificationConfirmationMessageProps = { @@ -127,7 +128,7 @@ const AdditionalMessageStyles = ({ theme }: { theme: Theme }) => css` `; function handleButtonKeyDown(event: KeyboardEvent, handleClick: () => void) { - if (event.key === "Enter" || event.key === " ") { + if (EventSecurity.isEventTrusted(event) && (event.key === "Enter" || event.key === " ")) { event.preventDefault(); handleClick(); } diff --git a/apps/browser/src/autofill/content/components/option-selection/option-item.ts b/apps/browser/src/autofill/content/components/option-selection/option-item.ts index 6af6a2d6538..1cbabcb4f85 100644 --- a/apps/browser/src/autofill/content/components/option-selection/option-item.ts +++ b/apps/browser/src/autofill/content/components/option-selection/option-item.ts @@ -3,6 +3,7 @@ import { html, nothing } from "lit"; import { Theme } from "@bitwarden/common/platform/enums"; +import { EventSecurity } from "../../../utils/event-security"; import { IconProps, Option } from "../common-types"; import { themes, spacing } from "../constants/styles"; @@ -29,6 +30,13 @@ export function OptionItem({ handleSelection, }: OptionItemProps) { const handleSelectionKeyUpProxy = (event: KeyboardEvent) => { + /** + * Reject synthetic events (not originating from the user agent) + */ + if (!EventSecurity.isEventTrusted(event)) { + return; + } + const listenedForKeys = new Set(["Enter", "Space"]); if (listenedForKeys.has(event.code) && event.target instanceof Element) { handleSelection(); @@ -37,6 +45,17 @@ export function OptionItem({ return; }; + const handleSelectionClickProxy = (event: MouseEvent) => { + /** + * Reject synthetic events (not originating from the user agent) + */ + if (!EventSecurity.isEventTrusted(event)) { + return; + } + + handleSelection(); + }; + const iconProps: IconProps = { color: themes[theme].text.main, theme }; const itemIcon = icon?.(iconProps); const ariaLabel = @@ -52,7 +71,7 @@ export function OptionItem({ title=${text} role="option" aria-label=${ariaLabel} - @click=${handleSelection} + @click=${handleSelectionClickProxy} @keyup=${handleSelectionKeyUpProxy} > ${itemIcon ? html`
${itemIcon}
` : nothing} diff --git a/apps/browser/src/autofill/content/components/option-selection/option-items.ts b/apps/browser/src/autofill/content/components/option-selection/option-items.ts index 58216b6c1b2..4c24a2fde8b 100644 --- a/apps/browser/src/autofill/content/components/option-selection/option-items.ts +++ b/apps/browser/src/autofill/content/components/option-selection/option-items.ts @@ -3,6 +3,7 @@ import { html, nothing } from "lit"; import { Theme } from "@bitwarden/common/platform/enums"; +import { EventSecurity } from "../../../utils/event-security"; import { Option } from "../common-types"; import { themes, typography, scrollbarStyles, spacing } from "../constants/styles"; @@ -57,6 +58,10 @@ export function OptionItems({ } function handleMenuKeyUp(event: KeyboardEvent) { + if (!EventSecurity.isEventTrusted(event)) { + return; + } + const items = [ ...(event.currentTarget as HTMLElement).querySelectorAll('[tabindex="0"]'), ]; diff --git a/apps/browser/src/autofill/content/components/option-selection/option-selection.ts b/apps/browser/src/autofill/content/components/option-selection/option-selection.ts index ee711456e9c..78c7d9f0646 100644 --- a/apps/browser/src/autofill/content/components/option-selection/option-selection.ts +++ b/apps/browser/src/autofill/content/components/option-selection/option-selection.ts @@ -4,6 +4,7 @@ import { property, state } from "lit/decorators.js"; import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; +import { EventSecurity } from "../../../utils/event-security"; import { OptionSelectionButton } from "../buttons/option-selection-button"; import { Option } from "../common-types"; @@ -54,7 +55,7 @@ export class OptionSelection extends LitElement { private static currentOpenInstance: OptionSelection | null = null; private handleButtonClick = async (event: Event) => { - if (!this.disabled) { + if (EventSecurity.isEventTrusted(event) && !this.disabled) { const isOpening = !this.showMenu; if (isOpening) { diff --git a/apps/browser/src/autofill/content/content-message-handler.spec.ts b/apps/browser/src/autofill/content/content-message-handler.spec.ts index 874e1cc76ff..fb17874b0b7 100644 --- a/apps/browser/src/autofill/content/content-message-handler.spec.ts +++ b/apps/browser/src/autofill/content/content-message-handler.spec.ts @@ -3,6 +3,7 @@ import { mock } from "jest-mock-extended"; import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum"; import { postWindowMessage, sendMockExtensionMessage } from "../spec/testing-utils"; +import { EventSecurity } from "../utils/event-security"; describe("ContentMessageHandler", () => { const sendMessageSpy = jest.spyOn(chrome.runtime, "sendMessage"); @@ -19,6 +20,7 @@ describe("ContentMessageHandler", () => { ); beforeEach(() => { + jest.spyOn(EventSecurity, "isEventTrusted").mockReturnValue(true); // FIXME: Remove when updating file. Eslint update // eslint-disable-next-line @typescript-eslint/no-require-imports require("./content-message-handler"); diff --git a/apps/browser/src/autofill/content/content-message-handler.ts b/apps/browser/src/autofill/content/content-message-handler.ts index 63afc215923..874e760c4f8 100644 --- a/apps/browser/src/autofill/content/content-message-handler.ts +++ b/apps/browser/src/autofill/content/content-message-handler.ts @@ -1,6 +1,8 @@ import { ExtensionPageUrls } from "@bitwarden/common/vault/enums"; import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum"; +import { EventSecurity } from "../utils/event-security"; + import { ContentMessageWindowData, ContentMessageWindowEventHandlers, @@ -92,7 +94,10 @@ function handleOpenBrowserExtensionToUrlMessage({ url }: { url?: ExtensionPageUr */ function handleWindowMessageEvent(event: MessageEvent) { const { source, data, origin } = event; - if (source !== window || !data?.command) { + /** + * Reject synthetic events (not originating from the user agent) + */ + if (!EventSecurity.isEventTrusted(event) || source !== window || !data?.command) { return; } diff --git a/apps/browser/src/autofill/content/context-menu-handler.ts b/apps/browser/src/autofill/content/context-menu-handler.ts index d3926d57c9a..919ab5f1a3d 100644 --- a/apps/browser/src/autofill/content/context-menu-handler.ts +++ b/apps/browser/src/autofill/content/context-menu-handler.ts @@ -1,3 +1,5 @@ +import { EventSecurity } from "../utils/event-security"; + const inputTags = ["input", "textarea", "select"]; const labelTags = ["label", "span"]; const attributeKeys = ["id", "name", "label-aria", "placeholder"]; @@ -52,6 +54,12 @@ function isNullOrEmpty(s: string | null) { // We only have access to the element that's been clicked when the context menu is first opened. // Remember it for use later. document.addEventListener("contextmenu", (event) => { + /** + * Reject synthetic events (not originating from the user agent) + */ + if (!EventSecurity.isEventTrusted(event)) { + return; + } clickedElement = event.target as HTMLElement; }); diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.spec.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.spec.ts index 1e99ac9df90..212fe6d8c89 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.spec.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.spec.ts @@ -10,6 +10,7 @@ import { createInitAutofillInlineMenuListMessageMock, } from "../../../../spec/autofill-mocks"; import { flushPromises, postWindowMessage } from "../../../../spec/testing-utils"; +import { EventSecurity } from "../../../../utils/event-security"; import { AutofillInlineMenuList } from "./autofill-inline-menu-list"; @@ -28,6 +29,7 @@ describe("AutofillInlineMenuList", () => { const events: { eventName: any; callback: any }[] = []; beforeEach(() => { + jest.spyOn(EventSecurity, "isEventTrusted").mockReturnValue(true); const oldEv = globalThis.addEventListener; globalThis.addEventListener = (eventName: any, callback: any) => { events.push({ eventName, callback }); diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts index c13c523e30a..39b7fa5c17b 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts @@ -10,6 +10,7 @@ import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; import { InlineMenuCipherData } from "../../../../background/abstractions/overlay.background"; import { InlineMenuFillType } from "../../../../enums/autofill-overlay.enum"; import { buildSvgDomElement, specialCharacterToKeyMap, throttle } from "../../../../utils"; +import { EventSecurity } from "../../../../utils/event-security"; import { creditCardIcon, globeIcon, @@ -203,7 +204,14 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { private handleSaveLoginInlineMenuKeyUp = (event: KeyboardEvent) => { const listenedForKeys = new Set(["ArrowDown"]); - if (!listenedForKeys.has(event.code) || !(event.target instanceof Element)) { + if ( + /** + * Reject synthetic events (not originating from the user agent) + */ + !EventSecurity.isEventTrusted(event) || + !listenedForKeys.has(event.code) || + !(event.target instanceof Element) + ) { return; } @@ -229,7 +237,14 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { * Handles the click event for the unlock button. * Sends a message to the parent window to unlock the vault. */ - private handleUnlockButtonClick = () => { + private handleUnlockButtonClick = (event: MouseEvent) => { + /** + * Reject synthetic events (not originating from the user agent) + */ + if (!EventSecurity.isEventTrusted(event)) { + return; + } + this.postMessageToParent({ command: "unlockVault" }); }; @@ -352,7 +367,14 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { * Handles the click event for the fill generated password button. Triggers * a message to the background script to fill the generated password. */ - private handleFillGeneratedPasswordClick = () => { + private handleFillGeneratedPasswordClick = (event?: MouseEvent) => { + /** + * Reject synthetic events (not originating from the user agent) + */ + if (event && !EventSecurity.isEventTrusted(event)) { + return; + } + this.postMessageToParent({ command: "fillGeneratedPassword" }); }; @@ -362,7 +384,16 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { * @param event - The keyup event. */ private handleFillGeneratedPasswordKeyUp = (event: KeyboardEvent) => { - if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) { + /** + * Reject synthetic events (not originating from the user agent) + */ + if ( + !EventSecurity.isEventTrusted(event) || + event.ctrlKey || + event.altKey || + event.metaKey || + event.shiftKey + ) { return; } @@ -388,6 +419,13 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { * @param event - The click event. */ private handleRefreshGeneratedPasswordClick = (event?: MouseEvent) => { + /** + * Reject synthetic events (not originating from the user agent) + */ + if (event && !EventSecurity.isEventTrusted(event)) { + return; + } + if (event) { (event.target as HTMLElement) .closest(".password-generator-actions") @@ -403,7 +441,16 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { * @param event - The keyup event. */ private handleRefreshGeneratedPasswordKeyUp = (event: KeyboardEvent) => { - if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) { + /** + * Reject synthetic events (not originating from the user agent) + */ + if ( + !EventSecurity.isEventTrusted(event) || + event.ctrlKey || + event.altKey || + event.metaKey || + event.shiftKey + ) { return; } @@ -620,7 +667,14 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { * Handles the click event for the new item button. * Sends a message to the parent window to add a new vault item. */ - private handleNewLoginVaultItemAction = () => { + private handleNewLoginVaultItemAction = (event: MouseEvent) => { + /** + * Reject synthetic events (not originating from the user agent) + */ + if (!EventSecurity.isEventTrusted(event)) { + return; + } + let addNewCipherType = this.inlineMenuFillType; if (this.showInlineMenuAccountCreation) { @@ -958,7 +1012,16 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { private handleFillCipherClickEvent = (cipher: InlineMenuCipherData) => { const usePasskey = !!cipher.login?.passkey; return this.useEventHandlersMemo( - () => this.triggerFillCipherClickEvent(cipher, usePasskey), + (event: MouseEvent) => { + /** + * Reject synthetic events (not originating from the user agent) + */ + if (!EventSecurity.isEventTrusted(event)) { + return; + } + + this.triggerFillCipherClickEvent(cipher, usePasskey); + }, `${cipher.id}-fill-cipher-button-click-handler-${usePasskey ? "passkey" : ""}`, ); }; @@ -990,7 +1053,14 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { */ private handleFillCipherKeyUpEvent = (event: KeyboardEvent) => { const listenedForKeys = new Set(["ArrowDown", "ArrowUp", "ArrowRight"]); - if (!listenedForKeys.has(event.code) || !(event.target instanceof Element)) { + /** + * Reject synthetic events (not originating from the user agent) + */ + if ( + !EventSecurity.isEventTrusted(event) || + !listenedForKeys.has(event.code) || + !(event.target instanceof Element) + ) { return; } @@ -1018,7 +1088,14 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { */ private handleNewItemButtonKeyUpEvent = (event: KeyboardEvent) => { const listenedForKeys = new Set(["ArrowDown", "ArrowUp"]); - if (!listenedForKeys.has(event.code) || !(event.target instanceof Element)) { + /** + * Reject synthetic events (not originating from the user agent) + */ + if ( + !EventSecurity.isEventTrusted(event) || + !listenedForKeys.has(event.code) || + !(event.target instanceof Element) + ) { return; } @@ -1063,11 +1140,16 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { * @param cipher - The cipher to view. */ private handleViewCipherClickEvent = (cipher: InlineMenuCipherData) => { - return this.useEventHandlersMemo( - () => - this.postMessageToParent({ command: "viewSelectedCipher", inlineMenuCipherId: cipher.id }), - `${cipher.id}-view-cipher-button-click-handler`, - ); + return this.useEventHandlersMemo((event: MouseEvent) => { + /** + * Reject synthetic events (not originating from the user agent) + */ + if (!EventSecurity.isEventTrusted(event)) { + return; + } + + this.postMessageToParent({ command: "viewSelectedCipher", inlineMenuCipherId: cipher.id }); + }, `${cipher.id}-view-cipher-button-click-handler`); }; /** @@ -1080,7 +1162,14 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { */ private handleViewCipherKeyUpEvent = (event: KeyboardEvent) => { const listenedForKeys = new Set(["ArrowDown", "ArrowUp", "ArrowLeft"]); - if (!listenedForKeys.has(event.code) || !(event.target instanceof Element)) { + /** + * Reject synthetic events (not originating from the user agent) + */ + if ( + !EventSecurity.isEventTrusted(event) || + !listenedForKeys.has(event.code) || + !(event.target instanceof Element) + ) { return; } diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/shared/autofill-inline-menu-page-element.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/shared/autofill-inline-menu-page-element.ts index 5df6e7cd190..e7f99b28ecc 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/shared/autofill-inline-menu-page-element.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/shared/autofill-inline-menu-page-element.ts @@ -1,6 +1,7 @@ import { EVENTS } from "@bitwarden/common/autofill/constants"; import { RedirectFocusDirection } from "../../../../enums/autofill-overlay.enum"; +import { EventSecurity } from "../../../../utils/event-security"; import { AutofillInlineMenuPageElementWindowMessage, AutofillInlineMenuPageElementWindowMessageHandlers, @@ -163,7 +164,10 @@ export class AutofillInlineMenuPageElement extends HTMLElement { */ private handleDocumentKeyDownEvent = (event: KeyboardEvent) => { const listenedForKeys = new Set(["Tab", "Escape", "ArrowUp", "ArrowDown"]); - if (!listenedForKeys.has(event.code)) { + /** + * Reject synthetic events (not originating from the user agent) + */ + if (!EventSecurity.isEventTrusted(event) || !listenedForKeys.has(event.code)) { return; } diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts index 0fb031b52e8..c9a522c6b8c 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts @@ -23,6 +23,7 @@ import { sendMockExtensionMessage, } from "../spec/testing-utils"; import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types"; +import { EventSecurity } from "../utils/event-security"; import { AutoFillConstants } from "./autofill-constants"; import { AutofillOverlayContentService } from "./autofill-overlay-content.service"; @@ -55,6 +56,9 @@ describe("AutofillOverlayContentService", () => { const mockQuerySelectorAll = mockQuerySelectorAllDefinedCall(); beforeEach(async () => { + // Mock EventSecurity to allow synthetic events in tests + jest.spyOn(EventSecurity, "isEventTrusted").mockReturnValue(true); + inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); domQueryService = new DomQueryService(); domElementVisibilityService = new DomElementVisibilityService(); @@ -331,6 +335,8 @@ describe("AutofillOverlayContentService", () => { pageDetailsMock, ); jest.spyOn(globalThis.customElements, "define").mockImplementation(); + // Mock EventSecurity to allow synthetic events in tests + jest.spyOn(EventSecurity, "isEventTrusted").mockReturnValue(true); }); it("closes the autofill inline menu when the `Escape` key is pressed", () => { diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts index 7ea89e114ab..eb02d05d671 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -45,6 +45,7 @@ import { sendExtensionMessage, throttle, } from "../utils"; +import { EventSecurity } from "../utils/event-security"; import { AutofillOverlayContentExtensionMessageHandlers, @@ -618,6 +619,10 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ */ private handleSubmitButtonInteraction = (event: PointerEvent) => { if ( + /** + * Reject synthetic events (not originating from the user agent) + */ + !EventSecurity.isEventTrusted(event) || !this.submitElements.has(event.target as HTMLElement) || (event.type === "keyup" && !["Enter", "Space"].includes((event as unknown as KeyboardEvent).code)) @@ -703,6 +708,13 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ * @param event - The keyup event. */ private handleFormFieldKeyupEvent = async (event: globalThis.KeyboardEvent) => { + /** + * Reject synthetic events (not originating from the user agent) + */ + if (!EventSecurity.isEventTrusted(event)) { + return; + } + const eventCode = event.code; if (eventCode === "Escape") { void this.sendExtensionMessage("closeAutofillInlineMenu", { diff --git a/apps/browser/src/autofill/utils/event-security.spec.ts b/apps/browser/src/autofill/utils/event-security.spec.ts new file mode 100644 index 00000000000..5cda484d4d2 --- /dev/null +++ b/apps/browser/src/autofill/utils/event-security.spec.ts @@ -0,0 +1,26 @@ +import { EventSecurity } from "./event-security"; + +describe("EventSecurity", () => { + describe("isEventTrusted", () => { + it("should call the event.isTrusted property", () => { + const testEvent = new KeyboardEvent("keyup", { code: "Escape" }); + const result = EventSecurity.isEventTrusted(testEvent); + + // In test environment, events are untrusted by default + expect(result).toBe(false); + expect(result).toBe(testEvent.isTrusted); + }); + + it("should be mockable with jest.spyOn", () => { + const testEvent = new KeyboardEvent("keyup", { code: "Escape" }); + const spy = jest.spyOn(EventSecurity, "isEventTrusted").mockReturnValue(true); + + const result = EventSecurity.isEventTrusted(testEvent); + + expect(result).toBe(true); + expect(spy).toHaveBeenCalledWith(testEvent); + + spy.mockRestore(); + }); + }); +}); diff --git a/apps/browser/src/autofill/utils/event-security.ts b/apps/browser/src/autofill/utils/event-security.ts new file mode 100644 index 00000000000..e53517058df --- /dev/null +++ b/apps/browser/src/autofill/utils/event-security.ts @@ -0,0 +1,13 @@ +/** + * Event security utilities for validating trusted events + */ +export class EventSecurity { + /** + * Validates that an event is trusted (originated from user agent) + * @param event - The event to validate + * @returns true if the event is trusted, false otherwise + */ + static isEventTrusted(event: Event): boolean { + return event.isTrusted; + } +} From 396286ff9a8f225deaa207585b44072a5eb22ec6 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Wed, 11 Feb 2026 16:54:05 -0800 Subject: [PATCH 011/134] [PM-26703] - Update Item Action Behavior for Extension (#18921) * Revert "Revert "[PM-26703]- Browser - Update autofill Behavior (#18467)" (#18723)" This reverts commit 5d17d9ee718aba156b071493e8f57c98eed072cd. * fix title in non-autofill list * add feature flag * add old logic. add specs * revert changes * remove comments * update language in spec * update appearance spec * revert change to security-tasks * fix logic for blocked uri. add deprecated notice. * fix test * fix type error --- .../autofill-vault-list-items.component.html | 5 +- .../item-more-options.component.html | 8 +- .../item-more-options.component.ts | 21 +- .../vault-list-items-container.component.html | 63 ++-- ...ult-list-items-container.component.spec.ts | 332 ++++++++++++++++++ .../vault-list-items-container.component.ts | 164 +++++---- .../popup/settings/appearance.component.html | 16 +- .../settings/appearance.component.spec.ts | 54 ++- .../popup/settings/appearance.component.ts | 11 +- libs/common/src/enums/feature-flag.enum.ts | 2 + 10 files changed, 558 insertions(+), 118 deletions(-) create mode 100644 apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.spec.ts diff --git a/apps/browser/src/vault/popup/components/vault/autofill-vault-list-items/autofill-vault-list-items.component.html b/apps/browser/src/vault/popup/components/vault/autofill-vault-list-items/autofill-vault-list-items.component.html index 47ef0284d6a..38d60233200 100644 --- a/apps/browser/src/vault/popup/components/vault/autofill-vault-list-items/autofill-vault-list-items.component.html +++ b/apps/browser/src/vault/popup/components/vault/autofill-vault-list-items/autofill-vault-list-items.component.html @@ -5,8 +5,9 @@ [showRefresh]="showRefresh" (onRefresh)="refreshCurrentTab()" [description]="(showEmptyAutofillTip$ | async) ? ('autofillSuggestionsTip' | i18n) : undefined" - showAutofillButton + isAutofillList [disableDescriptionMargin]="showEmptyAutofillTip$ | async" - [primaryActionAutofill]="clickItemsToAutofillVaultView$ | async" [groupByType]="groupByType()" + [showAutofillButton]="(clickItemsToAutofillVaultView$ | async) === false" + [primaryActionAutofill]="clickItemsToAutofillVaultView$ | async" > diff --git a/apps/browser/src/vault/popup/components/vault/item-more-options/item-more-options.component.html b/apps/browser/src/vault/popup/components/vault/item-more-options/item-more-options.component.html index be67869d3df..4df3c8a5c73 100644 --- a/apps/browser/src/vault/popup/components/vault/item-more-options/item-more-options.component.html +++ b/apps/browser/src/vault/popup/components/vault/item-more-options/item-more-options.component.html @@ -8,18 +8,18 @@ > @if (!decryptionFailure) { - + @if (canAutofill && showAutofill()) { - - + } + @if (showViewOption()) { - + } diff --git a/apps/browser/src/vault/popup/components/vault/item-more-options/item-more-options.component.ts b/apps/browser/src/vault/popup/components/vault/item-more-options/item-more-options.component.ts index 8ed2699254e..ef4c4a111b6 100644 --- a/apps/browser/src/vault/popup/components/vault/item-more-options/item-more-options.component.ts +++ b/apps/browser/src/vault/popup/components/vault/item-more-options/item-more-options.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from "@angular/common"; -import { booleanAttribute, Component, Input } from "@angular/core"; +import { booleanAttribute, Component, input, Input } from "@angular/core"; import { Router, RouterModule } from "@angular/router"; import { BehaviorSubject, combineLatest, firstValueFrom, map, Observable, switchMap } from "rxjs"; import { filter } from "rxjs/operators"; @@ -76,22 +76,17 @@ export class ItemMoreOptionsComponent { } /** - * Flag to show view item menu option. Used when something else is - * assigned as the primary action for the item, such as autofill. + * Flag to show the autofill menu option. + * When true, the "Autofill" option appears in the menu. */ - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input({ transform: booleanAttribute }) - showViewOption = false; + readonly showAutofill = input(false, { transform: booleanAttribute }); /** - * Flag to hide the autofill menu options. Used for items that are - * already in the autofill list suggestion. + * Flag to show the view menu option. + * When true, the "View" option appears in the menu. + * Used when the primary action is autofill (so users can view without autofilling). */ - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input({ transform: booleanAttribute }) - hideAutofillOptions = false; + readonly showViewOption = input(false, { transform: booleanAttribute }); protected autofillAllowed$ = this.vaultPopupAutofillService.autofillAllowed$; diff --git a/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.html b/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.html index 3dac158b8e1..e9e89776dde 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.html +++ b/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.html @@ -90,11 +90,11 @@ - + - - - - + @if (showFillTextOnHover()) { + + + {{ "fill" | i18n }} + + + } + @if (showAutofillBadge()) { + + + + } + @if (showLaunchButton() && CipherViewLikeUtils.canLaunch(cipher)) { + + + + } diff --git a/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.spec.ts b/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.spec.ts new file mode 100644 index 00000000000..eda84265e90 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.spec.ts @@ -0,0 +1,332 @@ +import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { NoopAnimationsModule } from "@angular/platform-browser/animations"; +import { Router } from "@angular/router"; +import { mock } from "jest-mock-extended"; +import { BehaviorSubject, of } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CompactModeService, DialogService } from "@bitwarden/components"; +import { PasswordRepromptService } from "@bitwarden/vault"; + +import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service"; +import { VaultPopupSectionService } from "../../../services/vault-popup-section.service"; +import { PopupCipherViewLike } from "../../../views/popup-cipher.view"; + +import { VaultListItemsContainerComponent } from "./vault-list-items-container.component"; + +describe("VaultListItemsContainerComponent", () => { + let fixture: ComponentFixture; + let component: VaultListItemsContainerComponent; + + const featureFlag$ = new BehaviorSubject(false); + const currentTabIsOnBlocklist$ = new BehaviorSubject(false); + + const mockCipher = { + id: "cipher-1", + name: "Test Login", + type: CipherType.Login, + login: { + username: "user@example.com", + uris: [{ uri: "https://example.com", match: null }], + }, + favorite: false, + reprompt: 0, + organizationId: null, + collectionIds: [], + edit: true, + viewPassword: true, + } as any; + + const configService = { + getFeatureFlag$: jest.fn().mockImplementation((flag: FeatureFlag) => { + if (flag === FeatureFlag.PM31039ItemActionInExtension) { + return featureFlag$.asObservable(); + } + return of(false); + }), + }; + + const vaultPopupAutofillService = { + currentTabIsOnBlocklist$: currentTabIsOnBlocklist$.asObservable(), + doAutofill: jest.fn(), + }; + + const compactModeService = { + enabled$: of(false), + }; + + const vaultPopupSectionService = { + getOpenDisplayStateForSection: jest.fn().mockReturnValue(() => true), + updateSectionOpenStoredState: jest.fn(), + }; + + beforeEach(async () => { + jest.clearAllMocks(); + featureFlag$.next(false); + currentTabIsOnBlocklist$.next(false); + + await TestBed.configureTestingModule({ + imports: [VaultListItemsContainerComponent, NoopAnimationsModule], + providers: [ + { provide: ConfigService, useValue: configService }, + { provide: VaultPopupAutofillService, useValue: vaultPopupAutofillService }, + { provide: CompactModeService, useValue: compactModeService }, + { provide: VaultPopupSectionService, useValue: vaultPopupSectionService }, + { provide: I18nService, useValue: { t: (k: string) => k } }, + { provide: AccountService, useValue: { activeAccount$: of({ id: "UserId" }) } }, + { provide: CipherService, useValue: mock() }, + { provide: Router, useValue: { navigate: jest.fn() } }, + { provide: PlatformUtilsService, useValue: { getAutofillKeyboardShortcut: () => "" } }, + { provide: DialogService, useValue: mock() }, + { provide: PasswordRepromptService, useValue: mock() }, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(VaultListItemsContainerComponent); + component = fixture.componentInstance; + }); + + describe("Updated item action feature flag", () => { + describe("when feature flag is OFF", () => { + beforeEach(() => { + featureFlag$.next(false); + fixture.detectChanges(); + }); + + it("should not show fill text on hover", () => { + fixture.componentRef.setInput("isAutofillList", true); + fixture.detectChanges(); + + expect(component.showFillTextOnHover()).toBe(false); + }); + + it("should show autofill badge when showAutofillButton is true and primaryActionAutofill is false", () => { + fixture.componentRef.setInput("showAutofillButton", true); + fixture.componentRef.setInput("primaryActionAutofill", false); + fixture.detectChanges(); + + expect(component.showAutofillBadge()).toBe(true); + }); + + it("should hide autofill badge when primaryActionAutofill is true", () => { + fixture.componentRef.setInput("showAutofillButton", true); + fixture.componentRef.setInput("primaryActionAutofill", true); + fixture.detectChanges(); + + expect(component.showAutofillBadge()).toBe(false); + }); + + it("should show launch button when showAutofillButton is false", () => { + fixture.componentRef.setInput("showAutofillButton", false); + fixture.detectChanges(); + + expect(component.showLaunchButton()).toBe(true); + }); + + it("should hide launch button when showAutofillButton is true", () => { + fixture.componentRef.setInput("showAutofillButton", true); + fixture.detectChanges(); + + expect(component.showLaunchButton()).toBe(false); + }); + + it("should show autofill in menu when showAutofillButton is false", () => { + fixture.componentRef.setInput("showAutofillButton", false); + fixture.detectChanges(); + + expect(component.showAutofillInMenu()).toBe(true); + }); + + it("should hide autofill in menu when showAutofillButton is true", () => { + fixture.componentRef.setInput("showAutofillButton", true); + fixture.detectChanges(); + + expect(component.showAutofillInMenu()).toBe(false); + }); + + it("should show view in menu when primaryActionAutofill is true", () => { + fixture.componentRef.setInput("primaryActionAutofill", true); + fixture.detectChanges(); + + expect(component.showViewInMenu()).toBe(true); + }); + + it("should hide view in menu when primaryActionAutofill is false", () => { + fixture.componentRef.setInput("primaryActionAutofill", false); + fixture.detectChanges(); + + expect(component.showViewInMenu()).toBe(false); + }); + + it("should autofill on select when primaryActionAutofill is true", () => { + fixture.componentRef.setInput("primaryActionAutofill", true); + fixture.detectChanges(); + + expect(component.canAutofill()).toBe(true); + }); + + it("should not autofill on select when primaryActionAutofill is false", () => { + fixture.componentRef.setInput("primaryActionAutofill", false); + fixture.detectChanges(); + + expect(component.canAutofill()).toBe(false); + }); + }); + + describe("when feature flag is ON", () => { + beforeEach(() => { + featureFlag$.next(true); + fixture.detectChanges(); + }); + + it("should show fill text on hover for autofill list items", () => { + fixture.componentRef.setInput("isAutofillList", true); + fixture.detectChanges(); + + expect(component.showFillTextOnHover()).toBe(true); + }); + + it("should not show fill text on hover for non-autofill list items", () => { + fixture.componentRef.setInput("isAutofillList", false); + fixture.detectChanges(); + + expect(component.showFillTextOnHover()).toBe(false); + }); + + it("should not show autofill badge", () => { + fixture.componentRef.setInput("isAutofillList", true); + fixture.componentRef.setInput("showAutofillButton", true); + fixture.detectChanges(); + + expect(component.showAutofillBadge()).toBe(false); + }); + + it("should hide launch button for autofill list items", () => { + fixture.componentRef.setInput("isAutofillList", true); + fixture.detectChanges(); + + expect(component.showLaunchButton()).toBe(false); + }); + + it("should show launch button for non-autofill list items", () => { + fixture.componentRef.setInput("isAutofillList", false); + fixture.detectChanges(); + + expect(component.showLaunchButton()).toBe(true); + }); + + it("should show autofill in menu for non-autofill list items", () => { + fixture.componentRef.setInput("isAutofillList", false); + fixture.detectChanges(); + + expect(component.showAutofillInMenu()).toBe(true); + }); + + it("should hide autofill in menu for autofill list items", () => { + fixture.componentRef.setInput("isAutofillList", true); + fixture.detectChanges(); + + expect(component.showAutofillInMenu()).toBe(false); + }); + + it("should show view in menu for autofill list items", () => { + fixture.componentRef.setInput("isAutofillList", true); + fixture.detectChanges(); + + expect(component.showViewInMenu()).toBe(true); + }); + + it("should hide view in menu for non-autofill list items", () => { + fixture.componentRef.setInput("isAutofillList", false); + fixture.detectChanges(); + + expect(component.showViewInMenu()).toBe(false); + }); + + it("should autofill on select for autofill list items", () => { + fixture.componentRef.setInput("isAutofillList", true); + fixture.detectChanges(); + + expect(component.canAutofill()).toBe(true); + }); + + it("should not autofill on select for non-autofill list items", () => { + fixture.componentRef.setInput("isAutofillList", false); + fixture.detectChanges(); + + expect(component.canAutofill()).toBe(false); + }); + }); + + describe("when current URI is blocked", () => { + beforeEach(() => { + currentTabIsOnBlocklist$.next(true); + fixture.detectChanges(); + }); + + it("should not autofill on select even when feature flag is ON and isAutofillList is true", () => { + featureFlag$.next(true); + fixture.componentRef.setInput("isAutofillList", true); + fixture.detectChanges(); + + expect(component.canAutofill()).toBe(false); + }); + + it("should not autofill on select even when primaryActionAutofill is true", () => { + featureFlag$.next(false); + fixture.componentRef.setInput("primaryActionAutofill", true); + fixture.detectChanges(); + + expect(component.canAutofill()).toBe(false); + }); + }); + }); + + describe("cipherItemTitleKey", () => { + it("should return autofillTitle when canAutofill is true", () => { + featureFlag$.next(true); + fixture.componentRef.setInput("isAutofillList", true); + fixture.detectChanges(); + + const titleKeyFn = component.cipherItemTitleKey(); + const result = titleKeyFn(mockCipher); + + expect(result).toBe("autofillTitleWithField"); + }); + + it("should return viewItemTitle when canAutofill is false", () => { + featureFlag$.next(true); + fixture.componentRef.setInput("isAutofillList", false); + fixture.detectChanges(); + + const titleKeyFn = component.cipherItemTitleKey(); + const result = titleKeyFn(mockCipher); + + expect(result).toBe("viewItemTitleWithField"); + }); + + it("should return title without WithField when cipher has no username", () => { + featureFlag$.next(true); + fixture.componentRef.setInput("isAutofillList", false); + fixture.detectChanges(); + + const cipherWithoutUsername = { + ...mockCipher, + login: { ...mockCipher.login, username: null }, + } as PopupCipherViewLike; + + const titleKeyFn = component.cipherItemTitleKey(); + const result = titleKeyFn(cipherWithoutUsername); + + expect(result).toBe("viewItemTitle"); + }); + }); +}); diff --git a/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.ts b/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.ts index 469247f9692..fb8d20c5cf6 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.ts +++ b/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.ts @@ -21,6 +21,8 @@ import { firstValueFrom, map } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { uuidAsString } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; @@ -88,8 +90,15 @@ import { ItemMoreOptionsComponent } from "../item-more-options/item-more-options export class VaultListItemsContainerComponent implements AfterViewInit { private compactModeService = inject(CompactModeService); private vaultPopupSectionService = inject(VaultPopupSectionService); + private configService = inject(ConfigService); protected CipherViewLikeUtils = CipherViewLikeUtils; + /** Signal for the feature flag that controls simplified item action behavior */ + protected readonly simplifiedItemActionEnabled = toSignal( + this.configService.getFeatureFlag$(FeatureFlag.PM31039ItemActionInExtension), + { initialValue: false }, + ); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild(CdkVirtualScrollViewport, { static: false }) viewPort!: CdkVirtualScrollViewport; @@ -136,24 +145,18 @@ export class VaultListItemsContainerComponent implements AfterViewInit { */ private viewCipherTimeout?: number; - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - ciphers = input([]); + readonly ciphers = input([]); /** * If true, we will group ciphers by type (Login, Card, Identity) * within subheadings in a single container, converted to a WritableSignal. */ - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - groupByType = input(false); + readonly groupByType = input(false); /** * Computed signal for a grouped list of ciphers with an optional header */ - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - cipherGroups = computed< + readonly cipherGroups = computed< { subHeaderKey?: string; ciphers: PopupCipherViewLike[]; @@ -195,9 +198,7 @@ export class VaultListItemsContainerComponent implements AfterViewInit { /** * Title for the vault list item section. */ - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - title = input(undefined); + readonly title = input(undefined); /** * Optionally allow the items to be collapsed. @@ -205,24 +206,20 @@ export class VaultListItemsContainerComponent implements AfterViewInit { * The key must be added to the state definition in `vault-popup-section.service.ts` since the * collapsed state is stored locally. */ - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - collapsibleKey = input(undefined); + readonly collapsibleKey = input(undefined); /** * Optional description for the vault list item section. Will be shown below the title even when * no ciphers are available. */ - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - description = input(undefined); + + readonly description = input(undefined); /** * Option to show a refresh button in the section header. */ - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - showRefresh = input(false, { transform: booleanAttribute }); + + readonly showRefresh = input(false, { transform: booleanAttribute }); /** * Event emitted when the refresh button is clicked. @@ -235,71 +232,124 @@ export class VaultListItemsContainerComponent implements AfterViewInit { /** * Flag indicating that the current tab location is blocked */ - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - currentURIIsBlocked = toSignal(this.vaultPopupAutofillService.currentTabIsOnBlocklist$); + readonly currentUriIsBlocked = toSignal(this.vaultPopupAutofillService.currentTabIsOnBlocklist$); /** * Resolved i18n key to use for suggested cipher items */ - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - cipherItemTitleKey = computed(() => { + readonly cipherItemTitleKey = computed(() => { return (cipher: CipherViewLike) => { const login = CipherViewLikeUtils.getLogin(cipher); const hasUsername = login?.username != null; - const key = - this.primaryActionAutofill() && !this.currentURIIsBlocked() - ? "autofillTitle" - : "viewItemTitle"; + // Use autofill title when autofill is the primary action + const key = this.canAutofill() ? "autofillTitle" : "viewItemTitle"; return hasUsername ? `${key}WithField` : key; }; }); /** + * @deprecated - To be removed when PM31039ItemActionInExtension is fully rolled out * Option to show the autofill button for each item. + * Used when feature flag is disabled. */ - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - showAutofillButton = input(false, { transform: booleanAttribute }); + readonly showAutofillButton = input(false, { transform: booleanAttribute }); /** - * Flag indicating whether the suggested cipher item autofill button should be shown or not + * @deprecated - To be removed when PM31039ItemActionInExtension is fully rolled out + * Whether to show the autofill badge button (old behavior). + * Only shown when feature flag is disabled AND conditions are met. */ - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - hideAutofillButton = computed( - () => !this.showAutofillButton() || this.currentURIIsBlocked() || this.primaryActionAutofill(), + readonly showAutofillBadge = computed( + () => !this.simplifiedItemActionEnabled() && !this.hideAutofillButton(), ); /** - * Flag indicating whether the cipher item autofill menu options should be shown or not + * @deprecated - To be removed when PM31039ItemActionInExtension is fully rolled out + * Flag indicating whether the cipher item autofill menu options should be shown or not. + * Used when feature flag is disabled. */ - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - hideAutofillMenuOptions = computed(() => this.currentURIIsBlocked() || this.showAutofillButton()); + readonly hideAutofillMenuOptions = computed( + () => this.currentUriIsBlocked() || this.showAutofillButton(), + ); /** + * @deprecated - To be removed when PM31039ItemActionInExtension is fully rolled out * Option to perform autofill operation as the primary action for autofill suggestions. + * Used when feature flag is disabled. */ - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - primaryActionAutofill = input(false, { transform: booleanAttribute }); + readonly primaryActionAutofill = input(false, { transform: booleanAttribute }); + + /** + * @deprecated - To be removed when PM31039ItemActionInExtension is fully rolled out + * Flag indicating whether the suggested cipher item autofill button should be shown or not. + * Used when feature flag is disabled. + */ + readonly hideAutofillButton = computed( + () => !this.showAutofillButton() || this.currentUriIsBlocked() || this.primaryActionAutofill(), + ); + + /** + * Option to mark this container as an autofill list. + */ + readonly isAutofillList = input(false, { transform: booleanAttribute }); + + /** + * Computed property whether the cipher action may perform autofill. + * When feature flag is enabled, uses isAutofillList. + * When feature flag is disabled, uses primaryActionAutofill. + */ + readonly canAutofill = computed(() => { + if (this.currentUriIsBlocked()) { + return false; + } + return this.isAutofillList() + ? this.simplifiedItemActionEnabled() + : this.primaryActionAutofill(); + }); + + /** + * Whether to show the "Fill" text on hover. + * Only shown when feature flag is enabled AND this is an autofill list. + */ + readonly showFillTextOnHover = computed( + () => this.simplifiedItemActionEnabled() && this.canAutofill(), + ); + + /** + * Whether to show the launch button. + */ + readonly showLaunchButton = computed(() => + this.simplifiedItemActionEnabled() ? !this.isAutofillList() : !this.showAutofillButton(), + ); + + /** + * Whether to show the "Autofill" option in the more options menu. + * New behavior: show for non-autofill list items. + * Old behavior: show when not hidden by hideAutofillMenuOptions. + */ + readonly showAutofillInMenu = computed(() => + this.simplifiedItemActionEnabled() ? !this.canAutofill() : !this.hideAutofillMenuOptions(), + ); + + /** + * Whether to show the "View" option in the more options menu. + * New behavior: show for autofill list items (since click = autofill). + * Old behavior: show when primary action is autofill. + */ + readonly showViewInMenu = computed(() => + this.simplifiedItemActionEnabled() ? this.isAutofillList() : this.primaryActionAutofill(), + ); /** * Remove the bottom margin from the bit-section in this component * (used for containers at the end of the page where bottom margin is not needed) */ - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - disableSectionMargin = input(false, { transform: booleanAttribute }); + readonly disableSectionMargin = input(false, { transform: booleanAttribute }); /** * Remove the description margin */ - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - disableDescriptionMargin = input(false, { transform: booleanAttribute }); + readonly disableDescriptionMargin = input(false, { transform: booleanAttribute }); /** * The tooltip text for the organization icon for ciphers that belong to an organization. @@ -313,9 +363,7 @@ export class VaultListItemsContainerComponent implements AfterViewInit { return collections[0]?.name; } - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - protected autofillShortcutTooltip = signal(undefined); + protected readonly autofillShortcutTooltip = signal(undefined); constructor( private i18nService: I18nService, @@ -340,10 +388,8 @@ export class VaultListItemsContainerComponent implements AfterViewInit { } } - primaryActionOnSelect(cipher: PopupCipherViewLike) { - return this.primaryActionAutofill() && !this.currentURIIsBlocked() - ? this.doAutofill(cipher) - : this.onViewCipher(cipher); + onCipherSelect(cipher: PopupCipherViewLike) { + return this.canAutofill() ? this.doAutofill(cipher) : this.onViewCipher(cipher); } /** diff --git a/apps/browser/src/vault/popup/settings/appearance.component.html b/apps/browser/src/vault/popup/settings/appearance.component.html index b58316a8d64..d87c0640f52 100644 --- a/apps/browser/src/vault/popup/settings/appearance.component.html +++ b/apps/browser/src/vault/popup/settings/appearance.component.html @@ -50,16 +50,18 @@ - + {{ "showQuickCopyActions" | i18n }} - - - - {{ "clickToAutofill" | i18n }} - - + @if (!simplifiedItemActionEnabled()) { + + + + {{ "clickToAutofill" | i18n }} + + + } diff --git a/apps/browser/src/vault/popup/settings/appearance.component.spec.ts b/apps/browser/src/vault/popup/settings/appearance.component.spec.ts index 41e89ec30e8..465b78e232d 100644 --- a/apps/browser/src/vault/popup/settings/appearance.component.spec.ts +++ b/apps/browser/src/vault/popup/settings/appearance.component.spec.ts @@ -1,10 +1,12 @@ import { Component, Input } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; import { mock } from "jest-mock-extended"; -import { BehaviorSubject } from "rxjs"; +import { BehaviorSubject, of } from "rxjs"; import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { AnimationControlService } from "@bitwarden/common/platform/abstractions/animation-control.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -59,7 +61,7 @@ describe("AppearanceComponent", () => { const enableRoutingAnimation$ = new BehaviorSubject(true); const enableCompactMode$ = new BehaviorSubject(false); const showQuickCopyActions$ = new BehaviorSubject(false); - const clickItemsToAutofillVaultView$ = new BehaviorSubject(false); + const featureFlag$ = new BehaviorSubject(false); const setSelectedTheme = jest.fn().mockResolvedValue(undefined); const setShowFavicons = jest.fn().mockResolvedValue(undefined); const setEnableBadgeCounter = jest.fn().mockResolvedValue(undefined); @@ -78,11 +80,20 @@ describe("AppearanceComponent", () => { setShowFavicons.mockClear(); setEnableBadgeCounter.mockClear(); setEnableRoutingAnimation.mockClear(); + setClickItemsToAutofillVaultView.mockClear(); + + const configService = mock(); + configService.getFeatureFlag$.mockImplementation((flag: FeatureFlag) => { + if (flag === FeatureFlag.PM31039ItemActionInExtension) { + return featureFlag$.asObservable(); + } + return of(false); + }); await TestBed.configureTestingModule({ imports: [AppearanceComponent], providers: [ - { provide: ConfigService, useValue: mock() }, + { provide: ConfigService, useValue: configService }, { provide: PlatformUtilsService, useValue: mock() }, { provide: MessagingService, useValue: mock() }, { provide: I18nService, useValue: { t: (key: string) => key } }, @@ -114,7 +125,7 @@ describe("AppearanceComponent", () => { { provide: VaultSettingsService, useValue: { - clickItemsToAutofillVaultView$, + clickItemsToAutofillVaultView$: of(false), setClickItemsToAutofillVaultView, }, }, @@ -193,11 +204,40 @@ describe("AppearanceComponent", () => { expect(mockWidthService.setWidth).toHaveBeenCalledWith("wide"); }); + }); - it("updates the click items to autofill vault view setting", () => { - component.appearanceForm.controls.clickItemsToAutofillVaultView.setValue(true); + describe("PM31039ItemActionInExtension feature flag", () => { + describe("when set to OFF", () => { + it("should show clickItemsToAutofillVaultView checkbox", () => { + featureFlag$.next(false); + fixture.detectChanges(); - expect(setClickItemsToAutofillVaultView).toHaveBeenCalledWith(true); + const checkbox = fixture.debugElement.query( + By.css('input[formControlName="clickItemsToAutofillVaultView"]'), + ); + expect(checkbox).not.toBeNull(); + }); + + it("should update the clickItemsToAutofillVaultView setting when changed", () => { + featureFlag$.next(false); + fixture.detectChanges(); + + component.appearanceForm.controls.clickItemsToAutofillVaultView.setValue(true); + + expect(setClickItemsToAutofillVaultView).toHaveBeenCalledWith(true); + }); + }); + + describe("when set to ON", () => { + it("should hide clickItemsToAutofillVaultView checkbox", () => { + featureFlag$.next(true); + fixture.detectChanges(); + + const checkbox = fixture.debugElement.query( + By.css('input[formControlName="clickItemsToAutofillVaultView"]'), + ); + expect(checkbox).toBeNull(); + }); }); }); }); diff --git a/apps/browser/src/vault/popup/settings/appearance.component.ts b/apps/browser/src/vault/popup/settings/appearance.component.ts index bff51335192..47aa1804efc 100644 --- a/apps/browser/src/vault/popup/settings/appearance.component.ts +++ b/apps/browser/src/vault/popup/settings/appearance.component.ts @@ -2,14 +2,16 @@ // @ts-strict-ignore import { CommonModule } from "@angular/common"; import { Component, DestroyRef, inject, OnInit } from "@angular/core"; -import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { takeUntilDestroyed, toSignal } from "@angular/core/rxjs-interop"; import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; import { firstValueFrom } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { AnimationControlService } from "@bitwarden/common/platform/abstractions/animation-control.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; @@ -57,6 +59,13 @@ export class AppearanceComponent implements OnInit { private copyButtonsService = inject(VaultPopupCopyButtonsService); private popupSizeService = inject(PopupSizeService); private i18nService = inject(I18nService); + private configService = inject(ConfigService); + + /** Signal for the feature flag that controls simplified item action behavior */ + protected readonly simplifiedItemActionEnabled = toSignal( + this.configService.getFeatureFlag$(FeatureFlag.PM31039ItemActionInExtension), + { initialValue: false }, + ); appearanceForm = this.formBuilder.group({ enableFavicon: false, diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 1edbcc4e376..4db9ff37d42 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -72,6 +72,7 @@ export enum FeatureFlag { BrowserPremiumSpotlight = "pm-23384-browser-premium-spotlight", MigrateMyVaultToMyItems = "pm-20558-migrate-myvault-to-myitems", PM27632_SdkCipherCrudOperations = "pm-27632-cipher-crud-operations-to-sdk", + PM31039ItemActionInExtension = "pm-31039-item-action-in-extension", /* Platform */ ContentScriptIpcChannelFramework = "content-script-ipc-channel-framework", @@ -117,6 +118,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.WindowsDesktopAutotype]: FALSE, [FeatureFlag.WindowsDesktopAutotypeGA]: FALSE, [FeatureFlag.SSHAgentV2]: FALSE, + [FeatureFlag.PM31039ItemActionInExtension]: FALSE, /* Tools */ [FeatureFlag.UseSdkPasswordGenerators]: FALSE, From d06a895c78ab49fe2fdbe36cbc9f20cf645074ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ch=C4=99ci=C5=84ski?= Date: Thu, 12 Feb 2026 09:13:45 +0100 Subject: [PATCH 012/134] [BRE-1561] Fix flatpak install build desktop (#18814) * Remove redundant flatpak installation command in build workflow * Try select one of the packages * Update .github/workflows/build-desktop.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/build-desktop.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index c500e59d536..f3b76ae462d 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -2054,7 +2054,6 @@ jobs: sudo apt-get update sudo apt-get install -y libasound2 flatpak xvfb dbus-x11 flatpak remote-add --if-not-exists --user flathub https://flathub.org/repo/flathub.flatpakrepo - flatpak install -y --user flathub - name: Install flatpak working-directory: apps/desktop/artifacts/linux/flatpak From 9d69b15798986d545695d5d01c2a8a621544ff24 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 12 Feb 2026 10:31:48 +0100 Subject: [PATCH 013/134] [PM-32063] Disable cipher-key-downgrading (#18911) * Proposal: Disable cipher-key-downgrading * Cleanup --- .../src/vault/services/cipher.service.ts | 31 ++++++++----------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 6373a511724..06c6628f158 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -1191,33 +1191,28 @@ export class CipherService implements CipherServiceAbstraction { userId: UserId, admin = false, ): Promise { - const encKey = await this.getKeyForCipherKeyDecryption(cipher, userId); - const cipherKeyEncryptionEnabled = await this.getCipherKeyEncryptionEnabled(); + // The organization's symmetric key or the user's user key + const vaultKey = await this.getKeyForCipherKeyDecryption(cipher, userId); - const cipherEncKey = - cipherKeyEncryptionEnabled && cipher.key != null - ? ((await this.encryptService.unwrapSymmetricKey(cipher.key, encKey)) as UserKey) - : encKey; + const cipherKeyOrVaultKey = + cipher.key != null + ? ((await this.encryptService.unwrapSymmetricKey(cipher.key, vaultKey)) as UserKey) + : vaultKey; - //if cipher key encryption is disabled but the item has an individual key, - //then we rollback to using the user key as the main key of encryption of the item - //in order to keep item and it's attachments with the same encryption level - if (cipher.key != null && !cipherKeyEncryptionEnabled) { - const model = await this.decrypt(cipher, userId); - await this.updateWithServer(model, userId); - } + const encFileName = await this.encryptService.encryptString(filename, cipherKeyOrVaultKey); - const encFileName = await this.encryptService.encryptString(filename, cipherEncKey); - - const dataEncKey = await this.keyService.makeDataEncKey(cipherEncKey); - const encData = await this.encryptService.encryptFileData(new Uint8Array(data), dataEncKey[0]); + const attachmentKey = await this.keyService.makeDataEncKey(cipherKeyOrVaultKey); + const encData = await this.encryptService.encryptFileData( + new Uint8Array(data), + attachmentKey[0], + ); const response = await this.cipherFileUploadService.upload( cipher, encFileName, encData, admin, - dataEncKey, + attachmentKey, ); const cData = new CipherData(response, cipher.collectionIds); From ad8bde057f797195bbb9fe4b9b3b5cd870a9b54b Mon Sep 17 00:00:00 2001 From: Will Martin Date: Thu, 12 Feb 2026 09:50:31 -0500 Subject: [PATCH 014/134] Fix EventListener type errors in inline menu list handlers (#18943) Changed event parameter type from MouseEvent to Event in handleFillCipherClickEvent and handleViewCipherClickEvent to match the EventListener interface expected by useEventHandlersMemo. Co-authored-by: Claude Sonnet 4.5 --- .../inline-menu/pages/list/autofill-inline-menu-list.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts index 39b7fa5c17b..744e3579da1 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts @@ -1012,7 +1012,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { private handleFillCipherClickEvent = (cipher: InlineMenuCipherData) => { const usePasskey = !!cipher.login?.passkey; return this.useEventHandlersMemo( - (event: MouseEvent) => { + (event: Event) => { /** * Reject synthetic events (not originating from the user agent) */ @@ -1140,7 +1140,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { * @param cipher - The cipher to view. */ private handleViewCipherClickEvent = (cipher: InlineMenuCipherData) => { - return this.useEventHandlersMemo((event: MouseEvent) => { + return this.useEventHandlersMemo((event: Event) => { /** * Reject synthetic events (not originating from the user agent) */ From 7fcb1a7a7660ed2e1440b1a501c35a7dfc319389 Mon Sep 17 00:00:00 2001 From: blackwood Date: Thu, 12 Feb 2026 10:39:41 -0500 Subject: [PATCH 015/134] Expand generic pattern for notification queue messages. (#18543) --- .../abstractions/notification.background.ts | 99 ++++++++----------- .../notification.background.spec.ts | 96 ++++++++++++++---- .../background/notification.background.ts | 38 ++++--- 3 files changed, 146 insertions(+), 87 deletions(-) diff --git a/apps/browser/src/autofill/background/abstractions/notification.background.ts b/apps/browser/src/autofill/background/abstractions/notification.background.ts index e50a317e8a7..bc416d98634 100644 --- a/apps/browser/src/autofill/background/abstractions/notification.background.ts +++ b/apps/browser/src/autofill/background/abstractions/notification.background.ts @@ -4,70 +4,70 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { CollectionView } from "../../content/components/common-types"; -import { NotificationType, NotificationTypes } from "../../enums/notification-type.enum"; +import { NotificationType } from "../../enums/notification-type.enum"; import AutofillPageDetails from "../../models/autofill-page-details"; /** - * @todo Remove Standard_ label when implemented as standard NotificationQueueMessage. + * Generic notification queue message structure. + * All notification types use this structure with type-specific data. */ -export interface Standard_NotificationQueueMessage { - // universal notification properties +export interface NotificationQueueMessage { domain: string; tab: chrome.tabs.Tab; launchTimestamp: number; expires: Date; wasVaultLocked: boolean; - - type: T; // NotificationType - data: D; // notification-specific data + type: T; + data: D; } -/** - * @todo Deprecate in favor of Standard_NotificationQueueMessage. - */ -interface NotificationQueueMessage { - type: NotificationTypes; - domain: string; - tab: chrome.tabs.Tab; - launchTimestamp: number; - expires: Date; - wasVaultLocked: boolean; -} +// Notification data type definitions +export type AddLoginNotificationData = { + username: string; + password: string; + uri: string; +}; -type ChangePasswordNotificationData = { +export type ChangePasswordNotificationData = { cipherIds: CipherView["id"][]; newPassword: string; }; -type AddChangePasswordNotificationQueueMessage = Standard_NotificationQueueMessage< +export type UnlockVaultNotificationData = never; + +export type AtRiskPasswordNotificationData = { + organizationName: string; + passwordChangeUri?: string; +}; + +// Notification queue message types using generic pattern +export type AddLoginQueueMessage = NotificationQueueMessage< + typeof NotificationType.AddLogin, + AddLoginNotificationData +>; + +export type AddChangePasswordNotificationQueueMessage = NotificationQueueMessage< typeof NotificationType.ChangePassword, ChangePasswordNotificationData >; -interface AddLoginQueueMessage extends NotificationQueueMessage { - type: "add"; - username: string; - password: string; - uri: string; -} +export type AddUnlockVaultQueueMessage = NotificationQueueMessage< + typeof NotificationType.UnlockVault, + UnlockVaultNotificationData +>; -interface AddUnlockVaultQueueMessage extends NotificationQueueMessage { - type: "unlock"; -} +export type AtRiskPasswordQueueMessage = NotificationQueueMessage< + typeof NotificationType.AtRiskPassword, + AtRiskPasswordNotificationData +>; -interface AtRiskPasswordQueueMessage extends NotificationQueueMessage { - type: "at-risk-password"; - organizationName: string; - passwordChangeUri?: string; -} - -type NotificationQueueMessageItem = +export type NotificationQueueMessageItem = | AddLoginQueueMessage | AddChangePasswordNotificationQueueMessage | AddUnlockVaultQueueMessage | AtRiskPasswordQueueMessage; -type LockedVaultPendingNotificationsData = { +export type LockedVaultPendingNotificationsData = { commandToRetry: { message: { command: string; @@ -80,26 +80,26 @@ type LockedVaultPendingNotificationsData = { target: string; }; -type AdjustNotificationBarMessageData = { +export type AdjustNotificationBarMessageData = { height: number; }; -type AddLoginMessageData = { +export type AddLoginMessageData = { username: string; password: string; url: string; }; -type UnlockVaultMessageData = { +export type UnlockVaultMessageData = { skipNotification?: boolean; }; /** - * @todo Extend generics to this type, see Standard_NotificationQueueMessage + * @todo Extend generics to this type, see NotificationQueueMessage * - use new `data` types as generic * - eliminate optional status of properties as needed per Notification Type */ -type NotificationBackgroundExtensionMessage = { +export type NotificationBackgroundExtensionMessage = { [key: string]: any; command: string; data?: Partial & @@ -119,7 +119,7 @@ type BackgroundMessageParam = { message: NotificationBackgroundExtensionMessage type BackgroundSenderParam = { sender: chrome.runtime.MessageSender }; type BackgroundOnMessageHandlerParams = BackgroundMessageParam & BackgroundSenderParam; -type NotificationBackgroundExtensionMessageHandlers = { +export type NotificationBackgroundExtensionMessageHandlers = { [key: string]: CallableFunction; unlockCompleted: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgGetFolderData: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; @@ -150,16 +150,3 @@ type NotificationBackgroundExtensionMessageHandlers = { bgGetActiveUserServerConfig: () => Promise; getWebVaultUrlForNotification: () => Promise; }; - -export { - AddChangePasswordNotificationQueueMessage, - AddLoginQueueMessage, - AddUnlockVaultQueueMessage, - NotificationQueueMessageItem, - LockedVaultPendingNotificationsData, - AdjustNotificationBarMessageData, - UnlockVaultMessageData, - AddLoginMessageData, - NotificationBackgroundExtensionMessage, - NotificationBackgroundExtensionMessageHandlers, -}; diff --git a/apps/browser/src/autofill/background/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts index 7d33d79a697..95d4111987b 100644 --- a/apps/browser/src/autofill/background/notification.background.spec.ts +++ b/apps/browser/src/autofill/background/notification.background.spec.ts @@ -126,9 +126,11 @@ describe("NotificationBackground", () => { it("returns a cipher view when passed an `AddLoginQueueMessage`", () => { const message: AddLoginQueueMessage = { type: "add", - username: "test", - password: "password", - uri: "https://example.com", + data: { + username: "test", + password: "password", + uri: "https://example.com", + }, domain: "", tab: createChromeTabMock(), expires: new Date(), @@ -140,13 +142,13 @@ describe("NotificationBackground", () => { expect(cipherView.name).toEqual("example.com"); expect(cipherView.login).toEqual({ fido2Credentials: [], - password: message.password, + password: message.data.password, uris: [ { - _uri: message.uri, + _uri: message.data.uri, }, ], - username: message.username, + username: message.data.username, }); }); @@ -154,9 +156,11 @@ describe("NotificationBackground", () => { const folderId = "folder-id"; const message: AddLoginQueueMessage = { type: "add", - username: "test", - password: "password", - uri: "https://example.com", + data: { + username: "test", + password: "password", + uri: "https://example.com", + }, domain: "example.com", tab: createChromeTabMock(), expires: new Date(), @@ -170,6 +174,44 @@ describe("NotificationBackground", () => { expect(cipherView.folderId).toEqual(folderId); }); + + it("removes 'www.' prefix from hostname when generating cipher name", () => { + const message: AddLoginQueueMessage = { + type: "add", + data: { + username: "test", + password: "password", + uri: "https://www.example.com", + }, + domain: "www.example.com", + tab: createChromeTabMock(), + expires: new Date(), + wasVaultLocked: false, + launchTimestamp: 0, + }; + const cipherView = notificationBackground["convertAddLoginQueueMessageToCipherView"](message); + + expect(cipherView.name).toEqual("example.com"); + }); + + it("uses domain as fallback when hostname cannot be extracted from uri", () => { + const message: AddLoginQueueMessage = { + type: "add", + data: { + username: "test", + password: "password", + uri: "", + }, + domain: "fallback-domain.com", + tab: createChromeTabMock(), + expires: new Date(), + wasVaultLocked: false, + launchTimestamp: 0, + }; + const cipherView = notificationBackground["convertAddLoginQueueMessageToCipherView"](message); + + expect(cipherView.name).toEqual("fallback-domain.com"); + }); }); describe("notification bar extension message handlers and triggers", () => { @@ -2544,8 +2586,11 @@ describe("NotificationBackground", () => { type: NotificationType.AddLogin, tab, domain: "example.com", - username: "test", - password: "updated-password", + data: { + username: "test", + password: "updated-password", + uri: "https://example.com", + }, wasVaultLocked: true, }); notificationBackground["notificationQueue"] = [queueMessage]; @@ -2559,7 +2604,7 @@ describe("NotificationBackground", () => { expect(updatePasswordSpy).toHaveBeenCalledWith( cipherView, - queueMessage.password, + queueMessage.data.password, message.edit, sender.tab, "testId", @@ -2631,9 +2676,14 @@ describe("NotificationBackground", () => { type: NotificationType.AddLogin, tab, domain: "example.com", - username: "test", - password: "password", + data: { + username: "test", + password: "password", + uri: "https://example.com", + }, wasVaultLocked: false, + launchTimestamp: Date.now(), + expires: new Date(Date.now() + 10000), }); notificationBackground["notificationQueue"] = [queueMessage]; const cipherView = mock({ @@ -2670,9 +2720,14 @@ describe("NotificationBackground", () => { type: NotificationType.AddLogin, tab, domain: "example.com", - username: "test", - password: "password", + data: { + username: "test", + password: "password", + uri: "https://example.com", + }, wasVaultLocked: false, + launchTimestamp: Date.now(), + expires: new Date(Date.now() + 10000), }); notificationBackground["notificationQueue"] = [queueMessage]; const cipherView = mock({ @@ -2716,9 +2771,14 @@ describe("NotificationBackground", () => { type: NotificationType.AddLogin, tab, domain: "example.com", - username: "test", - password: "password", + data: { + username: "test", + password: "password", + uri: "https://example.com", + }, wasVaultLocked: false, + launchTimestamp: Date.now(), + expires: new Date(Date.now() + 10000), }); notificationBackground["notificationQueue"] = [queueMessage]; const cipherView = mock({ diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index e97672c1f0d..3713cd7c4c2 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -68,6 +68,7 @@ import { AddChangePasswordNotificationQueueMessage, AddLoginQueueMessage, AddLoginMessageData, + AtRiskPasswordQueueMessage, NotificationQueueMessageItem, LockedVaultPendingNotificationsData, NotificationBackgroundExtensionMessage, @@ -528,12 +529,14 @@ export default class NotificationBackground { this.removeTabFromNotificationQueue(tab); const launchTimestamp = new Date().getTime(); - const queueMessage: NotificationQueueMessageItem = { + const queueMessage: AtRiskPasswordQueueMessage = { domain, wasVaultLocked, type: NotificationType.AtRiskPassword, - passwordChangeUri, - organizationName: organization.name, + data: { + passwordChangeUri, + organizationName: organization.name, + }, tab: tab, launchTimestamp, expires: new Date(launchTimestamp + NOTIFICATION_BAR_LIFESPAN_MS), @@ -612,10 +615,12 @@ export default class NotificationBackground { const launchTimestamp = new Date().getTime(); const message: AddLoginQueueMessage = { type: NotificationType.AddLogin, - username: loginInfo.username, - password: loginInfo.password, + data: { + username: loginInfo.username, + password: loginInfo.password, + uri: loginInfo.url, + }, domain: loginDomain, - uri: loginInfo.url, tab: tab, launchTimestamp, expires: new Date(launchTimestamp + NOTIFICATION_BAR_LIFESPAN_MS), @@ -1291,16 +1296,23 @@ export default class NotificationBackground { // If the vault was locked, check if a cipher needs updating instead of creating a new one if (queueMessage.wasVaultLocked) { const allCiphers = await this.cipherService.getAllDecryptedForUrl( - queueMessage.uri, + queueMessage.data.uri, activeUserId, ); const existingCipher = allCiphers.find( (c) => - c.login.username != null && c.login.username.toLowerCase() === queueMessage.username, + c.login.username != null && + c.login.username.toLowerCase() === queueMessage.data.username, ); if (existingCipher != null) { - await this.updatePassword(existingCipher, queueMessage.password, edit, tab, activeUserId); + await this.updatePassword( + existingCipher, + queueMessage.data.password, + edit, + tab, + activeUserId, + ); return; } } @@ -1721,15 +1733,15 @@ export default class NotificationBackground { folderId?: string, ): CipherView { const uriView = new LoginUriView(); - uriView.uri = message.uri; + uriView.uri = message.data.uri; const loginView = new LoginView(); loginView.uris = [uriView]; - loginView.username = message.username; - loginView.password = message.password; + loginView.username = message.data.username; + loginView.password = message.data.password; const cipherView = new CipherView(); - cipherView.name = (Utils.getHostname(message.uri) || message.domain).replace(/^www\./, ""); + cipherView.name = (Utils.getHostname(message.data.uri) || message.domain).replace(/^www\./, ""); cipherView.folderId = folderId; cipherView.type = CipherType.Login; cipherView.login = loginView; From 5c7ee4e63a3eba07f05b378e3e38c71de024fcfe Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 12 Feb 2026 16:43:54 +0100 Subject: [PATCH 016/134] Add more package types (#18939) --- .../platform/services/electron-platform-utils.service.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/platform/services/electron-platform-utils.service.ts b/apps/desktop/src/platform/services/electron-platform-utils.service.ts index 9377ac567ec..70c28c66353 100644 --- a/apps/desktop/src/platform/services/electron-platform-utils.service.ts +++ b/apps/desktop/src/platform/services/electron-platform-utils.service.ts @@ -163,8 +163,14 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { return "Snap"; } else if (ipc.platform.isFlatpak) { return "Flatpak"; + } else if (this.getDevice() === DeviceType.WindowsDesktop) { + return "WindowsUnknown"; + } else if (this.getDevice() === DeviceType.MacOsDesktop) { + return "MacOSUnknown"; + } else if (this.getDevice() === DeviceType.LinuxDesktop) { + return "LinuxUnknown"; } else { - return "Unknown"; + return "DesktopUnknown"; } } } From 4d93348a2ed25d04337b6b989f0b68d16550d55d Mon Sep 17 00:00:00 2001 From: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Date: Thu, 12 Feb 2026 10:51:31 -0600 Subject: [PATCH 017/134] [PM-30812] Update userKey rotation to use saltForUser (#18697) --- .../user-key-rotation.service.spec.ts | 33 ++++++++++--------- .../key-rotation/user-key-rotation.service.ts | 19 +++++++---- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts index c0b734f17cc..a2330025c92 100644 --- a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts +++ b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts @@ -1,7 +1,8 @@ import { mock, MockProxy } from "jest-mock-extended"; -import { BehaviorSubject } from "rxjs"; +import { BehaviorSubject, of } from "rxjs"; import { OrganizationUserResetPasswordWithIdRequest } from "@bitwarden/admin-console/common"; +import { LogoutService } from "@bitwarden/auth/common"; import { Account } from "@bitwarden/common/auth/abstractions/account.service"; import { WebauthnRotateCredentialRequest } from "@bitwarden/common/auth/models/request/webauthn-rotate-credential.request"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; @@ -12,6 +13,8 @@ import { EncString, } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction"; +import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; +import { MasterPasswordSalt } from "@bitwarden/common/key-management/master-password/types/master-password.types"; import { SecurityStateService } from "@bitwarden/common/key-management/security-state/abstractions/security-state.service"; import { SignedPublicKey, @@ -21,7 +24,6 @@ import { WrappedPrivateKey, WrappedSigningKey, } from "@bitwarden/common/key-management/types"; -import { VaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -276,7 +278,7 @@ describe("KeyRotationService", () => { let mockSyncService: MockProxy; let mockWebauthnLoginAdminService: MockProxy; let mockLogService: MockProxy; - let mockVaultTimeoutService: MockProxy; + let mockLogoutService: MockProxy; let mockDialogService: MockProxy; let mockToastService: MockProxy; let mockI18nService: MockProxy; @@ -284,6 +286,7 @@ describe("KeyRotationService", () => { let mockKdfConfigService: MockProxy; let mockSdkClientFactory: MockProxy; let mockSecurityStateService: MockProxy; + let mockMasterPasswordService: MockProxy; const mockUser = { id: "mockUserId" as UserId, @@ -293,6 +296,8 @@ describe("KeyRotationService", () => { }), }; + const mockUserSalt = "usersalt"; + const mockTrustedPublicKeys = [Utils.fromUtf8ToArray("test-public-key")]; const mockMakeKeysForUserCryptoV2 = jest.fn(); @@ -337,7 +342,7 @@ describe("KeyRotationService", () => { mockSyncService = mock(); mockWebauthnLoginAdminService = mock(); mockLogService = mock(); - mockVaultTimeoutService = mock(); + mockLogoutService = mock(); mockToastService = mock(); mockI18nService = mock(); mockDialogService = mock(); @@ -354,6 +359,7 @@ describe("KeyRotationService", () => { }, } as BitwardenClient); mockSecurityStateService = mock(); + mockMasterPasswordService = mock(); keyRotationService = new TestUserKeyRotationService( mockApiService, @@ -368,7 +374,7 @@ describe("KeyRotationService", () => { mockSyncService, mockWebauthnLoginAdminService, mockLogService, - mockVaultTimeoutService, + mockLogoutService, mockToastService, mockI18nService, mockDialogService, @@ -377,6 +383,7 @@ describe("KeyRotationService", () => { mockKdfConfigService, mockSdkClientFactory, mockSecurityStateService, + mockMasterPasswordService, ); }); @@ -391,10 +398,10 @@ describe("KeyRotationService", () => { value: Promise.resolve(), configurable: true, }); + mockMasterPasswordService.saltForUser$.mockReturnValue(of(mockUserSalt as MasterPasswordSalt)); }); describe("rotateUserKeyMasterPasswordAndEncryptedData", () => { - let privateKey: BehaviorSubject; let keyPair: BehaviorSubject<{ privateKey: UserPrivateKey; publicKey: UserPublicKey }>; beforeEach(() => { @@ -420,10 +427,6 @@ describe("KeyRotationService", () => { mockKeyService.getFingerprint.mockResolvedValue(["a", "b"]); - // Mock private key - privateKey = new BehaviorSubject("mockPrivateKey" as any); - mockKeyService.userPrivateKeyWithLegacySupport$.mockReturnValue(privateKey); - keyPair = new BehaviorSubject({ privateKey: "mockPrivateKey", publicKey: "mockPublicKey", @@ -543,7 +546,7 @@ describe("KeyRotationService", () => { expect(spy).toHaveBeenCalledWith( mockUser.id, expect.any(PBKDF2KdfConfig), - mockUser.email, + mockUserSalt, expect.objectContaining({ version: 1 }), true, ); @@ -683,7 +686,7 @@ describe("KeyRotationService", () => { }, signingKey: TEST_VECTOR_SIGNING_KEY_V2 as WrappedSigningKey, securityState: TEST_VECTOR_SECURITY_STATE_V2 as SignedSecurityState, - }, + } as V2CryptographicStateParameters, ); expect(mockGetV2RotatedAccountKeys).toHaveBeenCalled(); expect(result).toEqual({ @@ -810,7 +813,7 @@ describe("KeyRotationService", () => { masterPasswordHash: "omitted", otp: undefined, authRequestAccessCode: undefined, - }, + } as OrganizationUserResetPasswordWithIdRequest, ]); mockKeyService.makeMasterKey.mockResolvedValue( new SymmetricCryptoKey(new Uint8Array(32)) as MasterKey, @@ -1122,7 +1125,7 @@ describe("KeyRotationService", () => { const cryptographicState = await keyRotationService.getCryptographicStateForUser(mockUser); expect(cryptographicState).toEqual({ masterKeyKdfConfig: new PBKDF2KdfConfig(100000), - masterKeySalt: "mockemail", // the email is lowercased to become the salt + masterKeySalt: mockUserSalt, cryptographicStateParameters: { version: 1, userKey: TEST_VECTOR_USER_KEY_V1, @@ -1138,7 +1141,7 @@ describe("KeyRotationService", () => { const cryptographicState = await keyRotationService.getCryptographicStateForUser(mockUser); expect(cryptographicState).toEqual({ masterKeyKdfConfig: new PBKDF2KdfConfig(100000), - masterKeySalt: "mockemail", // the email is lowercased to become the salt + masterKeySalt: mockUserSalt, cryptographicStateParameters: { version: 2, userKey: TEST_VECTOR_USER_KEY_V2, diff --git a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts index b9bd23b12de..68253a4a35d 100644 --- a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts +++ b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts @@ -8,6 +8,7 @@ import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/a import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction"; +import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { SecurityStateService } from "@bitwarden/common/key-management/security-state/abstractions/security-state.service"; import { SignedPublicKey, @@ -99,6 +100,7 @@ export class UserKeyRotationService { private kdfConfigService: KdfConfigService, private sdkClientFactory: SdkClientFactory, private securityStateService: SecurityStateService, + private masterPasswordService: MasterPasswordServiceAbstraction, ) {} /** @@ -146,7 +148,7 @@ export class UserKeyRotationService { const { userKey: newUserKey, accountKeysRequest } = await this.getRotatedAccountKeysFlagged( user.id, masterKeyKdfConfig, - user.email, + masterKeySalt, currentCryptographicStateParameters, upgradeToV2FeatureFlagEnabled, ); @@ -300,7 +302,7 @@ export class UserKeyRotationService { protected async upgradeV1UserToV2UserAccountKeys( userId: UserId, kdfConfig: KdfConfig, - email: string, + masterKeySalt: string, cryptographicStateParameters: V1CryptographicStateParameters, ): Promise { // Initialize an SDK with the current cryptographic state @@ -308,7 +310,7 @@ export class UserKeyRotationService { await sdk.crypto().initialize_user_crypto({ userId: asUuid(userId), kdfParams: kdfConfig.toSdkConfig(), - email: email, + email: masterKeySalt, accountCryptographicState: { V1: { private_key: cryptographicStateParameters.publicKeyEncryptionKeyPair.wrappedPrivateKey, @@ -328,7 +330,7 @@ export class UserKeyRotationService { protected async rotateV2UserAccountKeys( userId: UserId, kdfConfig: KdfConfig, - email: string, + masterKeySalt: string, cryptographicStateParameters: V2CryptographicStateParameters, ): Promise { // Initialize an SDK with the current cryptographic state @@ -336,7 +338,7 @@ export class UserKeyRotationService { await sdk.crypto().initialize_user_crypto({ userId: asUuid(userId), kdfParams: kdfConfig.toSdkConfig(), - email: email, + email: masterKeySalt, accountCryptographicState: { V2: { private_key: cryptographicStateParameters.publicKeyEncryptionKeyPair.wrappedPrivateKey, @@ -598,8 +600,11 @@ export class UserKeyRotationService { this.kdfConfigService.getKdfConfig$(user.id), "KDF config", ))!; - // The master key salt used for deriving the masterkey always needs to be trimmed and lowercased. - const masterKeySalt = user.email.trim().toLowerCase(); + + const masterKeySalt = await this.firstValueFromOrThrow( + this.masterPasswordService.saltForUser$(user.id), + "Master key salt", + ); // V1 and V2 users both have a user key and a private key const currentUserKey: UserKey = (await this.firstValueFromOrThrow( From 7342bf672fe4551b83182ca9af188db58e01ea14 Mon Sep 17 00:00:00 2001 From: Alex <55413326+AlexRubik@users.noreply.github.com> Date: Thu, 12 Feb 2026 12:01:30 -0500 Subject: [PATCH 018/134] [PM-31161] reports scroll bug (#18769) * Fix virtual scroll gap in exposed-passwords-report by setting rowSize to 54px * Fix virtual scroll gap in weak-passwords-report by setting rowSize to 54px --- .../dirt/reports/pages/exposed-passwords-report.component.html | 2 +- .../app/dirt/reports/pages/weak-passwords-report.component.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.html b/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.html index ba118ea6663..144396d6772 100644 --- a/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.html +++ b/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.html @@ -43,7 +43,7 @@ > } } - + {{ "name" | i18n }} diff --git a/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.html b/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.html index 5f047316a29..5a187427b5e 100644 --- a/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.html +++ b/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.html @@ -45,7 +45,7 @@ > } } - + {{ "name" | i18n }} From fe15b44ccc026092d1f111b54869fbd8a9674cb3 Mon Sep 17 00:00:00 2001 From: Will Martin Date: Thu, 12 Feb 2026 12:26:25 -0500 Subject: [PATCH 019/134] [CL-1046] Update dialog components to support attribute selector usage for form integration (#18929) - Add [bit-dialog] and [bit-simple-dialog] attribute selectors - Update documentation with recommended form usage pattern - Add Storybook examples demonstrating
pattern - Migrate simple-configurable-dialog template to new pattern Co-authored-by: Claude Sonnet 4.5 --- .../src/dialog/dialog/dialog.component.ts | 2 +- libs/components/src/dialog/dialog/dialog.mdx | 9 ++++ .../src/dialog/dialog/dialog.stories.ts | 8 ++-- libs/components/src/dialog/dialogs.mdx | 9 ++++ .../simple-configurable-dialog.component.html | 42 +++++++++---------- .../simple-dialog/simple-dialog.component.ts | 2 +- .../dialog/simple-dialog/simple-dialog.mdx | 9 ++++ .../simple-dialog/simple-dialog.stories.ts | 18 ++++++++ 8 files changed, 70 insertions(+), 29 deletions(-) diff --git a/libs/components/src/dialog/dialog/dialog.component.ts b/libs/components/src/dialog/dialog/dialog.component.ts index 63fbb69399d..c32ce176d27 100644 --- a/libs/components/src/dialog/dialog/dialog.component.ts +++ b/libs/components/src/dialog/dialog/dialog.component.ts @@ -45,7 +45,7 @@ const drawerSizeToWidth = { // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ - selector: "bit-dialog", + selector: "bit-dialog, [bit-dialog]", templateUrl: "./dialog.component.html", host: { "[class]": "classes()", diff --git a/libs/components/src/dialog/dialog/dialog.mdx b/libs/components/src/dialog/dialog/dialog.mdx index 056e4ac79bc..33dce6a53e0 100644 --- a/libs/components/src/dialog/dialog/dialog.mdx +++ b/libs/components/src/dialog/dialog/dialog.mdx @@ -82,3 +82,12 @@ The `background` input can be set to `alt` to change the background color. This dialogs that contain multiple card sections. + +## Using Forms with Dialogs + +When using forms with dialogs, apply the `bit-dialog` attribute directly to the `` element +instead of wrapping the dialog in a form. This ensures proper styling. + +```html +... +``` diff --git a/libs/components/src/dialog/dialog/dialog.stories.ts b/libs/components/src/dialog/dialog/dialog.stories.ts index 012bb77f2ac..9b96e529789 100644 --- a/libs/components/src/dialog/dialog/dialog.stories.ts +++ b/libs/components/src/dialog/dialog/dialog.stories.ts @@ -225,8 +225,7 @@ export const WithCards: Story = { ...args, }, template: /*html*/ ` -
- + @@ -270,7 +269,7 @@ export const WithCards: Story = { - + - -
+ `, }), args: { diff --git a/libs/components/src/dialog/dialogs.mdx b/libs/components/src/dialog/dialogs.mdx index 4a49804484b..2b8afb06783 100644 --- a/libs/components/src/dialog/dialogs.mdx +++ b/libs/components/src/dialog/dialogs.mdx @@ -92,3 +92,12 @@ Once closed, focus should remain on the element which triggered the Dialog. **Note:** If a Simple Dialog is triggered from a main Dialog, be sure to make sure focus is moved to the Simple Dialog. + +## Using Forms with Dialogs + +When using forms with dialogs, apply the `bit-dialog` attribute directly to the `
` element +instead of wrapping the dialog in a form. This ensures proper styling. + +```html +...
+``` diff --git a/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component.html b/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component.html index 470f4846785..2e285495934 100644 --- a/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component.html +++ b/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component.html @@ -1,27 +1,25 @@ -
- - + + - {{ title }} + {{ title }} -
{{ content }}
+
{{ content }}
- - + + @if (showCancelButton) { + - - @if (showCancelButton) { - - } - -
+ } +
diff --git a/libs/components/src/dialog/simple-dialog/simple-dialog.component.ts b/libs/components/src/dialog/simple-dialog/simple-dialog.component.ts index cd44a79c271..804c654186c 100644 --- a/libs/components/src/dialog/simple-dialog/simple-dialog.component.ts +++ b/libs/components/src/dialog/simple-dialog/simple-dialog.component.ts @@ -12,7 +12,7 @@ export class IconDirective {} // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ - selector: "bit-simple-dialog", + selector: "bit-simple-dialog, [bit-simple-dialog]", templateUrl: "./simple-dialog.component.html", animations: [fadeIn], imports: [DialogTitleContainerDirective, TypographyDirective], diff --git a/libs/components/src/dialog/simple-dialog/simple-dialog.mdx b/libs/components/src/dialog/simple-dialog/simple-dialog.mdx index 1d7a3668719..0720715478b 100644 --- a/libs/components/src/dialog/simple-dialog/simple-dialog.mdx +++ b/libs/components/src/dialog/simple-dialog/simple-dialog.mdx @@ -49,3 +49,12 @@ Simple dialogs can support scrolling content if necessary, but typically with la content a [Dialog component](?path=/docs/component-library-dialogs-dialog--docs). + +## Using Forms with Dialogs + +When using forms with dialogs, apply the `bit-simple-dialog` attribute directly to the `
` +element instead of wrapping the dialog in a form. This ensures proper styling. + +```html +...
+``` diff --git a/libs/components/src/dialog/simple-dialog/simple-dialog.stories.ts b/libs/components/src/dialog/simple-dialog/simple-dialog.stories.ts index 3a178892908..c67d52280b0 100644 --- a/libs/components/src/dialog/simple-dialog/simple-dialog.stories.ts +++ b/libs/components/src/dialog/simple-dialog/simple-dialog.stories.ts @@ -126,3 +126,21 @@ export const TextOverflow: Story = { `, }), }; + +export const WithForm: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` +
+ Confirm Action + + Are you sure you want to proceed with this action? This cannot be undone. + + + + + +
+ `, + }), +}; From bfc1833139687bb2b539e377be6fa87aad070049 Mon Sep 17 00:00:00 2001 From: Alex <55413326+AlexRubik@users.noreply.github.com> Date: Thu, 12 Feb 2026 12:29:18 -0500 Subject: [PATCH 020/134] [PM-32088] Switch phishing data source to GitHub (#18890) * Switch phishing data source to GitHub and remove fallback mechanism The phish.co.za mirror is down, causing every update cycle to timeout on the primary fetch before falling back to the GitHub raw URL. This removes phish.co.za entirely and uses GitHub as the sole data source, which was the original source before the mirror was introduced. - Rename `remoteUrl`/`fallbackUrl` to `ghSourceUrl` on PhishingResource type - Remove phish.co.za URLs from both Domains and Links resources - Remove catchError fallback block in `_updateFullDataSet()` - Errors now propagate to `_backgroundUpdate()` which already handles retries (3 attempts with 5-minute delays) and graceful degradation * revert the fallback logic removal, change prop name, add use fallback flag * Update Links primaryUrl to Bitwarden-hosted blocklist * remove all fallback logic --- .../phishing-detection/phishing-resources.ts | 11 ++----- .../services/phishing-data.service.ts | 33 ++----------------- 2 files changed, 6 insertions(+), 38 deletions(-) diff --git a/apps/browser/src/dirt/phishing-detection/phishing-resources.ts b/apps/browser/src/dirt/phishing-detection/phishing-resources.ts index 88068987dd7..1c6421912ab 100644 --- a/apps/browser/src/dirt/phishing-detection/phishing-resources.ts +++ b/apps/browser/src/dirt/phishing-detection/phishing-resources.ts @@ -1,8 +1,6 @@ export type PhishingResource = { name?: string; - remoteUrl: string; - /** Fallback URL to use if remoteUrl fails (e.g., due to SSL interception/cert issues) */ - fallbackUrl: string; + primaryUrl: string; checksumUrl: string; todayUrl: string; /** Matcher used to decide whether a given URL matches an entry from this resource */ @@ -20,8 +18,7 @@ export const PHISHING_RESOURCES: Record new Error("Invalid resource URL")); } - this.logService.info(`[PhishingDataService] Starting FULL update using ${resource.remoteUrl}`); - return from(this.apiService.nativeFetch(new Request(resource.remoteUrl))).pipe( + this.logService.info(`[PhishingDataService] Starting FULL update using ${resource.primaryUrl}`); + return from(this.apiService.nativeFetch(new Request(resource.primaryUrl))).pipe( switchMap((response) => { if (!response.ok || !response.body) { return throwError( @@ -322,33 +322,6 @@ export class PhishingDataService { return from(this.indexedDbService.saveUrlsFromStream(response.body)); }), - catchError((err: unknown) => { - this.logService.error( - `[PhishingDataService] Full dataset update failed using primary source ${err}`, - ); - this.logService.warning( - `[PhishingDataService] Falling back to: ${resource.fallbackUrl} (Note: Fallback data may be less up-to-date)`, - ); - // Try fallback URL - return from(this.apiService.nativeFetch(new Request(resource.fallbackUrl))).pipe( - switchMap((fallbackResponse) => { - if (!fallbackResponse.ok || !fallbackResponse.body) { - return throwError( - () => - new Error( - `[PhishingDataService] Fallback fetch failed: ${fallbackResponse.status}, ${fallbackResponse.statusText}`, - ), - ); - } - - return from(this.indexedDbService.saveUrlsFromStream(fallbackResponse.body)); - }), - catchError((fallbackError: unknown) => { - this.logService.error(`[PhishingDataService] Fallback source failed`); - return throwError(() => fallbackError); - }), - ); - }), ); } From 2e832441585f0238d32cca2cd4be182bc3ab6908 Mon Sep 17 00:00:00 2001 From: Jared Date: Thu, 12 Feb 2026 13:02:11 -0500 Subject: [PATCH 021/134] Update showDescription property in BasePolicyEditDefinition to false (#18915) --- .../organizations/policies/base-policy-edit.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/admin-console/organizations/policies/base-policy-edit.component.ts b/apps/web/src/app/admin-console/organizations/policies/base-policy-edit.component.ts index 08897299d81..e8e48f41716 100644 --- a/apps/web/src/app/admin-console/organizations/policies/base-policy-edit.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/base-policy-edit.component.ts @@ -56,7 +56,7 @@ export abstract class BasePolicyEditDefinition { * If true, the {@link description} will be reused in the policy edit modal. Set this to false if you * have more complex requirements that you will implement in your template instead. **/ - showDescription: boolean = true; + showDescription: boolean = false; /** * A method that determines whether to display this policy in the Admin Console Policies page. From 2ea2a20fd8b99ab26b05f2f7381a74a1f91c3c72 Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Thu, 12 Feb 2026 13:43:16 -0500 Subject: [PATCH 022/134] [PM-31700] Desktop Vault V3 Unarchive and Save Button (#18885) * update vault-v3 to use new btn text for archive and use signals --- .../vault/app/vault-v3/vault.component.html | 5 +- .../src/vault/app/vault-v3/vault.component.ts | 104 +++++++++++------- 2 files changed, 68 insertions(+), 41 deletions(-) diff --git a/apps/desktop/src/vault/app/vault-v3/vault.component.html b/apps/desktop/src/vault/app/vault-v3/vault.component.html index 51f6426a1ba..5d8c3491710 100644 --- a/apps/desktop/src/vault/app/vault-v3/vault.component.html +++ b/apps/desktop/src/vault/app/vault-v3/vault.component.html @@ -15,7 +15,7 @@
@if (action === "view") { - + } @if (action === "add" || action === "edit" || action === "clone") { (null); collections: CollectionView[] | null = null; config: CipherFormConfig | null = null; private userId$ = this.accountService.activeAccount$.pipe(getUserId); @@ -183,6 +191,16 @@ export class VaultComponent implements OnInit, OnDestroy, CopyClickListener { /** Tracks the disabled status of the edit cipher form */ protected formDisabled: boolean = false; + + readonly userHasPremium = toSignal( + this.accountService.activeAccount$.pipe( + filter((account): account is Account => !!account), + switchMap((account) => + this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), + ), + ), + { initialValue: false }, + ); protected itemTypesIcon = ItemTypes; private organizations$: Observable = this.accountService.activeAccount$.pipe( @@ -191,6 +209,14 @@ export class VaultComponent implements OnInit, OnDestroy, CopyClickListener { switchMap((id) => this.organizationService.organizations$(id)), ); + protected readonly submitButtonText = computed(() => { + return this.cipher()?.isArchived && + !this.userHasPremium() && + this.cipherArchiveService.hasArchiveFlagEnabled$ + ? this.i18nService.t("unArchiveAndSave") + : this.i18nService.t("save"); + }); + protected hasArchivedCiphers$ = this.userId$.pipe( switchMap((userId) => this.cipherArchiveService.archivedCiphers$(userId).pipe(map((ciphers) => ciphers.length > 0)), @@ -237,18 +263,6 @@ export class VaultComponent implements OnInit, OnDestroy, CopyClickListener { ) {} async ngOnInit() { - this.accountService.activeAccount$ - .pipe( - filter((account): account is Account => !!account), - switchMap((account) => - this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), - ), - takeUntil(this.componentIsDestroyed$), - ) - .subscribe((canAccessPremium: boolean) => { - this.userHasPremiumAccess = canAccessPremium; - }); - // Subscribe to filter changes from router params via the bridge service // Use combineLatest to react to changes in both the filter and archive flag combineLatest([ @@ -306,30 +320,40 @@ export class VaultComponent implements OnInit, OnDestroy, CopyClickListener { this.showingModal = false; break; case "copyUsername": { - if (this.cipher?.login?.username) { - this.copyValue(this.cipher, this.cipher?.login?.username, "username", "Username"); + if (this.cipher()?.login?.username) { + this.copyValue( + this.cipher(), + this.cipher()?.login?.username, + "username", + "Username", + ); } break; } case "copyPassword": { - if (this.cipher?.login?.password && this.cipher.viewPassword) { - this.copyValue(this.cipher, this.cipher.login.password, "password", "Password"); + if (this.cipher()?.login?.password && this.cipher().viewPassword) { + this.copyValue( + this.cipher(), + this.cipher().login.password, + "password", + "Password", + ); await this.eventCollectionService - .collect(EventType.Cipher_ClientCopiedPassword, this.cipher.id) + .collect(EventType.Cipher_ClientCopiedPassword, this.cipher().id) .catch(() => {}); } break; } case "copyTotp": { if ( - this.cipher?.login?.hasTotp && - (this.cipher.organizationUseTotp || this.userHasPremiumAccess) + this.cipher()?.login?.hasTotp && + (this.cipher().organizationUseTotp || this.userHasPremium()) ) { const value = await firstValueFrom( - this.totpService.getCode$(this.cipher.login.totp), + this.totpService.getCode$(this.cipher().login.totp), ).catch((): any => null); if (value) { - this.copyValue(this.cipher, value.code, "verificationCodeTotp", "TOTP"); + this.copyValue(this.cipher(), value.code, "verificationCodeTotp", "TOTP"); } } break; @@ -453,7 +477,7 @@ export class VaultComponent implements OnInit, OnDestroy, CopyClickListener { return; } this.cipherId = cipher.id; - this.cipher = cipher; + this.cipher.set(cipher); this.collections = this.filteredCollections?.filter((c) => cipher.collectionIds.includes(c.id)) ?? null; this.action = "view"; @@ -472,7 +496,7 @@ export class VaultComponent implements OnInit, OnDestroy, CopyClickListener { } async openAttachmentsDialog() { - if (!this.userHasPremiumAccess) { + if (!this.userHasPremium()) { return; } const dialogRef = AttachmentsV2Component.open(this.dialogService, { @@ -633,7 +657,7 @@ export class VaultComponent implements OnInit, OnDestroy, CopyClickListener { }, }); } - if (cipher.login.hasTotp && (cipher.organizationUseTotp || this.userHasPremiumAccess)) { + if (cipher.login.hasTotp && (cipher.organizationUseTotp || this.userHasPremium())) { menu.push({ label: this.i18nService.t("copyVerificationCodeTotp"), click: async () => { @@ -690,7 +714,7 @@ export class VaultComponent implements OnInit, OnDestroy, CopyClickListener { return; } this.cipherId = cipher.id; - this.cipher = cipher; + this.cipher.set(cipher); await this.buildFormConfig("edit"); if (!cipher.edit && this.config) { this.config.mode = "partial-edit"; @@ -704,7 +728,7 @@ export class VaultComponent implements OnInit, OnDestroy, CopyClickListener { return; } this.cipherId = cipher.id; - this.cipher = cipher; + this.cipher.set(cipher); await this.buildFormConfig("clone"); this.action = "clone"; await this.go().catch(() => {}); @@ -753,7 +777,7 @@ export class VaultComponent implements OnInit, OnDestroy, CopyClickListener { return; } this.addType = type || this.activeFilter.cipherType; - this.cipher = new CipherView(); + this.cipher.set(new CipherView()); this.cipherId = null; await this.buildFormConfig("add"); this.action = "add"; @@ -785,14 +809,14 @@ export class VaultComponent implements OnInit, OnDestroy, CopyClickListener { ); this.cipherId = cipher.id; - this.cipher = cipher; + this.cipher.set(cipher); await this.go().catch(() => {}); await this.vaultItemsComponent?.refresh().catch(() => {}); } async deleteCipher() { this.cipherId = null; - this.cipher = null; + this.cipher.set(null); this.action = null; await this.go().catch(() => {}); await this.vaultItemsComponent?.refresh().catch(() => {}); @@ -807,7 +831,7 @@ export class VaultComponent implements OnInit, OnDestroy, CopyClickListener { async cancelCipher(cipher: CipherView) { this.cipherId = cipher.id; - this.cipher = cipher; + this.cipher.set(cipher); this.action = this.cipherId ? "view" : null; await this.go().catch(() => {}); } @@ -881,14 +905,16 @@ export class VaultComponent implements OnInit, OnDestroy, CopyClickListener { /** Refresh the current cipher object */ protected async refreshCurrentCipher() { - if (!this.cipher) { + if (!this.cipher()) { return; } - this.cipher = await firstValueFrom( - this.cipherService.cipherViews$(this.activeUserId!).pipe( - filter((c) => !!c), - map((ciphers) => ciphers.find((c) => c.id === this.cipherId) ?? null), + this.cipher.set( + await firstValueFrom( + this.cipherService.cipherViews$(this.activeUserId!).pipe( + filter((c) => !!c), + map((ciphers) => ciphers.find((c) => c.id === this.cipherId) ?? null), + ), ), ); } From 1be55763a3f87de5efdbccd1282ec92ca9da611a Mon Sep 17 00:00:00 2001 From: Brad <44413459+lastbestdev@users.noreply.github.com> Date: Thu, 12 Feb 2026 11:17:09 -0800 Subject: [PATCH 023/134] [PM-31689] Fix Org 2FA report: cipher names should always show #18927 Fix issue where ciphers appearing in the Org 2FA report would render without the cipher name shown. This was happening for all ciphers in Collections the active User did not have access to. --- .../inactive-two-factor-report.component.html | 54 ++++++++----------- 1 file changed, 23 insertions(+), 31 deletions(-) diff --git a/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.html b/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.html index 4999d572969..83c7e566619 100644 --- a/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.html +++ b/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.html @@ -60,42 +60,34 @@ @if (!organization || canManageCipher(row)) { - - {{ row.name }} - + {{ row.name }} } @else { - - {{ row.name }} - + {{ row.name }} } @if (!organization && row.organizationId) { - - - {{ "shared" | i18n }} - + + {{ "shared" | i18n }} } @if (row.hasAttachments) { - - - {{ "attachments" | i18n }} - + + {{ "attachments" | i18n }} }
{{ row.subTitle }} From 2a72d2e74d08c4ea05ac22ac5414d9484e5ab0fb Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Thu, 12 Feb 2026 13:52:29 -0800 Subject: [PATCH 024/134] [PM-25685][PM-31077] - Migrate all Folder models (#17077) * enforce strict types on folders * fix folder api service * fix tests * fix test * fix type issue * fix test * add extra checks for folders. add specs * fix folder.id checks * fix id logic * remove unecessary check * name name and id optional in folder model * fix tests * Update folder and folderview * fix folder with id export * fix tests * fix tests * more defensive typing * fix tests * no need to check for presence * check for empty name in folder toDomain * fixes to folder * initialize id in folder constructor. fix failing tests * remove optional param to folder constructor * fix folder * fix test * remove remaining checks for null folder id * fix logic * pass null for empty folder ids * make id more explicit * fix failing test * fix failing test * fix "No Folder" filter --- .../vault-popup-list-filters.service.spec.ts | 4 +- .../vault-popup-list-filters.service.ts | 12 ++---- .../models/vault-filter.model.spec.ts | 11 +++++ .../vault-filter/models/vault-filter.model.ts | 15 +++++-- .../services/vault-filter.service.ts | 2 +- .../common/src/models/export/folder.export.ts | 10 ++--- .../src/vault/models/data/folder.data.ts | 19 +++++---- .../src/vault/models/domain/folder.spec.ts | 42 +++++++++++++------ libs/common/src/vault/models/domain/folder.ts | 39 +++++++---------- .../models/request/folder-with-id.request.ts | 2 +- .../src/vault/models/view/folder.view.ts | 21 ++++++---- .../services/folder/folder-api.service.ts | 6 +-- .../services/folder/folder.service.spec.ts | 11 ++--- .../src/components/import.component.ts | 2 +- libs/importer/src/importers/base-importer.ts | 7 ++-- .../importers/keepass2-xml-importer.spec.ts | 2 +- .../password-depot-17-xml-importer.spec.ts | 9 +++- .../individual-vault-export.service.spec.ts | 20 ++++----- .../individual-vault-export.service.ts | 6 +-- .../add-edit-folder-dialog.component.spec.ts | 2 +- libs/vault/src/models/filter-function.spec.ts | 8 ++++ libs/vault/src/models/filter-function.ts | 6 ++- .../routed-vault-filter-bridge.model.ts | 2 +- libs/vault/src/models/vault-filter.model.ts | 2 +- .../routed-vault-filter-bridge.service.ts | 2 +- .../src/services/vault-filter.service.spec.ts | 4 +- .../src/services/vault-filter.service.ts | 4 +- 27 files changed, 157 insertions(+), 113 deletions(-) diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts index 1358c5faebe..52703284679 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts @@ -438,7 +438,7 @@ describe("VaultPopupListFiltersService", () => { describe("folders$", () => { it('returns no folders when "No Folder" is the only option', (done) => { - folderViews$.next([{ id: null, name: "No Folder" }]); + folderViews$.next([{ id: "", name: "No Folder" }]); service.folders$.subscribe((folders) => { expect(folders).toEqual([]); @@ -448,7 +448,7 @@ describe("VaultPopupListFiltersService", () => { it('moves "No Folder" to the end of the list', (done) => { folderViews$.next([ - { id: null, name: "No Folder" }, + { id: "", name: "No Folder" }, { id: "2345", name: "Folder 2" }, { id: "1234", name: "Folder 1" }, ]); diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts index 85c415d01fe..3b220e4719c 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts @@ -387,7 +387,7 @@ export class VaultPopupListFiltersService { FolderView[], PopupCipherViewLike[], ] => { - if (folders.length === 1 && folders[0].id === null) { + if (folders.length === 1 && !folders[0].id) { // Do not display folder selections when only the "no folder" option is available. return [filters as PopupListFilter, [], cipherViews]; } @@ -396,7 +396,7 @@ export class VaultPopupListFiltersService { folders.sort(Utils.getSortFunction(this.i18nService, "name")); let arrangedFolders = folders; - const noFolder = folders.find((f) => f.id === null); + const noFolder = folders.find((f) => !f.id); if (noFolder) { // Update `name` of the "no folder" option to "Items with no folder" @@ -406,7 +406,7 @@ export class VaultPopupListFiltersService { }; // Move the "no folder" option to the end of the list - arrangedFolders = [...folders.filter((f) => f.id !== null), updatedNoFolder]; + arrangedFolders = [...folders.filter((f) => f.id), updatedNoFolder]; } return [filters as PopupListFilter, arrangedFolders, cipherViews]; }, @@ -545,11 +545,7 @@ export class VaultPopupListFiltersService { // When the organization filter changes and a folder is already selected, // reset the folder filter if the folder does not belong to the new organization filter - if ( - currentFilters.folder && - currentFilters.folder.id !== null && - organization.id !== MY_VAULT_ID - ) { + if (currentFilters.folder && currentFilters.folder.id && organization.id !== MY_VAULT_ID) { // Get all ciphers that belong to the new organization const orgCiphers = this.cipherViews.filter((c) => c.organizationId === organization.id); diff --git a/libs/angular/src/vault/vault-filter/models/vault-filter.model.spec.ts b/libs/angular/src/vault/vault-filter/models/vault-filter.model.spec.ts index a2f8aa7a352..b45472ad01c 100644 --- a/libs/angular/src/vault/vault-filter/models/vault-filter.model.spec.ts +++ b/libs/angular/src/vault/vault-filter/models/vault-filter.model.spec.ts @@ -143,6 +143,17 @@ describe("VaultFilter", () => { expect(result).toBe(true); }); + + it("should return true when filtering on unassigned folder via empty string id", () => { + const filterFunction = createFilterFunction({ + selectedFolder: true, + selectedFolderId: "", + }); + + const result = filterFunction(cipher); + + expect(result).toBe(true); + }); }); describe("given an organizational cipher (with organization and collections)", () => { diff --git a/libs/angular/src/vault/vault-filter/models/vault-filter.model.ts b/libs/angular/src/vault/vault-filter/models/vault-filter.model.ts index d3ad29142e2..e99cb5e9eeb 100644 --- a/libs/angular/src/vault/vault-filter/models/vault-filter.model.ts +++ b/libs/angular/src/vault/vault-filter/models/vault-filter.model.ts @@ -63,10 +63,19 @@ export class VaultFilter { if (this.cipherType != null && cipherPassesFilter) { cipherPassesFilter = CipherViewLikeUtils.getType(cipher) === this.cipherType; } - if (this.selectedFolder && this.selectedFolderId == null && cipherPassesFilter) { - cipherPassesFilter = cipher.folderId == null; + if ( + this.selectedFolder && + (this.selectedFolderId == null || this.selectedFolderId === "") && + cipherPassesFilter + ) { + cipherPassesFilter = cipher.folderId == null || cipher.folderId === ""; } - if (this.selectedFolder && this.selectedFolderId != null && cipherPassesFilter) { + if ( + this.selectedFolder && + this.selectedFolderId != null && + this.selectedFolderId !== "" && + cipherPassesFilter + ) { cipherPassesFilter = cipher.folderId === this.selectedFolderId; } if (this.selectedCollection && this.selectedCollectionId == null && cipherPassesFilter) { diff --git a/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts b/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts index 9b34890cbce..4a12bed1c66 100644 --- a/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts +++ b/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts @@ -83,7 +83,7 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti const ciphers = await this.cipherService.getAllDecrypted(userId); const orgCiphers = ciphers.filter((c) => c.organizationId == organizationId); folders = storedFolders.filter( - (f) => orgCiphers.some((oc) => oc.folderId == f.id) || f.id == null, + (f) => orgCiphers.some((oc) => oc.folderId == f.id) || !f.id, ); } diff --git a/libs/common/src/models/export/folder.export.ts b/libs/common/src/models/export/folder.export.ts index 96f0f1058b8..1bffcee8c2d 100644 --- a/libs/common/src/models/export/folder.export.ts +++ b/libs/common/src/models/export/folder.export.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { EncString } from "../../key-management/crypto/models/enc-string"; import { Folder as FolderDomain } from "../../vault/models/domain/folder"; import { FolderView } from "../../vault/models/view/folder.view"; @@ -7,6 +5,8 @@ import { FolderView } from "../../vault/models/view/folder.view"; import { safeGetString } from "./utils"; export class FolderExport { + name: string = ""; + static template(): FolderExport { const req = new FolderExport(); req.name = "Folder name"; @@ -19,14 +19,12 @@ export class FolderExport { } static toDomain(req: FolderExport, domain = new FolderDomain()) { - domain.name = req.name != null ? new EncString(req.name) : null; + domain.name = new EncString(req.name); return domain; } - name: string; - // Use build method instead of ctor so that we can control order of JSON stringify for pretty print build(o: FolderView | FolderDomain) { - this.name = safeGetString(o.name); + this.name = safeGetString(o.name ?? "") ?? ""; } } diff --git a/libs/common/src/vault/models/data/folder.data.ts b/libs/common/src/vault/models/data/folder.data.ts index c2eb585a6f4..5358cd713b3 100644 --- a/libs/common/src/vault/models/data/folder.data.ts +++ b/libs/common/src/vault/models/data/folder.data.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Jsonify } from "type-fest"; import { FolderResponse } from "../response/folder.response"; @@ -10,12 +8,19 @@ export class FolderData { revisionDate: string; constructor(response: Partial) { - this.name = response?.name; - this.id = response?.id; - this.revisionDate = response?.revisionDate; + this.name = response.name ?? ""; + this.id = response.id ?? ""; + this.revisionDate = response.revisionDate ?? new Date().toISOString(); } - static fromJSON(obj: Jsonify) { - return Object.assign(new FolderData({}), obj); + static fromJSON(obj: Jsonify) { + if (obj == null) { + return null; + } + return new FolderData({ + id: obj.id, + name: obj.name, + revisionDate: obj.revisionDate, + }); } } diff --git a/libs/common/src/vault/models/domain/folder.spec.ts b/libs/common/src/vault/models/domain/folder.spec.ts index d9e9e265d91..fd1455dbb66 100644 --- a/libs/common/src/vault/models/domain/folder.spec.ts +++ b/libs/common/src/vault/models/domain/folder.spec.ts @@ -8,7 +8,7 @@ import { mockFromJson, } from "../../../../spec"; import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; -import { EncryptedString, EncString } from "../../../key-management/crypto/models/enc-string"; +import { EncString } from "../../../key-management/crypto/models/enc-string"; import { FolderData } from "../../models/data/folder.data"; import { Folder } from "../../models/domain/folder"; @@ -49,6 +49,30 @@ describe("Folder", () => { }); }); + describe("constructor", () => { + it("initializes properties from FolderData", () => { + const revisionDate = new Date("2022-08-04T01:06:40.441Z"); + const folder = new Folder({ + id: "id", + name: "name", + revisionDate: revisionDate.toISOString(), + }); + + expect(folder.id).toBe("id"); + expect(folder.revisionDate).toEqual(revisionDate); + expect(folder.name).toBeInstanceOf(EncString); + expect((folder.name as EncString).encryptedString).toBe("name"); + }); + + it("initializes empty properties when no FolderData is provided", () => { + const folder = new Folder(); + + expect(folder.id).toBe(""); + expect(folder.name).toBeInstanceOf(EncString); + expect(folder.revisionDate).toBeInstanceOf(Date); + }); + }); + describe("fromJSON", () => { jest.mock("../../../key-management/crypto/models/enc-string"); jest.spyOn(EncString, "fromJSON").mockImplementation(mockFromJson); @@ -57,17 +81,13 @@ describe("Folder", () => { const revisionDate = new Date("2022-08-04T01:06:40.441Z"); const actual = Folder.fromJSON({ revisionDate: revisionDate.toISOString(), - name: "name" as EncryptedString, + name: "name", id: "id", }); - const expected = { - revisionDate: revisionDate, - name: "name_fromJSON", - id: "id", - }; - - expect(actual).toMatchObject(expected); + expect(actual?.id).toBe("id"); + expect(actual?.revisionDate).toEqual(revisionDate); + expect(actual?.name).toBe("name_fromJSON"); }); }); @@ -89,9 +109,7 @@ describe("Folder", () => { const view = await folder.decryptWithKey(key, encryptService); - expect(view).toEqual({ - name: "encName", - }); + expect(view.name).toBe("encName"); }); it("assigns the folder id and revision date", async () => { diff --git a/libs/common/src/vault/models/domain/folder.ts b/libs/common/src/vault/models/domain/folder.ts index c336095f15d..5f7f17ee751 100644 --- a/libs/common/src/vault/models/domain/folder.ts +++ b/libs/common/src/vault/models/domain/folder.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Jsonify } from "type-fest"; import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; @@ -9,16 +7,10 @@ import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-cr import { FolderData } from "../data/folder.data"; import { FolderView } from "../view/folder.view"; -export class Test extends Domain { - id: string; - name: EncString; - revisionDate: Date; -} - export class Folder extends Domain { - id: string; - name: EncString; - revisionDate: Date; + id: string = ""; + name: EncString = new EncString(""); + revisionDate: Date = new Date(); constructor(obj?: FolderData) { super(); @@ -26,17 +18,9 @@ export class Folder extends Domain { return; } - this.buildDomainModel( - this, - obj, - { - id: null, - name: null, - }, - ["id"], - ); - - this.revisionDate = obj.revisionDate != null ? new Date(obj.revisionDate) : null; + this.id = obj.id; + this.name = new EncString(obj.name); + this.revisionDate = new Date(obj.revisionDate); } decrypt(key: SymmetricCryptoKey): Promise { @@ -62,7 +46,14 @@ export class Folder extends Domain { } static fromJSON(obj: Jsonify) { - const revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate); - return Object.assign(new Folder(), obj, { name: EncString.fromJSON(obj.name), revisionDate }); + if (obj == null) { + return null; + } + + const folder = new Folder(); + folder.id = obj.id; + folder.name = EncString.fromJSON(obj.name); + folder.revisionDate = new Date(obj.revisionDate); + return folder; } } diff --git a/libs/common/src/vault/models/request/folder-with-id.request.ts b/libs/common/src/vault/models/request/folder-with-id.request.ts index 9d8078c12c1..8af890048ba 100644 --- a/libs/common/src/vault/models/request/folder-with-id.request.ts +++ b/libs/common/src/vault/models/request/folder-with-id.request.ts @@ -7,6 +7,6 @@ export class FolderWithIdRequest extends FolderRequest { constructor(folder: Folder) { super(folder); - this.id = folder.id; + this.id = folder.id ?? ""; } } diff --git a/libs/common/src/vault/models/view/folder.view.ts b/libs/common/src/vault/models/view/folder.view.ts index bc908e98eb8..6052ae9df37 100644 --- a/libs/common/src/vault/models/view/folder.view.ts +++ b/libs/common/src/vault/models/view/folder.view.ts @@ -1,19 +1,17 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Jsonify } from "type-fest"; import { View } from "../../../models/view/view"; -import { DecryptedObject } from "../../../platform/models/domain/domain-base"; import { Folder } from "../domain/folder"; import { ITreeNodeObject } from "../domain/tree-node"; export class FolderView implements View, ITreeNodeObject { - id: string = null; - name: string = null; - revisionDate: Date = null; + id: string = ""; + name: string = ""; + revisionDate: Date; - constructor(f?: Folder | DecryptedObject) { + constructor(f?: Folder) { if (!f) { + this.revisionDate = new Date(); return; } @@ -22,7 +20,12 @@ export class FolderView implements View, ITreeNodeObject { } static fromJSON(obj: Jsonify) { - const revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate); - return Object.assign(new FolderView(), obj, { revisionDate }); + const folderView = new FolderView(); + folderView.id = obj.id ?? ""; + folderView.name = obj.name ?? ""; + if (obj.revisionDate != null) { + folderView.revisionDate = new Date(obj.revisionDate); + } + return folderView; } } diff --git a/libs/common/src/vault/services/folder/folder-api.service.ts b/libs/common/src/vault/services/folder/folder-api.service.ts index fe9c3218a84..d5bd7fe9847 100644 --- a/libs/common/src/vault/services/folder/folder-api.service.ts +++ b/libs/common/src/vault/services/folder/folder-api.service.ts @@ -17,11 +17,11 @@ export class FolderApiService implements FolderApiServiceAbstraction { const request = new FolderRequest(folder); let response: FolderResponse; - if (folder.id == null) { + if (folder.id) { + response = await this.putFolder(folder.id, request); + } else { response = await this.postFolder(request); folder.id = response.id; - } else { - response = await this.putFolder(folder.id, request); } const data = new FolderData(response); diff --git a/libs/common/src/vault/services/folder/folder.service.spec.ts b/libs/common/src/vault/services/folder/folder.service.spec.ts index a520fd4852d..412e67e77d7 100644 --- a/libs/common/src/vault/services/folder/folder.service.spec.ts +++ b/libs/common/src/vault/services/folder/folder.service.spec.ts @@ -122,6 +122,7 @@ describe("Folder Service", () => { encryptedString: "ENC", encryptionType: 0, }, + revisionDate: expect.any(Date), }); }); @@ -132,7 +133,7 @@ describe("Folder Service", () => { expect(result).toEqual({ id: "1", name: makeEncString("ENC_STRING_" + 1), - revisionDate: null, + revisionDate: expect.any(Date), }); }); @@ -150,12 +151,12 @@ describe("Folder Service", () => { { id: "1", name: makeEncString("ENC_STRING_" + 1), - revisionDate: null, + revisionDate: expect.any(Date), }, { id: "2", name: makeEncString("ENC_STRING_" + 2), - revisionDate: null, + revisionDate: expect.any(Date), }, ]); }); @@ -167,7 +168,7 @@ describe("Folder Service", () => { { id: "4", name: makeEncString("ENC_STRING_" + 4), - revisionDate: null, + revisionDate: expect.any(Date), }, ]); }); @@ -203,7 +204,7 @@ describe("Folder Service", () => { const folderViews = await firstValueFrom(folderService.folderViews$(mockUserId)); expect(folderViews.length).toBe(1); - expect(folderViews[0].id).toBeNull(); // Should be the "No Folder" folder + expect(folderViews[0].id).toEqual(""); // Should be the "No Folder" folder }); describe("getRotatedData", () => { diff --git a/libs/importer/src/components/import.component.ts b/libs/importer/src/components/import.component.ts index d58859ac163..86f5d765d31 100644 --- a/libs/importer/src/components/import.component.ts +++ b/libs/importer/src/components/import.component.ts @@ -354,7 +354,7 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit { switchMap((userId) => { return this.folderService.folderViews$(userId); }), - map((folders) => folders.filter((f) => f.id != null)), + map((folders) => folders.filter((f) => !!f.id)), ); this.formGroup.controls.targetSelector.disable(); diff --git a/libs/importer/src/importers/base-importer.ts b/libs/importer/src/importers/base-importer.ts index a32a53f3e60..9c617971f8f 100644 --- a/libs/importer/src/importers/base-importer.ts +++ b/libs/importer/src/importers/base-importer.ts @@ -2,12 +2,12 @@ // @ts-strict-ignore import * as papa from "papaparse"; -import { CollectionView, Collection } from "@bitwarden/common/admin-console/models/collections"; +import { CollectionView } from "@bitwarden/common/admin-console/models/collections"; import { normalizeExpiryYearFormat } from "@bitwarden/common/autofill/utils"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; -import { OrganizationId } from "@bitwarden/common/types/guid"; +import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { FieldType, SecureNoteType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FieldView } from "@bitwarden/common/vault/models/view/field.view"; @@ -277,8 +277,7 @@ export abstract class BaseImporter { const collection = new CollectionView({ name: f.name, organizationId: this.organizationId, - // FIXME: Folder.id may be null, this should be changed when refactoring Folders to be ts-strict - id: Collection.isCollectionId(f.id) ? f.id : null, + id: f.id && f.id !== "" ? (f.id as CollectionId) : null, }); return collection; }); diff --git a/libs/importer/src/importers/keepass2-xml-importer.spec.ts b/libs/importer/src/importers/keepass2-xml-importer.spec.ts index c1c0947936b..e934a442ff5 100644 --- a/libs/importer/src/importers/keepass2-xml-importer.spec.ts +++ b/libs/importer/src/importers/keepass2-xml-importer.spec.ts @@ -23,7 +23,7 @@ describe("KeePass2 Xml Importer", () => { const actual = [folder]; const result = await importer.parse(TestData); - expect(result.folders).toEqual(actual); + expect(result.folders[0].name).toEqual(actual[0].name); }); it("parse XML should contains login details", async () => { diff --git a/libs/importer/src/importers/password-depot/password-depot-17-xml-importer.spec.ts b/libs/importer/src/importers/password-depot/password-depot-17-xml-importer.spec.ts index 8b78b33c154..121c1b5dd66 100644 --- a/libs/importer/src/importers/password-depot/password-depot-17-xml-importer.spec.ts +++ b/libs/importer/src/importers/password-depot/password-depot-17-xml-importer.spec.ts @@ -57,10 +57,15 @@ describe("Password Depot 17 Xml Importer", () => { const importer = new PasswordDepot17XmlImporter(); const folder = new FolderView(); folder.name = "tempDB"; - const actual = [folder]; const result = await importer.parse(PasswordTestData); - expect(result.folders).toEqual(actual); + expect(result.folders).toEqual([ + expect.objectContaining({ + id: "", + name: "tempDB", + revisionDate: expect.any(Date), + }), + ]); }); it("should parse password type into logins", async () => { diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts index 7d28d857403..55ad39bc8e0 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts @@ -138,7 +138,7 @@ function expectEqualCiphers(ciphers: CipherView[] | Cipher[], jsonResult: string } function expectEqualFolderViews(folderViews: FolderView[] | Folder[], jsonResult: string) { - const actual = JSON.stringify(JSON.parse(jsonResult).folders); + const actual = JSON.parse(jsonResult).folders; const folders: FolderResponse[] = []; folderViews.forEach((c) => { const folder = new FolderResponse(); @@ -148,21 +148,19 @@ function expectEqualFolderViews(folderViews: FolderView[] | Folder[], jsonResult }); expect(actual.length).toBeGreaterThan(0); - expect(actual).toEqual(JSON.stringify(folders)); + expect(actual).toEqual(folders); } function expectEqualFolders(folders: Folder[], jsonResult: string) { - const actual = JSON.stringify(JSON.parse(jsonResult).folders); - const items: Folder[] = []; - folders.forEach((c) => { - const item = new Folder(); - item.id = c.id; - item.name = c.name; - items.push(item); - }); + const actual = JSON.parse(jsonResult).folders; + + const expected = folders.map((c) => ({ + id: c.id, + name: c.name?.encryptedString, + })); expect(actual.length).toBeGreaterThan(0); - expect(actual).toEqual(JSON.stringify(items)); + expect(actual).toEqual(expected); } describe("VaultExportService", () => { diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts index b30f14872ca..a5e9f8aea6e 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts @@ -240,7 +240,7 @@ export class IndividualVaultExportService }; folders.forEach((f) => { - if (f.id == null) { + if (!f.id) { return; } const folder = new FolderWithIdExport(); @@ -268,7 +268,7 @@ export class IndividualVaultExportService private buildCsvExport(decFolders: FolderView[], decCiphers: CipherView[]): string { const foldersMap = new Map(); decFolders.forEach((f) => { - if (f.id != null) { + if (f.id) { foldersMap.set(f.id, f); } }); @@ -302,7 +302,7 @@ export class IndividualVaultExportService }; decFolders.forEach((f) => { - if (f.id == null) { + if (!f.id) { return; } const folder = new FolderWithIdExport(); diff --git a/libs/vault/src/components/add-edit-folder-dialog/add-edit-folder-dialog.component.spec.ts b/libs/vault/src/components/add-edit-folder-dialog/add-edit-folder-dialog.component.spec.ts index 9bf53826333..a783bdc7406 100644 --- a/libs/vault/src/components/add-edit-folder-dialog/add-edit-folder-dialog.component.spec.ts +++ b/libs/vault/src/components/add-edit-folder-dialog/add-edit-folder-dialog.component.spec.ts @@ -101,7 +101,7 @@ describe("AddEditFolderDialogComponent", () => { const newFolder = new FolderView(); newFolder.name = "New Folder"; - expect(encrypt).toHaveBeenCalledWith(newFolder, ""); + expect(encrypt).toHaveBeenCalledWith(expect.objectContaining({ name: "New Folder" }), ""); expect(save).toHaveBeenCalled(); }); diff --git a/libs/vault/src/models/filter-function.spec.ts b/libs/vault/src/models/filter-function.spec.ts index 1ffc1b119a8..de544a1a0d5 100644 --- a/libs/vault/src/models/filter-function.spec.ts +++ b/libs/vault/src/models/filter-function.spec.ts @@ -116,6 +116,14 @@ describe("createFilter", () => { expect(result).toBe(true); }); + + it("should return true when filtering on empty-string folder id", () => { + const filterFunction = createFilterFunction({ folderId: "" }); + + const result = filterFunction(cipher); + + expect(result).toBe(true); + }); }); describe("given an organizational cipher (with organization and collections)", () => { diff --git a/libs/vault/src/models/filter-function.ts b/libs/vault/src/models/filter-function.ts index 0252ef13094..bbd4127863b 100644 --- a/libs/vault/src/models/filter-function.ts +++ b/libs/vault/src/models/filter-function.ts @@ -55,8 +55,11 @@ export function createFilterFunction( return false; } } + const isNoFolderFilter = filter.folderId === Unassigned || filter.folderId === ""; + const cipherHasFolder = cipher.folderId != null && cipher.folderId !== ""; + // No folder - if (filter.folderId === Unassigned && cipher.folderId != null) { + if (isNoFolderFilter && cipherHasFolder) { return false; } // Folder @@ -64,6 +67,7 @@ export function createFilterFunction( filter.folderId !== undefined && filter.folderId !== All && filter.folderId !== Unassigned && + filter.folderId !== "" && cipher.folderId !== filter.folderId ) { return false; diff --git a/libs/vault/src/models/routed-vault-filter-bridge.model.ts b/libs/vault/src/models/routed-vault-filter-bridge.model.ts index 1d6d73ba7c5..32523684125 100644 --- a/libs/vault/src/models/routed-vault-filter-bridge.model.ts +++ b/libs/vault/src/models/routed-vault-filter-bridge.model.ts @@ -87,7 +87,7 @@ export class RoutedVaultFilterBridge implements VaultFilter { return this.legacyFilter.selectedFolderNode; } set selectedFolderNode(value: TreeNode) { - const folderId = value != null && value.node.id === null ? Unassigned : value?.node.id; + const folderId = value?.node.id ? value.node.id : Unassigned; this.bridgeService.navigate({ ...this.routedFilter, folderId, diff --git a/libs/vault/src/models/vault-filter.model.ts b/libs/vault/src/models/vault-filter.model.ts index 4617102cebe..5452a9f5c38 100644 --- a/libs/vault/src/models/vault-filter.model.ts +++ b/libs/vault/src/models/vault-filter.model.ts @@ -134,7 +134,7 @@ export class VaultFilter { if (this.selectedFolderNode) { // No folder if (this.folderId === null && cipherPassesFilter) { - cipherPassesFilter = cipher.folderId === null; + cipherPassesFilter = cipher.folderId == null || cipher.folderId === ""; } // Folder if (this.folderId !== null && cipherPassesFilter) { diff --git a/libs/vault/src/services/routed-vault-filter-bridge.service.ts b/libs/vault/src/services/routed-vault-filter-bridge.service.ts index 1bff764964e..25c75f464f0 100644 --- a/libs/vault/src/services/routed-vault-filter-bridge.service.ts +++ b/libs/vault/src/services/routed-vault-filter-bridge.service.ts @@ -145,7 +145,7 @@ function createLegacyFilterForEndUser( ); } - if (filter.folderId !== undefined && filter.folderId === Unassigned) { + if (filter.folderId !== undefined && (filter.folderId === Unassigned || filter.folderId === "")) { legacyFilter.selectedFolderNode = ServiceUtils.getTreeNodeObject(folderTree, null); } else if (filter.folderId !== undefined && filter.folderId !== Unassigned) { legacyFilter.selectedFolderNode = ServiceUtils.getTreeNodeObject(folderTree, filter.folderId); diff --git a/libs/vault/src/services/vault-filter.service.spec.ts b/libs/vault/src/services/vault-filter.service.spec.ts index 90af45e571f..537e9c9f542 100644 --- a/libs/vault/src/services/vault-filter.service.spec.ts +++ b/libs/vault/src/services/vault-filter.service.spec.ts @@ -195,8 +195,8 @@ describe("vault filter service", () => { ]; folderViews.next(storedFolders); - await expect(firstValueFrom(vaultFilterService.filteredFolders$)).resolves.toEqual([ - createFolderView("folder test id", "test"), + await expect(firstValueFrom(vaultFilterService.filteredFolders$)).resolves.toMatchObject([ + { id: "folder test id", name: "test" }, ]); }); diff --git a/libs/vault/src/services/vault-filter.service.ts b/libs/vault/src/services/vault-filter.service.ts index 445764827eb..5dbab72e1d3 100644 --- a/libs/vault/src/services/vault-filter.service.ts +++ b/libs/vault/src/services/vault-filter.service.ts @@ -290,9 +290,7 @@ export class VaultFilterService implements VaultFilterServiceAbstraction { // Otherwise, show only folders that have ciphers from the selected org and the "no folder" folder const orgCiphers = ciphers.filter((c) => c.organizationId == org?.id); - return storedFolders.filter( - (f) => orgCiphers.some((oc) => oc.folderId == f.id) || f.id == null, - ); + return storedFolders.filter((f) => orgCiphers.some((oc) => oc.folderId == f.id) || !f.id); } protected buildFolderTree(folders?: FolderView[]): TreeNode { From 8d3cbd3da617e718a4664f4b5973fca6bc261658 Mon Sep 17 00:00:00 2001 From: Brad <44413459+lastbestdev@users.noreply.github.com> Date: Thu, 12 Feb 2026 13:59:27 -0800 Subject: [PATCH 025/134] [PM-31801] Fix: Allow admins/owners to edit all ciphers in reports when Org setting is enabled#18856 This PR fixes an issue where admins couldn't edit ciphers in organization reports when the "Allow Admin Access to All Collection Items" setting was enabled. The fix adds a check for organization.allowAdminAccessToAllCollectionItems in the canManage() method across all organization report components. When this setting is enabled, admins/owners can now properly edit all ciphers regardless of collection membership. --- .../pages/organizations/exposed-passwords-report.component.ts | 3 +++ .../organizations/inactive-two-factor-report.component.ts | 3 +++ .../pages/organizations/reused-passwords-report.component.ts | 3 +++ .../pages/organizations/unsecured-websites-report.component.ts | 3 +++ .../pages/organizations/weak-passwords-report.component.ts | 3 +++ 5 files changed, 15 insertions(+) diff --git a/apps/web/src/app/dirt/reports/pages/organizations/exposed-passwords-report.component.ts b/apps/web/src/app/dirt/reports/pages/organizations/exposed-passwords-report.component.ts index 603c01bd2ab..ea1e6137d71 100644 --- a/apps/web/src/app/dirt/reports/pages/organizations/exposed-passwords-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/organizations/exposed-passwords-report.component.ts @@ -103,6 +103,9 @@ export class ExposedPasswordsReportComponent if (c.collectionIds.length === 0) { return true; } + if (this.organization?.allowAdminAccessToAllCollectionItems) { + return true; + } return this.manageableCiphers.some((x) => x.id === c.id); } } diff --git a/apps/web/src/app/dirt/reports/pages/organizations/inactive-two-factor-report.component.ts b/apps/web/src/app/dirt/reports/pages/organizations/inactive-two-factor-report.component.ts index 4104e16b3b5..ce81bef5f4b 100644 --- a/apps/web/src/app/dirt/reports/pages/organizations/inactive-two-factor-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/organizations/inactive-two-factor-report.component.ts @@ -108,6 +108,9 @@ export class InactiveTwoFactorReportComponent if (c.collectionIds.length === 0) { return true; } + if (this.organization?.allowAdminAccessToAllCollectionItems) { + return true; + } return this.manageableCiphers.some((x) => x.id === c.id); } } diff --git a/apps/web/src/app/dirt/reports/pages/organizations/reused-passwords-report.component.ts b/apps/web/src/app/dirt/reports/pages/organizations/reused-passwords-report.component.ts index 683b195b271..edb9001488f 100644 --- a/apps/web/src/app/dirt/reports/pages/organizations/reused-passwords-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/organizations/reused-passwords-report.component.ts @@ -102,6 +102,9 @@ export class ReusedPasswordsReportComponent if (c.collectionIds.length === 0) { return true; } + if (this.organization?.allowAdminAccessToAllCollectionItems) { + return true; + } return this.manageableCiphers.some((x) => x.id === c.id); } } diff --git a/apps/web/src/app/dirt/reports/pages/organizations/unsecured-websites-report.component.ts b/apps/web/src/app/dirt/reports/pages/organizations/unsecured-websites-report.component.ts index 893a5058bd2..7edcb003e4f 100644 --- a/apps/web/src/app/dirt/reports/pages/organizations/unsecured-websites-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/organizations/unsecured-websites-report.component.ts @@ -105,6 +105,9 @@ export class UnsecuredWebsitesReportComponent if (c.collectionIds.length === 0) { return true; } + if (this.organization?.allowAdminAccessToAllCollectionItems) { + return true; + } return this.manageableCiphers.some((x) => x.id === c.id); } } diff --git a/apps/web/src/app/dirt/reports/pages/organizations/weak-passwords-report.component.ts b/apps/web/src/app/dirt/reports/pages/organizations/weak-passwords-report.component.ts index aadd015e29d..62f91ff06b2 100644 --- a/apps/web/src/app/dirt/reports/pages/organizations/weak-passwords-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/organizations/weak-passwords-report.component.ts @@ -104,6 +104,9 @@ export class WeakPasswordsReportComponent if (c.collectionIds.length === 0) { return true; } + if (this.organization?.allowAdminAccessToAllCollectionItems) { + return true; + } return this.manageableCiphers.some((x) => x.id === c.id); } } From c9a125b338317a503149b15ac7bfa3b7b19d6501 Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Fri, 13 Feb 2026 10:24:50 +0100 Subject: [PATCH 026/134] Autosync the updated translations (#18961) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/src/_locales/ar/messages.json | 11 +- apps/browser/src/_locales/az/messages.json | 11 +- apps/browser/src/_locales/be/messages.json | 11 +- apps/browser/src/_locales/bg/messages.json | 13 +- apps/browser/src/_locales/bn/messages.json | 11 +- apps/browser/src/_locales/bs/messages.json | 11 +- apps/browser/src/_locales/ca/messages.json | 11 +- apps/browser/src/_locales/cs/messages.json | 11 +- apps/browser/src/_locales/cy/messages.json | 11 +- apps/browser/src/_locales/da/messages.json | 11 +- apps/browser/src/_locales/de/messages.json | 11 +- apps/browser/src/_locales/el/messages.json | 11 +- apps/browser/src/_locales/en_GB/messages.json | 11 +- apps/browser/src/_locales/en_IN/messages.json | 11 +- apps/browser/src/_locales/es/messages.json | 11 +- apps/browser/src/_locales/et/messages.json | 11 +- apps/browser/src/_locales/eu/messages.json | 11 +- apps/browser/src/_locales/fa/messages.json | 11 +- apps/browser/src/_locales/fi/messages.json | 11 +- apps/browser/src/_locales/fil/messages.json | 11 +- apps/browser/src/_locales/fr/messages.json | 11 +- apps/browser/src/_locales/gl/messages.json | 11 +- apps/browser/src/_locales/he/messages.json | 11 +- apps/browser/src/_locales/hi/messages.json | 11 +- apps/browser/src/_locales/hr/messages.json | 11 +- apps/browser/src/_locales/hu/messages.json | 11 +- apps/browser/src/_locales/id/messages.json | 11 +- apps/browser/src/_locales/it/messages.json | 11 +- apps/browser/src/_locales/ja/messages.json | 11 +- apps/browser/src/_locales/ka/messages.json | 11 +- apps/browser/src/_locales/km/messages.json | 11 +- apps/browser/src/_locales/kn/messages.json | 11 +- apps/browser/src/_locales/ko/messages.json | 11 +- apps/browser/src/_locales/lt/messages.json | 11 +- apps/browser/src/_locales/lv/messages.json | 11 +- apps/browser/src/_locales/ml/messages.json | 11 +- apps/browser/src/_locales/mr/messages.json | 11 +- apps/browser/src/_locales/my/messages.json | 11 +- apps/browser/src/_locales/nb/messages.json | 11 +- apps/browser/src/_locales/ne/messages.json | 11 +- apps/browser/src/_locales/nl/messages.json | 23 ++-- apps/browser/src/_locales/nn/messages.json | 11 +- apps/browser/src/_locales/or/messages.json | 11 +- apps/browser/src/_locales/pl/messages.json | 13 +- apps/browser/src/_locales/pt_BR/messages.json | 11 +- apps/browser/src/_locales/pt_PT/messages.json | 11 +- apps/browser/src/_locales/ro/messages.json | 11 +- apps/browser/src/_locales/ru/messages.json | 11 +- apps/browser/src/_locales/si/messages.json | 11 +- apps/browser/src/_locales/sk/messages.json | 11 +- apps/browser/src/_locales/sl/messages.json | 11 +- apps/browser/src/_locales/sr/messages.json | 11 +- apps/browser/src/_locales/sv/messages.json | 11 +- apps/browser/src/_locales/ta/messages.json | 11 +- apps/browser/src/_locales/te/messages.json | 11 +- apps/browser/src/_locales/th/messages.json | 11 +- apps/browser/src/_locales/tr/messages.json | 11 +- apps/browser/src/_locales/uk/messages.json | 11 +- apps/browser/src/_locales/vi/messages.json | 11 +- apps/browser/src/_locales/zh_CN/messages.json | 13 +- apps/browser/src/_locales/zh_TW/messages.json | 121 +++++++++--------- 61 files changed, 491 insertions(+), 308 deletions(-) diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 9f2428f2890..78cf90c3555 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden لن يطلب حفظ تفاصيل تسجيل الدخول لهذه النطاقات. يجب عليك تحديث الصفحة حتى تصبح التغييرات سارية المفعول." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden لن يطلب حفظ تفاصيل تسجيل الدخول لهذه النطافات لجميع الحسابات مسجلة الدخول. يجب عليك تحديث الصفحة لكي تصبح التغييرات نافذة المفعول." - }, "blockedDomainsDesc": { "message": "لن يتم توفير الملء التلقائي والمميزات الأخرى ذات الصلة لهذه المواقع. يجب عليك تحديث الصفحة لكي تصبح التغييرات نافذة المفعول." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Card number" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 6414cdd39f9..6a43475da32 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden, bu domenlər üçün giriş detallarını saxlamağı soruşmayacaq. Dəyişikliklərin qüvvəyə minməsi üçün səhifəni təzələməlisiniz." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden, giriş etmiş bütün hesablar üçün bu domenlərin giriş detallarını saxlamağı soruşmayacaq. Dəyişikliklərin qüvvəyə minməsi üçün səhifəni təzələməlisiniz." - }, "blockedDomainsDesc": { "message": "Bu veb saytlar üçün avto-doldurma və digər əlaqəli özəlliklər təklif olunmayacaq. Dəyişikliklərin qüvvəyə minməsi üçün səhifəni təzələməlisiniz." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Kart nömrəsi" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Təşkilatınız, artıq Bitwarden-ə giriş etmək üçün ana parol istifadə etmir. Davam etmək üçün təşkilatı və domeni doğrulayın." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "E-poçt qorunur" }, @@ -6134,4 +6137,4 @@ "message": "Şəxslər, Send-ə baxması üçün parolu daxil etməli olacaqlar", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 6bce3fdd891..9f4a65e3072 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Праграма не будзе прапаноўваць захаваць падрабязнасці ўваходу для гэтых даменаў. Вы павінны абнавіць старонку, каб змяненні пачалі дзейнічаць." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." - }, "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Card number" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 35fe16b2542..a46ad75065e 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Битуорден няма да пита дали да запазва данните за вход в тези сайтове. За да влезе правилото в сила, презаредете страницата." }, - "excludedDomainsDescAlt": { - "message": "Битуорден няма да пита дали да запазва данните за вход в тези сайтове за всички регистрации, в които сте вписан(а). За да влезе правилото в сила, презаредете страницата." - }, "blockedDomainsDesc": { "message": "Автоматичното попълване и други свързани функции няма да бъдат предлагани за тези уеб сайтове. Трябва да презаредите страницата, за да влязат в сила промените." }, @@ -5671,7 +5668,7 @@ "message": "Много широко" }, "narrow": { - "message": "Narrow" + "message": "Тясно" }, "sshKeyWrongPassword": { "message": "Въведената парола е неправилна." @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Номер на картата" }, + "errorCannotDecrypt": { + "message": "Грешка: не може да се дешифрира" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Вашата организация вече не използва главни пароли за вписване в Битуорден. За да продължите, потвърдете организацията и домейна." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "потребител@bitwarden.com , потребител@acme.com" }, + "downloadBitwardenApps": { + "message": "Сваляне на приложенията на Битуорден" + }, "emailProtected": { "message": "Е-пощата е защитена" }, @@ -6134,4 +6137,4 @@ "message": "Хората ще трябва да въведат паролата, за да видят това Изпращане", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index 4f6402fa8ea..b46d0664231 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden will not ask to save login details for these domains. You must refresh the page for changes to take effect." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." - }, "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Card number" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index 36aa422ff0d..e81fc637b5c 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden will not ask to save login details for these domains. You must refresh the page for changes to take effect." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." - }, "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Card number" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index 409a11ff253..2bd53876953 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden no demanarà que es guarden les dades d’inici de sessió d’aquests dominis. Heu d'actualitzar la pàgina perquè els canvis tinguen efecte." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden no demanarà que es guarden les dades d'inici de sessió d'aquests dominis per a tots els comptes iniciats. Heu d'actualitzar la pàgina perquè els canvis tinguen efecte." - }, "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Card number" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index 206566edcd5..1501c7d7c4a 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden nebude žádat o uložení přihlašovacích údajů pro tyto domény. Aby se změny projevily, musíte stránku obnovit." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden nebude žádat o uložení přihlašovacích údajů pro tyto domény pro všechny přihlášené účty. Aby se změny projevily, musíte stránku obnovit." - }, "blockedDomainsDesc": { "message": "Automatické vyplňování a další související funkce nebudou pro tyto webové stránky nabízeny. Aby se změny projevily, musíte stránku aktualizovat." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Číslo karty" }, + "errorCannotDecrypt": { + "message": "Chyba: Nelze dešifrovat" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Vaše organizace již k přihlášení do Bitwardenu nepoužívá hlavní hesla. Chcete-li pokračovat, ověřte organizaci a doménu." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Stáhnout aplikace Bitwarden" + }, "emailProtected": { "message": "E-mail je chráněný" }, @@ -6134,4 +6137,4 @@ "message": "Pro zobrazení tohoto Send budou muset jednotlivci zadat heslo", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index dc5ed2d6c7d..6910fe2efb3 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Fydd Bitwarden ddim yn gofyn i gadw manylion mewngofnodi'r parthau hyn. Rhaid i chi ail-lwytho'r dudalen i newidiadau ddod i rym." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." - }, "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Card number" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index 72a009b55bc..faf4fc855ec 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden vil ikke bede om at gemme login-detaljer for disse domæner. Du skal opdatere siden for at ændringerne kan træde i kraft." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden vil ikke anmode om at gemme login-detaljer for disse domæner for alle indloggede konti. Siden skal opfriskes for at effektuere ændringerne." - }, "blockedDomainsDesc": { "message": "Autofyldning og andre relaterede funktioner tilbydes ikke på disse websteder. Siden skal opdateres for at effektuere ændringerne." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Card number" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index ddebf64adf4..ad5b45159df 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden wird keine Login-Daten für diese Domäne speichern. Du musst die Seite aktualisieren, damit die Änderungen wirksam werden." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden wird für alle angemeldeten Konten nicht danach fragen Zugangsdaten für diese Domains speichern. Du musst die Seite neu laden, damit die Änderungen wirksam werden." - }, "blockedDomainsDesc": { "message": "Automatisches Ausfüllen und andere zugehörige Funktionen werden für diese Webseiten nicht angeboten. Du musst die Seite neu laden, damit die Änderungen wirksam werden." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Kartennummer" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Deine Organisation verwendet keine Master-Passwörter mehr, um sich bei Bitwarden anzumelden. Verifiziere die Organisation und Domain, um fortzufahren." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "benutzer@bitwarden.com, benutzer@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "E-Mail-Adresse geschützt" }, @@ -6134,4 +6137,4 @@ "message": "Personen müssen das Passwort eingeben, um dieses Send anzusehen", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index 04e2d2568f7..59f757008f2 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Το Bitwarden δεν θα ζητήσει να αποθηκεύσετε τα στοιχεία σύνδεσης για αυτούς τους τομείς. Πρέπει να ανανεώσετε τη σελίδα για να τεθούν σε ισχύ οι αλλαγές." }, - "excludedDomainsDescAlt": { - "message": "Το Bitwarden δε θα ρωτήσει για να αποθηκεύσετε τα στοιχεία σύνδεσης για αυτούς τους τομείς, για όλους τους συνδεδεμένους λογαριασμούς. Πρέπει να ανανεώσετε τη σελίδα για να τεθούν σε ισχύ οι αλλαγές." - }, "blockedDomainsDesc": { "message": "Η αυτόματη συμπλήρωση και άλλες σχετικές λειτουργίες δεν θα προσφερθούν για αυτούς τους ιστότοπους. Πρέπει να ανανεώσετε τη σελίδα για να τεθούν σε ισχύ οι αλλαγές." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Card number" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index ab3b511a009..e34e20844e6 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden will not ask to save login details for these domains. You must refresh the page for changes to take effect." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." - }, "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Card number" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organisation is no longer using master passwords to log into Bitwarden. To continue, verify the organisation and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index 3c19d7c8af0..9fd388a80d3 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden will not ask to save login details for these domains. You must refresh the page for changes to take effect." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." - }, "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Card number" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organisation is no longer using master passwords to log into Bitwarden. To continue, verify the organisation and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index b6d1f5f793b..ab5fad7e3af 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden no pedirá que se guarden los datos de acceso para estos dominios. Debe actualizar la página para que los cambios surtan efecto." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden no pedirá que se guarden los datos de acceso para estos dominios en todas las sesiones iniciadas. Debe actualizar la página para que los cambios surtan efecto." - }, "blockedDomainsDesc": { "message": "El autorrelleno y otras funcionalidades relacionadas no se ofrecerán para estos sitios web. Debe actualizar la página para que los cambios surtan efecto." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Número de tarjeta" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Los individuos tendrán que introducir la contraseña para ver este Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index 34ac5f523ca..e8efd12b1e2 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Nendel domeenidel Bitwarden paroolide salvestamise valikut ei paku. Muudatuste jõustamiseks pead lehekülge värskendama." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." - }, "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Card number" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index cd2cbb910ef..e7fcd4998e0 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwardenek ez du eskatuko domeinu horietarako saio-hasierako xehetasunak gordetzea. Orrialdea eguneratu behar duzu aldaketek eragina izan dezaten." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." - }, "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Card number" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index ea95452d409..bca4ad20d52 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden برای ذخیره جزئیات ورود به سیستم این دامنه‌ها سوال نمی‌کند. برای اینکه تغییرات اعمال شود باید صفحه را تازه کنید." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden برای هیچ یک از حساب‌های کاربری وارد شده، درخواست ذخیره اطلاعات ورود برای این دامنه‌ها را نخواهد داد. برای اعمال تغییرات باید صفحه را تازه‌سازی کنید." - }, "blockedDomainsDesc": { "message": "ویژگی‌های پر کردن خودکار و سایر قابلیت‌های مرتبط برای این وب‌سایت‌ها ارائه نخواهند شد. برای اعمال تغییرات باید صفحه را تازه‌سازی کنید." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Card number" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 630c6e91ff2..2997ed6c128 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden ei pyydä kirjautumistietojen tallennusta näille verkkotunnuksille. Päivitä sivu ottaaksesi muutokset käyttöön." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden ei pyydä kirjautumistietojen tallennusta näillä verkkotunnuksilla. Koskee kaikkia kirjautuneita tilejä. Ota muutokset käyttöön päivittämällä sivu." - }, "blockedDomainsDesc": { "message": "Näille sivustoille ei tarjota automaattista täyttöä eikä muita siihen liittyviä ominaisuuksia. Sinun on päivitettävä sivu, jotta muutokset tulevat voimaan." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Kortin numero" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index eb6687e810c..11da450cc0f 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Hindi tatanungin ng Bitwarden na i-save ang mga detalye ng pag-login para sa mga domain na ito. Kailangan mo nang i-refresh ang page para maipatupad ang mga pagbabago." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." - }, "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Card number" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index 9de933d34df..face33e0087 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden ne demandera pas d'enregistrer les détails de connexion pour ces domaines. Vous devez actualiser la page pour que les modifications prennent effet." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden ne demandera pas d'enregistrer les détails de connexion pour ces domaines pour tous les comptes connectés. Vous devez actualiser la page pour que les modifications prennent effet." - }, "blockedDomainsDesc": { "message": "La saisie automatique et d'autres fonctionnalités connexes ne seront pas proposées pour ces sites web. Vous devez actualiser la page pour que les modifications soient prises en compte." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Numéro de carte" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Votre organisation n'utilise plus les mots de passe principaux pour se connecter à Bitwarden. Pour continuer, vérifiez l'organisation et le domaine." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index 15ab24b16d7..69ef54f78eb 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden non ofrecerá gardar contas para estes dominios. Recarga a páxina para que os cambios fagan efecto." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden non ofrecerá gardar contas para estes dominios en ningunha das sesións iniciadas. Recarga a páxina para que os cambios fornezan efecto." - }, "blockedDomainsDesc": { "message": "O autoenchido e outras funcións relacionadas non estarán dispoñibles para estas webs. Debes recargar a páxina para que os cambios teñan efecto." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Card number" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index ab0fbfe9562..22939259639 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden לא יבקש לשמור פרטי כניסה עבור הדומיינים האלה. אתה מוכרח לרענן את העמוד כדי שהשינויים ייכנסו לתוקף." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden לא יבקש לשמור פרטי כניסה עבור הדומיינים האלה עבור כל החשבונות המחוברים. אתה מוכרח לרענן את העמוד כדי שהשינויים ייכנסו לתוקף." - }, "blockedDomainsDesc": { "message": "לא יוצעו מילוי אוטומטי ותכונות קשורות אחרות עבור האתרים האלה. אתה מוכרח לרענן את העמוד כדי שהשינויים ייכנסו לתוקף." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "מספר כרטיס" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index b07e6bcb5c9..298f0312be7 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "बिटवर्डन इन डोमेन के लिए लॉगिन विवरण सहेजने के लिए नहीं कहेगा।परिवर्तनों को प्रभावी बनाने के लिए आपको पृष्ठ को ताज़ा करना होगा |" }, - "excludedDomainsDescAlt": { - "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." - }, "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Card number" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index d5f7f21ddb0..d7814a22da0 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden neće pitati treba li spremiti prijavne podatke za ove domene. Za primjenu promjena, potrebno je osvježiti stranicu." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden neće nuditi spremanje podataka za prijavu za ove domene za sve prijavljene račune. Moraš osvježiti stranicu kako bi promjene stupile na snagu." - }, "blockedDomainsDesc": { "message": "Auto-ispuna i druge vezane značajke neće biti ponuđene za ova web mjesta. Potrebno je osvježiti stranicu zaprimjenu postavki." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Broj kartice" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index 36a929a8712..ec4b2d405bf 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "A Bitwarden nem fogja kérni a domainek bejelentkezési adatainak mentését. A változások életbe lépéséhez frissíteni kell az oldalt." }, - "excludedDomainsDescAlt": { - "message": "A Bitwarden nem kéri a bejelentkezési adatok mentését ezeknél a tartományoknál az összes bejelentkezési fiókra vonatkozva. A változtatások életbe lépéséhez frissíteni kell az oldalt." - }, "blockedDomainsDesc": { "message": "Az automatikus kitöltés és az egyéb kapcsolódó funkciók ezeken a webhelyeken nincsenek a kínálatban. A változtatások életbe lépéséhez frissíteni kell az oldalt." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Kártya szám" }, + "errorCannotDecrypt": { + "message": "Hiba: nem fejthető vissza." + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "A szervezet már nem használ mesterjelszavakat a Bitwardenbe bejelentkezéshez. A folytatáshoz ellenőrizzük a szervezetet és a tartományt." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Bitwarden alkalmazások letöltése" + }, "emailProtected": { "message": "Védett email cím" }, @@ -6134,4 +6137,4 @@ "message": "A személyeknek meg kell adniuk a jelszót a Send elem megtekintéséhez.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index 098879ce6fc..f364b2f7540 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden tidak akan meminta untuk menyimpan detail login untuk domain ini. Anda harus menyegarkan halaman agar perubahan diterapkan." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden tidak akan meminta untuk menyimpan rincian login untuk domain tersebut. Anda harus menyegarkan halaman agar perubahan diterapkan." - }, "blockedDomainsDesc": { "message": "Isi otomatis dan fitur terkait lain tidak akan ditawarkan bagi situs-situs web ini. Anda mesti menyegarkan halaman agar perubahan berdampak." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Card number" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 9cd4efec3ee..9c4ce6a0369 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden non ti chiederà di aggiungere nuovi login per questi domini. Ricorda di ricaricare la pagina perché le modifiche abbiano effetto." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden non chiederà di salvare le credenziali di accesso per questi domini per tutti gli account sul dispositivo. Ricarica la pagina affinché le modifiche abbiano effetto." - }, "blockedDomainsDesc": { "message": "Per questi siti, riempimento automatico e funzionalità simili non saranno disponibili. Ricarica la pagina per applicare le modifiche." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Numero di carta" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "La tua organizzazione non utilizza più le password principali per accedere a Bitwarden. Per continuare, verifica l'organizzazione e il dominio." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 8de6fb53c1c..c8a963fc744 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden はこれらのドメインのログイン情報を保存するよう尋ねません。変更を有効にするにはページを更新する必要があります。" }, - "excludedDomainsDescAlt": { - "message": "Bitwarden はログインしているすべてのアカウントで、これらのドメインのログイン情報を保存するよう要求しません。 変更を有効にするにはページを更新する必要があります。" - }, "blockedDomainsDesc": { "message": "自動入力やその他の関連機能はこれらのウェブサイトには提供されません。変更を反映するにはページを更新する必要があります。" }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "カード番号" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index 49e1eb3cabd..cb6129ed2bb 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden will not ask to save login details for these domains. You must refresh the page for changes to take effect." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." - }, "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Card number" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index c6d9d325e00..336e8783b75 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden will not ask to save login details for these domains. You must refresh the page for changes to take effect." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." - }, "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Card number" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index 36007446e97..e97ce2a95a4 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "ಬಿಟ್ವಾರ್ಡ್ ಈ ಡೊಮೇನ್ಗಳಿಗಾಗಿ ಲಾಗಿನ್ ವಿವರಗಳನ್ನು ಉಳಿಸಲು ಕೇಳುವುದಿಲ್ಲ. ಬದಲಾವಣೆಗಳನ್ನು ಜಾರಿಗೆ ತರಲು ನೀವು ಪುಟವನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಬೇಕು." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." - }, "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Card number" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index b0afb6d12b3..9f570d62abb 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden은 이 도메인들에 대해 로그인 정보를 저장할 것인지 묻지 않습니다. 페이지를 새로고침해야 변경된 내용이 적용됩니다." }, - "excludedDomainsDescAlt": { - "message": "BItwarden은 로그인한 모든 계정에 대해 이러한 도메인에 대한 로그인 세부 정보를 저장하도록 요청하지 않습니다. 변경 사항을 적용하려면 페이지를 새로 고쳐야 합니다" - }, "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Card number" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index 5489c489de3..6e105f044f3 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "„Bitwarden“ neprašys išsaugoti šių domenų prisijungimo duomenų. Turite atnaujinti puslapį, kad pokyčiai pradėtų galioti." }, - "excludedDomainsDescAlt": { - "message": "„Bitwarden“ neprašys išsaugoti prisijungimo detalių šiems domenams, visose prisijungusiose paskyrose. Turite atnaujinti puslapį, kad pokyčiai pradėtų galioti." - }, "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Card number" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index 603d7e85eaa..8c86d7040fe 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden nevaicās saglabāt pieteikšanās datus šiem domēniem. Ir jāpārlādē lapa, lai izmaiņas iedarbotos." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden nevaicās saglabāt pieteikšanās datus visiem šī domēna kontiem, kuri ir pieteikušies. Ir jāpārlādē lapa, lai iedarbotos izmaiņas." - }, "blockedDomainsDesc": { "message": "Automātiskā aizpilde un citas saistītās iespējas šajās tīmekļvietnēs netiks piedāvātas. Ir jāatsvaidzina lapa, lai izmaiņas iedarbotos." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Kartes numurs" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Apvienība vairs neizmanto galvenās paroles, lai pieteiktos Bitwarden. Lai turpinātu, jāapliecina apvienība un domēns." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Cilvēkiem būs jāievada parole, lai apskatītu šo Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index 740d9077351..61f69ffe22b 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden will not ask to save login details for these domains. You must refresh the page for changes to take effect." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." - }, "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Card number" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index ae2ef47131f..5cc614c5df7 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden will not ask to save login details for these domains. You must refresh the page for changes to take effect." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." - }, "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Card number" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index c6d9d325e00..336e8783b75 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden will not ask to save login details for these domains. You must refresh the page for changes to take effect." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." - }, "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Card number" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 877be294778..ce6c8d5a7d4 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden vil ikke be om å lagre innloggingsdetaljer for disse domenene. Du må oppdatere siden for at endringene skal tre i kraft." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." - }, "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Card number" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index c6d9d325e00..336e8783b75 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden will not ask to save login details for these domains. You must refresh the page for changes to take effect." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." - }, "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Card number" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index 895d6592b93..44522727429 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -35,7 +35,7 @@ "message": "Single sign-on gebruiken" }, "yourOrganizationRequiresSingleSignOn": { - "message": "Your organization requires single sign-on." + "message": "Je organisatie vereist single sign-on." }, "welcomeBack": { "message": "Welkom terug" @@ -589,10 +589,10 @@ "message": "Eenmaal gearchiveerd wordt dit item uitgesloten van zoekresultaten en suggesties voor automatisch invullen." }, "archived": { - "message": "Archived" + "message": "Gearchiveerd" }, "unarchiveAndSave": { - "message": "Unarchive and save" + "message": "Dearchiveren en opslaan" }, "upgradeToUseArchive": { "message": "Je hebt een Premium-abonnement nodig om te kunnen archiveren." @@ -1555,13 +1555,13 @@ "message": "Eigen opties voor tweestapsaanmelding zoals YubiKey en Duo." }, "premiumSubscriptionEnded": { - "message": "Your Premium subscription ended" + "message": "Je Premium-abonnement is afgelopen" }, "archivePremiumRestart": { - "message": "To regain access to your archive, restart your Premium subscription. If you edit details for an archived item before restarting, it'll be moved back into your vault." + "message": "Herstart je Premium-abonnement om toegang tot je archief te krijgen. Als je de details wijzigt voor een gearchiveerd item voor het opnieuw opstarten, zal het terug naar je kluis worden verplaatst." }, "restartPremium": { - "message": "Restart Premium" + "message": "Premium herstarten" }, "ppremiumSignUpReports": { "message": "Wachtwoordhygiëne, gezondheid van je account en datalekken om je kluis veilig te houden." @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden zal voor deze domeinen niet vragen om inloggegevens op te slaan. Je moet de pagina vernieuwen om de wijzigingen toe te passen." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden zal voor deze domeinen niet vragen om de wachtwoorden op te slaan voor alle ingelogde accounts. Je moet de pagina verversen om de wijzigingen op te slaan." - }, "blockedDomainsDesc": { "message": "Autofill en andere gerelateerde functies worden niet aangeboden voor deze websites. Vernieuw de pagina om de wijzigingen toe te passen." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Kaartnummer" }, + "errorCannotDecrypt": { + "message": "Fout: Kan niet ontsleutelen" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Je organisatie maakt niet langer gebruik van hoofdwachtwoorden om in te loggen op Bitwarden. Controleer de organisatie en het domein om door te gaan." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Bitwarden-apps downloaden" + }, "emailProtected": { "message": "E-mail beveiligd" }, @@ -6134,4 +6137,4 @@ "message": "Individuen moeten het wachtwoord invoeren om deze Send te bekijken", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index c6d9d325e00..336e8783b75 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden will not ask to save login details for these domains. You must refresh the page for changes to take effect." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." - }, "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Card number" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index c6d9d325e00..336e8783b75 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden will not ask to save login details for these domains. You must refresh the page for changes to take effect." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." - }, "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Card number" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index fa1c2956e9f..44c7d9e6d47 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden nie będzie proponował zapisywania danych logowania dla tych domen. Odśwież stronę, aby zastosowywać zmiany." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden nie będzie proponował zapisywania danych logowania dla tych domen dla wszystkich zalogowanych kont. Odśwież stronę, aby zastosowywać zmiany." - }, "blockedDomainsDesc": { "message": "Autouzupełnianie będzie zablokowane dla tych stron internetowych. Zmiany zaczną obowiązywać po odświeżeniu strony." }, @@ -5671,7 +5668,7 @@ "message": "Bardzo szeroka" }, "narrow": { - "message": "Narrow" + "message": "Wąska" }, "sshKeyWrongPassword": { "message": "Hasło jest nieprawidłowe." @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Numer karty" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 70de48fc293..5ad95b480db 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "O Bitwarden não irá pedir para salvar os detalhes de credencial para estes domínios. Você deve atualizar a página para que as alterações entrem em vigor." }, - "excludedDomainsDescAlt": { - "message": "O Bitwarden não irá pedir para salvar os detalhes de credencial para estes domínios, em todas as contas. Você deve recarregar a página para que as alterações entrem em vigor." - }, "blockedDomainsDesc": { "message": "O preenchimento automático e outros recursos relacionados não serão oferecidos para estes sites. Recarregue a página para que as mudanças surtam efeito." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Número do cartão" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "A sua organização não está mais usando senhas principais para se conectar ao Bitwarden. Para continuar, verifique a organização e o domínio." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index 351ac934091..604bf054707 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "O Bitwarden não pedirá para guardar os detalhes de início de sessão destes domínios. É necessário atualizar a página para que as alterações tenham efeito." }, - "excludedDomainsDescAlt": { - "message": "O Bitwarden não pedirá para guardar os detalhes de início de sessão destes domínios para todas as contas com sessão iniciada. É necessário atualizar a página para que as alterações tenham efeito." - }, "blockedDomainsDesc": { "message": "O preenchimento automático e outras funcionalidades relacionadas não serão disponibilizados para estes sites. É necessário atualizar a página para que as alterações tenham efeito." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Número do cartão" }, + "errorCannotDecrypt": { + "message": "Erro: Não é possível desencriptar" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "A sua organização já não utiliza palavras-passe mestras para iniciar sessão no Bitwarden. Para continuar, verifique a organização e o domínio." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "utilizador@bitwarden.com , utilizador@acme.com" }, + "downloadBitwardenApps": { + "message": "Descarregar as apps Bitwarden" + }, "emailProtected": { "message": "E-mail protegido" }, @@ -6134,4 +6137,4 @@ "message": "Os indivíduos terão de introduzir a palavra-passe para ver este Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index ebd8063cc4f..12706943e83 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden nu va cere să salveze detaliile de conectare pentru aceste domenii. Trebuie să reîmprospătați pagina pentru ca modificările să intre în vigoare." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." - }, "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Card number" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index a669d338fce..dab9a22f03a 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden не будет предлагать сохранить логины для этих доменов. Для вступления изменений в силу необходимо обновить страницу." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden не будет предлагать сохранение логинов для этих доменов для всех авторизованных аккаунтов. Для вступления изменений в силу необходимо обновить страницу." - }, "blockedDomainsDesc": { "message": "Автозаполнение и другие связанные с ним функции не будут предлагаться для этих сайтов. Чтобы изменения вступили в силу, необходимо обновить страницу." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Номер карты" }, + "errorCannotDecrypt": { + "message": "Ошибка: невозможно расшифровать" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Ваша организация больше не использует мастер-пароли для входа в Bitwarden. Чтобы продолжить, подтвердите организацию и домен." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Скачать приложения Bitwarden" + }, "emailProtected": { "message": "Email защищен" }, @@ -6134,4 +6137,4 @@ "message": "Пользователям необходимо будет ввести пароль для просмотра этой Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index 3f721641b6a..d228cdb512a 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "බිට්වර්ඩන් මෙම වසම් සඳහා පිවිසුම් තොරතුරු සුරැකීමට ඉල්ලා නොසිටිනු ඇත. බලාත්මක කිරීම සඳහා වෙනස්කම් සඳහා ඔබ පිටුව නැවුම් කළ යුතුය." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." - }, "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Card number" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index 1528079565a..db7efcd8b9f 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden nebude požadovať ukladanie prihlasovacích údajov pre tieto domény. Aby sa zmeny prejavili, musíte stránku obnoviť." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden nebude požadovať ukladanie prihlasovacích údajov pre tieto domény pre všetky prihlásené účty. Aby sa zmeny prejavili, musíte stránku obnoviť." - }, "blockedDomainsDesc": { "message": "Automatické vypĺňanie a ďalšie súvisiace funkcie sa na týchto webových stránkach nebudú ponúkať. Aby sa zmeny prejavili, musíte stránku obnoviť." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Číslo karty" }, + "errorCannotDecrypt": { + "message": "Chyba: Nedá sa dešifrovať" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Vaša organizácia už nepoužíva hlavné heslá na prihlásenie do Bitwardenu. Ak chcete pokračovať, overte organizáciu a doménu." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "pouzivate@bitwarden.com, pouzivatel@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Chránené e-mailom" }, @@ -6134,4 +6137,4 @@ "message": "Jednotlivci budú musieť zadať heslo, aby mohli zobraziť tento Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index e95822d96ea..07ee84ab810 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Za te domene Bitwarden ne bo predlagal shranjevanja prijavnih podatkov. Sprememba nastavitev stopi v veljavo šele, ko osvežite stran." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." - }, "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Card number" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index 5eef711ea1e..0ad71788514 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden неће тражити да сачува податке за пријављивање за ове домене. Морате освежити страницу да би промене ступиле на снагу." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden неће тражити да сачува податке за пријављивање за ове домене за све пријављене налоге. Морате освежити страницу да би промене ступиле на снагу." - }, "blockedDomainsDesc": { "message": "Аутоматско попуњавање и сродне функције неће бити понуђене за ове веб сајтове. Морате освежити страницу да би се измене примениле." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Број картице" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Ваша организација више не користи главне лозинке за пријаву на Bitwarden. Да бисте наставили, верификујте организацију и домен." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 756b19a81c0..08cec673d27 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden kommer inte att fråga om att få spara inloggningsuppgifter för dessa domäner. Du måste uppdatera sidan för att ändringarna ska träda i kraft." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden kommer inte att be om att få spara inloggningsuppgifter för dessa domäner för alla inloggade konton. Du måste uppdatera sidan för att ändringarna ska träda i kraft." - }, "blockedDomainsDesc": { "message": "Autofyll och andra relaterade funktioner kommer inte att erbjudas för dessa webbplatser. Du måste uppdatera sidan för att ändringarna ska träda i kraft." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Kortnummer" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Din organisation använder inte längre huvudlösenord för att logga in på Bitwarden. För att fortsätta, verifiera organisationen och domänen." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "användare@bitwarden.com , användare@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individer måste ange lösenordet för att visa denna Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/ta/messages.json b/apps/browser/src/_locales/ta/messages.json index a872eb9fe53..374c0968d2c 100644 --- a/apps/browser/src/_locales/ta/messages.json +++ b/apps/browser/src/_locales/ta/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "இந்த டொமைன்களுக்கான உள்நுழைவு விவரங்களைச் சேமிக்க Bitwarden கேட்காது. மாற்றங்கள் நடைமுறைக்கு வர பக்கத்தை மீண்டும் மீட்டமைக்க வேண்டும்." }, - "excludedDomainsDescAlt": { - "message": "உள்நுழைந்த அனைத்து கணக்குகளுக்கும் இந்த டொமைன்களுக்கான உள்நுழைவு விவரங்களைச் சேமிக்க Bitwarden கேட்காது. மாற்றங்கள் நடைமுறைக்கு வர பக்கத்தை மீண்டும் மீட்டமைக்க வேண்டும்." - }, "blockedDomainsDesc": { "message": "இந்த இணையதளங்களுக்கு தானாக நிரப்புதல் மற்றும் பிற தொடர்புடைய அம்சங்கள் வழங்கப்படாது. மாற்றங்கள் நடைமுறைக்கு வர பக்கத்தை மீண்டும் மீட்டமைக்க வேண்டும்." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "அட்டை எண்" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index c6d9d325e00..336e8783b75 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden will not ask to save login details for these domains. You must refresh the page for changes to take effect." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." - }, "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Card number" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index 19fc6e41ec8..5af1c742f45 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden จะไม่ถามให้บันทึกรายละเอียดการเข้าสู่ระบบสำหรับโดเมนเหล่านี้ คุณต้องรีเฟรชหน้าเว็บเพื่อให้การเปลี่ยนแปลงมีผล" }, - "excludedDomainsDescAlt": { - "message": "Bitwarden จะไม่ถามให้บันทึกรายละเอียดการเข้าสู่ระบบสำหรับโดเมนเหล่านี้สำหรับทุกบัญชีที่เข้าสู่ระบบ คุณต้องรีเฟรชหน้าเว็บเพื่อให้การเปลี่ยนแปลงมีผล" - }, "blockedDomainsDesc": { "message": "การป้อนอัตโนมัติและฟีเจอร์อื่น ๆ ที่เกี่ยวข้องจะไม่พร้อมใช้งานสำหรับเว็บไซต์เหล่านี้ คุณต้องรีเฟรชหน้าเว็บเพื่อให้การเปลี่ยนแปลงมีผล" }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "หมายเลขบัตร" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "องค์กรของคุณไม่ใช้รหัสผ่านหลักในการเข้าสู่ระบบ Bitwarden อีกต่อไป หากต้องการดำเนินการต่อ ให้ยืนยันองค์กรและโดเมน" }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 92b09280cba..33f600fb7a7 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden bu alan adlarında hesaplarınızı kaydetmeyi sormayacaktır. Değişikliklerin etkili olması için sayfayı yenilemelisiniz." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden, oturum açmış tüm hesaplar için bu alan adlarının hesap bilgilerini kaydetmeyi sormayacaktır. Değişikliklerin etkili olması için sayfayı yenilemeniz gerekir." - }, "blockedDomainsDesc": { "message": "Bu siteler için otomatik doldurma ve diğer ilgili özellikler önerilmeyecektir. Değişikliklerin devreye girmesi için sayfayı yenilemelisiniz." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Kart numarası" }, + "errorCannotDecrypt": { + "message": "Hata: Deşifre edilemiyor" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "kullanici@bitwarden.com , kullanici@acme.com" }, + "downloadBitwardenApps": { + "message": "Bitwarden uygulamalarını indir" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Bu Send'i görmek isteyen kişilerin parola girmesi gerekecektir", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index 9f6b376cbc1..b703cfeefce 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden не запитуватиме про збереження даних входу для цих доменів. Потрібно оновити сторінку для застосування змін." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden не запитуватиме про збереження даних входу для цих доменів для всіх облікових записів, до яких виконано вхід. Потрібно оновити сторінку для застосування змін." - }, "blockedDomainsDesc": { "message": "Автозаповнення та інші пов'язані функції не пропонуватимуться для цих вебсайтів. Вам слід оновити сторінку для застосування змін." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Номер картки" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Ваша організація більше не використовує головні паролі для входу в Bitwarden. Щоб продовжити, підтвердіть організацію та домен." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Е-пошту захищено" }, @@ -6134,4 +6137,4 @@ "message": "Особам необхідно ввести пароль для перегляду цього відправлення", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index ad03e96537a..0082ee1ece7 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden sẽ không yêu cầu lưu thông tin đăng nhập cho các miền này. Bạn phải làm mới trang để các thay đổi có hiệu lực." }, - "excludedDomainsDescAlt": { - "message": "Bitwarden sẽ không yêu cầu lưu thông tin đăng nhập cho các tên miền này đối với tất cả tài khoản đã đăng nhập. Bạn phải làm mới trang để các thay đổi có hiệu lực." - }, "blockedDomainsDesc": { "message": "Tự động điền và các tính năng liên quan khác sẽ không được cung cấp cho các trang web này. Bạn phải làm mới trang để các thay đổi có hiệu lực." }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "Số thẻ" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Tổ chức của bạn không còn sử dụng mật khẩu chính để đăng nhập vào Bitwarden. Để tiếp tục, hãy xác minh tổ chức và tên miền." }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "Download Bitwarden apps" + }, "emailProtected": { "message": "Email protected" }, @@ -6134,4 +6137,4 @@ "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index d9b78ca0d50..860a8c09f27 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden 将不会提示保存这些域名的登录信息。您必须刷新页面才能使更改生效。" }, - "excludedDomainsDescAlt": { - "message": "Bitwarden 将不会提示为所有已登录账户保存这些域名的登录信息。您必须刷新页面才能使更改生效。" - }, "blockedDomainsDesc": { "message": "将不会为这些网站提供自动填充和其他相关功能。您必须刷新页面才能使更改生效。" }, @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "卡号" }, + "errorCannotDecrypt": { + "message": "错误:无法解密" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "您的组织已不再使用主密码登录 Bitwarden。要继续,请验证组织和域名。" }, @@ -6127,11 +6127,14 @@ "emailPlaceholder": { "message": "user@bitwarden.com, user@acme.com" }, + "downloadBitwardenApps": { + "message": "下载 Bitwarden App" + }, "emailProtected": { - "message": "电子邮件受保护" + "message": "电子邮箱保护" }, "sendPasswordHelperText": { "message": "个人需要输入密码才能查看此 Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index eade6878396..3f387d935d4 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -10,7 +10,7 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bitwarden 是一款安全、免費、跨平台的密碼管理工具。", + "message": "無論在家、在工作場所或外出時,Bitwarden 都能輕鬆保護您的所有密碼、密碼金鑰及敏感資訊。", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { @@ -29,13 +29,13 @@ "message": "使用通行金鑰登入" }, "unlockWithPasskey": { - "message": "使用通行金鑰解鎖" + "message": "使用密碼金鑰解鎖" }, "useSingleSignOn": { "message": "使用單一登入(SSO)" }, "yourOrganizationRequiresSingleSignOn": { - "message": "您的組織需要單一登入。" + "message": "您的組織要求使用單一登入。" }, "welcomeBack": { "message": "歡迎回來" @@ -44,7 +44,7 @@ "message": "設定一組高強度密碼" }, "finishCreatingYourAccountBySettingAPassword": { - "message": "設定密碼以完成建立您的帳號" + "message": "請設定密碼以完成帳戶建立" }, "enterpriseSingleSignOn": { "message": "企業單一登入(SSO)" @@ -71,7 +71,7 @@ "message": "主密碼提示可在您忘記時幫助您回想主密碼。" }, "masterPassHintText": { - "message": "如果您忘記了密碼,可以傳送密碼提示到您的電子郵件。$CURRENT$ / 最多 $MAXIMUM$ 個字元", + "message": "若您忘記密碼,可將密碼提示傳送至您的電子郵件。\n$CURRENT$/$MAXIMUM$ 個字元上限。", "placeholders": { "current": { "content": "$1", @@ -209,7 +209,7 @@ "message": "自動填入登入資料" }, "autoFillCard": { - "message": "自動填入卡片資料" + "message": "自動填入付款卡" }, "autoFillIdentity": { "message": "自動填入身分資料" @@ -231,7 +231,7 @@ "message": "沒有符合的登入資料" }, "noCards": { - "message": "沒有卡片資料" + "message": "沒有付款卡" }, "noIdentities": { "message": "沒有身分資料" @@ -240,7 +240,7 @@ "message": "新增登入資料" }, "addCardMenu": { - "message": "新增卡片資料" + "message": "新增付款卡" }, "addIdentityMenu": { "message": "新增身分資料" @@ -261,7 +261,7 @@ "message": "新增項目" }, "accountEmail": { - "message": "帳號電子郵件" + "message": "帳戶電子郵件" }, "requestHint": { "message": "取得提示" @@ -2747,9 +2747,6 @@ "excludedDomainsDesc": { "message": "Bitwarden 不會在這些網域要求儲存登入資料。您必須重新整理頁面,變更才會生效。" }, - "excludedDomainsDescAlt": { - "message": "對於所有已登入的帳戶,Bitwarden 不會詢問是否儲存這些網域的登入資訊。您必須重新整理頁面變更才會生效。" - }, "blockedDomainsDesc": { "message": "自動填入及其它相關的功能無法在這些網站上使用。您必須重新整理頁面來使變更生效。" }, @@ -3158,7 +3155,7 @@ "message": "此組織有一項企業政策,會自動將您加入密碼重設。加入後,組織管理員將能變更您的主密碼。" }, "selectFolder": { - "message": "選擇資料夾⋯" + "message": "選擇資料夾…" }, "noFoldersFound": { "message": "未找到資料夾", @@ -3260,19 +3257,19 @@ } }, "vaultTimeoutTooLarge": { - "message": "您的密碼庫逾時時間超過組織設定的限制。" + "message": "您的密碼庫逾時時間超過您所屬組織設定的限制。" }, "vaultExportDisabled": { - "message": "密碼庫匯出已停用" + "message": "密碼庫匯出不可用" }, "personalVaultExportPolicyInEffect": { - "message": "一個或多個組織原則禁止您匯出個人密碼庫。" + "message": "一項或多項組織政策禁止您匯出個人密碼庫。" }, "copyCustomFieldNameInvalidElement": { - "message": "未能找出有效的表單元件。請試試看改用 HTML 檢查功能。" + "message": "無法識別有效的表單元素。請嘗試檢查 HTML。" }, "copyCustomFieldNameNotUnique": { - "message": "找不到唯一識別碼。" + "message": "未找到唯一識別碼。" }, "organizationName": { "message": "組織名稱" @@ -3290,22 +3287,22 @@ "message": "主密碼已移除" }, "leaveOrganizationConfirmation": { - "message": "您確定要離開這個組織嗎?" + "message": "您確定要離開此組織嗎?" }, "leftOrganization": { "message": "您已離開此組織。" }, "toggleCharacterCount": { - "message": "切換字元計數" + "message": "顯示/隱藏字元數" }, "sessionTimeout": { - "message": "您的登入階段已逾時,請返回並嘗試重新登入。" + "message": "您的工作階段已逾時,請返回並重新登入。" }, "exportingPersonalVaultTitle": { - "message": "正匯出個人密碼庫" + "message": "正在匯出個人密碼庫" }, "exportingIndividualVaultDescription": { - "message": "僅匯出與 $EMAIL$ 關聯的個人密碼庫項目。組織密碼庫項目將不包括在內。僅匯出密碼庫項目資訊,不包括關聯的附件。", + "message": "僅匯出與 $EMAIL$ 相關的個人密碼庫項目,不包含組織密碼庫項目。僅匯出項目資訊,不包含相關附件。", "placeholders": { "email": { "content": "$1", @@ -3423,13 +3420,13 @@ "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" }, "plusAddressedEmailDesc": { - "message": "使用您電子郵件提供者的子地址功能。" + "message": "使用您的電子郵件服務提供者的子地址功能。" }, "catchallEmail": { "message": "Catch-all 電子郵件" }, "catchallEmailDesc": { - "message": "使用您的網域設定的 Catch-all 收件匣。" + "message": "使用您網域中已設定的 Catch-all 收件匣。" }, "random": { "message": "隨機" @@ -3444,10 +3441,10 @@ "message": "服務" }, "forwardedEmail": { - "message": "轉寄的電子郵件別名" + "message": "轉寄電子郵件別名" }, "forwardedEmailDesc": { - "message": "使用外部轉寄服務產生一個電子郵件別名。" + "message": "使用外部轉寄服務產生電子郵件別名。" }, "forwarderDomainName": { "message": "電子郵件網域", @@ -3594,7 +3591,7 @@ "message": "API 金鑰" }, "ssoKeyConnectorError": { - "message": "Key Connector 錯誤:請確保 Key Connector 可用且運作正常。" + "message": "Key Connector 錯誤:請確認 Key Connector 可用且運作正常。" }, "premiumSubcriptionRequired": { "message": "需要進階版訂閲" @@ -3603,7 +3600,7 @@ "message": "組織已停用。" }, "disabledOrganizationFilterError": { - "message": "無法存取已停用組織中的項目。請聯絡您組織的擁有者以獲取協助。" + "message": "無法存取已停用組織中的項目。請聯絡組織擁有者以取得協助。" }, "loggingInTo": { "message": "正在登入至 $DOMAIN$", @@ -3618,13 +3615,13 @@ "message": "伺服器版本" }, "selfHostedServer": { - "message": "自架" + "message": "自行託管(自行部署並管理)" }, "thirdParty": { "message": "第三方" }, "thirdPartyServerMessage": { - "message": "已連線至第三方伺服器實作,$SERVERNAME$。 請使用官方伺服器驗證錯誤,或將其報告給第三方伺服器。", + "message": "已連線至第三方伺服器實作:$SERVERNAME$。請使用官方伺服器驗證是否為程式錯誤,或向第三方伺服器回報。", "placeholders": { "servername": { "content": "$1", @@ -3908,10 +3905,10 @@ "message": "記住這個裝置" }, "uncheckIfPublicDevice": { - "message": "若使用公用裝置,請勿勾選" + "message": "若為公用裝置,請取消勾選" }, "approveFromYourOtherDevice": { - "message": "使用其他裝置核准" + "message": "在其他裝置上核准" }, "requestAdminApproval": { "message": "要求管理員核准" @@ -4025,7 +4022,7 @@ "message": "搜尋" }, "inputMinLength": { - "message": "必須輸入至少 $COUNT$ 個字元。", + "message": "輸入內容至少需 $COUNT$ 個字元。", "placeholders": { "count": { "content": "$1", @@ -4034,7 +4031,7 @@ } }, "inputMaxLength": { - "message": "輸入的內容長度不得超過 $COUNT$ 字元。", + "message": "輸入內容不得超過 $COUNT$ 個字元。", "placeholders": { "count": { "content": "$1", @@ -4070,10 +4067,10 @@ } }, "multipleInputEmails": { - "message": "一個或多個電子郵件無效" + "message": "一或多個電子郵件地址無效" }, "inputTrimValidator": { - "message": "輸入不得僅包含空格。", + "message": "輸入內容不得僅包含空白字元。", "description": "Notification to inform the user that a form's input can't contain only whitespace." }, "inputEmail": { @@ -4101,7 +4098,7 @@ } }, "selectPlaceholder": { - "message": "-- 選擇 --" + "message": "-- 請選擇 --" }, "multiSelectPlaceholder": { "message": "-- 輸入以進行篩選 --" @@ -4295,7 +4292,7 @@ "message": "使用 PIN 碼" }, "useBiometrics": { - "message": "用生物識別" + "message": "使用生物辨識" }, "enterVerificationCodeSentToEmail": { "message": "輸入傳送到你的電子郵件的驗證碼。" @@ -4364,7 +4361,7 @@ "message": "選擇匯入檔案的格式" }, "selectImportFile": { - "message": "選擇要匯入的檔案" + "message": "選擇匯入的檔案" }, "chooseFile": { "message": "選擇檔案" @@ -4373,10 +4370,10 @@ "message": "未選擇任何檔案" }, "orCopyPasteFileContents": { - "message": "或複製/貼上要匯入的檔案內容" + "message": "或複製/貼上匯入檔案的內容" }, "instructionsFor": { - "message": "$NAME$ 教學", + "message": "$NAME$ 操作說明", "description": "The title for the import tool instructions.", "placeholders": { "name": { @@ -4386,10 +4383,10 @@ } }, "confirmVaultImport": { - "message": "確認匯入密碼庫" + "message": "確認密碼庫匯入" }, "confirmVaultImportDesc": { - "message": "此檔案受密碼保護,請輸入檔案密碼以匯入資料。" + "message": "此檔案受密碼保護。請輸入檔案密碼以匯入資料。" }, "confirmFilePassword": { "message": "確認檔案密碼" @@ -4413,13 +4410,13 @@ "message": "不會將密碼金鑰複製到拓製的項目中。您想繼續拓製該項目嗎?" }, "logInWithPasskeyQuestion": { - "message": "使用密碼金鑰登入?" + "message": "要使用密碼金鑰登入嗎?" }, "passkeyAlreadyExists": { - "message": "用於這個應用程式的密碼金鑰已經存在。" + "message": "此應用程式已存在密碼金鑰。" }, "noPasskeysFoundForThisApplication": { - "message": "未發現用於這個應用程式的密碼金鑰。" + "message": "此應用程式未發現密碼金鑰。" }, "noMatchingPasskeyLogin": { "message": "您沒有符合該網站的登入資訊。" @@ -4449,16 +4446,16 @@ "message": "密碼金鑰項目" }, "overwritePasskey": { - "message": "要覆寫密碼金鑰嗎?" + "message": "要覆寫目前的密碼金鑰嗎?" }, "overwritePasskeyAlert": { - "message": "該項目已包含密碼金鑰。您確定要覆寫目前的密碼金鑰嗎?" + "message": "此項目已包含密碼金鑰。確定要覆寫目前的密碼金鑰嗎?" }, "featureNotSupported": { "message": "尚未支援此功能" }, "yourPasskeyIsLocked": { - "message": "使用密碼金鑰需要身分驗證。請驗證您的身份以繼續。" + "message": "使用密碼金鑰需要身分驗證。請驗證身分以繼續。" }, "multifactorAuthenticationCancelled": { "message": "多因素驗證已取消" @@ -4467,7 +4464,7 @@ "message": "未找到任何 LastPass 資料" }, "incorrectUsernameOrPassword": { - "message": "使用者名稱或密碼不正確" + "message": "使用者名稱或密碼錯誤" }, "incorrectPassword": { "message": "密碼錯誤" @@ -4509,7 +4506,7 @@ "message": "需要 LastPass 驗證" }, "awaitingSSO": { - "message": "等待 SSO 驗證" + "message": "正在等待 SSO 驗證" }, "awaitingSSODesc": { "message": "請使用您的公司憑證繼續登入。" @@ -4546,7 +4543,7 @@ "message": "目前帳戶" }, "bitwardenAccount": { - "message": "Bitwarden 帳號" + "message": "Bitwarden 帳戶" }, "availableAccounts": { "message": "可用帳戶" @@ -4993,7 +4990,7 @@ "message": "下載 Bitwarden" }, "downloadBitwardenOnAllDevices": { - "message": "在所有裝置中下載 Bitwarden" + "message": "在所有裝置上下載 Bitwarden" }, "getTheMobileApp": { "message": "取得手機應用程式" @@ -5008,7 +5005,7 @@ "message": "在不使用瀏覽器的情況下存取你的密碼庫,然後設定生物辨識解鎖,以加快在桌面應用程式和瀏覽器擴充功能中的解鎖速度。" }, "downloadFromBitwardenNow": { - "message": "立即從 bitwarden.com 下載" + "message": "立即前往 bitwarden.com 下載" }, "getItOnGooglePlay": { "message": "在 Google Play上取得" @@ -5401,10 +5398,10 @@ "message": "企業政策已套用至您的選項中" }, "sshPrivateKey": { - "message": "私密金鑰" + "message": "私鑰" }, "sshPublicKey": { - "message": "公共金鑰" + "message": "公鑰" }, "sshFingerprint": { "message": "指紋" @@ -5431,7 +5428,7 @@ "message": "自訂逾時時間最小為 1 分鐘。" }, "fileSavedToDevice": { - "message": "檔案已儲存至裝置。在您的裝置中管理下載的檔案。" + "message": "檔案已儲存至裝置。請在裝置的下載項目中管理檔案。" }, "showCharacterCount": { "message": "顯示字元數" @@ -5966,6 +5963,9 @@ "cardNumberLabel": { "message": "付款卡號碼" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "您的組織已不再使用主密碼登入 Bitwarden。若要繼續,請驗證組織與網域。" }, @@ -6127,6 +6127,9 @@ "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, + "downloadBitwardenApps": { + "message": "下載 Bitwarden 應用程式" + }, "emailProtected": { "message": "電子郵件已受保護" }, @@ -6134,4 +6137,4 @@ "message": "對方必須輸入密碼才能檢視此 Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." } -} \ No newline at end of file +} From 7c6512c78f1a7730de7aba3bf345d3f03501b945 Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Fri, 13 Feb 2026 10:27:35 +0100 Subject: [PATCH 027/134] Autosync the updated translations (#18962) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/af/messages.json | 3 +++ apps/desktop/src/locales/ar/messages.json | 3 +++ apps/desktop/src/locales/az/messages.json | 3 +++ apps/desktop/src/locales/be/messages.json | 3 +++ apps/desktop/src/locales/bg/messages.json | 3 +++ apps/desktop/src/locales/bn/messages.json | 3 +++ apps/desktop/src/locales/bs/messages.json | 3 +++ apps/desktop/src/locales/ca/messages.json | 3 +++ apps/desktop/src/locales/cs/messages.json | 3 +++ apps/desktop/src/locales/cy/messages.json | 3 +++ apps/desktop/src/locales/da/messages.json | 3 +++ apps/desktop/src/locales/de/messages.json | 3 +++ apps/desktop/src/locales/el/messages.json | 3 +++ apps/desktop/src/locales/en_GB/messages.json | 3 +++ apps/desktop/src/locales/en_IN/messages.json | 3 +++ apps/desktop/src/locales/eo/messages.json | 3 +++ apps/desktop/src/locales/es/messages.json | 3 +++ apps/desktop/src/locales/et/messages.json | 3 +++ apps/desktop/src/locales/eu/messages.json | 3 +++ apps/desktop/src/locales/fa/messages.json | 3 +++ apps/desktop/src/locales/fi/messages.json | 3 +++ apps/desktop/src/locales/fil/messages.json | 3 +++ apps/desktop/src/locales/fr/messages.json | 3 +++ apps/desktop/src/locales/gl/messages.json | 3 +++ apps/desktop/src/locales/he/messages.json | 3 +++ apps/desktop/src/locales/hi/messages.json | 3 +++ apps/desktop/src/locales/hr/messages.json | 3 +++ apps/desktop/src/locales/hu/messages.json | 3 +++ apps/desktop/src/locales/id/messages.json | 3 +++ apps/desktop/src/locales/it/messages.json | 3 +++ apps/desktop/src/locales/ja/messages.json | 3 +++ apps/desktop/src/locales/ka/messages.json | 3 +++ apps/desktop/src/locales/km/messages.json | 3 +++ apps/desktop/src/locales/kn/messages.json | 3 +++ apps/desktop/src/locales/ko/messages.json | 3 +++ apps/desktop/src/locales/lt/messages.json | 3 +++ apps/desktop/src/locales/lv/messages.json | 3 +++ apps/desktop/src/locales/me/messages.json | 3 +++ apps/desktop/src/locales/ml/messages.json | 3 +++ apps/desktop/src/locales/mr/messages.json | 3 +++ apps/desktop/src/locales/my/messages.json | 3 +++ apps/desktop/src/locales/nb/messages.json | 3 +++ apps/desktop/src/locales/ne/messages.json | 3 +++ apps/desktop/src/locales/nl/messages.json | 7 +++-- apps/desktop/src/locales/nn/messages.json | 3 +++ apps/desktop/src/locales/or/messages.json | 3 +++ apps/desktop/src/locales/pl/messages.json | 3 +++ apps/desktop/src/locales/pt_BR/messages.json | 3 +++ apps/desktop/src/locales/pt_PT/messages.json | 21 ++++++++------- apps/desktop/src/locales/ro/messages.json | 3 +++ apps/desktop/src/locales/ru/messages.json | 3 +++ apps/desktop/src/locales/si/messages.json | 3 +++ apps/desktop/src/locales/sk/messages.json | 3 +++ apps/desktop/src/locales/sl/messages.json | 3 +++ apps/desktop/src/locales/sr/messages.json | 3 +++ apps/desktop/src/locales/sv/messages.json | 3 +++ apps/desktop/src/locales/ta/messages.json | 3 +++ apps/desktop/src/locales/te/messages.json | 3 +++ apps/desktop/src/locales/th/messages.json | 3 +++ apps/desktop/src/locales/tr/messages.json | 27 +++++++++++--------- apps/desktop/src/locales/uk/messages.json | 3 +++ apps/desktop/src/locales/vi/messages.json | 3 +++ apps/desktop/src/locales/zh_CN/messages.json | 7 +++-- apps/desktop/src/locales/zh_TW/messages.json | 3 +++ 64 files changed, 217 insertions(+), 25 deletions(-) diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json index e46d70f01dc..ef221f96878 100644 --- a/apps/desktop/src/locales/af/messages.json +++ b/apps/desktop/src/locales/af/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json index eb06f36b20d..bcac0529a8c 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index 179347a6942..f94ff2417cf 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Vaxt bitmə əməliyyatı" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Sessiya vaxt bitməsi" }, diff --git a/apps/desktop/src/locales/be/messages.json b/apps/desktop/src/locales/be/messages.json index 7712e82a251..3467fe20ae8 100644 --- a/apps/desktop/src/locales/be/messages.json +++ b/apps/desktop/src/locales/be/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index cf5ef5750a8..cab21191e37 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Действие при изтичането на времето за достъп" }, + "errorCannotDecrypt": { + "message": "Грешка: не може да се дешифрира" + }, "sessionTimeoutHeader": { "message": "Изтичане на времето за сесията" }, diff --git a/apps/desktop/src/locales/bn/messages.json b/apps/desktop/src/locales/bn/messages.json index 91a41c6d08f..544d88a72a6 100644 --- a/apps/desktop/src/locales/bn/messages.json +++ b/apps/desktop/src/locales/bn/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/bs/messages.json b/apps/desktop/src/locales/bs/messages.json index 25f24d82d1f..289554a237f 100644 --- a/apps/desktop/src/locales/bs/messages.json +++ b/apps/desktop/src/locales/bs/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index 3e3f32f8bd0..7b8d32a798c 100644 --- a/apps/desktop/src/locales/ca/messages.json +++ b/apps/desktop/src/locales/ca/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index b4be94ca123..478343b7e7d 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Akce vypršení časového limitu" }, + "errorCannotDecrypt": { + "message": "Chyba: Nelze dešifrovat" + }, "sessionTimeoutHeader": { "message": "Časový limit relace" }, diff --git a/apps/desktop/src/locales/cy/messages.json b/apps/desktop/src/locales/cy/messages.json index 23aa85a7de8..4feb0181431 100644 --- a/apps/desktop/src/locales/cy/messages.json +++ b/apps/desktop/src/locales/cy/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index 115bb3c3038..bd6a6f4379a 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index 6368e0cd054..205c8e95435 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout-Aktion" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Sitzungs-Timeout" }, diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index a604fc6f9db..97371668dca 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index a0d1ad10120..22b482ed04d 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json index 796cce7e711..42a63ff0db1 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/eo/messages.json b/apps/desktop/src/locales/eo/messages.json index dab5fb211ad..ae37e15d84c 100644 --- a/apps/desktop/src/locales/eo/messages.json +++ b/apps/desktop/src/locales/eo/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index 11cbabc019b..d6210f940b5 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/et/messages.json b/apps/desktop/src/locales/et/messages.json index 6d749dd0eee..8c9476cc69e 100644 --- a/apps/desktop/src/locales/et/messages.json +++ b/apps/desktop/src/locales/et/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json index b32be546075..75c33286fb7 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/fa/messages.json b/apps/desktop/src/locales/fa/messages.json index 01a1a587557..4136edbde06 100644 --- a/apps/desktop/src/locales/fa/messages.json +++ b/apps/desktop/src/locales/fa/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "اقدام وقفه زمانی" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "وقفه زمانی نشست" }, diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index e63cf83956a..b04b741bd3b 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/fil/messages.json b/apps/desktop/src/locales/fil/messages.json index 9087ac83562..c503efc39f9 100644 --- a/apps/desktop/src/locales/fil/messages.json +++ b/apps/desktop/src/locales/fil/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index b10b5fdd675..f04807aaeb9 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Action à l’expiration" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Délai d'expiration de la session" }, diff --git a/apps/desktop/src/locales/gl/messages.json b/apps/desktop/src/locales/gl/messages.json index 69234c1e5bf..76904276732 100644 --- a/apps/desktop/src/locales/gl/messages.json +++ b/apps/desktop/src/locales/gl/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json index 50fb5b7f0df..dbb2533e03e 100644 --- a/apps/desktop/src/locales/he/messages.json +++ b/apps/desktop/src/locales/he/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "פעולת פסק זמן" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "פסק זמן להפעלה" }, diff --git a/apps/desktop/src/locales/hi/messages.json b/apps/desktop/src/locales/hi/messages.json index 97b33dcede1..1bfc0674ffe 100644 --- a/apps/desktop/src/locales/hi/messages.json +++ b/apps/desktop/src/locales/hi/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json index 752df118853..5ef663ab52b 100644 --- a/apps/desktop/src/locales/hr/messages.json +++ b/apps/desktop/src/locales/hr/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Radnja nakon isteka" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Istek sesije" }, diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index 15c701719f9..5440b53e93d 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Időkifutási művelet" }, + "errorCannotDecrypt": { + "message": "Hiba: nem fejthető vissza." + }, "sessionTimeoutHeader": { "message": "Munkamenet időkifutás" }, diff --git a/apps/desktop/src/locales/id/messages.json b/apps/desktop/src/locales/id/messages.json index d62779a48ed..f4de0deff33 100644 --- a/apps/desktop/src/locales/id/messages.json +++ b/apps/desktop/src/locales/id/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index 68c3e824478..9cd4783407d 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Azione al timeout" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Timeout della sessione" }, diff --git a/apps/desktop/src/locales/ja/messages.json b/apps/desktop/src/locales/ja/messages.json index 3ba4aab861a..1d46532a980 100644 --- a/apps/desktop/src/locales/ja/messages.json +++ b/apps/desktop/src/locales/ja/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/ka/messages.json b/apps/desktop/src/locales/ka/messages.json index e12f6df3c01..4618fd024a9 100644 --- a/apps/desktop/src/locales/ka/messages.json +++ b/apps/desktop/src/locales/ka/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/km/messages.json b/apps/desktop/src/locales/km/messages.json index 69234c1e5bf..76904276732 100644 --- a/apps/desktop/src/locales/km/messages.json +++ b/apps/desktop/src/locales/km/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/kn/messages.json b/apps/desktop/src/locales/kn/messages.json index d527d42e75c..3bb7f513701 100644 --- a/apps/desktop/src/locales/kn/messages.json +++ b/apps/desktop/src/locales/kn/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json index f191bf3a5f8..70ffd234941 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json index 6ebb39a8d4f..f703b1f5d53 100644 --- a/apps/desktop/src/locales/lt/messages.json +++ b/apps/desktop/src/locales/lt/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index 4d064653073..e82a4b91487 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Noildzes darbība" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Sesijas noildze" }, diff --git a/apps/desktop/src/locales/me/messages.json b/apps/desktop/src/locales/me/messages.json index 773d54d0629..25bb0cbc816 100644 --- a/apps/desktop/src/locales/me/messages.json +++ b/apps/desktop/src/locales/me/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/ml/messages.json b/apps/desktop/src/locales/ml/messages.json index 6c60486bbaf..43e0dc85fb0 100644 --- a/apps/desktop/src/locales/ml/messages.json +++ b/apps/desktop/src/locales/ml/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/mr/messages.json b/apps/desktop/src/locales/mr/messages.json index 69234c1e5bf..76904276732 100644 --- a/apps/desktop/src/locales/mr/messages.json +++ b/apps/desktop/src/locales/mr/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/my/messages.json b/apps/desktop/src/locales/my/messages.json index b4af8edba98..ddc8bef0241 100644 --- a/apps/desktop/src/locales/my/messages.json +++ b/apps/desktop/src/locales/my/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/nb/messages.json b/apps/desktop/src/locales/nb/messages.json index df9f37574be..792c95eb1ec 100644 --- a/apps/desktop/src/locales/nb/messages.json +++ b/apps/desktop/src/locales/nb/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/ne/messages.json b/apps/desktop/src/locales/ne/messages.json index da1f787edeb..89c3d3ba231 100644 --- a/apps/desktop/src/locales/ne/messages.json +++ b/apps/desktop/src/locales/ne/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index 0a3350bd288..e4b1d6d8abc 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -3755,7 +3755,7 @@ "message": "Ga door met inloggen met de inloggegevens van je bedrijf." }, "importDirectlyFromBrowser": { - "message": "Import directly from browser" + "message": "Direct importeren vanuit browser" }, "browserProfile": { "message": "Browserprofiel" @@ -4439,7 +4439,7 @@ "message": "En meer!" }, "advancedOnlineSecurity": { - "message": "Advanced online security" + "message": "Geavanceerde online beveiliging" }, "upgradeToPremium": { "message": "Opwaarderen naar Premium" @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Time-out actie" }, + "errorCannotDecrypt": { + "message": "Fout: Kan niet ontsleutelen" + }, "sessionTimeoutHeader": { "message": "Sessietime-out" }, diff --git a/apps/desktop/src/locales/nn/messages.json b/apps/desktop/src/locales/nn/messages.json index fb478c5c484..850696f8fcf 100644 --- a/apps/desktop/src/locales/nn/messages.json +++ b/apps/desktop/src/locales/nn/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/or/messages.json b/apps/desktop/src/locales/or/messages.json index 83854ef4406..b63a970e9c2 100644 --- a/apps/desktop/src/locales/or/messages.json +++ b/apps/desktop/src/locales/or/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index bbd204fd385..e2d9fbce4a2 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/pt_BR/messages.json b/apps/desktop/src/locales/pt_BR/messages.json index 7ea0be8ea39..3a055bdb03d 100644 --- a/apps/desktop/src/locales/pt_BR/messages.json +++ b/apps/desktop/src/locales/pt_BR/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Ação do limite de tempo" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Limite de tempo da sessão" }, diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index a6a92896b77..bb99678bde6 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Ação de tempo limite" }, + "errorCannotDecrypt": { + "message": "Erro: Não é possível desencriptar" + }, "sessionTimeoutHeader": { "message": "Tempo limite da sessão" }, @@ -4589,30 +4592,30 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "emailProtected": { - "message": "Email protected" + "message": "E-mail protegido" }, "emails": { - "message": "Emails" + "message": "E-mails" }, "noAuth": { - "message": "Anyone with the link" + "message": "Qualquer pessoa com o link" }, "anyOneWithPassword": { - "message": "Anyone with a password set by you" + "message": "Qualquer pessoa com uma palavra-passe definida por si" }, "whoCanView": { - "message": "Who can view" + "message": "Quem pode ver" }, "specificPeople": { - "message": "Specific people" + "message": "Pessoas específicas" }, "emailVerificationDesc": { - "message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send." + "message": "Após partilhar este Send através do link, os indivíduos terão de verificar o e-mail com um código para poderem ver este Send." }, "enterMultipleEmailsSeparatedByComma": { - "message": "Enter multiple emails by separating with a comma." + "message": "Introduza vários e-mails, separados por vírgula." }, "emailPlaceholder": { - "message": "user@bitwarden.com , user@acme.com" + "message": "utilizador@bitwarden.com , utilizador@acme.com" } } diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json index 21a36891a8b..6b51cb9fecd 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index 62c18eb5272..389f4b37dfd 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Тайм-аут действия" }, + "errorCannotDecrypt": { + "message": "Ошибка: невозможно расшифровать" + }, "sessionTimeoutHeader": { "message": "Тайм-аут сессии" }, diff --git a/apps/desktop/src/locales/si/messages.json b/apps/desktop/src/locales/si/messages.json index db8371037b1..9e8a727ad5d 100644 --- a/apps/desktop/src/locales/si/messages.json +++ b/apps/desktop/src/locales/si/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index 5ba6fd05cfe..471984785d8 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Akcia pri vypršaní časového limitu" }, + "errorCannotDecrypt": { + "message": "Chyba: Nedá sa dešifrovať" + }, "sessionTimeoutHeader": { "message": "Časový limit relácie" }, diff --git a/apps/desktop/src/locales/sl/messages.json b/apps/desktop/src/locales/sl/messages.json index ce93e936c4f..23d9f18fadb 100644 --- a/apps/desktop/src/locales/sl/messages.json +++ b/apps/desktop/src/locales/sl/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index 310a921eae8..1a68810bca3 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Акција тајмаута" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Истек сесије" }, diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index a6a7d3e0261..1dd66c409c0 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Tidsgränsåtgärd" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Sessionstidsgräns" }, diff --git a/apps/desktop/src/locales/ta/messages.json b/apps/desktop/src/locales/ta/messages.json index 04abb91510e..3a7fc795668 100644 --- a/apps/desktop/src/locales/ta/messages.json +++ b/apps/desktop/src/locales/ta/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json index 69234c1e5bf..76904276732 100644 --- a/apps/desktop/src/locales/te/messages.json +++ b/apps/desktop/src/locales/te/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/th/messages.json b/apps/desktop/src/locales/th/messages.json index c1f5ec7fea5..bcff8d0849e 100644 --- a/apps/desktop/src/locales/th/messages.json +++ b/apps/desktop/src/locales/th/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index b47c5d28105..936a68516bd 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -4337,7 +4337,7 @@ "message": "Kısayolu yazın" }, "editAutotypeKeyboardModifiersDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." + "message": "Aşağıdaki değiştirici tuşlardan birini veya ikisini (Ctrl, Alt, Win) ve bir harf kullanın." }, "invalidShortcut": { "message": "Geçersiz kısayol" @@ -4448,44 +4448,47 @@ "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, "continueWithLogIn": { - "message": "Continue with log in" + "message": "Giriş yaparak devam et" }, "doNotContinue": { - "message": "Do not continue" + "message": "Devam etme" }, "domain": { - "message": "Domain" + "message": "Alan Adı" }, "keyConnectorDomainTooltip": { "message": "This domain will store your account encryption keys, so make sure you trust it. If you're not sure, check with your admin." }, "verifyYourOrganization": { - "message": "Verify your organization to log in" + "message": "Giriş yapmak için kuruluşunuzu doğrulayın" }, "organizationVerified": { - "message": "Organization verified" + "message": "Kuruluş doğrulandı" }, "domainVerified": { - "message": "Domain verified" + "message": "Alan adı doğrulandı" }, "leaveOrganizationContent": { - "message": "If you don't verify your organization, your access to the organization will be revoked." + "message": "Kuruluşunuzu doğrulamazsanız, kuruluşa erişiminiz iptal edilecektir." }, "leaveNow": { - "message": "Leave now" + "message": "Şimdi ayrıl" }, "verifyYourDomainToLogin": { - "message": "Verify your domain to log in" + "message": "Giriş yapmak için alan adınızı doğrulayın" }, "verifyYourDomainDescription": { - "message": "To continue with log in, verify this domain." + "message": "Giriş yaparak devam etmek için bu alan adını doğrulayın." }, "confirmKeyConnectorOrganizationUserDescription": { - "message": "To continue with log in, verify the organization and domain." + "message": "Giriş yaparak devam etmek için kuruluşu ve alan adını doğrulayın." }, "sessionTimeoutSettingsAction": { "message": "Zaman aşımı eylemi" }, + "errorCannotDecrypt": { + "message": "Hata: Deşifre edilemiyor" + }, "sessionTimeoutHeader": { "message": "Oturum zaman aşımı" }, diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index ee5537c616c..2b4cfcb7c22 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Timeout action" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Session timeout" }, diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json index 30327917ba5..56c9d9a5c6e 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "Hành động sau khi đóng kho" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "Thời gian hết phiên" }, diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index c0fe02050c6..cf42adba294 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "超时动作" }, + "errorCannotDecrypt": { + "message": "错误:无法解密" + }, "sessionTimeoutHeader": { "message": "会话超时" }, @@ -4589,13 +4592,13 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "emailProtected": { - "message": "电子邮件受保护" + "message": "电子邮箱保护" }, "emails": { "message": "电子邮件" }, "noAuth": { - "message": "任何拥有此链接的人" + "message": "拥有此链接的任何人" }, "anyOneWithPassword": { "message": "拥有您设置的密码的任何人" diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index 0ea329a4409..3157e929e8f 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -4486,6 +4486,9 @@ "sessionTimeoutSettingsAction": { "message": "逾時後動作" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "sessionTimeoutHeader": { "message": "工作階段逾時" }, From 412d1b541da053280b51c31df2f9a487ae7b0faf Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Fri, 13 Feb 2026 12:11:17 +0100 Subject: [PATCH 028/134] Autosync the updated translations (#18963) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/web/src/locales/af/messages.json | 158 +++++++++++++- apps/web/src/locales/ar/messages.json | 158 +++++++++++++- apps/web/src/locales/az/messages.json | 174 ++++++++++++++-- apps/web/src/locales/be/messages.json | 158 +++++++++++++- apps/web/src/locales/bg/messages.json | 156 +++++++++++++- apps/web/src/locales/bn/messages.json | 158 +++++++++++++- apps/web/src/locales/bs/messages.json | 158 +++++++++++++- apps/web/src/locales/ca/messages.json | 158 +++++++++++++- apps/web/src/locales/cs/messages.json | 158 +++++++++++++- apps/web/src/locales/cy/messages.json | 158 +++++++++++++- apps/web/src/locales/da/messages.json | 158 +++++++++++++- apps/web/src/locales/de/messages.json | 156 +++++++++++++- apps/web/src/locales/el/messages.json | 158 +++++++++++++- apps/web/src/locales/en_GB/messages.json | 158 +++++++++++++- apps/web/src/locales/en_IN/messages.json | 158 +++++++++++++- apps/web/src/locales/eo/messages.json | 158 +++++++++++++- apps/web/src/locales/es/messages.json | 158 +++++++++++++- apps/web/src/locales/et/messages.json | 158 +++++++++++++- apps/web/src/locales/eu/messages.json | 158 +++++++++++++- apps/web/src/locales/fa/messages.json | 158 +++++++++++++- apps/web/src/locales/fi/messages.json | 158 +++++++++++++- apps/web/src/locales/fil/messages.json | 158 +++++++++++++- apps/web/src/locales/fr/messages.json | 158 +++++++++++++- apps/web/src/locales/gl/messages.json | 158 +++++++++++++- apps/web/src/locales/he/messages.json | 162 ++++++++++++++- apps/web/src/locales/hi/messages.json | 158 +++++++++++++- apps/web/src/locales/hr/messages.json | 158 +++++++++++++- apps/web/src/locales/hu/messages.json | 156 +++++++++++++- apps/web/src/locales/id/messages.json | 158 +++++++++++++- apps/web/src/locales/it/messages.json | 158 +++++++++++++- apps/web/src/locales/ja/messages.json | 158 +++++++++++++- apps/web/src/locales/ka/messages.json | 158 +++++++++++++- apps/web/src/locales/km/messages.json | 158 +++++++++++++- apps/web/src/locales/kn/messages.json | 158 +++++++++++++- apps/web/src/locales/ko/messages.json | 158 +++++++++++++- apps/web/src/locales/lv/messages.json | 156 +++++++++++++- apps/web/src/locales/ml/messages.json | 158 +++++++++++++- apps/web/src/locales/mr/messages.json | 158 +++++++++++++- apps/web/src/locales/my/messages.json | 158 +++++++++++++- apps/web/src/locales/nb/messages.json | 158 +++++++++++++- apps/web/src/locales/ne/messages.json | 158 +++++++++++++- apps/web/src/locales/nl/messages.json | 242 +++++++++++++++++----- apps/web/src/locales/nn/messages.json | 158 +++++++++++++- apps/web/src/locales/or/messages.json | 158 +++++++++++++- apps/web/src/locales/pl/messages.json | 158 +++++++++++++- apps/web/src/locales/pt_BR/messages.json | 158 +++++++++++++- apps/web/src/locales/pt_PT/messages.json | 158 +++++++++++++- apps/web/src/locales/ro/messages.json | 158 +++++++++++++- apps/web/src/locales/ru/messages.json | 156 +++++++++++++- apps/web/src/locales/si/messages.json | 158 +++++++++++++- apps/web/src/locales/sk/messages.json | 158 +++++++++++++- apps/web/src/locales/sl/messages.json | 158 +++++++++++++- apps/web/src/locales/sr_CS/messages.json | 158 +++++++++++++- apps/web/src/locales/sr_CY/messages.json | 158 +++++++++++++- apps/web/src/locales/sv/messages.json | 156 +++++++++++++- apps/web/src/locales/ta/messages.json | 158 +++++++++++++- apps/web/src/locales/te/messages.json | 158 +++++++++++++- apps/web/src/locales/th/messages.json | 158 +++++++++++++- apps/web/src/locales/tr/messages.json | 250 ++++++++++++++++++----- apps/web/src/locales/uk/messages.json | 158 +++++++++++++- apps/web/src/locales/vi/messages.json | 158 +++++++++++++- apps/web/src/locales/zh_CN/messages.json | 164 ++++++++++++++- apps/web/src/locales/zh_TW/messages.json | 158 +++++++++++++- 63 files changed, 9608 insertions(+), 536 deletions(-) diff --git a/apps/web/src/locales/af/messages.json b/apps/web/src/locales/af/messages.json index fb359d3a02e..7b6c7778d70 100644 --- a/apps/web/src/locales/af/messages.json +++ b/apps/web/src/locales/af/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Minimum Aantal Woorde" }, - "overridePasswordTypePolicy": { - "message": "Password Type", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Suksesvol verwyder" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/ar/messages.json b/apps/web/src/locales/ar/messages.json index 5a58089abf9..65b8578a206 100644 --- a/apps/web/src/locales/ar/messages.json +++ b/apps/web/src/locales/ar/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "راجع كلمات المرور المعرضة للخطر (الضعيفة، المكشوفة، أو المعاد استخدامها) عبر التطبيقات. اختر تطبيقاتك الحرجة لتحديد أولويات إجراءات الأمان لمستخدميك لمعالجة كلمات المرور المعرضة للخطر." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Minimum number of words" }, - "overridePasswordTypePolicy": { - "message": "Password Type", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Removed successfully" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index a9a25f658b5..6c2150ec158 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -15,7 +15,7 @@ "message": "Risk altında heç bir kritik tətbiq yoxdur" }, "critical": { - "message": "Critical ($COUNT$)", + "message": "Kritik ($COUNT$)", "placeholders": { "count": { "content": "$1", @@ -24,7 +24,7 @@ } }, "notCritical": { - "message": "Not critical ($COUNT$)", + "message": "Kritik olmayan ($COUNT$)", "placeholders": { "count": { "content": "$1", @@ -33,7 +33,7 @@ } }, "criticalBadge": { - "message": "Critical" + "message": "Kritik" }, "accessIntelligence": { "message": "Giriş məlumatları" @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "Bu bəndə düzəliş etmə icazəniz yoxdur" }, - "reviewAtRiskPasswords": { - "message": "Proqramlar arasında risk altında olan açarları (zəif, açıq və ya təkrar istifadə olunmuş) nəzərdən keçirin. İstehlakçılarınızın risk altında olan açarları həll etməsinə yönəlmiş təhlükəsizlik tədbirlərinə üstünlük vermək üçün ən vacib proqramlarınızı seçin." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Risk altında olan girişi nəzərdən keçirin" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Müraciətlər kritik olaraq işarələnmədi" }, @@ -275,7 +311,7 @@ "message": "Tətbiq" }, "applications": { - "message": "Applications" + "message": "Tətbiqlər" }, "atRiskPasswords": { "message": "Riskli parollar" @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Minimum söz sayı" }, - "overridePasswordTypePolicy": { - "message": "Parol növü", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$/$SELECTEDCOUNT$ istifadəçi təkrar dəvət edildi. $LIMIT$ dəvət limitinə görə $EXCLUDEDCOUNT$ dəvət edilmədi.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Uğurla çıxarıldı" }, @@ -6987,16 +7079,16 @@ "message": "Bir və ya daha çox təşkilat siyasəti, fərdi seyfi xaricə köçürməyinizi əngəlləyir." }, "activateAutofillPolicy": { - "message": "Activate autofill" + "message": "Avto-doldurmanı aktivləşdir" }, "activateAutofillPolicyDescription": { "message": "Bütün mövcud və yeni üzvlər üçün brauzer uzantısında \"Səhifə yüklənəndə avto-doldur\" ayarını aktivləşdirin." }, "autofillOnPageLoadExploitWarning": { - "message": "Compromised or untrusted websites can exploit autofill on page load." + "message": "Təhlükəli və ya güvənilməyən veb saytlar, səhifə yüklənərkən avto-doldurmanı sui-istifadə edə bilər." }, "learnMoreAboutAutofillPolicy": { - "message": "Learn more about autofill" + "message": "Avto-doldurma barədə daha ətraflı" }, "selectType": { "message": "SSO növü seçin" @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Tapşırıq təyin et" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Parol dəyişdirmə bildirişlərini göndər" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Element arxivə göndərildi" }, - "itemsWereSentToArchive": { - "message": "Elementlər arxivə göndərildi" - }, "itemWasUnarchived": { "message": "Element arxivdən çıxarıldı" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Davam etmək istədiyinizə əminsiniz?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "İstifadəçi doğrulaması uğursuz oldu." }, @@ -12771,7 +12866,7 @@ "message": "Anbar sahəsini xaric etdiyiniz zaman, növbəti hesabınıza avtomatik olaraq köçürüləcək mütənasib hesab krediti alacaqsınız." }, "ownerBadgeA11yDescription": { - "message": "Owner, $OWNER$, show all items owned by $OWNER$", + "message": "Sahibi, $OWNER$, $OWNER$ sahibi olduğu bütün elementləri göstər", "placeholders": { "owner": { "content": "$1", @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "bir istifadəçi üçün" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index cfe2bf4fa23..ef00d46b4c0 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "Праглядайце паролі, якія знаходзяцца ў зоне рызыкі (ненадзейныя, скампраметаваныя або паўторна выкарыстаныя) ва ўсіх вашых праграмах. Выберыце найбольш крытычныя праграмы для вызначэння прыярытэту бяспекі дзеянняў для вашых карыстальнікаў, якія выкарыстоўваюць рызыкоўныя паролі." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Мінімум лічбаў або слоў" }, - "overridePasswordTypePolicy": { - "message": "Password Type", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Паспяхова выдалена" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index 44fc1b675cb..e32b2b3d5f1 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "Нямате право за редактиране на този елемент" }, - "reviewAtRiskPasswords": { - "message": "Прегледайте паролите в риск (слаби, преизползвани или разобличени) в различните приложения. Изберете най-важните си приложения, за да дадете приоритет на действията по сигурността за потребителите си, така че те да обърнат внимание на паролите си в риск." + "reviewAccessIntelligence": { + "message": "Прегледайте докладите по сигурността, за да забележите и отстраните евентуалните рискове свързани с удостоверителните данни, преди те да ескалират." }, "reviewAtRiskLoginsPrompt": { "message": "Преглед на елементите за вписване в риск" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ приложения са отбелязани като важни", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ приложения са отбелязани като неважни", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Отбелязване на $COUNT$ като важни", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Отбелязване на $COUNT$ като неважни", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Приложенията не успяха да бъдат отбелязани като важни" }, @@ -5394,7 +5430,7 @@ "minimumNumberOfWords": { "message": "Минимален брой думи" }, - "overridePasswordTypePolicy": { + "passwordTypePolicyOverride": { "message": "Тип парола", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "Изпратена е 1 покана" + }, + "bulkReinviteSentToast": { + "message": "Изпратени са $COUNT$ покани", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ от $SELECTEDCOUNT$ повторно поканени потребители. $EXCLUDEDCOUNT$ не бяха поканени, поради ограничението от $LIMIT$.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "Изпратени са $COUNT$ от $TOTAL$ покани…", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Оставете тази страница отворена, докато приключи изпращането на поканите." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ покани не бяха изпратени", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 покана не беше изпратена" + }, + "bulkReinviteFailureDescription": { + "message": "Възникна грешка при изпращането на покани до $COUNT$ от общо $TOTAL$ членове. Опитайте да изпратите поканите отново, а ако този проблем се повтаря,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Опитайте да изпратите отново" + }, "bulkRemovedMessage": { "message": "Успешно премахване" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Назначаване на задачи" }, + "allTasksAssigned": { + "message": "Всички задачи бяха разпределени" + }, "assignSecurityTasksToMembers": { "message": "Изпращане на известия за промяна на пароли" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Елементът беше преместен в архива" }, - "itemsWereSentToArchive": { - "message": "Елементите бяха преместени в архива" - }, "itemWasUnarchived": { "message": "Елементът беше изваден от архива" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Наистина ли искате да продължите?" }, + "errorCannotDecrypt": { + "message": "Грешка: не може да се дешифрира" + }, "userVerificationFailed": { "message": "Проверката на потребителя беше неуспешна." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Надграждане до екипния план" + }, + "upgradeToEnterprise": { + "message": "Надграждане до плана за големи организации" + }, + "upgradeShareEvenMore": { + "message": "Споделяйте още повече със Семейния план, или преминете към подсилената защита на паролите с Екипния план или този за големи организации" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Цените не включват данък и се заплащат веднъж годишно." + }, + "invoicePreviewErrorMessage": { + "message": "Възникна грешка при създаването на предварителния вид на фактурата." + }, + "planProratedMembershipInMonths": { + "message": "Пропорционално членство в план „$PLAN$“ ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Членство в план за големи организации" + }, + "teamsMembership": { + "message": "Членство в екипен план" + }, + "plansUpdated": { + "message": "Вие надградихте до $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "Възникна грешка при обновяването на разплащателния метод." } } diff --git a/apps/web/src/locales/bn/messages.json b/apps/web/src/locales/bn/messages.json index 6175f78b814..53f29d874e7 100644 --- a/apps/web/src/locales/bn/messages.json +++ b/apps/web/src/locales/bn/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Minimum number of words" }, - "overridePasswordTypePolicy": { - "message": "Password Type", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Removed successfully" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/bs/messages.json b/apps/web/src/locales/bs/messages.json index 53570e37935..1ae5523c9ac 100644 --- a/apps/web/src/locales/bs/messages.json +++ b/apps/web/src/locales/bs/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Minimum number of words" }, - "overridePasswordTypePolicy": { - "message": "Password Type", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Removed successfully" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index 010426e3c55..88e4b0569d6 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "Reviseu les contrasenyes de risc (febles, exposades o reutilitzades) a totes les aplicacions. Seleccioneu les aplicacions més crítiques per prioritzar les accions de seguretat perquè els usuaris aborden les contrasenyes de risc." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Número mínim de paraules" }, - "overridePasswordTypePolicy": { - "message": "Password Type", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Suprimit correctament" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index 84139941a7b..d85f3fada18 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "Nemáte oprávnění upravit tuto položku" }, - "reviewAtRiskPasswords": { - "message": "Zkontrolujte ohrožená hesla (slabá, odhalená nebo opakovaně používaná) ve všech aplikacích. Vyberte nejkritičtější aplikace a stanovte priority bezpečnostních opatření pro uživatele, abyste se vypořádali s ohroženými hesly." + "reviewAccessIntelligence": { + "message": "Prohlédněte si bezpečnostní zprávy, abyste odhalili a opravili rizika související s přihlašovacími údaji, než se zhorší." }, "reviewAtRiskLoginsPrompt": { "message": "Zkontrolovat ohrožená přihlášení" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ aplikací označených jako kritických", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ aplikací označených jako nekritických", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Označit $COUNT$ jako kritických", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Označit $COUNT$ jako nekritických", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Nepodařilo se označit aplikace jako kritické" }, @@ -3548,7 +3584,7 @@ "message": "Pro osobní použití a sdílení s rodinou či přáteli." }, "planNameTeams": { - "message": "Týmy" + "message": "Teams" }, "planDescTeams": { "message": "Pro společnosti a různé týmy." @@ -5394,7 +5430,7 @@ "minimumNumberOfWords": { "message": "Minimální počet slov" }, - "overridePasswordTypePolicy": { + "passwordTypePolicyOverride": { "message": "Typ hesla", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 pozvánka odeslána" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ pozvánek odesláno", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ z $SELECTEDCOUNT$ uživatelů bylo znovu pozváno. $EXCLUDEDCOUNT$ nebylo pozváno z důvodu limitu pozvánky: $LIMIT$.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ z $TOTAL$ pozvánek odesláno...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Ponechte tuto stránku otevřenou, dokud nebude vše odesláno." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ pozvánek nebylo odesláno", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 pozvánka nebyla odeslána" + }, + "bulkReinviteFailureDescription": { + "message": "Při odesílání pozvánek pro $COUNT$ z $TOTAL$ členů došlo k chybě. Zkuste je odeslat znovu a pokud problém přetrvává,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Úspěšně odebráno" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Přiřadit úkoly" }, + "allTasksAssigned": { + "message": "Všechny úkoly byly přiřazeny" + }, "assignSecurityTasksToMembers": { "message": "Odeslat oznámení pro změnu hesla" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Položka byla přesunuta do archivu" }, - "itemsWereSentToArchive": { - "message": "Položky byly přesunuty do archivu" - }, "itemWasUnarchived": { "message": "Položka byla odebrána z archivu" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Opravdu chcete pokračovat?" }, + "errorCannotDecrypt": { + "message": "Chyba: Nelze dešifrovat" + }, "userVerificationFailed": { "message": "Ověření uživatele se nezdařilo." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "za uživatele" + }, + "upgradeToTeams": { + "message": "Aktualizovat na Teams" + }, + "upgradeToEnterprise": { + "message": "Aktualizovat na Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Sdílejte ještě více s rodinami, nebo získejte mocné, důvěryhodné zabezpečení s Teams nebo Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Ceny nezahrnují daň a účtují se každoročně." + }, + "invoicePreviewErrorMessage": { + "message": "Při generování náhledu faktury došlo k chybě." + }, + "planProratedMembershipInMonths": { + "message": "Poměrná část členství $PLAN$: ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Kredit předplatného Premium" + }, + "enterpriseMembership": { + "message": "Členství Enterprise" + }, + "teamsMembership": { + "message": "Členství Teams" + }, + "plansUpdated": { + "message": "Aktualizovali jste na $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "Při aktualizaci Vaší platební metody došlo k chybě." } } diff --git a/apps/web/src/locales/cy/messages.json b/apps/web/src/locales/cy/messages.json index 809021b63a3..c566ffaf831 100644 --- a/apps/web/src/locales/cy/messages.json +++ b/apps/web/src/locales/cy/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Minimum number of words" }, - "overridePasswordTypePolicy": { - "message": "Password Type", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Removed successfully" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index fd206ae10cf..42fbf0cf7c4 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "Gennemgå risikobetonede adgangskoder (svage, eksponerede eller genbrugte) på tværs af applikationer. Vælg de mest kritiske applikationer for at prioritere sikkerhedshandlinger for brugerne til at håndtere risikobetonede adgangskoder." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Minimum antal ord" }, - "overridePasswordTypePolicy": { - "message": "Adgangskodetype", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Hermed fjernet" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index 016ed2c73f4..89672d615dc 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "Du bist nicht berechtigt, diesen Eintrag zu bearbeiten" }, - "reviewAtRiskPasswords": { - "message": "Überprüfe gefährdete Passwörter (schwach, kompromittiert oder wiederverwendet) in allen Anwendungen. Wähle deine kritischsten Anwendungen aus, um die Sicherheitsmaßnahmen für deine Benutzer zu priorisieren, um gefährdete Passwörter zu beseitigen." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Gefährdete Zugangsdaten überprüfen" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Anwendungen konnten nicht als kritisch markiert werden" }, @@ -5394,7 +5430,7 @@ "minimumNumberOfWords": { "message": "Mindestanzahl an Wörtern" }, - "overridePasswordTypePolicy": { + "passwordTypePolicyOverride": { "message": "Passworttyp", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ von $SELECTEDCOUNT$ Benutzern erneut eingeladen. $EXCLUDEDCOUNT$ wurden wegen des Einladungslimits von $LIMIT$ nicht eingeladen.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Erfolgreich entfernt" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Aufgaben zuweisen" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Benachrichtigungen zum Ändern von Passwörtern senden" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Eintrag wurde ins Archiv verschoben" }, - "itemsWereSentToArchive": { - "message": "Einträge wurden ins Archiv verschoben" - }, "itemWasUnarchived": { "message": "Eintrag wird nicht mehr archiviert" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Bist du sicher, dass du fortfahren möchtest?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "Benutzerverifizierung fehlgeschlagen." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "pro Benutzer" + }, + "upgradeToTeams": { + "message": "Auf Teams upgraden" + }, + "upgradeToEnterprise": { + "message": "Auf Enterprise upgraden" + }, + "upgradeShareEvenMore": { + "message": "Teile noch mehr mit Families oder erhalte eine leistungsstarke und vertrauenswürdige Passwortsicherheit mit Teams oder Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Die Preise enthalten keine Steuern und werden jährlich in Rechnung gestellt." + }, + "invoicePreviewErrorMessage": { + "message": "Beim Generieren der Rechnungsvorschau ist ein Fehler aufgetreten." + }, + "planProratedMembershipInMonths": { + "message": "Anteilige $PLAN$-Mitgliedschaft ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium-Abonnement-Guthaben" + }, + "enterpriseMembership": { + "message": "Enterprise-Mitgliedschaft" + }, + "teamsMembership": { + "message": "Teams-Mitgliedschaft" + }, + "plansUpdated": { + "message": "Du hast ein Upgrade auf $PLAN$ durchgeführt!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "Beim Aktualisieren deiner Zahlungsmethode ist ein Fehler aufgetreten." } } diff --git a/apps/web/src/locales/el/messages.json b/apps/web/src/locales/el/messages.json index cda6c1eddb2..0aef25ee9cb 100644 --- a/apps/web/src/locales/el/messages.json +++ b/apps/web/src/locales/el/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "Ελέξτε τους κωδικούς πρόσβασης (αδύναμους, εκτεθειμένους ή επαναχρησιμοποιούμενους) σε όλες τις εφαρμογές. Επιλέξτε τις πιο κρίσιμες εφαρμογές σας για να δώσετε προτεραιότητα στις ενέργειες ασφαλείας για τους χρήστες σας ώστε να αντιμετωπίσουν τους εκτεθειμένους κωδικούς πρόσβασης." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Ελάχιστος Αριθμός Χαρακτήρων" }, - "overridePasswordTypePolicy": { - "message": "Τύπος κωδικού πρόσβασης", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Καταργήθηκε με επιτυχία" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index 45a22128710..1617bfb4580 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritise security actions for your users to address at-risk passwords." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Minimum number of words" }, - "overridePasswordTypePolicy": { - "message": "Password Type", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Removed successfully" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/en_IN/messages.json b/apps/web/src/locales/en_IN/messages.json index 33e6c0385ea..8845ef9f042 100644 --- a/apps/web/src/locales/en_IN/messages.json +++ b/apps/web/src/locales/en_IN/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritise security actions for your users to address at-risk passwords." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Minimum number of words" }, - "overridePasswordTypePolicy": { - "message": "Password Type", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Removed successfully" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/eo/messages.json b/apps/web/src/locales/eo/messages.json index b163dfc8603..1e289a324fe 100644 --- a/apps/web/src/locales/eo/messages.json +++ b/apps/web/src/locales/eo/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Minimuma Nombro de Vortoj" }, - "overridePasswordTypePolicy": { - "message": "Password Type", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Removed successfully" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/es/messages.json b/apps/web/src/locales/es/messages.json index 1c44f266bd5..2d85309144f 100644 --- a/apps/web/src/locales/es/messages.json +++ b/apps/web/src/locales/es/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Número mínimo de palabras" }, - "overridePasswordTypePolicy": { - "message": "Tipo de Contraseña", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Eliminado con éxito" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/et/messages.json b/apps/web/src/locales/et/messages.json index 10e067afadd..60a2690785c 100644 --- a/apps/web/src/locales/et/messages.json +++ b/apps/web/src/locales/et/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Minimaalne sõnade arv" }, - "overridePasswordTypePolicy": { - "message": "Parooli Tüüp", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Edukalt eemaldatud" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/eu/messages.json b/apps/web/src/locales/eu/messages.json index 8d4192cdc35..a75f95994c0 100644 --- a/apps/web/src/locales/eu/messages.json +++ b/apps/web/src/locales/eu/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Gutxieneko hitz kopurua" }, - "overridePasswordTypePolicy": { - "message": "Password Type", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Behar bezala kendua" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/fa/messages.json b/apps/web/src/locales/fa/messages.json index 89ef17fa870..5c046211648 100644 --- a/apps/web/src/locales/fa/messages.json +++ b/apps/web/src/locales/fa/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "شما مجاز به ویرایش این مورد نیستید" }, - "reviewAtRiskPasswords": { - "message": "کلمات عبور در معرض خطر (ضعیف، افشا شده یا تکراری) را در برنامه‌ها بررسی کنید. برنامه‌های حیاتی خود را انتخاب کنید تا اقدامات امنیتی را برای کاربران‌تان اولویت‌بندی کنید و به کلمات عبور در معرض خطر رسیدگی کنید." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "بررسی ورودهای پرخطر" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "علامت‌گذاری برنامه‌ها به عنوان مهم ناموفق بود" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "حداقل تعداد کلمات" }, - "overridePasswordTypePolicy": { - "message": "نوع کلمه عبور", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "با موفقیت حذف شد" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/fi/messages.json b/apps/web/src/locales/fi/messages.json index 16a7472fce2..7cc931fd9f8 100644 --- a/apps/web/src/locales/fi/messages.json +++ b/apps/web/src/locales/fi/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Sanojen vähimmäismäärä" }, - "overridePasswordTypePolicy": { - "message": "Salasanan tyyppi", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Poisto onnistui." }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/fil/messages.json b/apps/web/src/locales/fil/messages.json index a96ee6ad2df..415793a4b98 100644 --- a/apps/web/src/locales/fil/messages.json +++ b/apps/web/src/locales/fil/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Minimum na bilang ng mga salita" }, - "overridePasswordTypePolicy": { - "message": "Password Type", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Matagumpay na tinanggal" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index 7910f9e5816..eb9b2d1c915 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "Vous n'avez pas l'autorisation de modifier cet élément" }, - "reviewAtRiskPasswords": { - "message": "Examinez les mots de passe à risque (faibles, exposés ou réutilisés) à travers les applications. Sélectionnez vos applications les plus critiques pour prioriser les actions de sécurité pour que vos utilisateurs s'occupent des mots de passe à risque." + "reviewAccessIntelligence": { + "message": "Examinez les rapports de sécurité pour trouver et corriger les risques liés aux identifiants avant de les escalader." }, "reviewAtRiskLoginsPrompt": { "message": "Examiner les identifiants à risque" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marquées comme critiques", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marquées comme non critiques", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Marquer $COUNT$ comme critique(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Marquer $COUNT$ comme non critique(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Échec du marquage de l'application comme étant critique" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Nombre minimum de mots" }, - "overridePasswordTypePolicy": { - "message": "Type de Mot de Passe", + "passwordTypePolicyOverride": { + "message": "Type de mot de passe", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation envoyée" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations envoyées", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ des utilisateurs de $SELECTEDCOUNT$ ont été ré-invités. $EXCLUDEDCOUNT$ n'ont pas été invités en raison de la limite d'invitation de $LIMIT$.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ de $TOTAL$ invitations envoyées...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Garder cette page ouverte jusqu'à ce que tout soit envoyé." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations non pas été envoyées", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation n'a pas été envoyée" + }, + "bulkReinviteFailureDescription": { + "message": "Une erreur s'est produite lors de l'envoi des invitations à $COUNT$ des $TOTAL$ membres. Essayez d'envoyer à nouveau et si le problème persiste,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Essayez d'envoyer à nouveau" + }, "bulkRemovedMessage": { "message": "Supprimé avec succès" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assigner des tâches" }, + "allTasksAssigned": { + "message": "Toutes les tâches ont été assignées" + }, "assignSecurityTasksToMembers": { "message": "Envoyer des notifications pour modifier les mots de passe" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "L'élément a été envoyé à l'archive" }, - "itemsWereSentToArchive": { - "message": "Les éléments ont été envoyés à l'archive" - }, "itemWasUnarchived": { "message": "L'élément a été désarchivé" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Êtes-vous sûr de vouloir continuer ?" }, + "errorCannotDecrypt": { + "message": "Erreur: Impossible de déchiffrer" + }, "userVerificationFailed": { "message": "La vérification de l'utilisateur a échoué." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "par utilisateur" + }, + "upgradeToTeams": { + "message": "Mettre à niveau vers Équipes" + }, + "upgradeToEnterprise": { + "message": "Mettre à niveau vers Entreprise" + }, + "upgradeShareEvenMore": { + "message": "Partagez encore plus avec Familles, ou obtenez une sécurité de mot de passe puissante et fiable avec Équipes ou Entreprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Les prix excluent les taxes et sont facturés annuellement." + }, + "invoicePreviewErrorMessage": { + "message": "Une erreur s'est produite lors de la génération de l'aperçu de la facture." + }, + "planProratedMembershipInMonths": { + "message": "Adhésion à $PLAN$ au prorata ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Crédit d'abonnement Premium" + }, + "enterpriseMembership": { + "message": "Adhésion à Entreprise" + }, + "teamsMembership": { + "message": "Adhésion à Équipes" + }, + "plansUpdated": { + "message": "Vous avez mis à niveau vers $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "Une erreur s'est produite lors de la mise à jour de votre mode de paiement." } } diff --git a/apps/web/src/locales/gl/messages.json b/apps/web/src/locales/gl/messages.json index 9fcece92871..8357f5d0747 100644 --- a/apps/web/src/locales/gl/messages.json +++ b/apps/web/src/locales/gl/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Minimum number of words" }, - "overridePasswordTypePolicy": { - "message": "Password Type", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Removed successfully" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/he/messages.json b/apps/web/src/locales/he/messages.json index 90acb236f87..b075b91a860 100644 --- a/apps/web/src/locales/he/messages.json +++ b/apps/web/src/locales/he/messages.json @@ -39,7 +39,7 @@ "message": "מודיעין גישות" }, "noApplicationsMatchTheseFilters": { - "message": "No applications match these filters" + "message": "אין יישומים התואמים למסננים האלה" }, "passwordRisk": { "message": "סיכון סיסמה" @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "אין לך הרשאות לערוך את הפריט הזה" }, - "reviewAtRiskPasswords": { - "message": "סקור סיסמאות בסיכון (חלשות, חשופות, או משומשות) בין יישומים. בחר את היישומים הכי קריטיים שלך על מנת לתעדף פעולות אבטחה עבור המשתמשים שלך כדי לטפל בסיסמאות בסיכון." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "סקור כניסות בסיכון" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ יישומים מסומנים כקריטיים", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ יישומים מסומנים כלא-קריטיים", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "סימון $COUNT$ כקריטיים", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "סימון $COUNT$ כלא-קריטיים", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "סימון יישומים כקריטיים נכשל" }, @@ -275,7 +311,7 @@ "message": "יישום" }, "applications": { - "message": "Applications" + "message": "יישומים" }, "atRiskPasswords": { "message": "סיסמאות בסיכון" @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "מספר מינימלי של מילים" }, - "overridePasswordTypePolicy": { - "message": "סוג סיסמה", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "הוסרו בהצלחה" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "הקצה משימות" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "הפריט נשלח לארכיון" }, - "itemsWereSentToArchive": { - "message": "פריטים שנשלחו לארכיון" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/hi/messages.json b/apps/web/src/locales/hi/messages.json index cf0a3d625c4..4e261d672bb 100644 --- a/apps/web/src/locales/hi/messages.json +++ b/apps/web/src/locales/hi/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Minimum number of words" }, - "overridePasswordTypePolicy": { - "message": "Password Type", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Removed successfully" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/hr/messages.json b/apps/web/src/locales/hr/messages.json index c01086848f2..ced4fc03b1c 100644 --- a/apps/web/src/locales/hr/messages.json +++ b/apps/web/src/locales/hr/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "Nemaš dozvolu za uređivanje ove stavke" }, - "reviewAtRiskPasswords": { - "message": "Pregledaj rizične lozinke (slabe, izložene ili ponovno korištene) u svim aplikacijama. Odaberi svoje najkritičnije aplikacije za davanje prioriteta sigurnosnim radnjama da tvoji korisnici riješe rizične lozinke." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Pregledaj rizične prijave" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Nije uspjelo označavanje aplikacija kao kritičnih" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Najmanji broj riječi" }, - "overridePasswordTypePolicy": { - "message": "Vrsta lozinke", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ od $SELECTEDCOUNT$ korisnika ponovno pozvano. $EXCLUDEDCOUNT$ nije pozvatno zbog ograničenja poziva ($LIMIT$).", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Uspješno uklonjeno" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Dodijeli zadatke" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Pošalji podsjetnike za promjenu lozinki" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Stavka poslana u arhivu" }, - "itemsWereSentToArchive": { - "message": "Stavke poslane u arhivu" - }, "itemWasUnarchived": { "message": "Stavka vraćena iz arhive" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Sigurno želiš nastaviti?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index d540c59df3a..f4c9144cc6d 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "Nincs jogosulltság ezen elem szerkesztéséhez." }, - "reviewAtRiskPasswords": { - "message": "Tekintsük meg a veszélyeztetett jelszavakat (gyenge, nyilvános vagy újrafelhasznált) az alkalmazásokban. Válasszuk ki a legkritikusabb alkalmazásokat, hogy előnyben részesítsük a biztonsági műveleteket a felhasználók számára a veszélyeztetett jelszavak kezeléséhez." + "reviewAccessIntelligence": { + "message": "Tekintsük át a biztonsági jelentéseket, hogy megtaláljuk és kijavítsuk a hitelesítő adatok kockázatait, mielőtt azok fokozódnának." }, "reviewAtRiskLoginsPrompt": { "message": "Kockázatos bejelentkezések áttekintése" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ alkalmazás kritikusként lett megjelölve.", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ alkalmazás nem kritikusként lett megjelölve.", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "$COUNT$ megjelölése kritikusként", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "$COUNT$ megjelölése nem kritikusként", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Nem sikerült kritikusként megjelölni a kérelmeket." }, @@ -5394,7 +5430,7 @@ "minimumNumberOfWords": { "message": "Szavak minimális száma" }, - "overridePasswordTypePolicy": { + "passwordTypePolicyOverride": { "message": "Jelszótípus", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 meghívó lett elküldve." + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ meghívó lett elküldve.", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ / $SELECTEDCOUNT$ felhasználó ismételten meghívásra került. $EXCLUDEDCOUNT$ nem kapott meghívást $LIMIT$ meghívási korlát miatt.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ / $TOTAL$ meghívó lett elküldve.", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Tartsuk nyitva ezt az oldalt, amíg az összes elküldésre kerül." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ meghívó nem lett elküldve.", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1meghívó nem lett elküldve." + }, + "bulkReinviteFailureDescription": { + "message": "Hiba történt a meghívók elküldésekor $COUNT$ / $TOTAL$ részére. Próbáljuk meg újra elküldeni és ha a probléma folytatódik,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Próbáljuk újra elküldeni" + }, "bulkRemovedMessage": { "message": "Az eltávolítás sikeres volt." }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Feladatok hozzárendelése" }, + "allTasksAssigned": { + "message": "Az összes feladat hozzárendelésre került." + }, "assignSecurityTasksToMembers": { "message": "Értesítések küldése a jelszavak megváltoztatásához" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Az elem az archivumba került." }, - "itemsWereSentToArchive": { - "message": "Az elemek az archivumba kerültek." - }, "itemWasUnarchived": { "message": "Az elem visszavételre került az archivumból." }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Biztos folytatni szeretnénk?" }, + "errorCannotDecrypt": { + "message": "Hiba: nem fejthető vissza." + }, "userVerificationFailed": { "message": "A felhasználó ellenőrzése sikertelen volt." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "felhasználónként" + }, + "upgradeToTeams": { + "message": "Áttérés Csapatok csomagba" + }, + "upgradeToEnterprise": { + "message": "Áttérés Vállalati csomagba" + }, + "upgradeShareEvenMore": { + "message": "Osszunk meg még többet a Családi csomaggal vagy kapjunk hatékony, megbízható jelszóbiztonságot a Csapatok vagy a Vállalati segítségével." + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Az árak nem tartalmazzák az adót és évente kerülnek számlázásra." + }, + "invoicePreviewErrorMessage": { + "message": "Hiba történt a számla előnézet generálása közben." + }, + "planProratedMembershipInMonths": { + "message": "Arányos $PLAN$ tagság ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Prémium előfizetés jóváírás" + }, + "enterpriseMembership": { + "message": "Vállalati tagság" + }, + "teamsMembership": { + "message": "Csapatok tagság" + }, + "plansUpdated": { + "message": "Megtörtént az áttérés $PLAN$ csomagra!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "Hiba történt a fizetési mód frissítésekor." } } diff --git a/apps/web/src/locales/id/messages.json b/apps/web/src/locales/id/messages.json index bffb574e894..270b8624c24 100644 --- a/apps/web/src/locales/id/messages.json +++ b/apps/web/src/locales/id/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "Anda tidak memiliki izin untuk mengubah item ini" }, - "reviewAtRiskPasswords": { - "message": "Tinjau sandi berisiko (lemah, bocor, atau digunakan ulang) lintas aplikasi. Pilih aplikasi paling genting Anda untuk mengutamakan aksi keamanan untuk pengguna Anda guna menangani sandi berisiko." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Tinjau login yang berisiko" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Gagal menandai aplikasi sebagai penting" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Jumlah kata minimum" }, - "overridePasswordTypePolicy": { - "message": "Jenis Sandi", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Penghapusan sukses" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/it/messages.json b/apps/web/src/locales/it/messages.json index 691c22c2912..c2fb3effdf0 100644 --- a/apps/web/src/locales/it/messages.json +++ b/apps/web/src/locales/it/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "Non hai il permesso per modificare questo elemento" }, - "reviewAtRiskPasswords": { - "message": "Controlla le password a rischio (deboli, esposte o riutilizzate). Seleziona le applicazioni critiche per determinare la priorità delle azioni di sicurezza." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Rivedi i login a rischio" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Impossibile contrassegnare le applicazioni come critiche" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Numero minimo di parole" }, - "overridePasswordTypePolicy": { - "message": "Tipo di password", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ utenti su $SELECTEDCOUNT$ ri-invitati. $EXCLUDEDCOUNT$ non hanno ricevuto l'invito a causa del limite di $LIMIT$ inviti.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Rimosso" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assegna attività" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Invia notifiche ai membri per invitarli a modificare le loro password" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Elemento archiviato" }, - "itemsWereSentToArchive": { - "message": "Elementi archiviati" - }, "itemWasUnarchived": { "message": "Elemento estratto dall'archivio" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Vuoi davvero continuare?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "Verifica dell'utente non riuscita." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index 963e59c4ffc..cd78e0a269a 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "危険なパスワード(強度が低い、流出済み、再利用)を、アプリをまたいで調査します。特に重要なアプリを選択して、危険なパスワードに優先対応するようユーザーに促しましょう。" + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "文字の最小数" }, - "overridePasswordTypePolicy": { - "message": "パスワードの種類", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "削除しました " }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/ka/messages.json b/apps/web/src/locales/ka/messages.json index 73cc59d65e1..6de5179793d 100644 --- a/apps/web/src/locales/ka/messages.json +++ b/apps/web/src/locales/ka/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Minimum number of words" }, - "overridePasswordTypePolicy": { - "message": "Password Type", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Removed successfully" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/km/messages.json b/apps/web/src/locales/km/messages.json index 2e96d9b844d..b95256dfacd 100644 --- a/apps/web/src/locales/km/messages.json +++ b/apps/web/src/locales/km/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Minimum number of words" }, - "overridePasswordTypePolicy": { - "message": "Password Type", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Removed successfully" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/kn/messages.json b/apps/web/src/locales/kn/messages.json index 2b6adb295b0..7a378184a39 100644 --- a/apps/web/src/locales/kn/messages.json +++ b/apps/web/src/locales/kn/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "ಪದಗಳ ಕನಿಷ್ಠ ಸಂಖ್ಯೆ" }, - "overridePasswordTypePolicy": { - "message": "Password Type", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "ಯಶಸ್ವಿಯಾಗಿ ತೆಗೆದುಹಾಕಲಾಗಿದೆ" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/ko/messages.json b/apps/web/src/locales/ko/messages.json index d72fcaad6bb..ad9b80b9f6a 100644 --- a/apps/web/src/locales/ko/messages.json +++ b/apps/web/src/locales/ko/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "단어 최소 개수" }, - "overridePasswordTypePolicy": { - "message": "Password Type", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "성공적으로 제거됨" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index 406177f0f37..4875dc4fe31 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "Nav nepieciešamo atļauju, lai labotu šo vienumu" }, - "reviewAtRiskPasswords": { - "message": "Riskam pakļautu (vāju, atklātu vai vairākkārt izmantotu) paroļu pārskatīšana dažādās lietotnēs. Jāatlasa viskritiskākās paroles, lai noteiktu svarīgas drošības darbības, ko lietotājiem pielietot riskam pakļautām parolēm." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Pārskatīt riskam pakļautos pieteikšanās vienumus" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Neizdevās lietotnes atzīmēt kā būtiskas" }, @@ -5394,7 +5430,7 @@ "minimumNumberOfWords": { "message": "Mazākais pieļaujamais vārdu skaits" }, - "overridePasswordTypePolicy": { + "passwordTypePolicyOverride": { "message": "Paroles veids", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "Atkārtoti uzaicināti $LIMIT$ no $SELECTEDCOUNT$ lieotājiem. $EXCLUDEDCOUNT$ netika uzaicināt uzaicinājumu ierobežojuma ($LIMIT$) dēļ.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Veiksmīgi noņemts" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Piešķirt uzdevumus" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Vienums tika ievietots arhīvā" }, - "itemsWereSentToArchive": { - "message": "Vienumi tika ievietoti arhīvā" - }, "itemWasUnarchived": { "message": "Vienums tika izņemts no arhīva" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Vai tiešām turpināt?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "Lietotāja apliecināšana neizdevās." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "1 lietotājam" + }, + "upgradeToTeams": { + "message": "Uzlabot uz komandu plānu" + }, + "upgradeToEnterprise": { + "message": "Uzlabot uz uzņēmējdarbības plānu" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/ml/messages.json b/apps/web/src/locales/ml/messages.json index aa2892a92fa..f0173da95e5 100644 --- a/apps/web/src/locales/ml/messages.json +++ b/apps/web/src/locales/ml/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "വാക്കുകളുടെ ഏറ്റവും കുറഞ്ഞ എണ്ണം" }, - "overridePasswordTypePolicy": { - "message": "Password Type", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Removed successfully" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/mr/messages.json b/apps/web/src/locales/mr/messages.json index 2be3cda468e..35031280ec3 100644 --- a/apps/web/src/locales/mr/messages.json +++ b/apps/web/src/locales/mr/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Minimum number of words" }, - "overridePasswordTypePolicy": { - "message": "Password Type", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Removed successfully" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/my/messages.json b/apps/web/src/locales/my/messages.json index 2e96d9b844d..b95256dfacd 100644 --- a/apps/web/src/locales/my/messages.json +++ b/apps/web/src/locales/my/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Minimum number of words" }, - "overridePasswordTypePolicy": { - "message": "Password Type", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Removed successfully" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/nb/messages.json b/apps/web/src/locales/nb/messages.json index d6be0481648..07db562d56b 100644 --- a/apps/web/src/locales/nb/messages.json +++ b/apps/web/src/locales/nb/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Minimum antall ord" }, - "overridePasswordTypePolicy": { - "message": "Password Type", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Fjernet" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/ne/messages.json b/apps/web/src/locales/ne/messages.json index 7f69d34a54c..cd5e07cc33e 100644 --- a/apps/web/src/locales/ne/messages.json +++ b/apps/web/src/locales/ne/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Minimum number of words" }, - "overridePasswordTypePolicy": { - "message": "Password Type", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Removed successfully" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index 95e38404b7f..6754c7a0c51 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "Je hebt geen toestemming om dit item te bewerken" }, - "reviewAtRiskPasswords": { - "message": "Herzie risicovolle wachtwoorden (zwak, blootgelegd of herbruikt) tussen applicaties. Selecteer je meest belangrijke applicaties om prioriteit te geven aan beveiligingsacties voor je gebruikers om risicovolle wachtwoorden aan te pakken." + "reviewAccessIntelligence": { + "message": "Bekijk veiligheidsrapportages om risicovolle inloggegevens te vinden en verhelpen voordat ze uit de hand lopen." }, "reviewAtRiskLoginsPrompt": { "message": "Risicovolle logins bekijken" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applicaties als belangrijk gemarkeerd", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applicaties als niet-belangrijk gemarkeerd", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "$COUNT$ belangrijk markeren", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "$COUNT$ niet-belangrijk markeren", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Kon applicaties niet als kritiek markeren" }, @@ -389,10 +425,10 @@ "message": "Nu beoordelen" }, "allCaughtUp": { - "message": "All caught up!" + "message": "Alles bijgewerkt!" }, "noNewApplicationsToReviewAtThisTime": { - "message": "No new applications to review at this time" + "message": "Geen nieuwe toepassingen om te beoordelen op dit moment" }, "organizationHasItemsSavedForApplications": { "message": "Je organisatie heeft items opgeslagen voor $COUNT$ applicaties", @@ -428,16 +464,16 @@ "message": "Markeren als kritieke functionaliteit wordt geïmplementeerd in een toekomstige update" }, "applicationReviewSaved": { - "message": "Application review saved" + "message": "Beoordeling opgeslagen" }, "newApplicationsReviewed": { - "message": "New applications reviewed" + "message": "Nieuwe toepassingen beoordeeld" }, "errorSavingReviewStatus": { - "message": "Error saving review status" + "message": "Fout bij opslaan beoordelingsstatus" }, "pleaseTryAgain": { - "message": "Please try again" + "message": "Probeer opnieuw" }, "unmarkAsCritical": { "message": "Markeren als belangrijk ongedaan maken" @@ -1248,7 +1284,7 @@ "message": "Alles selecteren" }, "deselectAll": { - "message": "Deselect all" + "message": "Selectie ongedaan maken" }, "unselectAll": { "message": "Alles deselecteren" @@ -1441,7 +1477,7 @@ "message": "Single sign-on gebruiken" }, "yourOrganizationRequiresSingleSignOn": { - "message": "Your organization requires single sign-on." + "message": "Je organisatie vereist single sign-on." }, "welcomeBack": { "message": "Welkom terug" @@ -5394,7 +5430,7 @@ "minimumNumberOfWords": { "message": "Minimum aantal woorden" }, - "overridePasswordTypePolicy": { + "passwordTypePolicyOverride": { "message": "Type wachtwoord", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, @@ -5664,7 +5700,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendCreatedSuccessfully": { - "message": "Send created successfully!", + "message": "Send succesvol aangemaakt!", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendCreatedDescriptionV2": { @@ -5698,7 +5734,7 @@ } }, "durationTimeHours": { - "message": "$HOURS$ hours", + "message": "$HOURS$ uur", "placeholders": { "hours": { "content": "$1", @@ -5707,11 +5743,11 @@ } }, "newTextSend": { - "message": "New Text Send", + "message": "Nieuwe Send (tekst)", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "newFileSend": { - "message": "New File Send", + "message": "Nieuwe Send (bestand)", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "editedSend": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 uitnodiging verzonden" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ uitnodigingen verzonden", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ van $SELECTEDCOUNT$ gebruikers opnieuw uitgenodigd. $EXCLUDEDCOUNT$ werden niet uitgenodigd vanwege de $LIMIT$ uitnodigingslimiet.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ van $TOTAL$ uitnodigingen verzonden...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Houd deze pagina open totdat alles is verstuurd." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ uitnodigingen niet verzonden", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "$COUNT$ uitnodiging niet verzonden" + }, + "bulkReinviteFailureDescription": { + "message": "Er is een fout opgetreden tijdens het verzenden van uitnodigingen naar $COUNT$ van $TOTAL$ leden. Probeer het opnieuw te verzenden en als het probleem aanhoudt,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Probeer opnieuw te verzenden" + }, "bulkRemovedMessage": { "message": "Succesvol verwijderd" }, @@ -7353,7 +7445,7 @@ "message": "SSO ingeschakeld" }, "ssoTurnedOff": { - "message": "SSO turned off" + "message": "SSO uitgeschakeld" }, "emailMustLoginWithSso": { "message": "$EMAIL$ moet met Single Sign-on inloggen", @@ -9644,7 +9736,7 @@ "message": "SSO login is vereist" }, "emailRequiredForSsoLogin": { - "message": "Email is required for SSO" + "message": "E-mail is vereist voor SSO" }, "selectedRegionFlag": { "message": "Geselecteerde regionale vlag" @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Taken toewijzen" }, + "allTasksAssigned": { + "message": "Alle taken zijn toegewezen" + }, "assignSecurityTasksToMembers": { "message": "Meldingen verzenden om wachtwoorden te wijzigen" }, @@ -11773,7 +11868,7 @@ "message": "Gratis organisaties kunnen maximaal twee collecties hebben. Upgrade naar een betaald abonnement voor het toevoegen van meer collecties." }, "searchArchive": { - "message": "Search archive" + "message": "Archief doorzoeken" }, "archiveNoun": { "message": "Archief", @@ -11796,7 +11891,7 @@ "message": "Items in archief" }, "noItemsInArchive": { - "message": "No items in archive" + "message": "Geen items in archief" }, "noItemsInArchiveDesc": { "message": "Gearchiveerde items verschijnen hier en worden uitgesloten van algemene zoekresultaten en automatisch invulsuggesties." @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item naar archief verzonden" }, - "itemsWereSentToArchive": { - "message": "Item naar archief verzonden" - }, "itemWasUnarchived": { "message": "Item uit het archief gehaald" }, @@ -12490,8 +12582,11 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Weet je zeker dat je wilt doorgaan?" }, + "errorCannotDecrypt": { + "message": "Fout: Kan niet ontsleutelen" + }, "userVerificationFailed": { - "message": "User verification failed." + "message": "Gebruikersverificatie is mislukt." }, "resizeSideNavigation": { "message": "Formaat zijnavigatie wijzigen" @@ -12583,16 +12678,16 @@ "message": "Stel een ontgrendelingsmethode in om je kluis time-out actie te wijzigen" }, "leaveConfirmationDialogTitle": { - "message": "Are you sure you want to leave?" + "message": "Weet je zeker dat je wilt verlaten?" }, "leaveConfirmationDialogContentOne": { - "message": "By declining, your personal items will stay in your account, but you'll lose access to shared items and organization features." + "message": "Door te weigeren, blijven je persoonlijke items in je account, maar verlies je toegang tot gedeelde items en organisatiefunctionaliteit." }, "leaveConfirmationDialogContentTwo": { - "message": "Contact your admin to regain access." + "message": "Neem contact op met je beheerder om weer toegang te krijgen." }, "leaveConfirmationDialogConfirmButton": { - "message": "Leave $ORGANIZATION$", + "message": "$ORGANIZATION$ verlaten", "placeholders": { "organization": { "content": "$1", @@ -12601,10 +12696,10 @@ } }, "howToManageMyVault": { - "message": "How do I manage my vault?" + "message": "Hoe beheer ik mijn kluis?" }, "transferItemsToOrganizationTitle": { - "message": "Transfer items to $ORGANIZATION$", + "message": "Items overdragen aan $ORGANIZATION$", "placeholders": { "organization": { "content": "$1", @@ -12613,7 +12708,7 @@ } }, "transferItemsToOrganizationContent": { - "message": "$ORGANIZATION$ is requiring all items to be owned by the organization for security and compliance. Click accept to transfer ownership of your items.", + "message": "$ORGANIZATION$ vereist dat alle items eigendom zijn van de organisatie voor veiligheid en naleving. Klik op accepteren voor het overdragen van eigendom van je items.", "placeholders": { "organization": { "content": "$1", @@ -12622,13 +12717,13 @@ } }, "acceptTransfer": { - "message": "Accept transfer" + "message": "Overdacht accepteren" }, "declineAndLeave": { - "message": "Decline and leave" + "message": "Weigeren en verlaten" }, "whyAmISeeingThis": { - "message": "Why am I seeing this?" + "message": "Waarom zie ik dit?" }, "youHaveBitwardenPremium": { "message": "Je hebt Bitwarden Premium" @@ -12679,16 +12774,16 @@ "message": "Online beveiliging voltooien" }, "updatePayment": { - "message": "Update payment" + "message": "Betalingswijze bijwerken" }, "weCouldNotProcessYourPayment": { - "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + "message": "We konden je betaling niet verwerken. Werk je betalingsmethode bij of neem contact op met het ondersteuningsteam voor assistentie." }, "yourSubscriptionHasExpired": { - "message": "Your subscription has expired. Please contact the support team for assistance." + "message": "Je abonnement is verlopen. Neem voor hulp contact op met het supportteam." }, "yourSubscriptionIsScheduledToCancel": { - "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "message": "Je abonnement verloopt op $DATE$. Je kunt het op elk gewenst moment hervatten.", "placeholders": { "date": { "content": "$1", @@ -12697,10 +12792,10 @@ } }, "premiumShareEvenMore": { - "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + "message": "Deel nog meer met Families, of krijg krachtige, betrouwbare wachtwoordbeveiliging met Teams of Enterprise." }, "youHaveAGracePeriod": { - "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "message": "Je hebt een periode van $DAYS$ dagen tot de vervaldatum van je abonnement. Probeer de achterstallige facturen vóór $DATE$ op te lossen.", "placeholders": { "days": { "content": "$1", @@ -12713,31 +12808,31 @@ } }, "manageInvoices": { - "message": "Manage invoices" + "message": "Facturen beheren" }, "yourNextChargeIsFor": { - "message": "Your next charge is for" + "message": "Je volgende betaling is voor" }, "dueOn": { - "message": "due on" + "message": "verschuldigd op" }, "yourSubscriptionWillBeSuspendedOn": { - "message": "Your subscription will be suspended on" + "message": "Je abonnement zal worden opgeschort op" }, "yourSubscriptionWasSuspendedOn": { - "message": "Your subscription was suspended on" + "message": "Je abonnement is opgeschort op" }, "yourSubscriptionWillBeCanceledOn": { - "message": "Your subscription will be canceled on" + "message": "Je abonnement zal worden geannuleerd op" }, "yourSubscriptionWasCanceledOn": { - "message": "Your subscription was canceled on" + "message": "Je abonnement is geannuleerd op" }, "storageFull": { - "message": "Storage full" + "message": "Opslag is vol" }, "storageUsedDescription": { - "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "message": "Je hebt $USED$ gebruikt van de $AVAILABLE$ GB versleutelde bestandsopslag.", "placeholders": { "used": { "content": "$1", @@ -12750,7 +12845,7 @@ } }, "storageFullDescription": { - "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + "message": "Je hebt alle $GB$ GB aan versleutelde opslag gebruikt. Voeg meer opslagruimte toe om door te gaan met het opslaan van bestanden." }, "whoCanView": { "message": "Wie kan weergeven" @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per gebruiker" + }, + "upgradeToTeams": { + "message": "Upgrade naar Teams" + }, + "upgradeToEnterprise": { + "message": "Upgraden naar Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Deel nog meer met Families, of krijg krachtige, betrouwbare wachtwoordbeveiliging met Teams of Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prijzen exclusief btw en worden jaarlijks gefactureerd." + }, + "invoicePreviewErrorMessage": { + "message": "Fout opgetreden bij het genereren van de voorbeeldfactuur." + }, + "planProratedMembershipInMonths": { + "message": "Verrekening lidmaatschap $PLAN$ ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Krediet Premium-abonnement" + }, + "enterpriseMembership": { + "message": "Enterprise-lidmaatschap" + }, + "teamsMembership": { + "message": "Teams-lidmaatschap" + }, + "plansUpdated": { + "message": "Je hebt opgewaardeerd naar $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "Er is een fout opgetreden bij het bijwerken van je betaalmethode." } } diff --git a/apps/web/src/locales/nn/messages.json b/apps/web/src/locales/nn/messages.json index 82a4950f681..7987b4077d1 100644 --- a/apps/web/src/locales/nn/messages.json +++ b/apps/web/src/locales/nn/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Minimum number of words" }, - "overridePasswordTypePolicy": { - "message": "Password Type", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Removed successfully" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/or/messages.json b/apps/web/src/locales/or/messages.json index 2e96d9b844d..b95256dfacd 100644 --- a/apps/web/src/locales/or/messages.json +++ b/apps/web/src/locales/or/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Minimum number of words" }, - "overridePasswordTypePolicy": { - "message": "Password Type", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Removed successfully" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index 4017edab3c4..9736337ee0c 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "Przejrzyj hasła zagrożone (słabe, ujawnione lub ponownie używane) we wszystkich aplikacjach. Wybierz swoje najbardziej krytyczne aplikacje, aby nadać priorytet działaniom bezpieczeństwa swoim użytkownikom, aby zająć się hasłami zagrożonymi." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Minimalna liczba słów" }, - "overridePasswordTypePolicy": { - "message": "Rodzaj hasła", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Usunięto" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index 4334dba17b1..f29637803f0 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "Você não tem permissão para editar este item" }, - "reviewAtRiskPasswords": { - "message": "Revise senhas em risco (fracas, expostas, ou reutilizadas) entre aplicativos. Selecione seus aplicativos mais críticos para priorizar ações de segurança para que seus usuários resolvem senhas em risco." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Revisar credenciais em risco" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Falha ao marcar aplicativos como críticos" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Número mínimo de palavras" }, - "overridePasswordTypePolicy": { - "message": "Tipo de senha", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ dos $SELECTEDCOUNT$ usuários foram re-convidados. $EXCLUDEDCOUNT$ não foram convidados devido ao limite de convite de $LIMIT$.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Removido com sucesso" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Atribuir tarefas" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Envie notificações para alteração de senhas" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "O item foi enviado para o arquivo" }, - "itemsWereSentToArchive": { - "message": "Itens foram enviados para o arquivo" - }, "itemWasUnarchived": { "message": "O item foi desarquivado" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Tem certeza que deseja continuar?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "Falha na verificação do usuário." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/pt_PT/messages.json b/apps/web/src/locales/pt_PT/messages.json index 912019b298c..e0626be7255 100644 --- a/apps/web/src/locales/pt_PT/messages.json +++ b/apps/web/src/locales/pt_PT/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "Não tem permissão para editar este item" }, - "reviewAtRiskPasswords": { - "message": "Reveja as palavras-passe em risco (fracas, expostas ou reutilizadas) em todas as aplicações. Selecione as suas aplicações mais críticas para dar prioridade a ações de segurança para os seus utilizadores para resolver as palavras-passe em risco." + "reviewAccessIntelligence": { + "message": "Reveja os relatórios de segurança para identificar e corrigir riscos associados às credenciais antes que se agravem." }, "reviewAtRiskLoginsPrompt": { "message": "Rever credenciais em risco" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ aplicações marcadas como críticas", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ aplicações marcadas como não críticas", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Marcar $COUNT$ como críticas", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Marcar $COUNT$ como não críticas", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Falha ao marcar aplicações como críticas" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Número mínimo de palavras" }, - "overridePasswordTypePolicy": { - "message": "Tipo de palavra-passe", + "passwordTypePolicyOverride": { + "message": "Tipo de palavras-passe", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 convite enviado" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ convites enviados", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ de $SELECTEDCOUNT$ utilizadores convidados novamente. $EXCLUDEDCOUNT$ não foram convidados devido ao limite de $LIMIT$ convites.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ de $TOTAL$ convites enviados...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Mantenha esta página aberta até que todos sejam enviados." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ convites não enviados", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 convite não enviado" + }, + "bulkReinviteFailureDescription": { + "message": "Ocorreu um erro ao enviar convites para $COUNT$ dos $TOTAL$ membros. Tente enviar novamente e, se o problema persistir,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Tente enviar novamente" + }, "bulkRemovedMessage": { "message": "Removido com sucesso" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Atribuir tarefas" }, + "allTasksAssigned": { + "message": "Todas as tarefas foram atribuídas" + }, "assignSecurityTasksToMembers": { "message": "Enviar notificações para alterar palavras-passe" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "O item foi movido para o arquivo" }, - "itemsWereSentToArchive": { - "message": "Os itens foram movidos para o arquivo" - }, "itemWasUnarchived": { "message": "O item foi desarquivado" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Tem a certeza de que deseja continuar?" }, + "errorCannotDecrypt": { + "message": "Erro: Não é possível desencriptar" + }, "userVerificationFailed": { "message": "Falha na verificação do utilizador." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "por utilizador" + }, + "upgradeToTeams": { + "message": "Atualizar para o plano Equipas" + }, + "upgradeToEnterprise": { + "message": "Atualizar para o plano Empresarial" + }, + "upgradeShareEvenMore": { + "message": "Partilhe ainda mais com o plano Familiar ou obtenha uma segurança de palavras-passe poderosa e fiável com os planos Equipas ou Empresarial" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Os preços não incluem impostos e são cobrados anualmente." + }, + "invoicePreviewErrorMessage": { + "message": "Foi encontrado um erro ao gerar a pré-visualização da fatura." + }, + "planProratedMembershipInMonths": { + "message": "Adesão ao plano $PLAN$ proporcional ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Crédito de subscrição Premium" + }, + "enterpriseMembership": { + "message": "Adesão Empresarial" + }, + "teamsMembership": { + "message": "Adesão Equipas" + }, + "plansUpdated": { + "message": "Atualizou para o plano $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "Ocorreu um erro ao atualizar o seu método de pagamento." } } diff --git a/apps/web/src/locales/ro/messages.json b/apps/web/src/locales/ro/messages.json index f7c232a321e..0b567f2f969 100644 --- a/apps/web/src/locales/ro/messages.json +++ b/apps/web/src/locales/ro/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Număr minim de cuvinte" }, - "overridePasswordTypePolicy": { - "message": "Password Type", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Eliminat cu succes" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index 2e4cf303394..5433c0ac312 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "У вас нет разрешения на редактирование этого элемента" }, - "reviewAtRiskPasswords": { - "message": "Проанализируйте пароли, подверженные риску (слабые, скомпрометированные или повторно используемые), во всех приложениях. Выберите наиболее критичные приложения, чтобы определить приоритетные меры безопасности для пользователей, направленные на устранение подверженных риску паролей." + "reviewAccessIntelligence": { + "message": "Просматривайте отчеты по безопасности, чтобы выявлять и устранять риски, связанные с учетными данными, до того, как они приведут к серьезным последствиям." }, "reviewAtRiskLoginsPrompt": { "message": "Обзор логинов, подверженных риску" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "Помечены как критичные приложений: $COUNT$", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "Помечены как некритичные приложений: $COUNT$", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Пометить как критичное: $COUNT$", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Пометить как некритичное: $COUNT$", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Не удалось пометить приложения как критичные" }, @@ -5394,7 +5430,7 @@ "minimumNumberOfWords": { "message": "Минимальное количество слов" }, - "overridePasswordTypePolicy": { + "passwordTypePolicyOverride": { "message": "Тип пароля", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ из $SELECTEDCOUNT$ пользователей повторно приглашены. $EXCLUDEDCOUNT$ не был приглашен из-за лимита приглашений $LIMIT$.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Удален(-о) успешно" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Назначить задачи" }, + "allTasksAssigned": { + "message": "Все задачи были назначены" + }, "assignSecurityTasksToMembers": { "message": "Отправляйте уведомления о смене паролей" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Элемент был отправлен в архив" }, - "itemsWereSentToArchive": { - "message": "Элементы были отправлены в архив" - }, "itemWasUnarchived": { "message": "Элемент был разархивирован" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Вы действительно хотите продолжить?" }, + "errorCannotDecrypt": { + "message": "Ошибка: невозможно расшифровать" + }, "userVerificationFailed": { "message": "Проверка пользователя не удалась." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "на пользователя" + }, + "upgradeToTeams": { + "message": "Перейти на Teams" + }, + "upgradeToEnterprise": { + "message": "Перейти на Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Делитесь еще большим количеством информации с семьями или обеспечьте надежную защиту паролем с командами или организациями" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Цены указаны без учета налогов и оплачиваются ежегодно." + }, + "invoicePreviewErrorMessage": { + "message": "Обнаружена ошибка при создании предварительного просмотра счета." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Кредит на Премиум" + }, + "enterpriseMembership": { + "message": "Членство в организации" + }, + "teamsMembership": { + "message": "Членство в команде" + }, + "plansUpdated": { + "message": "Вы обновились до $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "Произошла ошибка при обновлении способа оплаты." } } diff --git a/apps/web/src/locales/si/messages.json b/apps/web/src/locales/si/messages.json index b6cd6268bce..ac073315340 100644 --- a/apps/web/src/locales/si/messages.json +++ b/apps/web/src/locales/si/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Minimum number of words" }, - "overridePasswordTypePolicy": { - "message": "Password Type", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Removed successfully" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index 10e52755a17..dffa3975792 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "Na úpravu tejto položky nemáte oprávnenie" }, - "reviewAtRiskPasswords": { - "message": "Skontrolujte ohrozené heslá (slabé, odhalené, alebo opätovne použité) naprieč aplikáciami. Vyberte najkritickejšie aplikácie a priorizujte vaším používateľom bezpečnostné opatrenia ohľadom ohrozených hesiel." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Prehľad ohrozených prihlasovacích mien" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Nepodarilo sa označiť aplikácie za kritické" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Minimálny počet slov" }, - "overridePasswordTypePolicy": { - "message": "Typ hesla", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ z $SELECTEDCOUNT$ používateľov opätovne pozvaných. $EXCLUDEDCOUNT$ nebolo pozvaných kvôli limitu $LIMIT$ pozvánok.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Odstránenie úspešné" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Priradiť úlohy" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Odoslať upozornenia na zmenu hesla" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Položka bola archivovaná" }, - "itemsWereSentToArchive": { - "message": "Položky boli archivované" - }, "itemWasUnarchived": { "message": "Položka bola odobraná z archívu" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Ste si istí, že chcete pokračovať?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "Zlyhalo overenie používateľa." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/sl/messages.json b/apps/web/src/locales/sl/messages.json index e6dc9d1b2ab..aef3a8ca3ac 100644 --- a/apps/web/src/locales/sl/messages.json +++ b/apps/web/src/locales/sl/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Vsaj toliko besed" }, - "overridePasswordTypePolicy": { - "message": "Password Type", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Removed successfully" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/sr_CS/messages.json b/apps/web/src/locales/sr_CS/messages.json index 341d0047e14..7e7bcbd97b1 100644 --- a/apps/web/src/locales/sr_CS/messages.json +++ b/apps/web/src/locales/sr_CS/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "Pregledaj rizične lozinke (slabe, izložene ili ponovo korišćene) u aplikacijama. Izaberi svoje najkritičnije aplikacije da bi dao prioritet bezbednosnim radnjama kako bi tvoji korisnici adresirali rizične lozinke." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Minimalan broj reči" }, - "overridePasswordTypePolicy": { - "message": "Password Type", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Removed successfully" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/sr_CY/messages.json b/apps/web/src/locales/sr_CY/messages.json index 2300d3fe67f..d457b16f44e 100644 --- a/apps/web/src/locales/sr_CY/messages.json +++ b/apps/web/src/locales/sr_CY/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "Немате дозволу да уређујете ову ставку" }, - "reviewAtRiskPasswords": { - "message": "Прегледај ризичне лозинке (слабе, изложене или поново коришћене) у апликацијама. Изабери своје најкритичније апликације да би дао приоритет безбедносним радњама како би твоји корисници адресирали ризичне лозинке." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Прегледајте ризичне пријаве" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Означавање апликација као критичних није успело" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Минимални број речи" }, - "overridePasswordTypePolicy": { - "message": "Тип лозинке", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Успешно уклоњено" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Додели задатке" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Ставка је послата у архиву" }, - "itemsWereSentToArchive": { - "message": "Ставке су послате у архиву" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Желите ли заиста да наставите?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index bc10ec0ed99..f4bab640104 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "Du har inte behörighet att redigera detta objekt" }, - "reviewAtRiskPasswords": { - "message": "Granska risklösenord (svaga, exponerade eller återanvända) i olika applikationer. Välj ut de mest kritiska applikationerna för att prioritera säkerhetsåtgärder för användarna för att hantera risklösenord." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Granska inloggningar i riskzonen" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Misslyckades med att markera applikationer som kritiska" }, @@ -5394,7 +5430,7 @@ "minimumNumberOfWords": { "message": "Minsta antal ord" }, - "overridePasswordTypePolicy": { + "passwordTypePolicyOverride": { "message": "Lösenordstyp", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ av $SELECTEDCOUNT$ användare återinbjudna. $EXCLUDEDCOUNT$ blev inte inbjudna på grund av gränsen på $LIMIT$ inbjudningar.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Tog bort" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Tilldela uppgifter" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Skicka aviseringar om att ändra lösenord" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Objektet skickades till arkivet" }, - "itemsWereSentToArchive": { - "message": "Objekten har skickats till arkivet" - }, "itemWasUnarchived": { "message": "Objektet har avarkiverats" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Är du säker på att du vill fortsätta?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "Verifiering av användare misslyckades." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per användare" + }, + "upgradeToTeams": { + "message": "Uppgradera till Teams" + }, + "upgradeToEnterprise": { + "message": "Uppgradera till Företag" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "Du har uppgraderat till $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/ta/messages.json b/apps/web/src/locales/ta/messages.json index 3189d19e43b..b93591182db 100644 --- a/apps/web/src/locales/ta/messages.json +++ b/apps/web/src/locales/ta/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "பயன்பாடுகள் முழுவதும் ஆபத்தான கடவுச்சொற்களை (பலவீனமான, அம்பலப்படுத்தப்பட்ட, அல்லது மீண்டும் பயன்படுத்தப்பட்ட) மதிப்பாய்வு செய்யவும். உங்கள் பயனர்களுக்கான ஆபத்தான கடவுச்சொற்களை நிவர்த்தி செய்ய பாதுகாப்பு நடவடிக்கைகளுக்கு முன்னுரிமை அளிக்க, உங்கள் மிக முக்கியமான பயன்பாடுகளைத் தேர்ந்தெடுக்கவும்." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "குறைந்தபட்ச சொற்களின் எண்ணிக்கை" }, - "overridePasswordTypePolicy": { - "message": "கடவுச்சொல் வகை", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "வெற்றிகரமாக அகற்றப்பட்டது" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/te/messages.json b/apps/web/src/locales/te/messages.json index 2e96d9b844d..b95256dfacd 100644 --- a/apps/web/src/locales/te/messages.json +++ b/apps/web/src/locales/te/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Minimum number of words" }, - "overridePasswordTypePolicy": { - "message": "Password Type", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Removed successfully" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/th/messages.json b/apps/web/src/locales/th/messages.json index 810be6dadcf..e5e449038ac 100644 --- a/apps/web/src/locales/th/messages.json +++ b/apps/web/src/locales/th/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, - "reviewAtRiskPasswords": { - "message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Review at-risk logins" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Minimum number of words" }, - "overridePasswordTypePolicy": { - "message": "Password Type", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Removed successfully" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/tr/messages.json b/apps/web/src/locales/tr/messages.json index 93340b58c79..97a29756a89 100644 --- a/apps/web/src/locales/tr/messages.json +++ b/apps/web/src/locales/tr/messages.json @@ -39,7 +39,7 @@ "message": "Access Intelligence" }, "noApplicationsMatchTheseFilters": { - "message": "No applications match these filters" + "message": "Bu filtrelerle eşleşen uygulama yok" }, "passwordRisk": { "message": "Parola Riski" @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "Bu kaydı düzenleme yetkisine sahip değilsiniz" }, - "reviewAtRiskPasswords": { - "message": "Uygulamalar genelinde risk altındaki parolaları (zayıf, açığa çıkmış veya farklı yerlerde kullanılan) gözden geçirin. Kullanıcılarınız için risk altındaki parolalara yönelik güvenlik eylemlerine öncelik vermek üzere en kritik uygulamalarınızı seçin." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Risk altındaki hesapları inceleyin" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ uygulama kritik olarak işaretlendi", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ uygulama kritik değil olarak işaretlendi", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "$COUNT$ tane şeyi kritik olarak işaretle", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "$COUNT$ tane şeyi kritik değil olarak işaretle", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Uygulamaların kritik olarak işaretlenmesi başarısız oldu" }, @@ -5394,7 +5430,7 @@ "minimumNumberOfWords": { "message": "Minimum kelime sayısı" }, - "overridePasswordTypePolicy": { + "passwordTypePolicyOverride": { "message": "Parola türü", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, @@ -5987,7 +6023,7 @@ "description": "This will be used as a hyperlink" }, "benefits": { - "message": "Benefits" + "message": "Avantajlar" }, "centralizeDataOwnershipBenefit1": { "message": "Gain full visibility into credential health, including shared and unshared items." @@ -6005,7 +6041,7 @@ "message": "If members have items in their individual vault, they will be prompted to either transfer them to the organization or leave. If they leave, their access is revoked but can be restored anytime." }, "centralizeDataOwnershipWarningLink": { - "message": "Learn more about the transfer" + "message": "Transfer hakkında daha fazla bilgi edinin" }, "organizationDataOwnership": { "message": "Kuruluş veri sahipliğini zorunlu kılın" @@ -6446,10 +6482,10 @@ "message": "Hesap kurtarmaya kaydolundu" }, "enrolled": { - "message": "Enrolled" + "message": "Kayıtlı" }, "notEnrolled": { - "message": "Not enrolled" + "message": "Kayıtlı değil" }, "withdrawAccountRecovery": { "message": "Hesap kurtarmadan ayrıl" @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 davet gönder" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ davet gönder", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$SELECTEDCOUNT$ kullanıcıdan $LIMIT$ tanesi yeniden davet edildi. $EXCLUDEDCOUNT$ kullanıcı, $LIMIT$ davet sınırı nedeniyle davet edilmedi.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Tüm davetler gönderilene dek bu sayfayı açık tutunuz." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ davet gönderilmedi", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 davet gönderilmedi" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Tekrar göndermeyi dene" + }, "bulkRemovedMessage": { "message": "Başarıyla kaldırıldı" }, @@ -6975,7 +7067,7 @@ "message": "Kasa zaman aşımı izin verilen aralıkta değil." }, "disableExport": { - "message": "Remove export" + "message": "Dışa aktarmayı kaldır" }, "disablePersonalVaultExportDescription": { "message": "Üyelerin kişisel kasalarındaki verileri dışa aktarmasına izin verme." @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Görev ata" }, + "allTasksAssigned": { + "message": "Tüm görevler atandı" + }, "assignSecurityTasksToMembers": { "message": "Parolaları değiştirmek için bildirim gönder" }, @@ -10611,10 +10706,10 @@ "message": "İndeks" }, "httpEventCollectorUrl": { - "message": "HTTP Event Collector URL" + "message": "HTTP Olay Toplayıcı URL'si" }, "httpEventCollectorToken": { - "message": "HTTP Event Collector Token" + "message": "HTTP Olay Toplayıcı Token'i" }, "selectAPlan": { "message": "Bir plan seçin" @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Kayıt arşive gönderildi" }, - "itemsWereSentToArchive": { - "message": "Kayıtlar arşive gönderildi" - }, "itemWasUnarchived": { "message": "Kayıt arşivden çıkarıldı" }, @@ -12449,40 +12541,40 @@ "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, "continueWithLogIn": { - "message": "Continue with log in" + "message": "Giriş yaparak devam et" }, "doNotContinue": { - "message": "Do not continue" + "message": "Devam etme" }, "domain": { - "message": "Domain" + "message": "Alan Adı" }, "keyConnectorDomainTooltip": { "message": "This domain will store your account encryption keys, so make sure you trust it. If you're not sure, check with your admin." }, "verifyYourOrganization": { - "message": "Verify your organization to log in" + "message": "Giriş yapmak için kuruluşunuzu doğrulayın" }, "organizationVerified": { - "message": "Organization verified" + "message": "Kuruluş doğrulandı" }, "domainVerified": { - "message": "Domain verified" + "message": "Alan adı doğrulandı" }, "leaveOrganizationContent": { - "message": "If you don't verify your organization, your access to the organization will be revoked." + "message": "Kuruluşunuzu doğrulamazsanız, kuruluşa erişiminiz iptal edilecektir." }, "leaveNow": { - "message": "Leave now" + "message": "Şimdi ayrıl" }, "verifyYourDomainToLogin": { - "message": "Verify your domain to log in" + "message": "Giriş yapmak için alan adınızı doğrulayın" }, "verifyYourDomainDescription": { - "message": "To continue with log in, verify this domain." + "message": "Giriş yaparak devam etmek için bu alan adını doğrulayın." }, "confirmKeyConnectorOrganizationUserDescription": { - "message": "To continue with log in, verify the organization and domain." + "message": "Giriş yaparak devam etmek için kuruluşu ve alan adını doğrulayın." }, "confirmNoSelectedCriticalApplicationsTitle": { "message": "Hiçbir kritik uygulama seçili değil" @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Devam etmek istediğinizden emin misiniz?" }, + "errorCannotDecrypt": { + "message": "Hata: Deşifre edilemiyor" + }, "userVerificationFailed": { "message": "Kullanıcı doğrulaması başarısız oldu." }, @@ -12509,13 +12604,13 @@ "message": "Some of your folders could not be recovered. Do you want to delete these unrecoverable folders from your vault?" }, "recoveryReplacePrivateKeyTitle": { - "message": "Replace encryption key" + "message": "Şifreleme anahtarını değiştir" }, "recoveryReplacePrivateKeyDesc": { "message": "Your public-key encryption key pair could not be recovered. Do you want to replace your encryption key with a new key pair? This will require you to set up existing emergency-access and organization memberships again." }, "recoveryStepSyncTitle": { - "message": "Synchronizing data" + "message": "Veri eşitleme" }, "recoveryStepPrivateKeyTitle": { "message": "Verifying encryption key integrity" @@ -12536,13 +12631,13 @@ "message": "Use the data recovery tool to diagnose and repair issues with your account. After running diagnostics you have the option to save diagnostic logs for support and the option to repair any detected issues." }, "runDiagnostics": { - "message": "Run Diagnostics" + "message": "Tanılamayı Çalıştır" }, "repairIssues": { "message": "Repair Issues" }, "saveDiagnosticLogs": { - "message": "Save Diagnostic Logs" + "message": "Tanılama Günlüklerini Kaydet" }, "sessionTimeoutSettingsManagedByOrganization": { "message": "Bu ayar kuruluşunuz tarafından yönetiliyor." @@ -12583,16 +12678,16 @@ "message": "Zaman aşımı eyleminizi değiştirmek için kilit açma yönteminizi ayarlayın" }, "leaveConfirmationDialogTitle": { - "message": "Are you sure you want to leave?" + "message": "Ayrılmak istediğinizden emin misiniz?" }, "leaveConfirmationDialogContentOne": { - "message": "By declining, your personal items will stay in your account, but you'll lose access to shared items and organization features." + "message": "Reddederseniz kişisel kayıtlarınız hesabınızda kalır, ancak paylaşılan kayıtlara ve kuruluş özelliklerine erişiminizi kaybedersiniz." }, "leaveConfirmationDialogContentTwo": { - "message": "Contact your admin to regain access." + "message": "Erişiminizi yeniden kazanmak için yöneticinizle iletişime geçin." }, "leaveConfirmationDialogConfirmButton": { - "message": "Leave $ORGANIZATION$", + "message": "$ORGANIZATION$ kuruluşundan ayrıl", "placeholders": { "organization": { "content": "$1", @@ -12601,10 +12696,10 @@ } }, "howToManageMyVault": { - "message": "How do I manage my vault?" + "message": "Kasamı nasıl yönetebilirim?" }, "transferItemsToOrganizationTitle": { - "message": "Transfer items to $ORGANIZATION$", + "message": "Kayıtları $ORGANIZATION$ kuruluşuna aktar", "placeholders": { "organization": { "content": "$1", @@ -12613,7 +12708,7 @@ } }, "transferItemsToOrganizationContent": { - "message": "$ORGANIZATION$ is requiring all items to be owned by the organization for security and compliance. Click accept to transfer ownership of your items.", + "message": "$ORGANIZATION$, güvenlik ve mevzuata uyum amacıyla tüm kayıtların kuruluşa ait olmasını zorunlu kılıyor. Kayıtlarınızın sahipliğini devretmek için \"Kabul et\"e tıklayın.", "placeholders": { "organization": { "content": "$1", @@ -12622,22 +12717,22 @@ } }, "acceptTransfer": { - "message": "Accept transfer" + "message": "Aktarımı kabul et" }, "declineAndLeave": { - "message": "Decline and leave" + "message": "Reddet ve ayrıl" }, "whyAmISeeingThis": { - "message": "Why am I seeing this?" + "message": "Bunu neden görüyorum?" }, "youHaveBitwardenPremium": { "message": "Bitwarden Premium abonesisiniz" }, "viewAndManagePremiumSubscription": { - "message": "View and manage your Premium subscription" + "message": "Premium aboneliğinizi görüntüleyin ve yönetin" }, "youNeedToUpdateLicenseFile": { - "message": "You'll need to update your license file" + "message": "Lisans dosyanızı güncellemeniz gerekecektir" }, "youNeedToUpdateLicenseFileDate": { "message": "$DATE$.", @@ -12649,16 +12744,16 @@ } }, "uploadLicenseFile": { - "message": "Upload license file" + "message": "Lisans dosyasını yükle" }, "uploadYourLicenseFile": { - "message": "Upload your license file" + "message": "Lisans dosyasınızı yükleyin" }, "uploadYourPremiumLicenseFile": { - "message": "Upload your Premium license file" + "message": "Premium lisans dosyasınızı yükleyin" }, "uploadLicenseFileDesc": { - "message": "Your license file name will be similar to: $FILE_NAME$", + "message": "Lisans dosyanızın adı şuna benzer olacaktır: $FILE_NAME$", "placeholders": { "file_name": { "content": "$1", @@ -12667,13 +12762,13 @@ } }, "alreadyHaveSubscriptionQuestion": { - "message": "Already have a subscription?" + "message": "Halihazırda bir aboneliğiniz var mı?" }, "alreadyHaveSubscriptionSelfHostedMessage": { "message": "Open the subscription page on your Bitwarden cloud account and download your license file. Then return to this screen and upload it below." }, "viewAllPlans": { - "message": "View all plans" + "message": "Tüm planları görüntüle" }, "planDescPremium": { "message": "Eksiksiz çevrimiçi güvenlik" @@ -12722,16 +12817,16 @@ "message": "due on" }, "yourSubscriptionWillBeSuspendedOn": { - "message": "Your subscription will be suspended on" + "message": "Aboneliğiniz şu tarihte askıya alınacaktır" }, "yourSubscriptionWasSuspendedOn": { - "message": "Your subscription was suspended on" + "message": "Aboneliğiniz şu tarihte askıya alındı" }, "yourSubscriptionWillBeCanceledOn": { - "message": "Your subscription will be canceled on" + "message": "Aboneliğiniz şu tarihte iptal edilecektir" }, "yourSubscriptionWasCanceledOn": { - "message": "Your subscription was canceled on" + "message": "Aboneliğiniz şu tarihte iptal edildi" }, "storageFull": { "message": "Depolama alanı dolu" @@ -12783,7 +12878,7 @@ "message": "Premium abonesiniz" }, "emailProtected": { - "message": "Email protected" + "message": "E-posta korumalı" }, "invalidSendPassword": { "message": "Geçersiz Send parolası" @@ -12793,6 +12888,55 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "perUser": { - "message": "per user" + "message": "kişi başı" + }, + "upgradeToTeams": { + "message": "Ekip planına yükselt" + }, + "upgradeToEnterprise": { + "message": "Kurumsal planına yükselt" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Fiyatlara vergi dahil değildir ve yıllık olarak faturalandırılır." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium abonelik kredisi" + }, + "enterpriseMembership": { + "message": "Kurumsal üyelik" + }, + "teamsMembership": { + "message": "Ekip üyeliği" + }, + "plansUpdated": { + "message": "$PLAN$ planına geçtiniz!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "Ödeme yönteminizi güncellerken bir hata oluştu." } } diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index edcc4d39cb1..6fc3db5d64e 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "Вам не дозволено редагувати цей запис" }, - "reviewAtRiskPasswords": { - "message": "Переглядайте ризиковані паролі в різних програмах (слабкі, викриті, або повторно використані). Виберіть найбільш критичні програми, щоб визначити пріоритети дій щодо безпеки для користувачів, які використовують ризиковані паролі." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Переглянути записи з ризиком" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Failed to mark applications as critical" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Мінімальна кількість слів" }, - "overridePasswordTypePolicy": { - "message": "Тип пароля", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Успішно вилучено" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Assign tasks" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Send notifications to change passwords" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemsWereSentToArchive": { - "message": "Items were sent to archive" - }, "itemWasUnarchived": { "message": "Item was unarchived" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Are you sure you want to continue?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "User verification failed." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/vi/messages.json b/apps/web/src/locales/vi/messages.json index 5e468b4ed2a..d54ae1933a7 100644 --- a/apps/web/src/locales/vi/messages.json +++ b/apps/web/src/locales/vi/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "Bạn không có quyền chỉnh sửa mục này" }, - "reviewAtRiskPasswords": { - "message": "Kiểm tra các mật khẩu có rủi ro (yếu, bị lộ hoặc được sử dụng lại) trên các ứng dụng. Chọn các ứng dụng quan trọng nhất của bạn để ưu tiên các biện pháp bảo mật cho người dùng nhằm giải quyết các mật khẩu có rủi ro." + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "Xem lại các đăng nhập có rủi ro" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "Không thể đánh dấu các ứng dụng là quan trọng" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "Số từ tối thiểu" }, - "overridePasswordTypePolicy": { - "message": "Loại mật khẩu", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "Đã mời lại $LIMIT$ trong số $SELECTEDCOUNT$ người dùng. $EXCLUDEDCOUNT$ người đã không được mời do giới hạn mời là $LIMIT$.", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "Đã xóa thành công" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "Giao tác vụ" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "Gửi thông báo thay đổi mật khẩu" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "Mục đã được chuyển vào kho lưu trữ" }, - "itemsWereSentToArchive": { - "message": "Các mục đã được chuyển vào lưu trữ" - }, "itemWasUnarchived": { "message": "Mục đã được bỏ lưu trữ" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "Bạn có chắc chắn muốn tiếp tục không?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "Xác minh người dùng thất bại." }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "per user" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index b479ae94f36..76b95446091 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "您没有编辑此项目的权限" }, - "reviewAtRiskPasswords": { - "message": "审查各个应用程序中存在风险的密码(弱、暴露或重复使用)。选择最关键的应用程序,优先为您的用户采取安全措施,以处理存在风险的密码。" + "reviewAccessIntelligence": { + "message": "审查安全报告,在凭据风险升级之前发现并修复它们。" }, "reviewAtRiskLoginsPrompt": { "message": "审查存在风险的登录" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ 个应用程序标记为关键", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ 个应用程序标记为非关键", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "将 $COUNT$ 个标记为关键", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "将 $COUNT$ 个标记为非关键", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "将应用程序标记为关键失败" }, @@ -4919,7 +4955,7 @@ } }, "subscriptionUserSeatsWithoutAdditionalSeatsOption": { - "message": "您最多可邀请 $COUNT$ 名成员,而无需额外付费。要升级您的方案并邀请更多成员,请联系客户支持。", + "message": "您最多可邀请 $COUNT$ 位成员,而无需额外付费。要升级您的方案并邀请更多成员,请联系客户支持。", "placeholders": { "count": { "content": "$1", @@ -5394,7 +5430,7 @@ "minimumNumberOfWords": { "message": "单词最少个数" }, - "overridePasswordTypePolicy": { + "passwordTypePolicyOverride": { "message": "密码类型", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, @@ -5678,7 +5714,7 @@ } }, "sendCreatedDescriptionPassword": { - "message": "复制并分享此 Send 链接。在接下来的 $TIME$ 内,任何拥有此链接以及您设置的密码的人都可以访问此 Send。", + "message": "复制并分享此 Send 链接。在接下来的 $TIME$ 内,拥有此链接以及您设置的密码的任何人都可以访问此 Send。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "time": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "已发送 1 份邀请" + }, + "bulkReinviteSentToast": { + "message": "已发送 $COUNT$ 份邀请", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "已重新邀请 $SELECTEDCOUNT$ 位用户中的 $LIMIT$ 位。由于邀请限制为 $LIMIT$ 人,$EXCLUDEDCOUNT$ 位用户没有被邀请。", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "已发送 $TOTAL$ 份邀请中的 $COUNT$ 份...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "全部发送完毕前,请保持此页面打开。" + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ 份邀请未发送", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 份邀请未发送" + }, + "bulkReinviteFailureDescription": { + "message": "向 $TOTAL$ 位成员中的 $COUNT$ 位发送邀请时发生错误。请尝试重新发送,如果问题仍然存在,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "尝试再次发送" + }, "bulkRemovedMessage": { "message": "移除成功" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "分配任务" }, + "allTasksAssigned": { + "message": "所有任务已分配" + }, "assignSecurityTasksToMembers": { "message": "发送更改密码的通知" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "项目已发送到归档" }, - "itemsWereSentToArchive": { - "message": "项目已发送到归档" - }, "itemWasUnarchived": { "message": "项目已取消归档" }, @@ -12184,7 +12276,7 @@ "message": "使用通行密钥解锁失败。请重试或使用其他解锁方式。" }, "noPrfCredentialsAvailable": { - "message": "没有可用于解锁的 PRF 通行密钥。" + "message": "没有可用于解锁的支持 PRF 的通行密钥。" }, "additionalStorageGB": { "message": "附加存储 GB" @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "确定要继续吗?" }, + "errorCannotDecrypt": { + "message": "错误:无法解密" + }, "userVerificationFailed": { "message": "用户验证失败。" }, @@ -12793,6 +12888,55 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "perUser": { - "message": "每位用户" + "message": "每用户" + }, + "upgradeToTeams": { + "message": "升级为团队版" + }, + "upgradeToEnterprise": { + "message": "升级为企业版" + }, + "upgradeShareEvenMore": { + "message": "使用家庭版共享更多内容,或使用团队版或企业版获得强大、可信赖的密码安全防护" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "价格不含税,按年计费。" + }, + "invoicePreviewErrorMessage": { + "message": "生成账单预览时遇到错误。" + }, + "planProratedMembershipInMonths": { + "message": "按比例计费的 $PLAN$ 成员资格($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "高级版订阅信用额度" + }, + "enterpriseMembership": { + "message": "企业版成员资格" + }, + "teamsMembership": { + "message": "团队版成员资格" + }, + "plansUpdated": { + "message": "您已升级为 $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "更新您的付款方式时出错。" } } diff --git a/apps/web/src/locales/zh_TW/messages.json b/apps/web/src/locales/zh_TW/messages.json index b80112b4d39..8056e49b08a 100644 --- a/apps/web/src/locales/zh_TW/messages.json +++ b/apps/web/src/locales/zh_TW/messages.json @@ -47,8 +47,8 @@ "noEditPermissions": { "message": "您沒有權限編輯此項目" }, - "reviewAtRiskPasswords": { - "message": "檢視各項應用程式中具有風險的密碼(包含強度不足、已外洩或重複使用的密碼)。請選取最關鍵的應用程式,以便優先採取安全措施,引導使用者處理風險密碼。" + "reviewAccessIntelligence": { + "message": "Review security reports to find and fix credential risks before they escalate." }, "reviewAtRiskLoginsPrompt": { "message": "檢視高風險登入" @@ -268,6 +268,42 @@ } } }, + "numCriticalApplicationsMarkedSuccess": { + "message": "$COUNT$ applications marked critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "numApplicationsUnmarkedCriticalSuccess": { + "message": "$COUNT$ applications marked not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsCritical": { + "message": "Mark $COUNT$ as critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "markAppCountAsNotCritical": { + "message": "Mark $COUNT$ as not critical", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "applicationsMarkedAsCriticalFail": { "message": "標記應用程式為關鍵失敗" }, @@ -5394,8 +5430,8 @@ "minimumNumberOfWords": { "message": "最少單字個數" }, - "overridePasswordTypePolicy": { - "message": "密碼類型", + "passwordTypePolicyOverride": { + "message": "Password type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -6637,6 +6673,18 @@ } } }, + "reinviteSuccessToast": { + "message": "1 invitation sent" + }, + "bulkReinviteSentToast": { + "message": "$COUNT$ invitations sent", + "placeholders": { + "count": { + "content": "$1", + "example": "12" + } + } + }, "bulkReinviteLimitedSuccessToast": { "message": "已重新邀請 $SELECTEDCOUNT$ 位使用者中的 $LIMIT$ 位。有 $EXCLUDEDCOUNT$ 位因達到 $LIMIT$ 的邀請上限而未被邀請。", "placeholders": { @@ -6654,6 +6702,50 @@ } } }, + "bulkReinviteProgressTitle": { + "message": "$COUNT$ of $TOTAL$ invitations sent...", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkReinviteProgressSubtitle": { + "message": "Keep this page open until all are sent." + }, + "bulkReinviteFailuresTitle": { + "message": "$COUNT$ invitations didn't send", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + } + } + }, + "bulkReinviteFailureTitle": { + "message": "1 invitation didn't send" + }, + "bulkReinviteFailureDescription": { + "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "placeholders": { + "count": { + "content": "$1", + "example": "1,000" + }, + "total": { + "content": "$2", + "example": "2,000" + } + } + }, + "bulkResendInvitations": { + "message": "Try sending again" + }, "bulkRemovedMessage": { "message": "已成功移除。" }, @@ -10092,6 +10184,9 @@ "assignTasks": { "message": "指派任務" }, + "allTasksAssigned": { + "message": "All tasks have been assigned" + }, "assignSecurityTasksToMembers": { "message": "傳送變更密碼的通知" }, @@ -11804,9 +11899,6 @@ "itemWasSentToArchive": { "message": "項目已移至封存" }, - "itemsWereSentToArchive": { - "message": "項目已移至封存" - }, "itemWasUnarchived": { "message": "已取消封存項目" }, @@ -12490,6 +12582,9 @@ "confirmNoSelectedCriticalApplicationsDesc": { "message": "您確定要繼續嗎?" }, + "errorCannotDecrypt": { + "message": "Error: Cannot decrypt" + }, "userVerificationFailed": { "message": "使用者驗證失敗。" }, @@ -12794,5 +12889,54 @@ }, "perUser": { "message": "每位使用者" + }, + "upgradeToTeams": { + "message": "Upgrade to Teams" + }, + "upgradeToEnterprise": { + "message": "Upgrade to Enterprise" + }, + "upgradeShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + }, + "organizationUpgradeTaxInformationMessage": { + "message": "Prices exclude tax and are billed annually." + }, + "invoicePreviewErrorMessage": { + "message": "Encountered an error while generating the invoice preview." + }, + "planProratedMembershipInMonths": { + "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + }, + "numofmonths": { + "content": "$2", + "example": "6 Months" + } + } + }, + "premiumSubscriptionCredit": { + "message": "Premium subscription credit" + }, + "enterpriseMembership": { + "message": "Enterprise membership" + }, + "teamsMembership": { + "message": "Teams membership" + }, + "plansUpdated": { + "message": "You've upgraded to $PLAN$!", + "placeholders": { + "plan": { + "content": "$1", + "example": "Families" + } + } + }, + "paymentMethodUpdateError": { + "message": "There was an error updating your payment method." } } From 69264c884147728fa8a4d27c2326454d6ffa8496 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Fri, 13 Feb 2026 15:43:42 +0100 Subject: [PATCH 029/134] [PM-32212] Migrate platform font icons to bit-icon (#18970) * Changes on browser * Changes on desktop * Changes on web * Fix chromatic story --------- Co-authored-by: Daniel James Smith --- .../popup/layout/popup-page.component.html | 2 +- .../popup/layout/popup-page.component.ts | 4 +- .../src/app/accounts/settings.component.html | 42 ++++++------------- .../src/app/accounts/settings.component.ts | 2 + .../layout/account-switcher.component.html | 17 +++----- .../app/layout/search/search.component.html | 2 +- apps/desktop/src/app/shared/shared.module.ts | 3 ++ apps/desktop/src/index.html | 1 + .../environment-selector.component.html | 10 ++--- .../layouts/header/web-header.component.html | 10 ++--- .../org-switcher/org-switcher.component.html | 17 ++++---- .../account-fingerprint.component.html | 2 +- .../onboarding/onboarding-task.component.html | 8 +++- .../onboarding/onboarding-task.component.ts | 4 +- .../onboarding/onboarding.component.html | 8 +--- .../onboarding/onboarding.stories.ts | 4 +- apps/web/src/app/shared/shared.module.ts | 3 ++ .../platform/proxy-cookie-redirect.html | 1 + apps/web/src/index.html | 1 + 19 files changed, 66 insertions(+), 75 deletions(-) diff --git a/apps/browser/src/platform/popup/layout/popup-page.component.html b/apps/browser/src/platform/popup/layout/popup-page.component.html index bb24fb800aa..dc07d025e60 100644 --- a/apps/browser/src/platform/popup/layout/popup-page.component.html +++ b/apps/browser/src/platform/popup/layout/popup-page.component.html @@ -40,7 +40,7 @@ class="tw-absolute tw-inset-0 tw-flex tw-items-center tw-justify-center tw-text-main" [ngClass]="{ 'tw-invisible': !loading() }" > - + diff --git a/apps/browser/src/platform/popup/layout/popup-page.component.ts b/apps/browser/src/platform/popup/layout/popup-page.component.ts index 7d4b7decb7f..e661bf2ca00 100644 --- a/apps/browser/src/platform/popup/layout/popup-page.component.ts +++ b/apps/browser/src/platform/popup/layout/popup-page.component.ts @@ -13,7 +13,7 @@ import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { filter, switchMap, fromEvent, startWith, map } from "rxjs"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { ScrollLayoutHostDirective, ScrollLayoutService } from "@bitwarden/components"; +import { IconModule, ScrollLayoutHostDirective, ScrollLayoutService } from "@bitwarden/components"; @Component({ selector: "popup-page", @@ -21,7 +21,7 @@ import { ScrollLayoutHostDirective, ScrollLayoutService } from "@bitwarden/compo host: { class: "tw-h-full tw-flex tw-flex-col tw-overflow-y-hidden", }, - imports: [CommonModule, ScrollLayoutHostDirective], + imports: [CommonModule, IconModule, ScrollLayoutHostDirective], changeDetection: ChangeDetectionStrategy.OnPush, }) export class PopupPageComponent { diff --git a/apps/desktop/src/app/accounts/settings.component.html b/apps/desktop/src/app/accounts/settings.component.html index d5042918d2f..90ff8f3a791 100644 --- a/apps/desktop/src/app/accounts/settings.component.html +++ b/apps/desktop/src/app/accounts/settings.component.html @@ -16,16 +16,10 @@ [attr.aria-expanded]="showSecurity" appAutofocus > - - + {{ "security" | i18n }} @@ -147,16 +141,10 @@ (click)="showAccountPreferences = !showAccountPreferences" [attr.aria-expanded]="showAccountPreferences" > - - + {{ "accountPreferences" | i18n }} @@ -222,16 +210,10 @@ (click)="showAppPreferences = !showAppPreferences" [attr.aria-expanded]="showAppPreferences" > - - + {{ "appPreferences" | i18n }} diff --git a/apps/desktop/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts index f2e828b95ce..7bab7db3c29 100644 --- a/apps/desktop/src/app/accounts/settings.component.ts +++ b/apps/desktop/src/app/accounts/settings.component.ts @@ -45,6 +45,7 @@ import { DialogService, FormFieldModule, IconButtonModule, + IconModule, ItemModule, LinkModule, SectionComponent, @@ -89,6 +90,7 @@ import { NativeMessagingManifestService } from "../services/native-messaging-man FormsModule, ReactiveFormsModule, IconButtonModule, + IconModule, ItemModule, JslibModule, LinkModule, diff --git a/apps/desktop/src/app/layout/account-switcher.component.html b/apps/desktop/src/app/layout/account-switcher.component.html index ef177ea1bb6..7d0ee8fac83 100644 --- a/apps/desktop/src/app/layout/account-switcher.component.html +++ b/apps/desktop/src/app/layout/account-switcher.component.html @@ -31,11 +31,7 @@ {{ "switchAccount" | i18n }} - + )
- + class="bwi-2x text-muted" + >
diff --git a/apps/desktop/src/app/layout/search/search.component.html b/apps/desktop/src/app/layout/search/search.component.html index 515385c2076..b5bcd264897 100644 --- a/apps/desktop/src/app/layout/search/search.component.html +++ b/apps/desktop/src/app/layout/search/search.component.html @@ -7,5 +7,5 @@ [formControl]="searchText" appAutofocus /> - +
diff --git a/apps/desktop/src/app/shared/shared.module.ts b/apps/desktop/src/app/shared/shared.module.ts index 6eed4a197f3..85b3b800e83 100644 --- a/apps/desktop/src/app/shared/shared.module.ts +++ b/apps/desktop/src/app/shared/shared.module.ts @@ -7,6 +7,7 @@ import { NgModule } from "@angular/core"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { IconModule } from "@bitwarden/components"; import { AvatarComponent } from "../components/avatar.component"; import { ServicesModule } from "../services/services.module"; @@ -17,6 +18,7 @@ import { ServicesModule } from "../services/services.module"; A11yModule, DragDropModule, FormsModule, + IconModule, JslibModule, OverlayModule, ReactiveFormsModule, @@ -30,6 +32,7 @@ import { ServicesModule } from "../services/services.module"; DatePipe, DragDropModule, FormsModule, + IconModule, JslibModule, OverlayModule, ReactiveFormsModule, diff --git a/apps/desktop/src/index.html b/apps/desktop/src/index.html index 37eb64adf35..044d7eb0e2f 100644 --- a/apps/desktop/src/index.html +++ b/apps/desktop/src/index.html @@ -13,6 +13,7 @@ +
diff --git a/apps/web/src/app/components/environment-selector/environment-selector.component.html b/apps/web/src/app/components/environment-selector/environment-selector.component.html index 9862f62c2e2..44039bfe605 100644 --- a/apps/web/src/app/components/environment-selector/environment-selector.component.html +++ b/apps/web/src/app/components/environment-selector/environment-selector.component.html @@ -7,11 +7,11 @@ region == currentRegion ? 'javascript:void(0)' : region.urls.webVault + routeAndParams " > - + > {{ region.domain }}
@@ -19,7 +19,7 @@ {{ "accessing" | i18n }}: {{ currentRegion?.domain }} - +
diff --git a/apps/web/src/app/layouts/header/web-header.component.html b/apps/web/src/app/layouts/header/web-header.component.html index 995169e3dc1..9288c96237e 100644 --- a/apps/web/src/app/layouts/header/web-header.component.html +++ b/apps/web/src/app/layouts/header/web-header.component.html @@ -60,11 +60,11 @@ - + {{ "accountSettings" | i18n }} - + {{ "getHelp" | i18n }} - + {{ "getApps" | i18n }} diff --git a/apps/web/src/app/layouts/org-switcher/org-switcher.component.html b/apps/web/src/app/layouts/org-switcher/org-switcher.component.html index a9acddeb0b8..b8f7c5ab0c0 100644 --- a/apps/web/src/app/layouts/org-switcher/org-switcher.component.html +++ b/apps/web/src/app/layouts/org-switcher/org-switcher.component.html @@ -7,13 +7,14 @@ [routerLinkActiveOptions]="{ exact: true }" [(open)]="open" > - + > - + > -
{{ fingerprint }} diff --git a/apps/web/src/app/shared/components/onboarding/onboarding-task.component.html b/apps/web/src/app/shared/components/onboarding/onboarding-task.component.html index f0c0b01e06e..e52771a282b 100644 --- a/apps/web/src/app/shared/components/onboarding/onboarding-task.component.html +++ b/apps/web/src/app/shared/components/onboarding/onboarding-task.component.html @@ -1,10 +1,14 @@ - {{ title }}{{ title }} diff --git a/apps/web/src/app/shared/components/onboarding/onboarding-task.component.ts b/apps/web/src/app/shared/components/onboarding/onboarding-task.component.ts index 277a4d2d26e..47a618a1269 100644 --- a/apps/web/src/app/shared/components/onboarding/onboarding-task.component.ts +++ b/apps/web/src/app/shared/components/onboarding/onboarding-task.component.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Component, Input } from "@angular/core"; +import { BitwardenIcon } from "@bitwarden/components"; + // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ @@ -21,7 +23,7 @@ export class OnboardingTaskComponent { // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals // eslint-disable-next-line @angular-eslint/prefer-signals @Input() - icon = "bwi-info-circle"; + icon: BitwardenIcon = "bwi-info-circle"; // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals // eslint-disable-next-line @angular-eslint/prefer-signals diff --git a/apps/web/src/app/shared/components/onboarding/onboarding.component.html b/apps/web/src/app/shared/components/onboarding/onboarding.component.html index 2433ec51fcc..ca98ceb8fbf 100644 --- a/apps/web/src/app/shared/components/onboarding/onboarding.component.html +++ b/apps/web/src/app/shared/components/onboarding/onboarding.component.html @@ -6,11 +6,7 @@ {{ "complete" | i18n: amountCompleted : tasks.length }} - +
    @@ -24,5 +20,5 @@ - + diff --git a/apps/web/src/app/shared/components/onboarding/onboarding.stories.ts b/apps/web/src/app/shared/components/onboarding/onboarding.stories.ts index 6873700e2bc..26c951fb11f 100644 --- a/apps/web/src/app/shared/components/onboarding/onboarding.stories.ts +++ b/apps/web/src/app/shared/components/onboarding/onboarding.stories.ts @@ -4,7 +4,7 @@ import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/an import { delay, of, startWith } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { LinkModule, SvgModule, ProgressModule } from "@bitwarden/components"; +import { LinkModule, SvgModule, ProgressModule, IconModule } from "@bitwarden/components"; import { PreloadedEnglishI18nModule } from "../../../core/tests"; @@ -16,7 +16,7 @@ export default { component: OnboardingComponent, decorators: [ moduleMetadata({ - imports: [JslibModule, RouterModule, LinkModule, SvgModule, ProgressModule], + imports: [JslibModule, RouterModule, LinkModule, IconModule, SvgModule, ProgressModule], declarations: [OnboardingTaskComponent], }), applicationConfig({ diff --git a/apps/web/src/app/shared/shared.module.ts b/apps/web/src/app/shared/shared.module.ts index b83555fd84e..729238e0b0d 100644 --- a/apps/web/src/app/shared/shared.module.ts +++ b/apps/web/src/app/shared/shared.module.ts @@ -18,6 +18,7 @@ import { DialogModule, FormFieldModule, IconButtonModule, + IconModule, SvgModule, LinkModule, MenuModule, @@ -63,6 +64,7 @@ import { DialogModule, FormFieldModule, IconButtonModule, + IconModule, SvgModule, LinkModule, MenuModule, @@ -99,6 +101,7 @@ import { DialogModule, FormFieldModule, IconButtonModule, + IconModule, SvgModule, LinkModule, MenuModule, diff --git a/apps/web/src/connectors/platform/proxy-cookie-redirect.html b/apps/web/src/connectors/platform/proxy-cookie-redirect.html index 1daa6d2e412..1918fcd771c 100644 --- a/apps/web/src/connectors/platform/proxy-cookie-redirect.html +++ b/apps/web/src/connectors/platform/proxy-cookie-redirect.html @@ -18,6 +18,7 @@
    Bitwarden
    +
    + Date: Fri, 13 Feb 2026 10:02:36 -0500 Subject: [PATCH 030/134] [PM-32075] Fix self host bug due to type mismatch (#18919) * fix self host bug with data model * fix type issues * fix types, make successful required --- .../members/components/bulk/bulk-status.component.ts | 5 ++--- .../members/deprecated_members.component.ts | 2 +- .../organizations/members/members.component.spec.ts | 4 ++-- .../organizations/members/members.component.ts | 2 +- .../member-actions/member-actions.service.spec.ts | 2 +- .../services/member-actions/member-actions.service.ts | 8 ++------ .../member-dialog-manager.service.ts | 4 +++- .../providers/manage/deprecated_members.component.ts | 10 ++++++---- .../providers/manage/members.component.ts | 10 ++++++---- 9 files changed, 24 insertions(+), 23 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.ts index 5c9bf919ed4..cfddb17627a 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.ts @@ -9,7 +9,6 @@ import { } from "@bitwarden/common/admin-console/enums"; import { ProviderUserBulkResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk.response"; import { ProviderUserUserDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user.response"; -import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { DIALOG_DATA, DialogConfig, DialogService } from "@bitwarden/components"; @@ -34,7 +33,7 @@ type BulkStatusEntry = { type BulkStatusDialogData = { users: Array; filteredUsers: Array; - request: Promise>; + request: Promise; successfulMessage: string; }; @@ -63,7 +62,7 @@ export class BulkStatusComponent implements OnInit { async showBulkStatus(data: BulkStatusDialogData) { try { const response = await data.request; - const keyedErrors: any = response.data + const keyedErrors: any = (response ?? []) .filter((r) => r.error !== "") .reduce((a, x) => ({ ...a, [x.id]: x.error }), {}); const keyedFilteredUsers: any = data.filteredUsers.reduce( diff --git a/apps/web/src/app/admin-console/organizations/members/deprecated_members.component.ts b/apps/web/src/app/admin-console/organizations/members/deprecated_members.component.ts index dae9bafbcfe..1f1e19e2a6f 100644 --- a/apps/web/src/app/admin-console/organizations/members/deprecated_members.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/deprecated_members.component.ts @@ -446,7 +446,7 @@ export class MembersComponent extends BaseMembersComponent try { const result = await this.memberActionsService.bulkReinvite(organization, filteredUsers); - if (!result.successful) { + if (result.successful.length === 0) { throw new Error(); } diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.spec.ts b/apps/web/src/app/admin-console/organizations/members/members.component.spec.ts index 1cd90989b12..9a371de1acd 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.spec.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.component.spec.ts @@ -515,7 +515,7 @@ describe("vNextMembersComponent", () => { }; jest.spyOn(component["dataSource"](), "isIncreasedBulkLimitEnabled").mockReturnValue(false); jest.spyOn(component["dataSource"](), "getCheckedUsers").mockReturnValue([invitedUser]); - mockMemberActionsService.bulkReinvite.mockResolvedValue({ successful: true }); + mockMemberActionsService.bulkReinvite.mockResolvedValue({ successful: [{}], failed: [] }); await component.bulkReinvite(mockOrg); @@ -549,7 +549,7 @@ describe("vNextMembersComponent", () => { jest.spyOn(component["dataSource"](), "isIncreasedBulkLimitEnabled").mockReturnValue(false); jest.spyOn(component["dataSource"](), "getCheckedUsers").mockReturnValue([invitedUser]); const error = new Error("Bulk reinvite failed"); - mockMemberActionsService.bulkReinvite.mockResolvedValue({ successful: false, failed: error }); + mockMemberActionsService.bulkReinvite.mockResolvedValue({ successful: [], failed: error }); await component.bulkReinvite(mockOrg); diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.ts b/apps/web/src/app/admin-console/organizations/members/members.component.ts index 6139c5f07a5..826bdfb5f69 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.component.ts @@ -426,7 +426,7 @@ export class vNextMembersComponent { const result = await this.memberActionsService.bulkReinvite(organization, filteredUsers); - if (!result.successful) { + if (result.successful.length === 0) { this.validationService.showError(result.failed); } diff --git a/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.spec.ts b/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.spec.ts index 688c7ed77ce..1ba056a24f6 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.spec.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.spec.ts @@ -507,7 +507,7 @@ describe("MemberActionsService", () => { const result = await service.bulkReinvite(mockOrganization, users); - expect(result.successful).toBeUndefined(); + expect(result.successful).toHaveLength(0); expect(result.failed).toHaveLength(totalUsers); expect(result.failed.every((f) => f.error === errorMessage)).toBe(true); expect(organizationUserApiService.postManyOrganizationUserReinvite).toHaveBeenCalledTimes(2); diff --git a/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.ts b/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.ts index e5f8c0c6673..7d573c8eeef 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.ts @@ -37,11 +37,7 @@ export interface MemberActionResult { } export class BulkActionResult { - constructor() { - this.failed = []; - } - - successful?: OrganizationUserBulkResponse[]; + successful: OrganizationUserBulkResponse[] = []; failed: { id: string; error: string }[] = []; } @@ -316,7 +312,7 @@ export class MemberActionsService { } return { - successful: allSuccessful.length > 0 ? allSuccessful : undefined, + successful: allSuccessful, failed: allFailed, }; } diff --git a/apps/web/src/app/admin-console/organizations/members/services/member-dialog-manager/member-dialog-manager.service.ts b/apps/web/src/app/admin-console/organizations/members/services/member-dialog-manager/member-dialog-manager.service.ts index 18106031fd0..6c367692376 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/member-dialog-manager/member-dialog-manager.service.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/member-dialog-manager/member-dialog-manager.service.ts @@ -1,8 +1,10 @@ import { Injectable, WritableSignal } from "@angular/core"; import { firstValueFrom, lastValueFrom } from "rxjs"; +import { OrganizationUserBulkResponse } from "@bitwarden/admin-console/common"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { ProviderUserBulkResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk.response"; import { ProductTierType } from "@bitwarden/common/billing/enums"; import { OrganizationBillingMetadataResponse } from "@bitwarden/common/billing/models/response/organization-billing-metadata.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -197,7 +199,7 @@ export class MemberDialogManagerService { async openBulkStatusDialog( users: OrganizationUserView[], filteredUsers: OrganizationUserView[], - request: Promise, + request: Promise, successMessage: string, ): Promise { const dialogRef = BulkStatusComponent.open(this.dialogService, { diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/deprecated_members.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/deprecated_members.component.ts index 1b1ae25c027..464b9982689 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/deprecated_members.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/deprecated_members.component.ts @@ -223,10 +223,12 @@ export class MembersComponent extends BaseMembersComponent { } } else { // Feature flag disabled - show legacy dialog - const request = this.apiService.postManyProviderUserReinvite( - this.providerId, - new ProviderUserBulkRequest(checkedInvitedUsers.map((user) => user.id)), - ); + const request = this.apiService + .postManyProviderUserReinvite( + this.providerId, + new ProviderUserBulkRequest(checkedInvitedUsers.map((user) => user.id)), + ) + .then((response) => response.data); const dialogRef = BulkStatusComponent.open(this.dialogService, { data: { diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts index c63bda449c5..308b93ac2e3 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts @@ -236,10 +236,12 @@ export class vNextMembersComponent { } } else { // In self-hosted environments, show legacy dialog - const request = this.apiService.postManyProviderUserReinvite( - providerId, - new ProviderUserBulkRequest(checkedInvitedUsers.map((user) => user.id)), - ); + const request = this.apiService + .postManyProviderUserReinvite( + providerId, + new ProviderUserBulkRequest(checkedInvitedUsers.map((user) => user.id)), + ) + .then((response) => response.data); const dialogRef = BulkStatusComponent.open(this.dialogService, { data: { From b567fea7e77ecb3a1f30afb2d6acfddb644f6330 Mon Sep 17 00:00:00 2001 From: Jared Date: Fri, 13 Feb 2026 11:38:35 -0500 Subject: [PATCH 031/134] [PM-29506] Rid of old feature flag for members feature flag (#18884) * [PM-31750] Refactor members routing and user confirmation logic * Simplified user confirmation process by removing feature flag checks. * Updated routing to directly use the new members component without feature flagging. * Removed deprecated members component references from routing modules. * Cleaned up feature flag enum by removing unused entries. * trigger claude * [PM-31750] Refactor members component and remove deprecated files * Renamed vNextMembersComponent to MembersComponent for consistency. * Removed deprecated_members.component.ts and associated HTML files. * Updated routing and references to use the new MembersComponent. * Cleaned up related tests to reflect the component name change. * Refactor import statements in security-tasks.service.ts for improved readability * Update apps/web/src/app/admin-console/organizations/manage/user-confirm.component.ts Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> * Remove BaseMembersComponent and related imports from the admin console, streamlining member management functionality. * Remove unused ConfigService import from UserConfirmComponent to clean up code. * Implement feature flag logic for user restoration in MemberDialogComponent, allowing conditional restoration based on DefaultUserCollectionRestore flag. --------- Co-authored-by: Thomas Rittson Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> --- .../common/base-members.component.ts | 245 ------- .../manage/user-confirm.component.ts | 13 - .../member-dialog/member-dialog.component.ts | 2 +- .../members/deprecated_members.component.html | 495 -------------- .../members/deprecated_members.component.ts | 624 ------------------ .../members/members-routing.module.ts | 23 +- .../members/members.component.spec.ts | 14 +- .../members/members.component.ts | 2 +- .../organizations/members/members.module.ts | 4 +- .../manage/deprecated_members.component.html | 225 ------- .../manage/deprecated_members.component.ts | 351 ---------- .../providers/manage/members.component.ts | 2 +- .../providers/providers-routing.module.ts | 27 +- .../providers/providers.module.ts | 4 +- .../shared/security-tasks.service.ts | 6 +- libs/common/src/enums/feature-flag.enum.ts | 2 - 16 files changed, 34 insertions(+), 2005 deletions(-) delete mode 100644 apps/web/src/app/admin-console/common/base-members.component.ts delete mode 100644 apps/web/src/app/admin-console/organizations/members/deprecated_members.component.html delete mode 100644 apps/web/src/app/admin-console/organizations/members/deprecated_members.component.ts delete mode 100644 bitwarden_license/bit-web/src/app/admin-console/providers/manage/deprecated_members.component.html delete mode 100644 bitwarden_license/bit-web/src/app/admin-console/providers/manage/deprecated_members.component.ts diff --git a/apps/web/src/app/admin-console/common/base-members.component.ts b/apps/web/src/app/admin-console/common/base-members.component.ts deleted file mode 100644 index 5ecf4269a1a..00000000000 --- a/apps/web/src/app/admin-console/common/base-members.component.ts +++ /dev/null @@ -1,245 +0,0 @@ -import { Directive } from "@angular/core"; -import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { FormControl } from "@angular/forms"; -import { firstValueFrom, lastValueFrom, debounceTime, combineLatest, BehaviorSubject } from "rxjs"; - -import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service"; -import { - OrganizationUserStatusType, - OrganizationUserType, - ProviderUserStatusType, - ProviderUserType, -} from "@bitwarden/common/admin-console/enums"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { ProviderUserUserDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user.response"; -import { ListResponse } from "@bitwarden/common/models/response/list.response"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { DialogService, ToastService } from "@bitwarden/components"; -import { KeyService } from "@bitwarden/key-management"; - -import { OrganizationUserView } from "../organizations/core/views/organization-user.view"; -import { UserConfirmComponent } from "../organizations/manage/user-confirm.component"; -import { MemberActionResult } from "../organizations/members/services/member-actions/member-actions.service"; - -import { PeopleTableDataSource, peopleFilter } from "./people-table-data-source"; - -export type StatusType = OrganizationUserStatusType | ProviderUserStatusType; -export type UserViewTypes = ProviderUserUserDetailsResponse | OrganizationUserView; - -/** - * A refactored copy of BasePeopleComponent, using the component library table and other modern features. - * This will replace BasePeopleComponent once all subclasses have been changed over to use this class. - */ -@Directive() -export abstract class BaseMembersComponent { - /** - * Shows a banner alerting the admin that users need to be confirmed. - */ - get showConfirmUsers(): boolean { - return ( - this.dataSource.activeUserCount > 1 && - this.dataSource.confirmedUserCount > 0 && - this.dataSource.confirmedUserCount < 3 && - this.dataSource.acceptedUserCount > 0 - ); - } - - get showBulkConfirmUsers(): boolean { - return this.dataSource - .getCheckedUsers() - .every((member) => member.status == this.userStatusType.Accepted); - } - - get showBulkReinviteUsers(): boolean { - return this.dataSource - .getCheckedUsers() - .every((member) => member.status == this.userStatusType.Invited); - } - - abstract userType: typeof OrganizationUserType | typeof ProviderUserType; - abstract userStatusType: typeof OrganizationUserStatusType | typeof ProviderUserStatusType; - - protected abstract dataSource: PeopleTableDataSource; - - firstLoaded: boolean = false; - - /** - * The currently selected status filter, or undefined to show all active users. - */ - status?: StatusType; - - /** - * The currently executing promise - used to avoid multiple user actions executing at once. - */ - actionPromise?: Promise; - - protected searchControl = new FormControl("", { nonNullable: true }); - protected statusToggle = new BehaviorSubject(undefined); - - constructor( - protected apiService: ApiService, - protected i18nService: I18nService, - protected keyService: KeyService, - protected validationService: ValidationService, - protected logService: LogService, - protected userNamePipe: UserNamePipe, - protected dialogService: DialogService, - protected organizationManagementPreferencesService: OrganizationManagementPreferencesService, - protected toastService: ToastService, - ) { - // Connect the search input and status toggles to the table dataSource filter - combineLatest([this.searchControl.valueChanges.pipe(debounceTime(200)), this.statusToggle]) - .pipe(takeUntilDestroyed()) - .subscribe( - ([searchText, status]) => (this.dataSource.filter = peopleFilter(searchText, status)), - ); - } - - abstract edit(user: UserView, organization?: Organization): void; - abstract getUsers(organization?: Organization): Promise | UserView[]>; - abstract removeUser(id: string, organization?: Organization): Promise; - abstract reinviteUser(id: string, organization?: Organization): Promise; - abstract confirmUser( - user: UserView, - publicKey: Uint8Array, - organization?: Organization, - ): Promise; - abstract invite(organization?: Organization): void; - - async load(organization?: Organization) { - // Load new users from the server - const response = await this.getUsers(organization); - - // GetUsers can return a ListResponse or an Array - if (response instanceof ListResponse) { - this.dataSource.data = response.data != null && response.data.length > 0 ? response.data : []; - } else if (Array.isArray(response)) { - this.dataSource.data = response; - } - - this.firstLoaded = true; - } - - protected async removeUserConfirmationDialog(user: UserView) { - return this.dialogService.openSimpleDialog({ - title: this.userNamePipe.transform(user), - content: { key: "removeUserConfirmation" }, - type: "warning", - }); - } - - async remove(user: UserView, organization?: Organization) { - const confirmed = await this.removeUserConfirmationDialog(user); - if (!confirmed) { - return false; - } - - this.actionPromise = this.removeUser(user.id, organization); - try { - const result = await this.actionPromise; - if (result.success) { - this.toastService.showToast({ - variant: "success", - message: this.i18nService.t("removedUserId", this.userNamePipe.transform(user)), - }); - this.dataSource.removeUser(user); - } else { - throw new Error(result.error); - } - } catch (e) { - this.validationService.showError(e); - } - this.actionPromise = undefined; - } - - async reinvite(user: UserView, organization?: Organization) { - if (this.actionPromise != null) { - return; - } - - this.actionPromise = this.reinviteUser(user.id, organization); - try { - const result = await this.actionPromise; - if (result.success) { - this.toastService.showToast({ - variant: "success", - message: this.i18nService.t("hasBeenReinvited", this.userNamePipe.transform(user)), - }); - } else { - throw new Error(result.error); - } - } catch (e) { - this.validationService.showError(e); - } - this.actionPromise = undefined; - } - - async confirm(user: UserView, organization?: Organization) { - const confirmUser = async (publicKey: Uint8Array) => { - try { - this.actionPromise = this.confirmUser(user, publicKey, organization); - const result = await this.actionPromise; - if (result.success) { - user.status = this.userStatusType.Confirmed; - this.dataSource.replaceUser(user); - - this.toastService.showToast({ - variant: "success", - message: this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(user)), - }); - } else { - throw new Error(result.error); - } - } catch (e) { - this.validationService.showError(e); - throw e; - } finally { - this.actionPromise = undefined; - } - }; - - if (this.actionPromise != null) { - return; - } - - try { - const publicKeyResponse = await this.apiService.getUserPublicKey(user.userId); - const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey); - - const autoConfirm = await firstValueFrom( - this.organizationManagementPreferencesService.autoConfirmFingerPrints.state$, - ); - if (user == null) { - throw new Error("Cannot confirm null user."); - } - if (autoConfirm == null || !autoConfirm) { - const dialogRef = UserConfirmComponent.open(this.dialogService, { - data: { - name: this.userNamePipe.transform(user), - userId: user.userId, - publicKey: publicKey, - confirmUser: () => confirmUser(publicKey), - }, - }); - await lastValueFrom(dialogRef.closed); - - return; - } - - try { - const fingerprint = await this.keyService.getFingerprint(user.userId, publicKey); - this.logService.info(`User's fingerprint: ${fingerprint.join("-")}`); - } catch (e) { - this.logService.error(e); - } - await confirmUser(publicKey); - } catch (e) { - this.logService.error(`Handled exception: ${e}`); - } - } -} diff --git a/apps/web/src/app/admin-console/organizations/manage/user-confirm.component.ts b/apps/web/src/app/admin-console/organizations/manage/user-confirm.component.ts index 03130d0b946..788d01695b0 100644 --- a/apps/web/src/app/admin-console/organizations/manage/user-confirm.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/user-confirm.component.ts @@ -2,11 +2,8 @@ // @ts-strict-ignore import { Component, Inject, OnInit } from "@angular/core"; import { FormControl, FormGroup } from "@angular/forms"; -import { firstValueFrom } from "rxjs"; import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; @@ -17,8 +14,6 @@ export type UserConfirmDialogData = { name: string; userId: string; publicKey: Uint8Array; - // @TODO remove this when doing feature flag cleanup for members component refactor. - confirmUser?: (publicKey: Uint8Array) => Promise; }; // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush @@ -46,7 +41,6 @@ export class UserConfirmComponent implements OnInit { private keyService: KeyService, private logService: LogService, private organizationManagementPreferencesService: OrganizationManagementPreferencesService, - private configService: ConfigService, ) { this.name = data.name; this.userId = data.userId; @@ -76,13 +70,6 @@ export class UserConfirmComponent implements OnInit { await this.organizationManagementPreferencesService.autoConfirmFingerPrints.set(true); } - const membersComponentRefactorEnabled = await firstValueFrom( - this.configService.getFeatureFlag$(FeatureFlag.MembersComponentRefactor), - ); - if (!membersComponentRefactorEnabled) { - await this.data.confirmUser(this.publicKey); - } - this.dialogRef.close(true); }; diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts index 6848f76286f..43520449535 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts @@ -195,9 +195,9 @@ export class MemberDialogComponent implements OnDestroy { private accountService: AccountService, organizationService: OrganizationService, private toastService: ToastService, - private configService: ConfigService, private deleteManagedMemberWarningService: DeleteManagedMemberWarningService, private organizationUserService: OrganizationUserService, + private configService: ConfigService, ) { this.organization$ = accountService.activeAccount$.pipe( getUserId, diff --git a/apps/web/src/app/admin-console/organizations/members/deprecated_members.component.html b/apps/web/src/app/admin-console/organizations/members/deprecated_members.component.html deleted file mode 100644 index 65bab31c728..00000000000 --- a/apps/web/src/app/admin-console/organizations/members/deprecated_members.component.html +++ /dev/null @@ -1,495 +0,0 @@ -@let organization = this.organization(); -@if (organization) { - - - - - - - - -
    - - - {{ "all" | i18n }} - {{ - allCount - }} - - - - {{ "invited" | i18n }} - {{ - invitedCount - }} - - - - {{ "needsConfirmation" | i18n }} - {{ - acceptedUserCount - }} - - - - {{ "revoked" | i18n }} - {{ - revokedCount - }} - - -
    - - - {{ "loading" | i18n }} - - -

    {{ "noMembersInList" | i18n }}

    - - - {{ "usersNeedConfirmed" | i18n }} - - - - - - - - - - - {{ "name" | i18n }} - {{ (organization.useGroups ? "groups" : "collections") | i18n }} - {{ "role" | i18n }} - {{ "policies" | i18n }} - -
    - - -
    - - - - - - - - - - - - - - - -
    - - - - - - - -
    - -
    -
    - - - {{ "invited" | i18n }} - - - {{ "needsConfirmation" | i18n }} - - - {{ "revoked" | i18n }} - -
    -
    - {{ u.email }} -
    -
    -
    - -
    - - -
    - -
    -
    - {{ u.name ?? u.email }} - - {{ "invited" | i18n }} - - - {{ "needsConfirmation" | i18n }} - - - {{ "revoked" | i18n }} - -
    -
    - {{ u.email }} -
    -
    -
    - -
    - - - - - - - - - - - - - - - {{ u.type | userType }} - - - - - {{ u.type | userType }} - - - - - - - {{ "userUsingTwoStep" | i18n }} - - @let resetPasswordPolicyEnabled = resetPasswordPolicyEnabled$ | async; - - - {{ "enrolledAccountRecovery" | i18n }} - - - -
    -
    - -
    - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    -
    -
    -
    -} diff --git a/apps/web/src/app/admin-console/organizations/members/deprecated_members.component.ts b/apps/web/src/app/admin-console/organizations/members/deprecated_members.component.ts deleted file mode 100644 index 1f1e19e2a6f..00000000000 --- a/apps/web/src/app/admin-console/organizations/members/deprecated_members.component.ts +++ /dev/null @@ -1,624 +0,0 @@ -import { Component, computed, Signal } from "@angular/core"; -import { takeUntilDestroyed, toSignal } from "@angular/core/rxjs-interop"; -import { ActivatedRoute } from "@angular/router"; -import { - combineLatest, - concatMap, - filter, - firstValueFrom, - from, - map, - merge, - Observable, - shareReplay, - switchMap, - take, -} from "rxjs"; - -import { OrganizationUserUserDetailsResponse } from "@bitwarden/admin-console/common"; -import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service"; -import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; -import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { - OrganizationUserStatusType, - OrganizationUserType, - PolicyType, -} from "@bitwarden/common/admin-console/enums"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { OrganizationMetadataServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-metadata.service.abstraction"; -import { OrganizationBillingMetadataResponse } from "@bitwarden/common/billing/models/response/organization-billing-metadata.response"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { getById } from "@bitwarden/common/platform/misc"; -import { DialogService, ToastService } from "@bitwarden/components"; -import { KeyService } from "@bitwarden/key-management"; -import { UserId } from "@bitwarden/user-core"; -import { BillingConstraintService } from "@bitwarden/web-vault/app/billing/members/billing-constraint/billing-constraint.service"; -import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services"; - -import { BaseMembersComponent } from "../../common/base-members.component"; -import { - CloudBulkReinviteLimit, - MaxCheckedCount, - PeopleTableDataSource, -} from "../../common/people-table-data-source"; -import { OrganizationUserView } from "../core/views/organization-user.view"; - -import { AccountRecoveryDialogResultType } from "./components/account-recovery/account-recovery-dialog.component"; -import { MemberDialogResult, MemberDialogTab } from "./components/member-dialog"; -import { - MemberDialogManagerService, - MemberExportService, - OrganizationMembersService, -} from "./services"; -import { DeleteManagedMemberWarningService } from "./services/delete-managed-member/delete-managed-member-warning.service"; -import { - MemberActionsService, - MemberActionResult, -} from "./services/member-actions/member-actions.service"; - -class MembersTableDataSource extends PeopleTableDataSource { - protected statusType = OrganizationUserStatusType; -} - -// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush -// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -@Component({ - templateUrl: "deprecated_members.component.html", - standalone: false, -}) -export class MembersComponent extends BaseMembersComponent { - userType = OrganizationUserType; - userStatusType = OrganizationUserStatusType; - memberTab = MemberDialogTab; - protected dataSource: MembersTableDataSource; - - readonly organization: Signal; - status: OrganizationUserStatusType | undefined; - - private userId$: Observable = this.accountService.activeAccount$.pipe(getUserId); - - resetPasswordPolicyEnabled$: Observable; - - protected readonly canUseSecretsManager: Signal = computed( - () => this.organization()?.useSecretsManager ?? false, - ); - protected readonly showUserManagementControls: Signal = computed( - () => this.organization()?.canManageUsers ?? false, - ); - protected billingMetadata$: Observable; - - // Fixed sizes used for cdkVirtualScroll - protected rowHeight = 66; - protected rowHeightClass = `tw-h-[66px]`; - - constructor( - apiService: ApiService, - i18nService: I18nService, - organizationManagementPreferencesService: OrganizationManagementPreferencesService, - keyService: KeyService, - validationService: ValidationService, - logService: LogService, - userNamePipe: UserNamePipe, - dialogService: DialogService, - toastService: ToastService, - private route: ActivatedRoute, - protected deleteManagedMemberWarningService: DeleteManagedMemberWarningService, - private organizationWarningsService: OrganizationWarningsService, - private memberActionsService: MemberActionsService, - private memberDialogManager: MemberDialogManagerService, - protected billingConstraint: BillingConstraintService, - protected memberService: OrganizationMembersService, - private organizationService: OrganizationService, - private accountService: AccountService, - private policyService: PolicyService, - private policyApiService: PolicyApiServiceAbstraction, - private organizationMetadataService: OrganizationMetadataServiceAbstraction, - private memberExportService: MemberExportService, - private environmentService: EnvironmentService, - ) { - super( - apiService, - i18nService, - keyService, - validationService, - logService, - userNamePipe, - dialogService, - organizationManagementPreferencesService, - toastService, - ); - - this.dataSource = new MembersTableDataSource(this.environmentService); - - const organization$ = this.route.params.pipe( - concatMap((params) => - this.userId$.pipe( - switchMap((userId) => - this.organizationService.organizations$(userId).pipe(getById(params.organizationId)), - ), - filter((organization): organization is Organization => organization != null), - shareReplay({ refCount: true, bufferSize: 1 }), - ), - ), - ); - - this.organization = toSignal(organization$); - - const policies$ = combineLatest([this.userId$, organization$]).pipe( - switchMap(([userId, organization]) => - organization.isProviderUser - ? from(this.policyApiService.getPolicies(organization.id)).pipe( - map((response) => Policy.fromListResponse(response)), - ) - : this.policyService.policies$(userId), - ), - ); - - this.resetPasswordPolicyEnabled$ = combineLatest([organization$, policies$]).pipe( - map( - ([organization, policies]) => - policies - .filter((policy) => policy.type === PolicyType.ResetPassword) - .find((p) => p.organizationId === organization.id)?.enabled ?? false, - ), - ); - - combineLatest([this.route.queryParams, organization$]) - .pipe( - concatMap(async ([qParams, organization]) => { - await this.load(organization!); - - this.searchControl.setValue(qParams.search); - - if (qParams.viewEvents != null) { - const user = this.dataSource.data.filter((u) => u.id === qParams.viewEvents); - if (user.length > 0 && user[0].status === OrganizationUserStatusType.Confirmed) { - this.openEventsDialog(user[0], organization!); - } - } - }), - takeUntilDestroyed(), - ) - .subscribe(); - - organization$ - .pipe( - switchMap((organization) => - merge( - this.organizationWarningsService.showInactiveSubscriptionDialog$(organization), - this.organizationWarningsService.showSubscribeBeforeFreeTrialEndsDialog$(organization), - ), - ), - takeUntilDestroyed(), - ) - .subscribe(); - - this.billingMetadata$ = organization$.pipe( - switchMap((organization) => - this.organizationMetadataService.getOrganizationMetadata$(organization.id), - ), - shareReplay({ bufferSize: 1, refCount: false }), - ); - - // Stripe is slow, so kick this off in the background but without blocking page load. - // Anyone who needs it will still await the first emission. - this.billingMetadata$.pipe(take(1), takeUntilDestroyed()).subscribe(); - } - - override async load(organization: Organization) { - await super.load(organization); - } - - async getUsers(organization: Organization): Promise { - return await this.memberService.loadUsers(organization); - } - - async removeUser(id: string, organization: Organization): Promise { - return await this.memberActionsService.removeUser(organization, id); - } - - async revokeUser(id: string, organization: Organization): Promise { - return await this.memberActionsService.revokeUser(organization, id); - } - - async restoreUser(id: string, organization: Organization): Promise { - return await this.memberActionsService.restoreUser(organization, id); - } - - async reinviteUser(id: string, organization: Organization): Promise { - return await this.memberActionsService.reinviteUser(organization, id); - } - - async confirmUser( - user: OrganizationUserView, - publicKey: Uint8Array, - organization: Organization, - ): Promise { - return await this.memberActionsService.confirmUser(user, publicKey, organization); - } - - async revoke(user: OrganizationUserView, organization: Organization) { - const confirmed = await this.revokeUserConfirmationDialog(user); - - if (!confirmed) { - return false; - } - - this.actionPromise = this.revokeUser(user.id, organization); - try { - const result = await this.actionPromise; - if (result.success) { - this.toastService.showToast({ - variant: "success", - message: this.i18nService.t("revokedUserId", this.userNamePipe.transform(user)), - }); - await this.load(organization); - } else { - throw new Error(result.error); - } - } catch (e) { - this.validationService.showError(e); - } - this.actionPromise = undefined; - } - - async restore(user: OrganizationUserView, organization: Organization) { - this.actionPromise = this.restoreUser(user.id, organization); - try { - const result = await this.actionPromise; - if (result.success) { - this.toastService.showToast({ - variant: "success", - message: this.i18nService.t("restoredUserId", this.userNamePipe.transform(user)), - }); - await this.load(organization); - } else { - throw new Error(result.error); - } - } catch (e) { - this.validationService.showError(e); - } - this.actionPromise = undefined; - } - - allowResetPassword( - orgUser: OrganizationUserView, - organization: Organization, - orgResetPasswordPolicyEnabled: boolean, - ): boolean { - return this.memberActionsService.allowResetPassword( - orgUser, - organization, - orgResetPasswordPolicyEnabled, - ); - } - - showEnrolledStatus( - orgUser: OrganizationUserUserDetailsResponse, - organization: Organization, - orgResetPasswordPolicyEnabled: boolean, - ): boolean { - return ( - organization.useResetPassword && - orgUser.resetPasswordEnrolled && - orgResetPasswordPolicyEnabled - ); - } - - private async handleInviteDialog(organization: Organization) { - const billingMetadata = await firstValueFrom(this.billingMetadata$); - const allUserEmails = this.dataSource.data?.map((user) => user.email) ?? []; - - const result = await this.memberDialogManager.openInviteDialog( - organization, - billingMetadata, - allUserEmails, - ); - - if (result === MemberDialogResult.Saved) { - await this.load(organization); - } - } - - async invite(organization: Organization) { - const billingMetadata = await firstValueFrom(this.billingMetadata$); - const seatLimitResult = this.billingConstraint.checkSeatLimit(organization, billingMetadata); - if (!(await this.billingConstraint.seatLimitReached(seatLimitResult, organization))) { - await this.handleInviteDialog(organization); - this.organizationMetadataService.refreshMetadataCache(); - } - } - - async edit( - user: OrganizationUserView, - organization: Organization, - initialTab: MemberDialogTab = MemberDialogTab.Role, - ) { - const billingMetadata = await firstValueFrom(this.billingMetadata$); - - const result = await this.memberDialogManager.openEditDialog( - user, - organization, - billingMetadata, - initialTab, - ); - - switch (result) { - case MemberDialogResult.Deleted: - this.dataSource.removeUser(user); - break; - case MemberDialogResult.Saved: - case MemberDialogResult.Revoked: - case MemberDialogResult.Restored: - await this.load(organization); - break; - } - } - - async bulkRemove(organization: Organization) { - if (this.actionPromise != null) { - return; - } - - const users = this.dataSource.getCheckedUsersWithLimit(MaxCheckedCount); - - await this.memberDialogManager.openBulkRemoveDialog(organization, users); - this.organizationMetadataService.refreshMetadataCache(); - await this.load(organization); - } - - async bulkDelete(organization: Organization) { - if (this.actionPromise != null) { - return; - } - - const users = this.dataSource.getCheckedUsersWithLimit(MaxCheckedCount); - - await this.memberDialogManager.openBulkDeleteDialog(organization, users); - await this.load(organization); - } - - async bulkRevoke(organization: Organization) { - await this.bulkRevokeOrRestore(true, organization); - } - - async bulkRestore(organization: Organization) { - await this.bulkRevokeOrRestore(false, organization); - } - - async bulkRevokeOrRestore(isRevoking: boolean, organization: Organization) { - if (this.actionPromise != null) { - return; - } - - const users = this.dataSource.getCheckedUsersWithLimit(MaxCheckedCount); - - await this.memberDialogManager.openBulkRestoreRevokeDialog(organization, users, isRevoking); - await this.load(organization); - } - - async bulkReinvite(organization: Organization) { - if (this.actionPromise != null) { - return; - } - - let users: OrganizationUserView[]; - if (this.dataSource.isIncreasedBulkLimitEnabled()) { - users = this.dataSource.getCheckedUsersInVisibleOrder(); - } else { - users = this.dataSource.getCheckedUsers(); - } - - const allInvitedUsers = users.filter((u) => u.status === OrganizationUserStatusType.Invited); - - // Capture the original count BEFORE enforcing the limit - const originalInvitedCount = allInvitedUsers.length; - - // When feature flag is enabled, limit invited users and uncheck the excess - let filteredUsers: OrganizationUserView[]; - if (this.dataSource.isIncreasedBulkLimitEnabled()) { - filteredUsers = this.dataSource.limitAndUncheckExcess( - allInvitedUsers, - CloudBulkReinviteLimit, - ); - } else { - filteredUsers = allInvitedUsers; - } - - if (filteredUsers.length <= 0) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("noSelectedUsersApplicable"), - }); - return; - } - - try { - const result = await this.memberActionsService.bulkReinvite(organization, filteredUsers); - - if (result.successful.length === 0) { - throw new Error(); - } - - // When feature flag is enabled, show toast instead of dialog - if (this.dataSource.isIncreasedBulkLimitEnabled()) { - const selectedCount = originalInvitedCount; - const invitedCount = filteredUsers.length; - - if (selectedCount > CloudBulkReinviteLimit) { - const excludedCount = selectedCount - CloudBulkReinviteLimit; - this.toastService.showToast({ - variant: "success", - message: this.i18nService.t( - "bulkReinviteLimitedSuccessToast", - CloudBulkReinviteLimit.toLocaleString(), - selectedCount.toLocaleString(), - excludedCount.toLocaleString(), - ), - }); - } else { - this.toastService.showToast({ - variant: "success", - message: - invitedCount === 1 - ? this.i18nService.t("reinviteSuccessToast") - : this.i18nService.t("bulkReinviteSentToast", invitedCount.toString()), - }); - } - } else { - // Feature flag disabled - show legacy dialog - await this.memberDialogManager.openBulkStatusDialog( - users, - filteredUsers, - Promise.resolve(result.successful), - this.i18nService.t("bulkReinviteMessage"), - ); - } - } catch (e) { - this.validationService.showError(e); - } - this.actionPromise = undefined; - } - - async bulkConfirm(organization: Organization) { - if (this.actionPromise != null) { - return; - } - - const users = this.dataSource.getCheckedUsersWithLimit(MaxCheckedCount); - - await this.memberDialogManager.openBulkConfirmDialog(organization, users); - await this.load(organization); - } - - async bulkEnableSM(organization: Organization) { - const users = this.dataSource.getCheckedUsersWithLimit(MaxCheckedCount); - - await this.memberDialogManager.openBulkEnableSecretsManagerDialog(organization, users); - - this.dataSource.uncheckAllUsers(); - await this.load(organization); - } - - openEventsDialog(user: OrganizationUserView, organization: Organization) { - this.memberDialogManager.openEventsDialog(user, organization); - } - - async resetPassword(user: OrganizationUserView, organization: Organization) { - if (!user || !user.email || !user.id) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("orgUserDetailsNotFound"), - }); - this.logService.error("Org user details not found when attempting account recovery"); - - return; - } - - const result = await this.memberDialogManager.openAccountRecoveryDialog(user, organization); - if (result === AccountRecoveryDialogResultType.Ok) { - await this.load(organization); - } - - return; - } - - protected async removeUserConfirmationDialog(user: OrganizationUserView) { - return await this.memberDialogManager.openRemoveUserConfirmationDialog(user); - } - - protected async revokeUserConfirmationDialog(user: OrganizationUserView) { - return await this.memberDialogManager.openRevokeUserConfirmationDialog(user); - } - - async deleteUser(user: OrganizationUserView, organization: Organization) { - const confirmed = await this.memberDialogManager.openDeleteUserConfirmationDialog( - user, - organization, - ); - - if (!confirmed) { - return false; - } - - this.actionPromise = this.memberActionsService.deleteUser(organization, user.id); - try { - const result = await this.actionPromise; - if (!result.success) { - throw new Error(result.error); - } - this.toastService.showToast({ - variant: "success", - message: this.i18nService.t("organizationUserDeleted", this.userNamePipe.transform(user)), - }); - this.dataSource.removeUser(user); - } catch (e) { - this.validationService.showError(e); - } - this.actionPromise = undefined; - } - - get showBulkRestoreUsers(): boolean { - return this.dataSource - .getCheckedUsers() - .every((member) => member.status == this.userStatusType.Revoked); - } - - get showBulkRevokeUsers(): boolean { - return this.dataSource - .getCheckedUsers() - .every((member) => member.status != this.userStatusType.Revoked); - } - - get showBulkRemoveUsers(): boolean { - return this.dataSource.getCheckedUsers().every((member) => !member.managedByOrganization); - } - - get showBulkDeleteUsers(): boolean { - const validStatuses = [ - this.userStatusType.Accepted, - this.userStatusType.Confirmed, - this.userStatusType.Revoked, - ]; - - return this.dataSource - .getCheckedUsers() - .every((member) => member.managedByOrganization && validStatuses.includes(member.status)); - } - - get selectedInvitedCount(): number { - return this.dataSource - .getCheckedUsers() - .filter((member) => member.status === this.userStatusType.Invited).length; - } - - get isSingleInvite(): boolean { - return this.selectedInvitedCount === 1; - } - - exportMembers = () => { - const result = this.memberExportService.getMemberExport(this.dataSource.data); - if (result.success) { - this.toastService.showToast({ - variant: "success", - title: undefined, - message: this.i18nService.t("dataExportSuccess"), - }); - } - - if (result.error != null) { - this.validationService.showError(result.error.message); - } - }; -} diff --git a/apps/web/src/app/admin-console/organizations/members/members-routing.module.ts b/apps/web/src/app/admin-console/organizations/members/members-routing.module.ts index 2f22b9871b7..153a2f3a956 100644 --- a/apps/web/src/app/admin-console/organizations/members/members-routing.module.ts +++ b/apps/web/src/app/admin-console/organizations/members/members-routing.module.ts @@ -1,30 +1,23 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; -import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route"; import { canAccessMembersTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { FreeBitwardenFamiliesComponent } from "../../../billing/members/free-bitwarden-families.component"; import { organizationPermissionsGuard } from "../guards/org-permissions.guard"; import { canAccessSponsoredFamilies } from "./../../../billing/guards/can-access-sponsored-families.guard"; -import { MembersComponent } from "./deprecated_members.component"; -import { vNextMembersComponent } from "./members.component"; +import { MembersComponent } from "./members.component"; const routes: Routes = [ - ...featureFlaggedRoute({ - defaultComponent: MembersComponent, - flaggedComponent: vNextMembersComponent, - featureFlag: FeatureFlag.MembersComponentRefactor, - routeOptions: { - path: "", - canActivate: [organizationPermissionsGuard(canAccessMembersTab)], - data: { - titleId: "members", - }, + { + path: "", + component: MembersComponent, + canActivate: [organizationPermissionsGuard(canAccessMembersTab)], + data: { + titleId: "members", }, - }), + }, { path: "sponsored-families", component: FreeBitwardenFamiliesComponent, diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.spec.ts b/apps/web/src/app/admin-console/organizations/members/members.component.spec.ts index 9a371de1acd..72c12fd4d79 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.spec.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.component.spec.ts @@ -36,7 +36,7 @@ import { OrganizationUserView } from "../core/views/organization-user.view"; import { AccountRecoveryDialogResultType } from "./components/account-recovery/account-recovery-dialog.component"; import { MemberDialogResult } from "./components/member-dialog"; -import { vNextMembersComponent } from "./members.component"; +import { MembersComponent } from "./members.component"; import { MemberDialogManagerService, MemberExportService, @@ -48,9 +48,9 @@ import { MemberActionResult, } from "./services/member-actions/member-actions.service"; -describe("vNextMembersComponent", () => { - let component: vNextMembersComponent; - let fixture: ComponentFixture; +describe("MembersComponent", () => { + let component: MembersComponent; + let fixture: ComponentFixture; let mockApiService: MockProxy; let mockI18nService: MockProxy; @@ -172,7 +172,7 @@ describe("vNextMembersComponent", () => { mockFileDownloadService = mock(); await TestBed.configureTestingModule({ - declarations: [vNextMembersComponent], + declarations: [MembersComponent], providers: [ { provide: ApiService, useValue: mockApiService }, { provide: I18nService, useValue: mockI18nService }, @@ -211,13 +211,13 @@ describe("vNextMembersComponent", () => { ], schemas: [NO_ERRORS_SCHEMA], }) - .overrideComponent(vNextMembersComponent, { + .overrideComponent(MembersComponent, { remove: { imports: [] }, add: { template: "
    " }, }) .compileComponents(); - fixture = TestBed.createComponent(vNextMembersComponent); + fixture = TestBed.createComponent(MembersComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.ts b/apps/web/src/app/admin-console/organizations/members/members.component.ts index 826bdfb5f69..6b93edc8c6b 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.component.ts @@ -82,7 +82,7 @@ interface BulkMemberFlags { templateUrl: "members.component.html", standalone: false, }) -export class vNextMembersComponent { +export class MembersComponent { protected i18nService = inject(I18nService); protected validationService = inject(ValidationService); protected logService = inject(LogService); diff --git a/apps/web/src/app/admin-console/organizations/members/members.module.ts b/apps/web/src/app/admin-console/organizations/members/members.module.ts index 54e2d1b6373..92ae71123cc 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.module.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.module.ts @@ -19,9 +19,8 @@ import { BulkRemoveDialogComponent } from "./components/bulk/bulk-remove-dialog. import { BulkRestoreRevokeComponent } from "./components/bulk/bulk-restore-revoke.component"; import { BulkStatusComponent } from "./components/bulk/bulk-status.component"; import { UserDialogModule } from "./components/member-dialog"; -import { MembersComponent } from "./deprecated_members.component"; import { MembersRoutingModule } from "./members-routing.module"; -import { vNextMembersComponent } from "./members.component"; +import { MembersComponent } from "./members.component"; import { UserStatusPipe } from "./pipes"; import { OrganizationMembersService, @@ -52,7 +51,6 @@ import { BulkProgressDialogComponent, BulkReinviteFailureDialogComponent, MembersComponent, - vNextMembersComponent, BulkDeleteDialogComponent, UserStatusPipe, ], diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/deprecated_members.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/deprecated_members.component.html deleted file mode 100644 index 5478601e72c..00000000000 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/deprecated_members.component.html +++ /dev/null @@ -1,225 +0,0 @@ - - - - - - -
    - - - {{ "all" | i18n }} - - {{ allCount }} - - - - {{ "invited" | i18n }} - - {{ invitedCount }} - - - - {{ "needsConfirmation" | i18n }} - - {{ acceptedCount }} - - - -
    - - - - {{ "loading" | i18n }} - - - -

    {{ "noMembersInList" | i18n }}

    - - - {{ "providerUsersNeedConfirmed" | i18n }} - - - - - - - - - - {{ "name" | i18n }} - {{ "role" | i18n }} - - - - - - - - - - - - - - - - -
    - -
    -
    - - - {{ "invited" | i18n }} - - - {{ "needsConfirmation" | i18n }} - - - {{ "revoked" | i18n }} - -
    -
    - {{ user.email }} -
    -
    -
    - - - {{ "providerAdmin" | i18n }} - {{ "serviceUser" | i18n }} - - - - - - - - - - - -
    -
    -
    -
    -
    diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/deprecated_members.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/deprecated_members.component.ts deleted file mode 100644 index 464b9982689..00000000000 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/deprecated_members.component.ts +++ /dev/null @@ -1,351 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component } from "@angular/core"; -import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { ActivatedRoute, Router } from "@angular/router"; -import { combineLatest, firstValueFrom, lastValueFrom, switchMap } from "rxjs"; -import { first, map } from "rxjs/operators"; - -import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service"; -import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; -import { ProviderUserStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums"; -import { ProviderUserBulkRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-bulk.request"; -import { ProviderUserConfirmRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-confirm.request"; -import { ProviderUserUserDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user.response"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { assertNonNullish } from "@bitwarden/common/auth/utils"; -import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; -import { ListResponse } from "@bitwarden/common/models/response/list.response"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { ProviderId } from "@bitwarden/common/types/guid"; -import { DialogRef, DialogService, ToastService } from "@bitwarden/components"; -import { KeyService } from "@bitwarden/key-management"; -import { BaseMembersComponent } from "@bitwarden/web-vault/app/admin-console/common/base-members.component"; -import { - CloudBulkReinviteLimit, - MaxCheckedCount, - peopleFilter, - PeopleTableDataSource, -} from "@bitwarden/web-vault/app/admin-console/common/people-table-data-source"; -import { openEntityEventsDialog } from "@bitwarden/web-vault/app/admin-console/organizations/manage/entity-events.component"; -import { BulkStatusComponent } from "@bitwarden/web-vault/app/admin-console/organizations/members/components/bulk/bulk-status.component"; -import { MemberActionResult } from "@bitwarden/web-vault/app/admin-console/organizations/members/services/member-actions/member-actions.service"; - -import { - AddEditMemberDialogComponent, - AddEditMemberDialogParams, - AddEditMemberDialogResultType, -} from "./dialogs/add-edit-member-dialog.component"; -import { BulkConfirmDialogComponent } from "./dialogs/bulk-confirm-dialog.component"; -import { BulkRemoveDialogComponent } from "./dialogs/bulk-remove-dialog.component"; - -type ProviderUser = ProviderUserUserDetailsResponse; - -class MembersTableDataSource extends PeopleTableDataSource { - protected statusType = ProviderUserStatusType; -} - -// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush -// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -@Component({ - templateUrl: "deprecated_members.component.html", - standalone: false, -}) -export class MembersComponent extends BaseMembersComponent { - accessEvents = false; - dataSource: MembersTableDataSource; - loading = true; - providerId: string; - rowHeight = 70; - rowHeightClass = `tw-h-[70px]`; - status: ProviderUserStatusType = null; - - userStatusType = ProviderUserStatusType; - userType = ProviderUserType; - - constructor( - apiService: ApiService, - keyService: KeyService, - dialogService: DialogService, - i18nService: I18nService, - logService: LogService, - organizationManagementPreferencesService: OrganizationManagementPreferencesService, - toastService: ToastService, - userNamePipe: UserNamePipe, - validationService: ValidationService, - private encryptService: EncryptService, - private activatedRoute: ActivatedRoute, - private providerService: ProviderService, - private router: Router, - private accountService: AccountService, - private environmentService: EnvironmentService, - ) { - super( - apiService, - i18nService, - keyService, - validationService, - logService, - userNamePipe, - dialogService, - organizationManagementPreferencesService, - toastService, - ); - - this.dataSource = new MembersTableDataSource(this.environmentService); - - combineLatest([ - this.activatedRoute.parent.params, - this.activatedRoute.queryParams.pipe(first()), - ]) - .pipe( - switchMap(async ([urlParams, queryParams]) => { - this.searchControl.setValue(queryParams.search); - this.dataSource.filter = peopleFilter(queryParams.search, null); - - this.providerId = urlParams.providerId; - const provider = await firstValueFrom( - this.accountService.activeAccount$.pipe( - getUserId, - switchMap((userId) => this.providerService.get$(this.providerId, userId)), - ), - ); - - if (!provider || !provider.canManageUsers) { - return await this.router.navigate(["../"], { relativeTo: this.activatedRoute }); - } - this.accessEvents = provider.useEvents; - await this.load(); - - if (queryParams.viewEvents != null) { - const user = this.dataSource.data.find((user) => user.id === queryParams.viewEvents); - if (user && user.status === ProviderUserStatusType.Confirmed) { - this.openEventsDialog(user); - } - } - }), - takeUntilDestroyed(), - ) - .subscribe(); - } - - async bulkConfirm(): Promise { - if (this.actionPromise != null) { - return; - } - - const users = this.dataSource.getCheckedUsersWithLimit(MaxCheckedCount); - - const dialogRef = BulkConfirmDialogComponent.open(this.dialogService, { - data: { - providerId: this.providerId, - users: users, - }, - }); - - await lastValueFrom(dialogRef.closed); - await this.load(); - } - - async bulkReinvite(): Promise { - if (this.actionPromise != null) { - return; - } - - let users: ProviderUser[]; - if (this.dataSource.isIncreasedBulkLimitEnabled()) { - users = this.dataSource.getCheckedUsersInVisibleOrder(); - } else { - users = this.dataSource.getCheckedUsers(); - } - - const allInvitedUsers = users.filter((user) => user.status === ProviderUserStatusType.Invited); - - // Capture the original count BEFORE enforcing the limit - const originalInvitedCount = allInvitedUsers.length; - - // When feature flag is enabled, limit invited users and uncheck the excess - let checkedInvitedUsers: ProviderUser[]; - if (this.dataSource.isIncreasedBulkLimitEnabled()) { - checkedInvitedUsers = this.dataSource.limitAndUncheckExcess( - allInvitedUsers, - CloudBulkReinviteLimit, - ); - } else { - checkedInvitedUsers = allInvitedUsers; - } - - if (checkedInvitedUsers.length <= 0) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("noSelectedUsersApplicable"), - }); - return; - } - - try { - // When feature flag is enabled, show toast instead of dialog - if (this.dataSource.isIncreasedBulkLimitEnabled()) { - await this.apiService.postManyProviderUserReinvite( - this.providerId, - new ProviderUserBulkRequest(checkedInvitedUsers.map((user) => user.id)), - ); - - const selectedCount = originalInvitedCount; - const invitedCount = checkedInvitedUsers.length; - - if (selectedCount > CloudBulkReinviteLimit) { - const excludedCount = selectedCount - CloudBulkReinviteLimit; - this.toastService.showToast({ - variant: "success", - message: this.i18nService.t( - "bulkReinviteLimitedSuccessToast", - CloudBulkReinviteLimit.toLocaleString(), - selectedCount.toLocaleString(), - excludedCount.toLocaleString(), - ), - }); - } else { - this.toastService.showToast({ - variant: "success", - message: - invitedCount === 1 - ? this.i18nService.t("reinviteSuccessToast") - : this.i18nService.t("bulkReinviteSentToast", invitedCount.toString()), - }); - } - } else { - // Feature flag disabled - show legacy dialog - const request = this.apiService - .postManyProviderUserReinvite( - this.providerId, - new ProviderUserBulkRequest(checkedInvitedUsers.map((user) => user.id)), - ) - .then((response) => response.data); - - const dialogRef = BulkStatusComponent.open(this.dialogService, { - data: { - users: users, - filteredUsers: checkedInvitedUsers, - request, - successfulMessage: this.i18nService.t("bulkReinviteMessage"), - }, - }); - await lastValueFrom(dialogRef.closed); - } - } catch (error) { - this.validationService.showError(error); - } - } - - async invite() { - await this.edit(null); - } - - async bulkRemove(): Promise { - if (this.actionPromise != null) { - return; - } - - const users = this.dataSource.getCheckedUsersWithLimit(MaxCheckedCount); - - const dialogRef = BulkRemoveDialogComponent.open(this.dialogService, { - data: { - providerId: this.providerId, - users: users, - }, - }); - - await lastValueFrom(dialogRef.closed); - await this.load(); - } - - async confirmUser(user: ProviderUser, publicKey: Uint8Array): Promise { - try { - const providerKey = await firstValueFrom( - this.accountService.activeAccount$.pipe( - getUserId, - switchMap((userId) => this.keyService.providerKeys$(userId)), - map((providerKeys) => providerKeys?.[this.providerId as ProviderId] ?? null), - ), - ); - assertNonNullish(providerKey, "Provider key not found"); - - const key = await this.encryptService.encapsulateKeyUnsigned(providerKey, publicKey); - const request = new ProviderUserConfirmRequest(key.encryptedString); - await this.apiService.postProviderUserConfirm(this.providerId, user.id, request); - return { success: true }; - } catch (error) { - return { success: false, error: error.message }; - } - } - - removeUser = async (id: string): Promise => { - try { - await this.apiService.deleteProviderUser(this.providerId, id); - return { success: true }; - } catch (error) { - return { success: false, error: error.message }; - } - }; - - edit = async (user: ProviderUser | null): Promise => { - const data: AddEditMemberDialogParams = { - providerId: this.providerId, - user, - }; - - const dialogRef = AddEditMemberDialogComponent.open(this.dialogService, { - data, - }); - - const result = await lastValueFrom(dialogRef.closed); - - switch (result) { - case AddEditMemberDialogResultType.Saved: - case AddEditMemberDialogResultType.Deleted: - await this.load(); - break; - } - }; - - openEventsDialog = (user: ProviderUser): DialogRef => - openEntityEventsDialog(this.dialogService, { - data: { - name: this.userNamePipe.transform(user), - providerId: this.providerId, - entityId: user.id, - showUser: false, - entity: "user", - }, - }); - - getUsers = (): Promise> => - this.apiService.getProviderUsers(this.providerId); - - reinviteUser = async (id: string): Promise => { - try { - await this.apiService.postProviderUserReinvite(this.providerId, id); - return { success: true }; - } catch (error) { - return { success: false, error: error.message }; - } - }; - - get selectedInvitedCount(): number { - return this.dataSource - .getCheckedUsers() - .filter((member) => member.status === this.userStatusType.Invited).length; - } - - get isSingleInvite(): boolean { - return this.selectedInvitedCount === 1; - } -} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts index 308b93ac2e3..d4a6ba92451 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts @@ -61,7 +61,7 @@ interface BulkProviderFlags { templateUrl: "members.component.html", standalone: false, }) -export class vNextMembersComponent { +export class MembersComponent { protected apiService = inject(ApiService); protected dialogService = inject(DialogService); protected i18nService = inject(I18nService); diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts index 447481a8bcb..5fadc935644 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts @@ -2,9 +2,7 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; import { authGuard } from "@bitwarden/angular/auth/guards"; -import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { AnonLayoutWrapperComponent } from "@bitwarden/components"; import { FrontendLayoutComponent } from "@bitwarden/web-vault/app/layouts/frontend-layout.component"; import { UserLayoutComponent } from "@bitwarden/web-vault/app/layouts/user-layout.component"; @@ -17,9 +15,8 @@ import { ProviderSubscriptionComponent } from "../../billing/providers/subscript import { ManageClientsComponent } from "./clients/manage-clients.component"; import { providerPermissionsGuard } from "./guards/provider-permissions.guard"; import { AcceptProviderComponent } from "./manage/accept-provider.component"; -import { MembersComponent } from "./manage/deprecated_members.component"; import { EventsComponent } from "./manage/events.component"; -import { vNextMembersComponent } from "./manage/members.component"; +import { MembersComponent } from "./manage/members.component"; import { ProvidersLayoutComponent } from "./providers-layout.component"; import { ProvidersComponent } from "./providers.component"; import { AccountComponent } from "./settings/account.component"; @@ -95,20 +92,16 @@ const routes: Routes = [ pathMatch: "full", redirectTo: "members", }, - ...featureFlaggedRoute({ - defaultComponent: MembersComponent, - flaggedComponent: vNextMembersComponent, - featureFlag: FeatureFlag.MembersComponentRefactor, - routeOptions: { - path: "members", - canActivate: [ - providerPermissionsGuard((provider: Provider) => provider.canManageUsers), - ], - data: { - titleId: "members", - }, + { + path: "members", + component: MembersComponent, + canActivate: [ + providerPermissionsGuard((provider: Provider) => provider.canManageUsers), + ], + data: { + titleId: "members", }, - }), + }, { path: "events", component: EventsComponent, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts index 44e2e51637f..abdd35c5e61 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts @@ -27,12 +27,11 @@ import { CreateClientDialogComponent } from "./clients/create-client-dialog.comp import { ManageClientNameDialogComponent } from "./clients/manage-client-name-dialog.component"; import { ManageClientSubscriptionDialogComponent } from "./clients/manage-client-subscription-dialog.component"; import { AcceptProviderComponent } from "./manage/accept-provider.component"; -import { MembersComponent } from "./manage/deprecated_members.component"; import { AddEditMemberDialogComponent } from "./manage/dialogs/add-edit-member-dialog.component"; import { BulkConfirmDialogComponent } from "./manage/dialogs/bulk-confirm-dialog.component"; import { BulkRemoveDialogComponent } from "./manage/dialogs/bulk-remove-dialog.component"; import { EventsComponent } from "./manage/events.component"; -import { vNextMembersComponent } from "./manage/members.component"; +import { MembersComponent } from "./manage/members.component"; import { ProviderActionsService } from "./manage/services/provider-actions/provider-actions.service"; import { ProvidersLayoutComponent } from "./providers-layout.component"; import { ProvidersRoutingModule } from "./providers-routing.module"; @@ -67,7 +66,6 @@ import { VerifyRecoverDeleteProviderComponent } from "./verify-recover-delete-pr BulkConfirmDialogComponent, BulkRemoveDialogComponent, EventsComponent, - vNextMembersComponent, MembersComponent, SetupComponent, SetupProviderComponent, diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/security-tasks.service.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/security-tasks.service.ts index 65a31896341..2307eab04fe 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/security-tasks.service.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/security-tasks.service.ts @@ -1,8 +1,10 @@ import { BehaviorSubject, combineLatest, Observable } from "rxjs"; import { map, shareReplay } from "rxjs/operators"; -import { RiskInsightsDataService } from "@bitwarden/bit-common/dirt/reports/risk-insights"; -import { SecurityTasksApiService } from "@bitwarden/bit-common/dirt/reports/risk-insights"; +import { + RiskInsightsDataService, + SecurityTasksApiService, +} from "@bitwarden/bit-common/dirt/reports/risk-insights"; import { CipherId, OrganizationId } from "@bitwarden/common/types/guid"; import { SecurityTask, SecurityTaskStatus, SecurityTaskType } from "@bitwarden/common/vault/tasks"; diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 4db9ff37d42..05fded6bcaf 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -13,7 +13,6 @@ export enum FeatureFlag { /* Admin Console Team */ AutoConfirm = "pm-19934-auto-confirm-organization-users", DefaultUserCollectionRestore = "pm-30883-my-items-restored-users", - MembersComponentRefactor = "pm-29503-refactor-members-inheritance", BulkReinviteUI = "pm-28416-bulk-reinvite-ux-improvements", /* Auth */ @@ -109,7 +108,6 @@ export const DefaultFeatureFlagValue = { /* Admin Console Team */ [FeatureFlag.AutoConfirm]: FALSE, [FeatureFlag.DefaultUserCollectionRestore]: FALSE, - [FeatureFlag.MembersComponentRefactor]: FALSE, [FeatureFlag.BulkReinviteUI]: FALSE, /* Autofill */ From fa40de92b140822b640587c9ff6436ede419ebf7 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Fri, 13 Feb 2026 11:01:27 -0600 Subject: [PATCH 032/134] Remove unneeded workaround to get credential ID from request (#18784) --- .../services/desktop-autofill.service.ts | 35 ------------------- 1 file changed, 35 deletions(-) diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts index e5cd85aa7a3..ae3c75d6f01 100644 --- a/apps/desktop/src/autofill/services/desktop-autofill.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts @@ -16,7 +16,6 @@ import { import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { getOptionalUserId } from "@bitwarden/common/auth/services/account.service"; import { DeviceType } from "@bitwarden/common/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service"; @@ -31,7 +30,6 @@ import { import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { parseCredentialId } from "@bitwarden/common/platform/services/fido2/credential-id-utils"; import { getCredentialsForAutofill } from "@bitwarden/common/platform/services/fido2/fido2-autofill-utils"; import { Fido2Utils } from "@bitwarden/common/platform/services/fido2/fido2-utils"; import { UserId } from "@bitwarden/common/types/guid"; @@ -258,39 +256,6 @@ export class DesktopAutofillService implements OnDestroy { const controller = new AbortController(); try { - // For some reason the credentialId is passed as an empty array in the request, so we need to - // get it from the cipher. For that we use the recordIdentifier, which is the cipherId. - if (request.recordIdentifier && request.credentialId.length === 0) { - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(getOptionalUserId), - ); - if (!activeUserId) { - this.logService.error("listenPasskeyAssertion error", "Active user not found"); - callback(new Error("Active user not found"), null); - return; - } - - const cipher = await this.cipherService.get(request.recordIdentifier, activeUserId); - if (!cipher) { - this.logService.error("listenPasskeyAssertion error", "Cipher not found"); - callback(new Error("Cipher not found"), null); - return; - } - - const decrypted = await this.cipherService.decrypt(cipher, activeUserId); - - const fido2Credential = decrypted.login.fido2Credentials?.[0]; - if (!fido2Credential) { - this.logService.error("listenPasskeyAssertion error", "Fido2Credential not found"); - callback(new Error("Fido2Credential not found"), null); - return; - } - - request.credentialId = Array.from( - new Uint8Array(parseCredentialId(decrypted.login.fido2Credentials?.[0].credentialId)), - ); - } - const response = await this.fido2AuthenticatorService.getAssertion( this.convertAssertionRequest(request, true), { windowXy: normalizePosition(request.windowXy) }, From ab702e3a1aeac20d777eb131cc9953a0f89a9b24 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Fri, 13 Feb 2026 11:01:42 -0600 Subject: [PATCH 033/134] Don't sync invalid password ciphers to autofill (#18783) --- .../desktop/src/autofill/services/desktop-autofill.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts index ae3c75d6f01..cca0097d65e 100644 --- a/apps/desktop/src/autofill/services/desktop-autofill.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts @@ -150,11 +150,13 @@ export class DesktopAutofillService implements OnDestroy { passwordCredentials = cipherViews .filter( (cipher) => + !cipher.isDeleted && cipher.type === CipherType.Login && cipher.login.uris?.length > 0 && cipher.login.uris.some((uri) => uri.match !== UriMatchStrategy.Never) && cipher.login.uris.some((uri) => !Utils.isNullOrWhitespace(uri.uri)) && - !Utils.isNullOrWhitespace(cipher.login.username), + !Utils.isNullOrWhitespace(cipher.login.username) && + !Utils.isNullOrWhitespace(cipher.login.password), ) .map((cipher) => ({ type: "password", From ab0739b693761e11979386c2d85ef0553ed15ff0 Mon Sep 17 00:00:00 2001 From: John Harrington <84741727+harr1424@users.noreply.github.com> Date: Fri, 13 Feb 2026 10:23:25 -0700 Subject: [PATCH 034/134] rename flag to emails (#18955) --- .../send/commands/create.command.spec.ts | 8 ++--- .../src/tools/send/commands/create.command.ts | 2 +- .../tools/send/commands/edit.command.spec.ts | 8 ++--- .../src/tools/send/commands/edit.command.ts | 2 +- apps/cli/src/tools/send/send.program.ts | 32 +++++++++++-------- 5 files changed, 29 insertions(+), 23 deletions(-) diff --git a/apps/cli/src/tools/send/commands/create.command.spec.ts b/apps/cli/src/tools/send/commands/create.command.spec.ts index d3702689812..20f4d3e722e 100644 --- a/apps/cli/src/tools/send/commands/create.command.spec.ts +++ b/apps/cli/src/tools/send/commands/create.command.spec.ts @@ -62,7 +62,7 @@ describe("SendCreateCommand", () => { }; const cmdOptions = { - email: ["test@example.com"], + emails: ["test@example.com"], }; sendService.encrypt.mockResolvedValue([ @@ -155,7 +155,7 @@ describe("SendCreateCommand", () => { }; const cmdOptions = { - email: ["test@example.com"], + emails: ["test@example.com"], password: "testPassword123", }; @@ -246,7 +246,7 @@ describe("SendCreateCommand", () => { }; const cmdOptions = { - email: ["cli@example.com"], + emails: ["cli@example.com"], }; const response = await command.run(requestJson, cmdOptions); @@ -282,7 +282,7 @@ describe("SendCreateCommand", () => { }; const cmdOptions = { - email: ["cli@example.com"], + emails: ["cli@example.com"], }; sendService.encrypt.mockResolvedValue([ diff --git a/apps/cli/src/tools/send/commands/create.command.ts b/apps/cli/src/tools/send/commands/create.command.ts index ad4ff9c4e18..41cf5143acc 100644 --- a/apps/cli/src/tools/send/commands/create.command.ts +++ b/apps/cli/src/tools/send/commands/create.command.ts @@ -173,7 +173,7 @@ class Options { this.file = passedOptions?.file; this.text = passedOptions?.text; this.password = passedOptions?.password; - this.emails = passedOptions?.email; + this.emails = passedOptions?.emails; this.hidden = CliUtils.convertBooleanOption(passedOptions?.hidden); this.maxAccessCount = passedOptions?.maxAccessCount != null ? parseInt(passedOptions.maxAccessCount, null) : null; diff --git a/apps/cli/src/tools/send/commands/edit.command.spec.ts b/apps/cli/src/tools/send/commands/edit.command.spec.ts index 5bac63d3821..b72e9fdd512 100644 --- a/apps/cli/src/tools/send/commands/edit.command.spec.ts +++ b/apps/cli/src/tools/send/commands/edit.command.spec.ts @@ -81,7 +81,7 @@ describe("SendEditCommand", () => { const requestJson = encodeRequest(requestData); const cmdOptions = { - email: ["test@example.com"], + emails: ["test@example.com"], }; sendService.encrypt.mockResolvedValue([ @@ -155,7 +155,7 @@ describe("SendEditCommand", () => { const requestJson = encodeRequest(requestData); const cmdOptions = { - email: ["test@example.com"], + emails: ["test@example.com"], password: "testPassword123", }; @@ -239,7 +239,7 @@ describe("SendEditCommand", () => { const requestJson = encodeRequest(requestData); const cmdOptions = { - email: ["cli@example.com"], + emails: ["cli@example.com"], }; const response = await command.run(requestJson, cmdOptions); @@ -277,7 +277,7 @@ describe("SendEditCommand", () => { const requestJson = encodeRequest(requestData); const cmdOptions = { - email: ["cli@example.com"], + emails: ["cli@example.com"], }; sendService.encrypt.mockResolvedValue([ diff --git a/apps/cli/src/tools/send/commands/edit.command.ts b/apps/cli/src/tools/send/commands/edit.command.ts index 0709a33b88f..f3828784979 100644 --- a/apps/cli/src/tools/send/commands/edit.command.ts +++ b/apps/cli/src/tools/send/commands/edit.command.ts @@ -124,6 +124,6 @@ class Options { constructor(passedOptions: Record) { this.itemId = passedOptions?.itemId || passedOptions?.itemid; this.password = passedOptions.password; - this.emails = passedOptions.email; + this.emails = passedOptions.emails; } } diff --git a/apps/cli/src/tools/send/send.program.ts b/apps/cli/src/tools/send/send.program.ts index e40cea4daa9..a2f43bc2df8 100644 --- a/apps/cli/src/tools/send/send.program.ts +++ b/apps/cli/src/tools/send/send.program.ts @@ -57,11 +57,11 @@ export class SendProgram extends BaseProgram { new Option( "--password ", "optional password to access this Send. Can also be specified in JSON.", - ).conflicts("email"), + ).conflicts("emails"), ) .addOption( new Option( - "--email ", + "--emails ", "optional emails to access this Send. Can also be specified in JSON.", ).argParser(parseEmail), ) @@ -85,9 +85,11 @@ export class SendProgram extends BaseProgram { .addCommand(this.removePasswordCommand()) .addCommand(this.deleteCommand()) .action(async (data: string, options: OptionValues) => { - if (options.email) { + if (options.emails) { if (!emailAuthEnabled) { - this.processResponse(Response.error("The --email feature is not currently available.")); + this.processResponse( + Response.error("The --emails feature is not currently available."), + ); return; } } @@ -225,11 +227,13 @@ export class SendProgram extends BaseProgram { }) .action(async (encodedJson: string, options: OptionValues, args: { parent: Command }) => { // subcommands inherit flags from their parent; they cannot override them - const { fullObject = false, email = undefined, password = undefined } = args.parent.opts(); + const { fullObject = false, emails = undefined, password = undefined } = args.parent.opts(); - if (email) { + if (emails) { if (!emailAuthEnabled) { - this.processResponse(Response.error("The --email feature is not currently available.")); + this.processResponse( + Response.error("The --emails feature is not currently available."), + ); return; } } @@ -237,7 +241,7 @@ export class SendProgram extends BaseProgram { const mergedOptions = { ...options, fullObject: fullObject, - email, + emails, password, }; @@ -262,10 +266,12 @@ export class SendProgram extends BaseProgram { }) .action(async (encodedJson: string, options: OptionValues, args: { parent: Command }) => { await this.exitIfLocked(); - const { email = undefined, password = undefined } = args.parent.opts(); - if (email) { + const { emails = undefined, password = undefined } = args.parent.opts(); + if (emails) { if (!emailAuthEnabled) { - this.processResponse(Response.error("The --email feature is not currently available.")); + this.processResponse( + Response.error("The --emails feature is not currently available."), + ); return; } } @@ -288,7 +294,7 @@ export class SendProgram extends BaseProgram { const mergedOptions = { ...options, - email, + emails, password, }; @@ -353,7 +359,7 @@ export class SendProgram extends BaseProgram { file: sendFile, text: sendText, type: type, - emails: options.email ?? undefined, + emails: options.emails ?? undefined, }); return Buffer.from(JSON.stringify(template), "utf8").toString("base64"); From f46511b3e86b6d7c2ae4d995d464b00f52177c4b Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Fri, 13 Feb 2026 18:56:35 +0100 Subject: [PATCH 035/134] [PM-30908]Correct Premium subscription status handling (#18475) * Implement the required changes * Fix the family plan creation for expired sub * Resolve the pr comments * resolve the resubscribe issue * Removed redirectOnCompletion: true from the resubscribe * Display the Change payment method dialog on the subscription page * adjust the page reload time * revert payment method open in subscription page * Enable cancel premium see the subscription page * Revert the removal of hasPremiumPersonally * remove extra space * Add can view subscription * Use the canViewSubscription * Resolve the tab default to premium * use the subscription Instead of hasPremium * Revert the changes on user-subscription * Use the flag to redirect to subscription page * revert the canViewSubscription change * resolve the route issue with premium * Change the path to * Revert the previous iteration changes * Fix the build error --- .../billing/clients/account-billing.client.ts | 17 +++-- .../individual-billing-routing.module.ts | 2 +- .../individual/subscription.component.ts | 23 ++++++- .../account-subscription.component.ts | 32 +++++++++- apps/web/src/locales/en/messages.json | 9 +++ .../subscription-card.component.mdx | 5 +- .../subscription-card.component.spec.ts | 63 ++++++++++++++++--- .../subscription-card.component.stories.ts | 7 ++- .../subscription-card.component.ts | 20 ++++-- 9 files changed, 153 insertions(+), 25 deletions(-) diff --git a/apps/web/src/app/billing/clients/account-billing.client.ts b/apps/web/src/app/billing/clients/account-billing.client.ts index 1334ff643dd..6864e1de981 100644 --- a/apps/web/src/app/billing/clients/account-billing.client.ts +++ b/apps/web/src/app/billing/clients/account-billing.client.ts @@ -4,6 +4,8 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ProductTierType } from "@bitwarden/common/billing/enums"; import { BitwardenSubscriptionResponse } from "@bitwarden/common/billing/models/response/bitwarden-subscription.response"; import { SubscriptionCadence } from "@bitwarden/common/billing/types/subscription-pricing-tier"; +import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; +import { Maybe } from "@bitwarden/pricing"; import { BitwardenSubscription } from "@bitwarden/subscription"; import { @@ -23,11 +25,18 @@ export class AccountBillingClient { return this.apiService.send("GET", path, null, true, true); }; - getSubscription = async (): Promise => { + getSubscription = async (): Promise> => { const path = `${this.endpoint}/subscription`; - const json = await this.apiService.send("GET", path, null, true, true); - const response = new BitwardenSubscriptionResponse(json); - return response.toDomain(); + try { + const json = await this.apiService.send("GET", path, null, true, true); + const response = new BitwardenSubscriptionResponse(json); + return response.toDomain(); + } catch (error: any) { + if (error instanceof ErrorResponse && error.statusCode === 404) { + return null; + } + throw error; + } }; purchaseSubscription = async ( diff --git a/apps/web/src/app/billing/individual/individual-billing-routing.module.ts b/apps/web/src/app/billing/individual/individual-billing-routing.module.ts index f85dab54fe7..8d9c999caec 100644 --- a/apps/web/src/app/billing/individual/individual-billing-routing.module.ts +++ b/apps/web/src/app/billing/individual/individual-billing-routing.module.ts @@ -19,7 +19,7 @@ const routes: Routes = [ component: SubscriptionComponent, data: { titleId: "subscription" }, children: [ - { path: "", pathMatch: "full", redirectTo: "premium" }, + { path: "", pathMatch: "full", redirectTo: "user-subscription" }, ...featureFlaggedRoute({ defaultComponent: UserSubscriptionComponent, flaggedComponent: AccountSubscriptionComponent, diff --git a/apps/web/src/app/billing/individual/subscription.component.ts b/apps/web/src/app/billing/individual/subscription.component.ts index 37fb2baf3a6..4f52f3c2ea2 100644 --- a/apps/web/src/app/billing/individual/subscription.component.ts +++ b/apps/web/src/app/billing/individual/subscription.component.ts @@ -1,17 +1,22 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; -import { Observable, switchMap } from "rxjs"; +import { combineLatest, from, map, Observable, switchMap } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { AccountBillingClient } from "../clients/account-billing.client"; + // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "subscription.component.html", standalone: false, + providers: [AccountBillingClient], }) export class SubscriptionComponent implements OnInit { hasPremium$: Observable; @@ -21,9 +26,21 @@ export class SubscriptionComponent implements OnInit { private platformUtilsService: PlatformUtilsService, billingAccountProfileStateService: BillingAccountProfileStateService, accountService: AccountService, + configService: ConfigService, + private accountBillingClient: AccountBillingClient, ) { - this.hasPremium$ = accountService.activeAccount$.pipe( - switchMap((account) => billingAccountProfileStateService.hasPremiumPersonally$(account.id)), + this.hasPremium$ = combineLatest([ + configService.getFeatureFlag$(FeatureFlag.PM29594_UpdateIndividualSubscriptionPage), + accountService.activeAccount$, + ]).pipe( + switchMap(([isFeatureFlagEnabled, account]) => { + if (isFeatureFlagEnabled) { + return from(accountBillingClient.getSubscription()).pipe( + map((subscription) => !!subscription), + ); + } + return billingAccountProfileStateService.hasPremiumPersonally$(account.id); + }), ); } diff --git a/apps/web/src/app/billing/individual/subscription/account-subscription.component.ts b/apps/web/src/app/billing/individual/subscription/account-subscription.component.ts index d8e25de7965..7fdc830effd 100644 --- a/apps/web/src/app/billing/individual/subscription/account-subscription.component.ts +++ b/apps/web/src/app/billing/individual/subscription/account-subscription.component.ts @@ -34,6 +34,11 @@ import { AdjustAccountSubscriptionStorageDialogComponent, AdjustAccountSubscriptionStorageDialogParams, } from "@bitwarden/web-vault/app/billing/individual/subscription/adjust-account-subscription-storage-dialog.component"; +import { + UnifiedUpgradeDialogComponent, + UnifiedUpgradeDialogStatus, + UnifiedUpgradeDialogStep, +} from "@bitwarden/web-vault/app/billing/individual/upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component"; import { OffboardingSurveyDialogResultType, openOffboardingSurvey, @@ -93,10 +98,11 @@ export class AccountSubscriptionComponent { if (!this.account()) { return await redirectToPremiumPage(); } - if (!this.hasPremiumPersonally()) { + const subscription = await this.accountBillingClient.getSubscription(); + if (!subscription) { return await redirectToPremiumPage(); } - return await this.accountBillingClient.getSubscription(); + return subscription; }, }); @@ -106,6 +112,7 @@ export class AccountSubscriptionComponent { const subscription = this.subscription.value(); if (subscription) { return ( + subscription.status === SubscriptionStatuses.Incomplete || subscription.status === SubscriptionStatuses.IncompleteExpired || subscription.status === SubscriptionStatuses.Canceled || subscription.status === SubscriptionStatuses.Unpaid @@ -230,6 +237,27 @@ export class AccountSubscriptionComponent { case SubscriptionCardActions.UpdatePayment: await this.router.navigate(["../payment-details"], { relativeTo: this.activatedRoute }); break; + case SubscriptionCardActions.Resubscribe: { + const account = this.account(); + if (!account) { + return; + } + + const dialogRef = UnifiedUpgradeDialogComponent.open(this.dialogService, { + data: { + account, + initialStep: UnifiedUpgradeDialogStep.Payment, + selectedPlan: PersonalSubscriptionPricingTierIds.Premium, + }, + }); + + const result = await lastValueFrom(dialogRef.closed); + + if (result?.status === UnifiedUpgradeDialogStatus.UpgradedToPremium) { + this.subscription.reload(); + } + break; + } case SubscriptionCardActions.UpgradePlan: await this.openUpgradeDialog(); break; diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 59f5bc88419..d70a2d88bae 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, diff --git a/libs/subscription/src/components/subscription-card/subscription-card.component.mdx b/libs/subscription/src/components/subscription-card/subscription-card.component.mdx index c9cc6df7263..d3bad6583f5 100644 --- a/libs/subscription/src/components/subscription-card/subscription-card.component.mdx +++ b/libs/subscription/src/components/subscription-card/subscription-card.component.mdx @@ -78,6 +78,7 @@ type SubscriptionCardAction = | "contact-support" | "manage-invoices" | "reinstate-subscription" + | "resubscribe" | "update-payment" | "upgrade-plan"; ``` @@ -279,7 +280,7 @@ Payment issue expired, subscription has been suspended: ``` -**Actions available:** Contact Support +**Actions available:** Resubscribe ### Past Due @@ -370,7 +371,7 @@ Subscription that has been canceled: ``` -**Note:** Canceled subscriptions display no callout or actions. +**Actions available:** Resubscribe ### Enterprise diff --git a/libs/subscription/src/components/subscription-card/subscription-card.component.spec.ts b/libs/subscription/src/components/subscription-card/subscription-card.component.spec.ts index cdb85360c74..f524c4b5c26 100644 --- a/libs/subscription/src/components/subscription-card/subscription-card.component.spec.ts +++ b/libs/subscription/src/components/subscription-card/subscription-card.component.spec.ts @@ -44,9 +44,11 @@ describe("SubscriptionCardComponent", () => { unpaid: "Unpaid", weCouldNotProcessYourPayment: "We could not process your payment", contactSupportShort: "Contact support", - yourSubscriptionHasExpired: "Your subscription has expired", + yourSubscriptionIsExpired: "Your subscription is expired", + yourSubscriptionIsCanceled: "Your subscription is canceled", yourSubscriptionIsScheduledToCancel: `Your subscription is scheduled to cancel on ${params[0]}`, reinstateSubscription: "Reinstate subscription", + resubscribe: "Resubscribe", upgradeYourPlan: "Upgrade your plan", premiumShareEvenMore: "Premium share even more", upgradeNow: "Upgrade now", @@ -253,7 +255,7 @@ describe("SubscriptionCardComponent", () => { expect(buttons[1].nativeElement.textContent.trim()).toBe("Contact support"); }); - it("should display incomplete_expired callout with contact support action", () => { + it("should display incomplete_expired callout with resubscribe action", () => { setupComponent({ ...baseSubscription, status: "incomplete_expired", @@ -265,18 +267,18 @@ describe("SubscriptionCardComponent", () => { expect(calloutData).toBeTruthy(); expect(calloutData!.type).toBe("danger"); expect(calloutData!.title).toBe("Expired"); - expect(calloutData!.description).toContain("Your subscription has expired"); + expect(calloutData!.description).toContain("Your subscription is expired"); expect(calloutData!.callsToAction?.length).toBe(1); const callout = fixture.debugElement.query(By.css("bit-callout")); expect(callout).toBeTruthy(); const description = callout.query(By.css("p")); - expect(description.nativeElement.textContent).toContain("Your subscription has expired"); + expect(description.nativeElement.textContent).toContain("Your subscription is expired"); const buttons = callout.queryAll(By.css("button")); expect(buttons.length).toBe(1); - expect(buttons[0].nativeElement.textContent.trim()).toBe("Contact support"); + expect(buttons[0].nativeElement.textContent.trim()).toBe("Resubscribe"); }); it("should display pending cancellation callout for active status with cancelAt", () => { @@ -364,15 +366,29 @@ describe("SubscriptionCardComponent", () => { expect(buttons[0].nativeElement.textContent.trim()).toBe("Manage invoices"); }); - it("should not display callout for canceled status", () => { + it("should display canceled callout with resubscribe action", () => { setupComponent({ ...baseSubscription, status: "canceled", canceled: new Date("2025-01-15"), }); + const calloutData = component.callout(); + expect(calloutData).toBeTruthy(); + expect(calloutData!.type).toBe("danger"); + expect(calloutData!.title).toBe("Canceled"); + expect(calloutData!.description).toContain("Your subscription is canceled"); + expect(calloutData!.callsToAction?.length).toBe(1); + const callout = fixture.debugElement.query(By.css("bit-callout")); - expect(callout).toBeFalsy(); + expect(callout).toBeTruthy(); + + const description = callout.query(By.css("p")); + expect(description.nativeElement.textContent).toContain("Your subscription is canceled"); + + const buttons = callout.queryAll(By.css("button")); + expect(buttons.length).toBe(1); + expect(buttons[0].nativeElement.textContent.trim()).toBe("Resubscribe"); }); it("should display unpaid callout with manage invoices action", () => { @@ -489,6 +505,39 @@ describe("SubscriptionCardComponent", () => { expect(emitSpy).toHaveBeenCalledWith("manage-invoices"); }); + + it("should emit resubscribe action when button is clicked for incomplete_expired status", () => { + setupComponent({ + ...baseSubscription, + status: "incomplete_expired", + suspension: new Date("2025-01-15"), + gracePeriod: 7, + }); + + const emitSpy = jest.spyOn(component.callToActionClicked, "emit"); + + const button = fixture.debugElement.query(By.css("bit-callout button")); + button.triggerEventHandler("click", { button: 0 }); + fixture.detectChanges(); + + expect(emitSpy).toHaveBeenCalledWith("resubscribe"); + }); + + it("should emit resubscribe action when button is clicked for canceled status", () => { + setupComponent({ + ...baseSubscription, + status: "canceled", + canceled: new Date("2025-01-15"), + }); + + const emitSpy = jest.spyOn(component.callToActionClicked, "emit"); + + const button = fixture.debugElement.query(By.css("bit-callout button")); + button.triggerEventHandler("click", { button: 0 }); + fixture.detectChanges(); + + expect(emitSpy).toHaveBeenCalledWith("resubscribe"); + }); }); describe("Cart summary header content", () => { diff --git a/libs/subscription/src/components/subscription-card/subscription-card.component.stories.ts b/libs/subscription/src/components/subscription-card/subscription-card.component.stories.ts index 32976c89cc2..3d99ded2e5c 100644 --- a/libs/subscription/src/components/subscription-card/subscription-card.component.stories.ts +++ b/libs/subscription/src/components/subscription-card/subscription-card.component.stories.ts @@ -51,10 +51,13 @@ export default { weCouldNotProcessYourPayment: "We could not process your payment. Please update your payment method or contact the support team for assistance.", contactSupportShort: "Contact Support", - yourSubscriptionHasExpired: - "Your subscription has expired. Please contact the support team for assistance.", + yourSubscriptionIsExpired: + "Your subscription is expired. Please resubscribe to continue using premium features.", + yourSubscriptionIsCanceled: + "Your subscription is canceled. Please resubscribe to continue using premium features.", yourSubscriptionIsScheduledToCancel: `Your subscription is scheduled to cancel on ${args[0]}. You can reinstate it anytime before then.`, reinstateSubscription: "Reinstate subscription", + resubscribe: "Resubscribe", upgradeYourPlan: "Upgrade your plan", premiumShareEvenMore: "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise.", diff --git a/libs/subscription/src/components/subscription-card/subscription-card.component.ts b/libs/subscription/src/components/subscription-card/subscription-card.component.ts index ebfb41df6c2..78d2c40eb3e 100644 --- a/libs/subscription/src/components/subscription-card/subscription-card.component.ts +++ b/libs/subscription/src/components/subscription-card/subscription-card.component.ts @@ -20,6 +20,7 @@ export const SubscriptionCardActions = { ContactSupport: "contact-support", ManageInvoices: "manage-invoices", ReinstateSubscription: "reinstate-subscription", + Resubscribe: "resubscribe", UpdatePayment: "update-payment", UpgradePlan: "upgrade-plan", } as const; @@ -154,12 +155,12 @@ export class SubscriptionCardComponent { return { title: this.i18nService.t("expired"), type: "danger", - description: this.i18nService.t("yourSubscriptionHasExpired"), + description: this.i18nService.t("yourSubscriptionIsExpired"), callsToAction: [ { - text: this.i18nService.t("contactSupportShort"), + text: this.i18nService.t("resubscribe"), buttonType: "unstyled", - action: SubscriptionCardActions.ContactSupport, + action: SubscriptionCardActions.Resubscribe, }, ], }; @@ -218,7 +219,18 @@ export class SubscriptionCardComponent { }; } case SubscriptionStatuses.Canceled: { - return null; + return { + title: this.i18nService.t("canceled"), + type: "danger", + description: this.i18nService.t("yourSubscriptionIsCanceled"), + callsToAction: [ + { + text: this.i18nService.t("resubscribe"), + buttonType: "unstyled", + action: SubscriptionCardActions.Resubscribe, + }, + ], + }; } case SubscriptionStatuses.Unpaid: { return { From 10a20a43a36be6d76f3f1a3bf4d57fa2b56678b1 Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Fri, 13 Feb 2026 13:53:11 -0500 Subject: [PATCH 036/134] [PM-31738] update archive toasts (#18923) * update archive toast for all clients and trash archive restore toast, update archive cipher utilities spec --- apps/browser/src/_locales/en/messages.json | 11 ++++------- .../item-more-options/item-more-options.component.ts | 2 +- .../popup/components/vault/view/view.component.ts | 9 ++++++++- .../src/vault/popup/settings/archive.component.ts | 2 +- apps/desktop/src/locales/en/messages.json | 8 ++++---- .../vault-item-dialog/vault-item-dialog.component.ts | 4 ++-- .../src/app/vault/individual-vault/vault.component.ts | 4 ++-- apps/web/src/locales/en/messages.json | 11 ++++------- .../services/archive-cipher-utilities.service.spec.ts | 4 ++-- .../src/services/archive-cipher-utilities.service.ts | 4 ++-- 10 files changed, 30 insertions(+), 29 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 7944904c44a..e77550b01dc 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/vault/popup/components/vault/item-more-options/item-more-options.component.ts b/apps/browser/src/vault/popup/components/vault/item-more-options/item-more-options.component.ts index ef4c4a111b6..f7fe9ee1494 100644 --- a/apps/browser/src/vault/popup/components/vault/item-more-options/item-more-options.component.ts +++ b/apps/browser/src/vault/popup/components/vault/item-more-options/item-more-options.component.ts @@ -383,7 +383,7 @@ export class ItemMoreOptionsComponent { await this.cipherArchiveService.archiveWithServer(this.cipher.id as CipherId, activeUserId); this.toastService.showToast({ variant: "success", - message: this.i18nService.t("itemWasSentToArchive"), + message: this.i18nService.t("itemArchiveToast"), }); } } diff --git a/apps/browser/src/vault/popup/components/vault/view/view.component.ts b/apps/browser/src/vault/popup/components/vault/view/view.component.ts index d63cd5920a1..48402a957d6 100644 --- a/apps/browser/src/vault/popup/components/vault/view/view.component.ts +++ b/apps/browser/src/vault/popup/components/vault/view/view.component.ts @@ -277,17 +277,24 @@ export class ViewComponent { }; restore = async (): Promise => { + let toastMessage; try { await this.cipherService.restoreWithServer(this.cipher.id, this.activeUserId); } catch (e) { this.logService.error(e); } + if (this.cipher.archivedDate) { + toastMessage = this.i18nService.t("archivedItemRestored"); + } else { + toastMessage = this.i18nService.t("restoredItem"); + } + await this.popupRouterCacheService.back(); this.toastService.showToast({ variant: "success", title: null, - message: this.i18nService.t("restoredItem"), + message: toastMessage, }); }; diff --git a/apps/browser/src/vault/popup/settings/archive.component.ts b/apps/browser/src/vault/popup/settings/archive.component.ts index 336d9be6d16..0d1baa56a21 100644 --- a/apps/browser/src/vault/popup/settings/archive.component.ts +++ b/apps/browser/src/vault/popup/settings/archive.component.ts @@ -213,7 +213,7 @@ export class ArchiveComponent { this.toastService.showToast({ variant: "success", - message: this.i18nService.t("itemUnarchived"), + message: this.i18nService.t("itemUnarchivedToast"), }); } diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 85742db94ab..f444265877d 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts index 4da2d05f12b..2340f74c32d 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts @@ -616,7 +616,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { this.toastService.showToast({ variant: "success", - message: this.i18nService.t("itemWasSentToArchive"), + message: this.i18nService.t("itemArchiveToast"), }); } catch { this.toastService.showToast({ @@ -638,7 +638,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { this.toastService.showToast({ variant: "success", - message: this.i18nService.t("itemWasUnarchived"), + message: this.i18nService.t("itemUnarchivedToast"), }); } catch { this.toastService.showToast({ diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 4b9d2ed59ee..6fbe3f08912 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -744,7 +744,7 @@ export class VaultComponent implements OnInit, OnDestr await this.cipherArchiveService.archiveWithServer(cipher.id as CipherId, activeUserId); this.toastService.showToast({ variant: "success", - message: this.i18nService.t("itemWasSentToArchive"), + message: this.i18nService.t("itemArchiveToast"), }); this.refresh(); } catch (e) { @@ -801,7 +801,7 @@ export class VaultComponent implements OnInit, OnDestr this.toastService.showToast({ variant: "success", - message: this.i18nService.t("itemUnarchived"), + message: this.i18nService.t("itemUnarchivedToast"), }); this.refresh(); diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index d70a2d88bae..485f1fc07df 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -11905,14 +11905,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/libs/vault/src/services/archive-cipher-utilities.service.spec.ts b/libs/vault/src/services/archive-cipher-utilities.service.spec.ts index ea00f482987..3eff8997177 100644 --- a/libs/vault/src/services/archive-cipher-utilities.service.spec.ts +++ b/libs/vault/src/services/archive-cipher-utilities.service.spec.ts @@ -80,7 +80,7 @@ describe("ArchiveCipherUtilitiesService", () => { ); expect(toastService.showToast).toHaveBeenCalledWith({ variant: "success", - message: "itemWasSentToArchive", + message: "itemArchiveToast", }); }); @@ -106,7 +106,7 @@ describe("ArchiveCipherUtilitiesService", () => { ); expect(toastService.showToast).toHaveBeenCalledWith({ variant: "success", - message: "itemWasUnarchived", + message: "itemUnarchivedToast", }); }); diff --git a/libs/vault/src/services/archive-cipher-utilities.service.ts b/libs/vault/src/services/archive-cipher-utilities.service.ts index b747961a701..751eba96e73 100644 --- a/libs/vault/src/services/archive-cipher-utilities.service.ts +++ b/libs/vault/src/services/archive-cipher-utilities.service.ts @@ -58,7 +58,7 @@ export class ArchiveCipherUtilitiesService { ); this.toastService.showToast({ variant: "success", - message: this.i18nService.t("itemWasSentToArchive"), + message: this.i18nService.t("itemArchiveToast"), }); return cipherResponse; } catch { @@ -90,7 +90,7 @@ export class ArchiveCipherUtilitiesService { ); this.toastService.showToast({ variant: "success", - message: this.i18nService.t("itemWasUnarchived"), + message: this.i18nService.t("itemUnarchivedToast"), }); return cipherResponse; } catch { From 2912bf05e148ef883f07b24d8f5fb357ff77ec8e Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Fri, 13 Feb 2026 14:36:11 -0500 Subject: [PATCH 037/134] [PM-26901] Add notification handler for auto confirm (#18886) * add notification handler for auto confirm * add missing state check * fix test * isolate angular specific code from shared lib code * clean up * use autoconfirm method * fix test --- .../browser/src/background/main.background.ts | 36 ++++++++++- apps/browser/src/popup/app-routing.module.ts | 2 +- .../components/vault/vault.component.spec.ts | 2 +- .../popup/components/vault/vault.component.ts | 2 +- .../settings/admin-settings.component.ts | 2 +- .../src/services/jslib-services.module.ts | 2 + .../auto-confirm.service.abstraction.ts | 10 +-- ...auto-confirm-extension-dialog.component.ts | 0 ...auto-confirm-warning-dialog.component.html | 0 .../auto-confirm-warning-dialog.component.ts | 0 .../src/{ => angular}/components/index.ts | 0 ...c-user-confirmation-settings.guard.spec.ts | 3 +- ...omatic-user-confirmation-settings.guard.ts | 3 +- .../src/{ => angular}/guards/index.ts | 0 libs/auto-confirm/src/angular/index.ts | 8 +++ libs/auto-confirm/src/index.ts | 2 - .../default-auto-confirm.service.spec.ts | 58 +++++++++++------ .../services/default-auto-confirm.service.ts | 55 ++++++++++------ .../src/enums/notification-type.enum.ts | 1 + .../response/notification.response.spec.ts | 63 +++++++++++++++++++ .../models/response/notification.response.ts | 16 +++++ ...ult-server-notifications.multiuser.spec.ts | 5 ++ ...fault-server-notifications.service.spec.ts | 28 +++++++++ .../default-server-notifications.service.ts | 10 +++ tsconfig.base.json | 1 + 25 files changed, 257 insertions(+), 52 deletions(-) rename libs/auto-confirm/src/{ => angular}/components/auto-confirm-extension-dialog.component.ts (100%) rename libs/auto-confirm/src/{ => angular}/components/auto-confirm-warning-dialog.component.html (100%) rename libs/auto-confirm/src/{ => angular}/components/auto-confirm-warning-dialog.component.ts (100%) rename libs/auto-confirm/src/{ => angular}/components/index.ts (100%) rename libs/auto-confirm/src/{ => angular}/guards/automatic-user-confirmation-settings.guard.spec.ts (97%) rename libs/auto-confirm/src/{ => angular}/guards/automatic-user-confirmation-settings.guard.ts (94%) rename libs/auto-confirm/src/{ => angular}/guards/index.ts (100%) create mode 100644 libs/auto-confirm/src/angular/index.ts create mode 100644 libs/common/src/models/response/notification.response.spec.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 585942d7537..25c7b344982 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -14,7 +14,14 @@ import { timeout, } from "rxjs"; -import { CollectionService, DefaultCollectionService } from "@bitwarden/admin-console/common"; +import { + CollectionService, + DefaultCollectionService, + DefaultOrganizationUserApiService, + DefaultOrganizationUserService, + OrganizationUserApiService, + OrganizationUserService, +} from "@bitwarden/admin-console/common"; import { AuthRequestApiServiceAbstraction, AuthRequestService, @@ -27,6 +34,10 @@ import { LogoutReason, UserDecryptionOptionsService, } from "@bitwarden/auth/common"; +import { + AutomaticUserConfirmationService, + DefaultAutomaticUserConfirmationService, +} from "@bitwarden/auto-confirm"; import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service"; import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; @@ -487,6 +498,9 @@ export default class MainBackground { onUpdatedRan: boolean; onReplacedRan: boolean; loginToAutoFill: CipherView = null; + organizationUserService: OrganizationUserService; + organizationUserApiService: OrganizationUserApiService; + autoConfirmService: AutomaticUserConfirmationService; private commandsBackground: CommandsBackground; private contextMenusBackground: ContextMenusBackground; @@ -763,6 +777,15 @@ export default class MainBackground { { createRequest: (url, request) => new Request(url, request) }, ); + this.organizationUserApiService = new DefaultOrganizationUserApiService(this.apiService); + this.organizationUserService = new DefaultOrganizationUserService( + this.keyService, + this.encryptService, + this.organizationUserApiService, + this.accountService, + this.i18nService, + ); + this.hibpApiService = new HibpApiService(this.apiService); this.fileUploadService = new FileUploadService(this.logService, this.apiService); this.cipherFileUploadService = new CipherFileUploadService( @@ -804,6 +827,16 @@ export default class MainBackground { this.authService, ); + this.autoConfirmService = new DefaultAutomaticUserConfirmationService( + this.configService, + this.apiService, + this.organizationUserService, + this.stateProvider, + this.organizationService, + this.organizationUserApiService, + this.policyService, + ); + const sdkClientFactory = flagEnabled("sdk") ? new DefaultSdkClientFactory() : new NoopSdkClientFactory(); @@ -1219,6 +1252,7 @@ export default class MainBackground { this.authRequestAnsweringService, this.configService, this.policyService, + this.autoConfirmService, ); this.fido2UserInterfaceService = new BrowserFido2UserInterfaceService(this.authService); diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 4e14d1171fd..0d85743bba7 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -42,7 +42,7 @@ import { TwoFactorAuthComponent, TwoFactorAuthGuard, } from "@bitwarden/auth/angular"; -import { canAccessAutoConfirmSettings } from "@bitwarden/auto-confirm"; +import { canAccessAutoConfirmSettings } from "@bitwarden/auto-confirm/angular"; import { AnonLayoutWrapperComponent, AnonLayoutWrapperData } from "@bitwarden/components"; import { LockComponent, diff --git a/apps/browser/src/vault/popup/components/vault/vault.component.spec.ts b/apps/browser/src/vault/popup/components/vault/vault.component.spec.ts index 70affd73ef3..f48b08566a1 100644 --- a/apps/browser/src/vault/popup/components/vault/vault.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault/vault.component.spec.ts @@ -12,7 +12,7 @@ import { NudgeType, NudgesService } from "@bitwarden/angular/vault"; import { AutoConfirmExtensionSetupDialogComponent, AutomaticUserConfirmationService, -} from "@bitwarden/auto-confirm"; +} from "@bitwarden/auto-confirm/angular"; import { CurrentAccountComponent } from "@bitwarden/browser/auth/popup/account-switching/current-account.component"; import AutofillService from "@bitwarden/browser/autofill/services/autofill.service"; import { PopOutComponent } from "@bitwarden/browser/platform/popup/components/pop-out.component"; diff --git a/apps/browser/src/vault/popup/components/vault/vault.component.ts b/apps/browser/src/vault/popup/components/vault/vault.component.ts index 281abc5f180..cb3cb5f5eec 100644 --- a/apps/browser/src/vault/popup/components/vault/vault.component.ts +++ b/apps/browser/src/vault/popup/components/vault/vault.component.ts @@ -28,7 +28,7 @@ import { AutoConfirmExtensionSetupDialogComponent, AutoConfirmState, AutomaticUserConfirmationService, -} from "@bitwarden/auto-confirm"; +} from "@bitwarden/auto-confirm/angular"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; diff --git a/apps/browser/src/vault/popup/settings/admin-settings.component.ts b/apps/browser/src/vault/popup/settings/admin-settings.component.ts index e4b676525ed..52da4318047 100644 --- a/apps/browser/src/vault/popup/settings/admin-settings.component.ts +++ b/apps/browser/src/vault/popup/settings/admin-settings.component.ts @@ -16,7 +16,7 @@ import { SpotlightComponent } from "@bitwarden/angular/vault/components/spotligh import { AutoConfirmWarningDialogComponent, AutomaticUserConfirmationService, -} from "@bitwarden/auto-confirm"; +} from "@bitwarden/auto-confirm/angular"; import { PopOutComponent } from "@bitwarden/browser/platform/popup/components/pop-out.component"; import { PopupHeaderComponent } from "@bitwarden/browser/platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "@bitwarden/browser/platform/popup/layout/popup-page.component"; diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 0f857e67247..2fbf55bf6c5 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -56,6 +56,7 @@ import { UserDecryptionOptionsService, UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; +import { AutomaticUserConfirmationService } from "@bitwarden/auto-confirm"; import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service"; import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; @@ -1079,6 +1080,7 @@ const safeProviders: SafeProvider[] = [ AuthRequestAnsweringService, ConfigService, InternalPolicyService, + AutomaticUserConfirmationService, ], }), safeProvider({ diff --git a/libs/auto-confirm/src/abstractions/auto-confirm.service.abstraction.ts b/libs/auto-confirm/src/abstractions/auto-confirm.service.abstraction.ts index 9ce6cb9c1a4..1ef3be4ff4e 100644 --- a/libs/auto-confirm/src/abstractions/auto-confirm.service.abstraction.ts +++ b/libs/auto-confirm/src/abstractions/auto-confirm.service.abstraction.ts @@ -1,6 +1,6 @@ import { Observable } from "rxjs"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/user-core"; import { AutoConfirmState } from "../models/auto-confirm-state.model"; @@ -27,12 +27,12 @@ export abstract class AutomaticUserConfirmationService { /** * Calls the API endpoint to initiate automatic user confirmation. * @param userId The userId of the logged in admin performing auto confirmation. This is neccesary to perform the key exchange and for permissions checks. - * @param confirmingUserId The userId of the user being confirmed. - * @param organization the organization the user is being auto confirmed to. + * @param confirmedUserId The userId of the member being confirmed. + * @param organization the organization the member is being auto confirmed to. **/ abstract autoConfirmUser( userId: UserId, - confirmingUserId: UserId, - organization: Organization, + confirmedUserId: UserId, + organization: OrganizationId, ): Promise; } diff --git a/libs/auto-confirm/src/components/auto-confirm-extension-dialog.component.ts b/libs/auto-confirm/src/angular/components/auto-confirm-extension-dialog.component.ts similarity index 100% rename from libs/auto-confirm/src/components/auto-confirm-extension-dialog.component.ts rename to libs/auto-confirm/src/angular/components/auto-confirm-extension-dialog.component.ts diff --git a/libs/auto-confirm/src/components/auto-confirm-warning-dialog.component.html b/libs/auto-confirm/src/angular/components/auto-confirm-warning-dialog.component.html similarity index 100% rename from libs/auto-confirm/src/components/auto-confirm-warning-dialog.component.html rename to libs/auto-confirm/src/angular/components/auto-confirm-warning-dialog.component.html diff --git a/libs/auto-confirm/src/components/auto-confirm-warning-dialog.component.ts b/libs/auto-confirm/src/angular/components/auto-confirm-warning-dialog.component.ts similarity index 100% rename from libs/auto-confirm/src/components/auto-confirm-warning-dialog.component.ts rename to libs/auto-confirm/src/angular/components/auto-confirm-warning-dialog.component.ts diff --git a/libs/auto-confirm/src/components/index.ts b/libs/auto-confirm/src/angular/components/index.ts similarity index 100% rename from libs/auto-confirm/src/components/index.ts rename to libs/auto-confirm/src/angular/components/index.ts diff --git a/libs/auto-confirm/src/guards/automatic-user-confirmation-settings.guard.spec.ts b/libs/auto-confirm/src/angular/guards/automatic-user-confirmation-settings.guard.spec.ts similarity index 97% rename from libs/auto-confirm/src/guards/automatic-user-confirmation-settings.guard.spec.ts rename to libs/auto-confirm/src/angular/guards/automatic-user-confirmation-settings.guard.spec.ts index aca51edb8dc..0261a1a86dc 100644 --- a/libs/auto-confirm/src/guards/automatic-user-confirmation-settings.guard.spec.ts +++ b/libs/auto-confirm/src/angular/guards/automatic-user-confirmation-settings.guard.spec.ts @@ -3,14 +3,13 @@ import { Router, UrlTree } from "@angular/router"; import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject, firstValueFrom, Observable, of } from "rxjs"; +import { AutomaticUserConfirmationService } from "@bitwarden/auto-confirm"; import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { UserId } from "@bitwarden/common/types/guid"; import { ToastService } from "@bitwarden/components"; import { newGuid } from "@bitwarden/guid"; -import { AutomaticUserConfirmationService } from "../abstractions"; - import { canAccessAutoConfirmSettings } from "./automatic-user-confirmation-settings.guard"; describe("canAccessAutoConfirmSettings", () => { diff --git a/libs/auto-confirm/src/guards/automatic-user-confirmation-settings.guard.ts b/libs/auto-confirm/src/angular/guards/automatic-user-confirmation-settings.guard.ts similarity index 94% rename from libs/auto-confirm/src/guards/automatic-user-confirmation-settings.guard.ts rename to libs/auto-confirm/src/angular/guards/automatic-user-confirmation-settings.guard.ts index 77f01ba2801..3ae6b5b4c52 100644 --- a/libs/auto-confirm/src/guards/automatic-user-confirmation-settings.guard.ts +++ b/libs/auto-confirm/src/angular/guards/automatic-user-confirmation-settings.guard.ts @@ -2,13 +2,12 @@ import { inject } from "@angular/core"; import { CanActivateFn, Router } from "@angular/router"; import { map, switchMap } from "rxjs"; +import { AutomaticUserConfirmationService } from "@bitwarden/auto-confirm"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities"; import { ToastService } from "@bitwarden/components"; -import { AutomaticUserConfirmationService } from "../abstractions"; - export const canAccessAutoConfirmSettings: CanActivateFn = () => { const accountService = inject(AccountService); const autoConfirmService = inject(AutomaticUserConfirmationService); diff --git a/libs/auto-confirm/src/guards/index.ts b/libs/auto-confirm/src/angular/guards/index.ts similarity index 100% rename from libs/auto-confirm/src/guards/index.ts rename to libs/auto-confirm/src/angular/guards/index.ts diff --git a/libs/auto-confirm/src/angular/index.ts b/libs/auto-confirm/src/angular/index.ts new file mode 100644 index 00000000000..ff2d69248b4 --- /dev/null +++ b/libs/auto-confirm/src/angular/index.ts @@ -0,0 +1,8 @@ +// Re-export core auto-confirm functionality for convenience +export * from "../abstractions"; +export * from "../models"; +export * from "../services"; + +// Angular-specific exports +export * from "./components"; +export * from "./guards"; diff --git a/libs/auto-confirm/src/index.ts b/libs/auto-confirm/src/index.ts index 56b9d0b0285..9187ccd39cf 100644 --- a/libs/auto-confirm/src/index.ts +++ b/libs/auto-confirm/src/index.ts @@ -1,5 +1,3 @@ export * from "./abstractions"; -export * from "./components"; -export * from "./guards"; export * from "./models"; export * from "./services"; diff --git a/libs/auto-confirm/src/services/default-auto-confirm.service.spec.ts b/libs/auto-confirm/src/services/default-auto-confirm.service.spec.ts index 1d37378b96c..0ea3ca9c23a 100644 --- a/libs/auto-confirm/src/services/default-auto-confirm.service.spec.ts +++ b/libs/auto-confirm/src/services/default-auto-confirm.service.spec.ts @@ -377,48 +377,70 @@ describe("DefaultAutomaticUserConfirmationService", () => { defaultUserCollectionName: "encrypted-collection", } as OrganizationUserConfirmRequest; - beforeEach(() => { + beforeEach(async () => { const organizations$ = new BehaviorSubject([mockOrganization]); organizationService.organizations$.mockReturnValue(organizations$); configService.getFeatureFlag$.mockReturnValue(of(true)); policyService.policyAppliesToUser$.mockReturnValue(of(true)); + // Enable auto-confirm configuration for the user + const enabledConfig = new AutoConfirmState(); + enabledConfig.enabled = true; + await stateProvider.setUserState( + AUTO_CONFIRM_STATE, + { [mockUserId]: enabledConfig }, + mockUserId, + ); + apiService.getUserPublicKey.mockResolvedValue({ publicKey: mockPublicKey, } as UserKeyResponse); jest.spyOn(Utils, "fromB64ToArray").mockReturnValue(mockPublicKeyArray); organizationUserService.buildConfirmRequest.mockReturnValue(of(mockConfirmRequest)); - organizationUserApiService.postOrganizationUserConfirm.mockResolvedValue(undefined); + organizationUserApiService.postOrganizationUserAutoConfirm.mockResolvedValue(undefined); }); - it("should successfully auto-confirm a user", async () => { - await service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganization); + it("should successfully auto-confirm a user with organizationId", async () => { + await service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganizationId); expect(apiService.getUserPublicKey).toHaveBeenCalledWith(mockUserId); expect(organizationUserService.buildConfirmRequest).toHaveBeenCalledWith( mockOrganization, mockPublicKeyArray, ); - expect(organizationUserApiService.postOrganizationUserConfirm).toHaveBeenCalledWith( + expect(organizationUserApiService.postOrganizationUserAutoConfirm).toHaveBeenCalledWith( mockOrganizationId, mockConfirmingUserId, mockConfirmRequest, ); }); - it("should not confirm user when canManageAutoConfirm returns false", async () => { + it("should return early when canManageAutoConfirm returns false", async () => { configService.getFeatureFlag$.mockReturnValue(of(false)); - await expect( - service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganization), - ).rejects.toThrow("Cannot automatically confirm user (insufficient permissions)"); + await service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganizationId); expect(apiService.getUserPublicKey).not.toHaveBeenCalled(); - expect(organizationUserApiService.postOrganizationUserConfirm).not.toHaveBeenCalled(); + expect(organizationUserApiService.postOrganizationUserAutoConfirm).not.toHaveBeenCalled(); + }); + + it("should return early when auto-confirm is disabled in configuration", async () => { + const disabledConfig = new AutoConfirmState(); + disabledConfig.enabled = false; + await stateProvider.setUserState( + AUTO_CONFIRM_STATE, + { [mockUserId]: disabledConfig }, + mockUserId, + ); + + await service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganizationId); + + expect(apiService.getUserPublicKey).not.toHaveBeenCalled(); + expect(organizationUserApiService.postOrganizationUserAutoConfirm).not.toHaveBeenCalled(); }); it("should build confirm request with organization and public key", async () => { - await service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganization); + await service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganizationId); expect(organizationUserService.buildConfirmRequest).toHaveBeenCalledWith( mockOrganization, @@ -427,10 +449,10 @@ describe("DefaultAutomaticUserConfirmationService", () => { }); it("should call API with correct parameters", async () => { - await service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganization); + await service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganizationId); - expect(organizationUserApiService.postOrganizationUserConfirm).toHaveBeenCalledWith( - mockOrganization.id, + expect(organizationUserApiService.postOrganizationUserAutoConfirm).toHaveBeenCalledWith( + mockOrganizationId, mockConfirmingUserId, mockConfirmRequest, ); @@ -441,10 +463,10 @@ describe("DefaultAutomaticUserConfirmationService", () => { apiService.getUserPublicKey.mockRejectedValue(apiError); await expect( - service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganization), + service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganizationId), ).rejects.toThrow("API Error"); - expect(organizationUserApiService.postOrganizationUserConfirm).not.toHaveBeenCalled(); + expect(organizationUserApiService.postOrganizationUserAutoConfirm).not.toHaveBeenCalled(); }); it("should handle buildConfirmRequest errors gracefully", async () => { @@ -452,10 +474,10 @@ describe("DefaultAutomaticUserConfirmationService", () => { organizationUserService.buildConfirmRequest.mockReturnValue(throwError(() => buildError)); await expect( - service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganization), + service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganizationId), ).rejects.toThrow("Build Error"); - expect(organizationUserApiService.postOrganizationUserConfirm).not.toHaveBeenCalled(); + expect(organizationUserApiService.postOrganizationUserAutoConfirm).not.toHaveBeenCalled(); }); }); }); diff --git a/libs/auto-confirm/src/services/default-auto-confirm.service.ts b/libs/auto-confirm/src/services/default-auto-confirm.service.ts index 109ccb6c9db..821340a0a9c 100644 --- a/libs/auto-confirm/src/services/default-auto-confirm.service.ts +++ b/libs/auto-confirm/src/services/default-auto-confirm.service.ts @@ -8,10 +8,11 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { getById } from "@bitwarden/common/platform/misc"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { StateProvider } from "@bitwarden/state"; import { UserId } from "@bitwarden/user-core"; @@ -66,26 +67,44 @@ export class DefaultAutomaticUserConfirmationService implements AutomaticUserCon async autoConfirmUser( userId: UserId, - confirmingUserId: UserId, - organization: Organization, + confirmedUserId: UserId, + organizationId: OrganizationId, ): Promise { + const canManage = await firstValueFrom(this.canManageAutoConfirm$(userId)); + + if (!canManage) { + return; + } + + // Only initiate auto confirmation if the local client setting has been turned on + const autoConfirmEnabled = await firstValueFrom( + this.configuration$(userId).pipe(map((state) => state.enabled)), + ); + + if (!autoConfirmEnabled) { + return; + } + + const organization$ = this.organizationService.organizations$(userId).pipe( + getById(organizationId), + map((organization) => { + if (organization == null) { + throw new Error("Organization not found"); + } + return organization; + }), + ); + + const publicKeyResponse = await this.apiService.getUserPublicKey(userId); + const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey); + await firstValueFrom( - this.canManageAutoConfirm$(userId).pipe( - map((canManage) => { - if (!canManage) { - throw new Error("Cannot automatically confirm user (insufficient permissions)"); - } - return canManage; - }), - switchMap(() => this.apiService.getUserPublicKey(userId)), - map((publicKeyResponse) => Utils.fromB64ToArray(publicKeyResponse.publicKey)), - switchMap((publicKey) => - this.organizationUserService.buildConfirmRequest(organization, publicKey), - ), + organization$.pipe( + switchMap((org) => this.organizationUserService.buildConfirmRequest(org, publicKey)), switchMap((request) => - this.organizationUserApiService.postOrganizationUserConfirm( - organization.id, - confirmingUserId, + this.organizationUserApiService.postOrganizationUserAutoConfirm( + organizationId, + confirmedUserId, request, ), ), diff --git a/libs/common/src/enums/notification-type.enum.ts b/libs/common/src/enums/notification-type.enum.ts index a10e6bf4448..d323dda4d74 100644 --- a/libs/common/src/enums/notification-type.enum.ts +++ b/libs/common/src/enums/notification-type.enum.ts @@ -35,4 +35,5 @@ export enum NotificationType { ProviderBankAccountVerified = 24, SyncPolicy = 25, + AutoConfirmMember = 26, } diff --git a/libs/common/src/models/response/notification.response.spec.ts b/libs/common/src/models/response/notification.response.spec.ts new file mode 100644 index 00000000000..91a1390bfdb --- /dev/null +++ b/libs/common/src/models/response/notification.response.spec.ts @@ -0,0 +1,63 @@ +import { NotificationType } from "../../enums"; + +import { AutoConfirmMemberNotification, NotificationResponse } from "./notification.response"; + +describe("NotificationResponse", () => { + describe("AutoConfirmMemberNotification", () => { + it("should parse AutoConfirmMemberNotification payload", () => { + const response = { + ContextId: "context-123", + Type: NotificationType.AutoConfirmMember, + Payload: { + TargetUserId: "target-user-id", + UserId: "user-id", + OrganizationId: "org-id", + }, + }; + + const notification = new NotificationResponse(response); + + expect(notification.type).toBe(NotificationType.AutoConfirmMember); + expect(notification.payload).toBeInstanceOf(AutoConfirmMemberNotification); + expect(notification.payload.targetUserId).toBe("target-user-id"); + expect(notification.payload.userId).toBe("user-id"); + expect(notification.payload.organizationId).toBe("org-id"); + }); + + it("should handle stringified JSON payload", () => { + const response = { + ContextId: "context-123", + Type: NotificationType.AutoConfirmMember, + Payload: JSON.stringify({ + TargetUserId: "target-user-id-2", + UserId: "user-id-2", + OrganizationId: "org-id-2", + }), + }; + + const notification = new NotificationResponse(response); + + expect(notification.type).toBe(NotificationType.AutoConfirmMember); + expect(notification.payload).toBeInstanceOf(AutoConfirmMemberNotification); + expect(notification.payload.targetUserId).toBe("target-user-id-2"); + expect(notification.payload.userId).toBe("user-id-2"); + expect(notification.payload.organizationId).toBe("org-id-2"); + }); + }); + + describe("AutoConfirmMemberNotification constructor", () => { + it("should extract all properties from response", () => { + const response = { + TargetUserId: "target-user-id", + UserId: "user-id", + OrganizationId: "org-id", + }; + + const notification = new AutoConfirmMemberNotification(response); + + expect(notification.targetUserId).toBe("target-user-id"); + expect(notification.userId).toBe("user-id"); + expect(notification.organizationId).toBe("org-id"); + }); + }); +}); diff --git a/libs/common/src/models/response/notification.response.ts b/libs/common/src/models/response/notification.response.ts index 2c0c0aae3f1..27232696d2e 100644 --- a/libs/common/src/models/response/notification.response.ts +++ b/libs/common/src/models/response/notification.response.ts @@ -75,6 +75,9 @@ export class NotificationResponse extends BaseResponse { case NotificationType.SyncPolicy: this.payload = new SyncPolicyNotification(payload); break; + case NotificationType.AutoConfirmMember: + this.payload = new AutoConfirmMemberNotification(payload); + break; default: break; } @@ -210,3 +213,16 @@ export class LogOutNotification extends BaseResponse { this.reason = this.getResponseProperty("Reason"); } } + +export class AutoConfirmMemberNotification extends BaseResponse { + userId: string; + targetUserId: string; + organizationId: string; + + constructor(response: any) { + super(response); + this.targetUserId = this.getResponseProperty("TargetUserId"); + this.userId = this.getResponseProperty("UserId"); + this.organizationId = this.getResponseProperty("OrganizationId"); + } +} diff --git a/libs/common/src/platform/server-notifications/internal/default-server-notifications.multiuser.spec.ts b/libs/common/src/platform/server-notifications/internal/default-server-notifications.multiuser.spec.ts index 2795e4c3003..70b93c77f1c 100644 --- a/libs/common/src/platform/server-notifications/internal/default-server-notifications.multiuser.spec.ts +++ b/libs/common/src/platform/server-notifications/internal/default-server-notifications.multiuser.spec.ts @@ -3,6 +3,7 @@ import { BehaviorSubject, bufferCount, firstValueFrom, Subject, ObservedValueOf // eslint-disable-next-line no-restricted-imports import { LogoutReason } from "@bitwarden/auth/common"; +import { AutomaticUserConfirmationService } from "@bitwarden/auto-confirm"; import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AuthRequestAnsweringService } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction"; @@ -36,6 +37,7 @@ describe("DefaultServerNotificationsService (multi-user)", () => { let authRequestAnsweringService: MockProxy; let configService: MockProxy; let policyService: MockProxy; + let autoConfirmService: MockProxy; let activeUserAccount$: BehaviorSubject>; let userAccounts$: BehaviorSubject>; @@ -131,6 +133,8 @@ describe("DefaultServerNotificationsService (multi-user)", () => { policyService = mock(); + autoConfirmService = mock(); + defaultServerNotificationsService = new DefaultServerNotificationsService( mock(), syncService, @@ -145,6 +149,7 @@ describe("DefaultServerNotificationsService (multi-user)", () => { authRequestAnsweringService, configService, policyService, + autoConfirmService, ); }); diff --git a/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.spec.ts b/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.spec.ts index f058e8794ac..a54509925ef 100644 --- a/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.spec.ts +++ b/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.spec.ts @@ -4,6 +4,7 @@ import { BehaviorSubject, bufferCount, firstValueFrom, ObservedValueOf, of, Subj // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { LogoutReason } from "@bitwarden/auth/common"; +import { AutomaticUserConfirmationService } from "@bitwarden/auto-confirm"; import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { AuthRequestAnsweringService } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction"; @@ -45,6 +46,7 @@ describe("NotificationsService", () => { let authRequestAnsweringService: MockProxy; let configService: MockProxy; let policyService: MockProxy; + let autoConfirmService: MockProxy; let activeAccount: BehaviorSubject>; let accounts: BehaviorSubject>; @@ -75,6 +77,7 @@ describe("NotificationsService", () => { authRequestAnsweringService = mock(); configService = mock(); policyService = mock(); + autoConfirmService = mock(); // For these tests, use the active-user implementation (feature flag disabled) configService.getFeatureFlag$.mockImplementation(() => of(true)); @@ -128,6 +131,7 @@ describe("NotificationsService", () => { authRequestAnsweringService, configService, policyService, + autoConfirmService, ); }); @@ -507,5 +511,29 @@ describe("NotificationsService", () => { }); }); }); + + describe("NotificationType.AutoConfirmMember", () => { + it("should call autoConfirmService.autoConfirmUser with correct parameters", async () => { + autoConfirmService.autoConfirmUser.mockResolvedValue(); + + const notification = new NotificationResponse({ + type: NotificationType.AutoConfirmMember, + payload: { + UserId: mockUser1, + TargetUserId: "target-user-id", + OrganizationId: "org-id", + }, + contextId: "different-app-id", + }); + + await sut["processNotification"](notification, mockUser1); + + expect(autoConfirmService.autoConfirmUser).toHaveBeenCalledWith( + mockUser1, + "target-user-id", + "org-id", + ); + }); + }); }); }); diff --git a/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.ts b/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.ts index 83ea12bf154..1a43c0edb09 100644 --- a/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.ts +++ b/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.ts @@ -15,6 +15,7 @@ import { // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { LogoutReason } from "@bitwarden/auth/common"; +import { AutomaticUserConfirmationService } from "@bitwarden/auto-confirm"; import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyData } from "@bitwarden/common/admin-console/models/data/policy.data"; import { AuthRequestAnsweringService } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction"; @@ -49,6 +50,7 @@ export const DISABLED_NOTIFICATIONS_URL = "http://-"; export const AllowedMultiUserNotificationTypes = new Set([ NotificationType.AuthRequest, + NotificationType.AutoConfirmMember, ]); export class DefaultServerNotificationsService implements ServerNotificationsService { @@ -70,6 +72,7 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer private readonly authRequestAnsweringService: AuthRequestAnsweringService, private readonly configService: ConfigService, private readonly policyService: InternalPolicyService, + private autoConfirmService: AutomaticUserConfirmationService, ) { this.notifications$ = this.accountService.accounts$.pipe( map((accounts: Record): Set => { @@ -292,6 +295,13 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer case NotificationType.SyncPolicy: await this.policyService.syncPolicy(PolicyData.fromPolicy(notification.payload.policy)); break; + case NotificationType.AutoConfirmMember: + await this.autoConfirmService.autoConfirmUser( + notification.payload.userId, + notification.payload.targetUserId, + notification.payload.organizationId, + ); + break; default: break; } diff --git a/tsconfig.base.json b/tsconfig.base.json index 68498cfae01..17f8f6d44fc 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -25,6 +25,7 @@ "@bitwarden/auth/angular": ["./libs/auth/src/angular"], "@bitwarden/auth/common": ["./libs/auth/src/common"], "@bitwarden/auto-confirm": ["libs/auto-confirm/src/index.ts"], + "@bitwarden/auto-confirm/angular": ["libs/auto-confirm/src/angular"], "@bitwarden/billing": ["./libs/billing/src"], "@bitwarden/bit-common/*": ["./bitwarden_license/bit-common/src/*"], "@bitwarden/browser/*": ["./apps/browser/src/*"], From 323f30c8e941d3bfb72acb812e6a90e949771a5d Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Fri, 13 Feb 2026 11:36:39 -0800 Subject: [PATCH 038/134] [PM-29892] - fix bulk share in vault (#18601) * fix bulk share in vault * clean up types. * remove unnecessary optional chain * add back defensive programming. update restore * fix searchableCollectionNodes * add back optional chains --- .../collections/vault.component.ts | 14 +- .../vault-item-dialog.component.ts | 2 +- .../vault/individual-vault/vault.component.ts | 145 +++++++++--------- 3 files changed, 81 insertions(+), 80 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts index 073d73f6a50..a641116f4de 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts @@ -472,7 +472,7 @@ export class VaultComponent implements OnInit, OnDestroy { collections, filter.collectionId, ); - searchableCollectionNodes = selectedCollection.children ?? []; + searchableCollectionNodes = selectedCollection?.children ?? []; } let collectionsToReturn: CollectionAdminView[] = []; @@ -962,10 +962,10 @@ export class VaultComponent implements OnInit, OnDestroy { await this.editCipher(cipher, true); } - restore = async (c: CipherViewLike): Promise => { + restore = async (c: CipherViewLike): Promise => { const organization = await firstValueFrom(this.organization$); if (!CipherViewLikeUtils.isDeleted(c)) { - return false; + return; } if ( @@ -974,11 +974,11 @@ export class VaultComponent implements OnInit, OnDestroy { !organization.allowAdminAccessToAllCollectionItems ) { this.showMissingPermissionsError(); - return false; + return; } if (!(await this.repromptCipher([c]))) { - return false; + return; } // Allow restore of an Unassigned Item @@ -996,10 +996,10 @@ export class VaultComponent implements OnInit, OnDestroy { message: this.i18nService.t("restoredItem"), }); this.refresh(); - return true; + return; } catch (e) { this.logService.error(e); - return false; + return; } }; diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts index 2340f74c32d..4c6efdee167 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts @@ -100,7 +100,7 @@ export interface VaultItemDialogParams { /** * Function to restore a cipher from the trash. */ - restore?: (c: CipherViewLike) => Promise; + restore?: (c: CipherViewLike) => Promise; } export const VaultItemDialogResult = { diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 6fbe3f08912..5ff72b0d147 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -1,15 +1,6 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core"; +import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit, viewChild } from "@angular/core"; import { ActivatedRoute, Params, Router } from "@angular/router"; -import { - BehaviorSubject, - combineLatest, - firstValueFrom, - lastValueFrom, - Observable, - Subject, -} from "rxjs"; +import { combineLatest, firstValueFrom, lastValueFrom, Observable, of, Subject } from "rxjs"; import { concatMap, debounceTime, @@ -18,6 +9,7 @@ import { first, map, shareReplay, + startWith, switchMap, take, takeUntil, @@ -89,7 +81,6 @@ import { CipherListView } from "@bitwarden/sdk-internal"; import { AddEditFolderDialogComponent, AddEditFolderDialogResult, - AttachmentDialogCloseResult, AttachmentDialogResult, AttachmentsV2Component, CipherFormConfig, @@ -179,14 +170,10 @@ type EmptyStateMap = Record; ], }) export class VaultComponent implements OnInit, OnDestroy { - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @ViewChild("vaultFilter", { static: true }) filterComponent: VaultFilterComponent; - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @ViewChild("vaultItems", { static: false }) vaultItemsComponent: VaultItemsComponent; + readonly filterComponent = viewChild(VaultFilterComponent); + readonly vaultItemsComponent = viewChild(VaultItemsComponent); - trashCleanupWarning: string = null; + trashCleanupWarning: string = ""; activeFilter: VaultFilter = new VaultFilter(); protected deactivatedOrgIcon = DeactivatedOrg; @@ -198,20 +185,20 @@ export class VaultComponent implements OnInit, OnDestr protected refreshing = false; protected processingEvent = false; protected filter: RoutedVaultFilterModel = {}; - protected showBulkMove: boolean; - protected canAccessPremium: boolean; - protected allCollections: CollectionView[]; + protected showBulkMove: boolean = false; + protected canAccessPremium: boolean = false; + protected allCollections: CollectionView[] = []; protected allOrganizations: Organization[] = []; - protected ciphers: C[]; - protected collections: CollectionView[]; - protected isEmpty: boolean; + protected ciphers: C[] = []; + protected collections: CollectionView[] = []; + protected isEmpty: boolean = false; protected selectedCollection: TreeNode | undefined; protected canCreateCollections = false; protected currentSearchText$: Observable = this.route.queryParams.pipe( map((queryParams) => queryParams.search), ); private searchText$ = new Subject(); - private refresh$ = new BehaviorSubject(null); + private refresh$ = new Subject(); private destroy$ = new Subject(); private vaultItemDialogRef?: DialogRef | undefined; @@ -220,7 +207,7 @@ export class VaultComponent implements OnInit, OnDestr organizations$ = this.accountService.activeAccount$ .pipe(map((a) => a?.id)) - .pipe(switchMap((id) => this.organizationService.organizations$(id))); + .pipe(switchMap((id) => (id ? this.organizationService.organizations$(id) : of([])))); emptyState$ = combineLatest([ this.currentSearchText$, @@ -228,7 +215,7 @@ export class VaultComponent implements OnInit, OnDestr this.organizations$, ]).pipe( map(([searchText, filter, organizations]) => { - const selectedOrg = organizations?.find((org) => org.id === filter.organizationId); + const selectedOrg = organizations.find((org) => org.id === filter.organizationId); const isOrgDisabled = selectedOrg && !selectedOrg.enabled; if (isOrgDisabled) { @@ -586,7 +573,7 @@ export class VaultComponent implements OnInit, OnDestr firstSetup$ .pipe( - switchMap(() => this.refresh$), + switchMap(() => this.refresh$.pipe(startWith(undefined))), tap(() => (this.refreshing = true)), switchMap(() => combineLatest([ @@ -712,7 +699,6 @@ export class VaultComponent implements OnInit, OnDestr async handleUnknownCipher() { this.toastService.showToast({ variant: "error", - title: null, message: this.i18nService.t("unknownCipher"), }); await this.router.navigate([], { @@ -842,9 +828,13 @@ export class VaultComponent implements OnInit, OnDestr if (orgId == null) { orgId = "MyVault"; } - const orgs = await firstValueFrom(this.filterComponent.filters.organizationFilter.data$); + const data = this.filterComponent()?.filters?.organizationFilter?.data$; + if (data == undefined) { + return; + } + const orgs = await firstValueFrom(data); const orgNode = ServiceUtils.getTreeNodeObject(orgs, orgId) as TreeNode; - await this.filterComponent.filters?.organizationFilter?.action(orgNode); + await this.filterComponent()?.filters?.organizationFilter?.action(orgNode); } addFolder = (): void => { @@ -912,7 +902,10 @@ export class VaultComponent implements OnInit, OnDestr canEditCipher: cipher.edit, }); - const result: AttachmentDialogCloseResult = await lastValueFrom(dialogRef.closed); + const result = await lastValueFrom(dialogRef.closed); + if (result === undefined) { + return; + } if ( result.action === AttachmentDialogResult.Uploaded || @@ -966,7 +959,7 @@ export class VaultComponent implements OnInit, OnDestr */ async addCipher(cipherType?: CipherType) { const type = cipherType ?? this.activeFilter.cipherType; - const cipherFormConfig = await this.cipherFormConfigService.buildConfig("add", null, type); + const cipherFormConfig = await this.cipherFormConfigService.buildConfig("add", undefined, type); const collectionId = this.activeFilter.collectionId !== "AllCollections" && this.activeFilter.collectionId != null ? this.activeFilter.collectionId @@ -994,7 +987,7 @@ export class VaultComponent implements OnInit, OnDestr } async editCipher(cipher: CipherView | CipherListView, cloneMode?: boolean) { - return this.editCipherId(uuidAsString(cipher?.id), cloneMode); + return this.editCipherId(uuidAsString(cipher.id), cloneMode); } /** @@ -1088,6 +1081,9 @@ export class VaultComponent implements OnInit, OnDestr }, }); const result = await lastValueFrom(dialog.closed); + if (result === undefined) { + return; + } if (result.action === CollectionDialogAction.Saved) { if (result.collection) { // Update CollectionService with the new collection @@ -1104,7 +1100,7 @@ export class VaultComponent implements OnInit, OnDestr async editCollection(c: CollectionView, tab: CollectionDialogTabType): Promise { const dialog = openCollectionDialog(this.dialogService, { data: { - collectionId: c?.id, + collectionId: c.id, organizationId: c.organizationId, initialTab: tab, limitNestedCollections: true, @@ -1112,6 +1108,9 @@ export class VaultComponent implements OnInit, OnDestr }); const result = await lastValueFrom(dialog.closed); + if (result === undefined) { + return; + } const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); if (result.action === CollectionDialogAction.Saved) { if (result.collection) { @@ -1163,7 +1162,6 @@ export class VaultComponent implements OnInit, OnDestr this.toastService.showToast({ variant: "success", - title: null, message: this.i18nService.t("deletedCollectionId", collection.name), }); if (navigateAway) { @@ -1196,12 +1194,12 @@ export class VaultComponent implements OnInit, OnDestr let availableCollections: CollectionView[] = []; const orgId = this.activeFilter.organizationId || - ciphers.find((c) => c.organizationId !== null)?.organizationId; + ciphers.find((c) => c.organizationId !== undefined)?.organizationId; if (orgId && orgId !== "MyVault") { const organization = this.allOrganizations.find((o) => o.id === orgId); availableCollections = this.allCollections.filter( - (c) => c.organizationId === organization.id, + (c) => c.organizationId === organization?.id, ); } @@ -1229,7 +1227,7 @@ export class VaultComponent implements OnInit, OnDestr ciphers: ciphersToAssign, organizationId: orgId as OrganizationId, availableCollections, - activeCollection: this.activeFilter?.selectedCollectionNode?.node, + activeCollection: this.activeFilter.selectedCollectionNode?.node, }, }); @@ -1255,7 +1253,7 @@ export class VaultComponent implements OnInit, OnDestr await this.editCipher(cipher, true); } - restore = async (c: C): Promise => { + restore = async (c: CipherViewLike) => { let toastMessage; if (!CipherViewLikeUtils.isDeleted(c)) { return; @@ -1281,13 +1279,14 @@ export class VaultComponent implements OnInit, OnDestr await this.cipherService.restoreWithServer(uuidAsString(c.id), activeUserId); this.toastService.showToast({ variant: "success", - title: null, message: toastMessage, }); this.refresh(); } catch (e) { this.logService.error(e); + return; } + return; }; async bulkRestore(ciphers: C[]) { @@ -1311,7 +1310,6 @@ export class VaultComponent implements OnInit, OnDestr if (selectedCipherIds.length === 0) { this.toastService.showToast({ variant: "error", - title: null, message: this.i18nService.t("nothingSelected"), }); return; @@ -1321,23 +1319,24 @@ export class VaultComponent implements OnInit, OnDestr await this.cipherService.restoreManyWithServer(selectedCipherIds, activeUserId); this.toastService.showToast({ variant: "success", - title: null, message: toastMessage, }); this.refresh(); } private async handleDeleteEvent(items: VaultItem[]) { - const ciphers: C[] = items.filter((i) => i.collection === undefined).map((i) => i.cipher); - const collections = items.filter((i) => i.cipher === undefined).map((i) => i.collection); + const ciphers = items + .filter((i) => i.collection === undefined && i.cipher !== undefined) + .map((i) => i.cipher as C); + const collections = items + .filter((i) => i.collection !== undefined) + .map((i) => i.collection as CollectionView); if (ciphers.length === 1 && collections.length === 0) { await this.deleteCipher(ciphers[0]); } else if (ciphers.length === 0 && collections.length === 1) { await this.deleteCollection(collections[0]); } else { - const orgIds = items - .filter((i) => i.cipher === undefined) - .map((i) => i.collection.organizationId); + const orgIds = collections.map((c) => c.organizationId); const orgs = await firstValueFrom( this.organizations$.pipe(map((orgs) => orgs.filter((o) => orgIds.includes(o.id)))), ); @@ -1345,7 +1344,7 @@ export class VaultComponent implements OnInit, OnDestr } } - async deleteCipher(c: C): Promise { + async deleteCipher(c: C) { if (!(await this.repromptCipher([c]))) { return; } @@ -1364,7 +1363,7 @@ export class VaultComponent implements OnInit, OnDestr }); if (!confirmed) { - return false; + return; } try { @@ -1373,7 +1372,6 @@ export class VaultComponent implements OnInit, OnDestr this.toastService.showToast({ variant: "success", - title: null, message: this.i18nService.t(permanent ? "permanentlyDeletedItem" : "deletedItem"), }); this.refresh(); @@ -1390,7 +1388,6 @@ export class VaultComponent implements OnInit, OnDestr if (ciphers.length === 0 && collections.length === 0) { this.toastService.showToast({ variant: "error", - title: null, message: this.i18nService.t("nothingSelected"), }); return; @@ -1430,7 +1427,6 @@ export class VaultComponent implements OnInit, OnDestr if (selectedCipherIds.length === 0) { this.toastService.showToast({ variant: "error", - title: null, message: this.i18nService.t("nothingSelected"), }); return; @@ -1454,11 +1450,8 @@ export class VaultComponent implements OnInit, OnDestr const login = CipherViewLikeUtils.getLogin(cipher); if (!login) { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t("unexpectedError"), - }); + this.showErrorToast(); + return; } if (field === "username") { @@ -1471,15 +1464,15 @@ export class VaultComponent implements OnInit, OnDestr typeI18nKey = "password"; } else if (field === "totp") { aType = "TOTP"; + if (!login.totp) { + this.showErrorToast(); + return; + } const totpResponse = await firstValueFrom(this.totpService.getCode$(login.totp)); value = totpResponse.code; typeI18nKey = "verificationCodeTotp"; } else { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t("unexpectedError"), - }); + this.showErrorToast(); return; } @@ -1494,10 +1487,13 @@ export class VaultComponent implements OnInit, OnDestr return; } + if (!value) { + this.showErrorToast(); + return; + } this.platformUtilsService.copyToClipboard(value, { window: window }); this.toastService.showToast({ variant: "info", - title: null, message: this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)), }); @@ -1514,6 +1510,13 @@ export class VaultComponent implements OnInit, OnDestr } } + showErrorToast() { + this.toastService.showToast({ + variant: "error", + message: this.i18nService.t("unexpectedError"), + }); + } + /** * Toggles the favorite status of the cipher and updates it on the server. */ @@ -1525,7 +1528,6 @@ export class VaultComponent implements OnInit, OnDestr this.toastService.showToast({ variant: "success", - title: null, message: this.i18nService.t( cipherFullView.favorite ? "itemAddedToFavorites" : "itemRemovedFromFavorites", ), @@ -1540,15 +1542,15 @@ export class VaultComponent implements OnInit, OnDestr : this.cipherService.softDeleteWithServer(id, userId); } - protected async repromptCipher(ciphers: C[]) { + protected async repromptCipher(ciphers: CipherViewLike[]) { const notProtected = !ciphers.find((cipher) => cipher.reprompt !== CipherRepromptType.None); return notProtected || (await this.passwordRepromptService.showPasswordPrompt()); } private refresh() { - this.refresh$.next(); - this.vaultItemsComponent?.clearSelection(); + this.refresh$.next(undefined); + this.vaultItemsComponent()?.clearSelection(); } private async go(queryParams: any = null) { @@ -1573,7 +1575,6 @@ export class VaultComponent implements OnInit, OnDestr private showMissingPermissionsError() { this.toastService.showToast({ variant: "error", - title: null, message: this.i18nService.t("missingPermissions"), }); } @@ -1584,13 +1585,13 @@ export class VaultComponent implements OnInit, OnDestr */ private async getPasswordFromCipherViewLike(cipher: C): Promise { if (!CipherViewLikeUtils.isCipherListView(cipher)) { - return Promise.resolve(cipher.login?.password); + return Promise.resolve(cipher?.login?.password); } const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const _cipher = await this.cipherService.get(uuidAsString(cipher.id), activeUserId); const cipherView = await this.cipherService.decrypt(_cipher, activeUserId); - return cipherView.login?.password; + return cipherView.login.password; } } From bd9734c14c349d54982b909aa4ee8111768e9d08 Mon Sep 17 00:00:00 2001 From: John Harrington <84741727+harr1424@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:14:06 -0700 Subject: [PATCH 039/134] reorder form fields to match design specs (#18964) --- .../send-details/send-details.component.html | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.html b/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.html index dc1894b0935..62fa65ae1e0 100644 --- a/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.html +++ b/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.html @@ -21,20 +21,6 @@ [originalSendView]="originalSendView" > - - {{ "sendLink" | i18n }} - - - - {{ "deletionDate" | i18n }} {{ "enterMultipleEmailsSeparatedByComma" | i18n }} } + + + {{ "sendLink" | i18n }} + + + From f5b1be7e62a0df1fd3f4e473e73b90c138c66d7d Mon Sep 17 00:00:00 2001 From: John Harrington <84741727+harr1424@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:14:31 -0700 Subject: [PATCH 040/134] add dynamic EV headers (#18949) --- .../send/send-access/send-auth.component.ts | 23 ++++++++++++++++++- .../send/send-access/send-view.component.ts | 3 +++ apps/web/src/locales/en/messages.json | 4 ++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/tools/send/send-access/send-auth.component.ts b/apps/web/src/app/tools/send/send-access/send-auth.component.ts index 9ed8106ad40..22ab04bd9dd 100644 --- a/apps/web/src/app/tools/send/send-access/send-auth.component.ts +++ b/apps/web/src/app/tools/send/send-access/send-auth.component.ts @@ -26,7 +26,7 @@ import { SendAccessResponse } from "@bitwarden/common/tools/send/models/response import { SEND_KDF_ITERATIONS } from "@bitwarden/common/tools/send/send-kdf"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { AuthType } from "@bitwarden/common/tools/send/types/auth-type"; -import { ToastService } from "@bitwarden/components"; +import { AnonLayoutWrapperDataService, ToastService } from "@bitwarden/components"; import { SharedModule } from "../../../shared"; @@ -69,9 +69,11 @@ export class SendAuthComponent implements OnInit { private formBuilder: FormBuilder, private configService: ConfigService, private sendTokenService: SendTokenService, + private anonLayoutWrapperDataService: AnonLayoutWrapperDataService, ) {} ngOnInit() { + this.updatePageTitle(); void this.onSubmit(); } @@ -160,8 +162,10 @@ export class SendAuthComponent implements OnInit { this.expiredAuthAttempts = 0; if (emailRequired(response.error)) { this.sendAuthType.set(AuthType.Email); + this.updatePageTitle(); } else if (emailAndOtpRequired(response.error)) { this.enterOtp.set(true); + this.updatePageTitle(); } else if (otpInvalid(response.error)) { this.toastService.showToast({ variant: "error", @@ -170,6 +174,7 @@ export class SendAuthComponent implements OnInit { }); } else if (passwordHashB64Required(response.error)) { this.sendAuthType.set(AuthType.Password); + this.updatePageTitle(); } else if (passwordHashB64Invalid(response.error)) { this.toastService.showToast({ variant: "error", @@ -207,4 +212,20 @@ export class SendAuthComponent implements OnInit { ); return Utils.fromBufferToB64(passwordHash) as SendHashedPasswordB64; } + + private updatePageTitle(): void { + const authType = this.sendAuthType(); + + if (authType === AuthType.Email) { + if (this.enterOtp()) { + this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({ + pageTitle: { key: "enterTheCodeSentToYourEmail" }, + }); + } else { + this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({ + pageTitle: { key: "verifyYourEmailToViewThisSend" }, + }); + } + } + } } diff --git a/apps/web/src/app/tools/send/send-access/send-view.component.ts b/apps/web/src/app/tools/send/send-access/send-view.component.ts index 1ab9a121ace..923a749db92 100644 --- a/apps/web/src/app/tools/send/send-access/send-view.component.ts +++ b/apps/web/src/app/tools/send/send-access/send-view.component.ts @@ -69,6 +69,9 @@ export class SendViewComponent implements OnInit { ) {} ngOnInit() { + this.layoutWrapperDataService.setAnonLayoutWrapperData({ + pageTitle: { key: "sendAccessContentTitle" }, + }); void this.load(); } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 485f1fc07df..e43b266de4b 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -6438,6 +6438,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." From 2297082b1aa8341a59280e3f8f5e565582c1dc70 Mon Sep 17 00:00:00 2001 From: SmithThe4th Date: Fri, 13 Feb 2026 16:46:29 -0500 Subject: [PATCH 041/134] [PM-31668] Race condition in cipher cache clearing causes stale failed decryption state after leaving organization (#18941) * Refactored the search index to index with the cipherlistview * Fixed comment * clear encrypted cipher state to prevent stale emissions during sync * skip decrypt call when cipher arry is emoty during sync --- .../src/platform/sync/default-sync.service.ts | 2 + .../src/vault/abstractions/search.service.ts | 3 +- .../src/vault/services/cipher.service.ts | 26 ++- .../src/vault/services/search.service.ts | 120 ++++++----- .../utils/cipher-view-like-utils.spec.ts | 194 ++++++++++++++++++ .../src/vault/utils/cipher-view-like-utils.ts | 66 ++++++ 6 files changed, 350 insertions(+), 61 deletions(-) diff --git a/libs/common/src/platform/sync/default-sync.service.ts b/libs/common/src/platform/sync/default-sync.service.ts index 68c03503e8d..a25b1b3c210 100644 --- a/libs/common/src/platform/sync/default-sync.service.ts +++ b/libs/common/src/platform/sync/default-sync.service.ts @@ -183,6 +183,8 @@ export class DefaultSyncService extends CoreSyncService { const response = await this.inFlightApiCalls.sync; + await this.cipherService.clear(response.profile.id); + await this.syncUserDecryption(response.profile.id, response.userDecryption); await this.syncProfile(response.profile); await this.syncFolders(response.folders, response.profile.id); diff --git a/libs/common/src/vault/abstractions/search.service.ts b/libs/common/src/vault/abstractions/search.service.ts index 29575ec3af9..b4dfc015efe 100644 --- a/libs/common/src/vault/abstractions/search.service.ts +++ b/libs/common/src/vault/abstractions/search.service.ts @@ -2,7 +2,6 @@ import { Observable } from "rxjs"; import { SendView } from "../../tools/send/models/view/send.view"; import { IndexedEntityId, UserId } from "../../types/guid"; -import { CipherView } from "../models/view/cipher.view"; import { CipherViewLike } from "../utils/cipher-view-like-utils"; export abstract class SearchService { @@ -20,7 +19,7 @@ export abstract class SearchService { abstract isSearchable(userId: UserId, query: string | null): Promise; abstract indexCiphers( userId: UserId, - ciphersToIndex: CipherView[], + ciphersToIndex: CipherViewLike[], indexedEntityGuid?: string, ): Promise; abstract searchCiphers( diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 06c6628f158..e4c4f892b4a 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -173,13 +173,14 @@ export class CipherService implements CipherServiceAbstraction { decryptStartTime = performance.now(); }), switchMap(async (ciphers) => { - const [decrypted, failures] = await this.decryptCiphersWithSdk(ciphers, userId, false); - void this.setFailedDecryptedCiphers(failures, userId); - // Trigger full decryption and indexing in background - void this.getAllDecrypted(userId); - return decrypted; + return await this.decryptCiphersWithSdk(ciphers, userId, false); }), - tap((decrypted) => { + tap(([decrypted, failures]) => { + void Promise.all([ + this.setFailedDecryptedCiphers(failures, userId), + this.searchService.indexCiphers(userId, decrypted), + ]); + this.logService.measure( decryptStartTime, "Vault", @@ -188,10 +189,11 @@ export class CipherService implements CipherServiceAbstraction { [["Items", decrypted.length]], ); }), + map(([decrypted]) => decrypted), ); }), ); - }); + }, this.clearCipherViewsForUser$); /** * Observable that emits an array of decrypted ciphers for the active user. @@ -530,6 +532,10 @@ export class CipherService implements CipherServiceAbstraction { ciphers: Cipher[], userId: UserId, ): Promise<[CipherView[], CipherView[]] | null> { + if (ciphers.length === 0) { + return [[], []]; + } + if (await this.configService.getFeatureFlag(FeatureFlag.PM19941MigrateCipherDomainToSdk)) { const decryptStartTime = performance.now(); @@ -2401,6 +2407,12 @@ export class CipherService implements CipherServiceAbstraction { userId: UserId, fullDecryption: boolean = true, ): Promise<[CipherViewLike[], CipherView[]]> { + // Short-circuit if there are no ciphers to decrypt + // Observables reacting to key changes may attempt to decrypt with a stale SDK reference. + if (ciphers.length === 0) { + return [[], []]; + } + if (fullDecryption) { const [decryptedViews, failedViews] = await this.cipherEncryptionService.decryptManyLegacy( ciphers, diff --git a/libs/common/src/vault/services/search.service.ts b/libs/common/src/vault/services/search.service.ts index feb6a7494b5..e14a66aad6f 100644 --- a/libs/common/src/vault/services/search.service.ts +++ b/libs/common/src/vault/services/search.service.ts @@ -21,7 +21,6 @@ import { IndexedEntityId, UserId } from "../../types/guid"; import { SearchService as SearchServiceAbstraction } from "../abstractions/search.service"; import { FieldType } from "../enums"; import { CipherType } from "../enums/cipher-type"; -import { CipherView } from "../models/view/cipher.view"; import { CipherViewLike, CipherViewLikeUtils } from "../utils/cipher-view-like-utils"; // Time to wait before performing a search after the user stops typing. @@ -169,7 +168,7 @@ export class SearchService implements SearchServiceAbstraction { async indexCiphers( userId: UserId, - ciphers: CipherView[], + ciphers: CipherViewLike[], indexedEntityId?: string, ): Promise { if (await this.getIsIndexing(userId)) { @@ -182,34 +181,47 @@ export class SearchService implements SearchServiceAbstraction { const builder = new lunr.Builder(); builder.pipeline.add(this.normalizeAccentsPipelineFunction); builder.ref("id"); - builder.field("shortid", { boost: 100, extractor: (c: CipherView) => c.id.substr(0, 8) }); + builder.field("shortid", { + boost: 100, + extractor: (c: CipherViewLike) => uuidAsString(c.id).substr(0, 8), + }); builder.field("name", { boost: 10, }); builder.field("subtitle", { boost: 5, - extractor: (c: CipherView) => { - if (c.subTitle != null && c.type === CipherType.Card) { - return c.subTitle.replace(/\*/g, ""); + extractor: (c: CipherViewLike) => { + const subtitle = CipherViewLikeUtils.subtitle(c); + if (subtitle != null && CipherViewLikeUtils.getType(c) === CipherType.Card) { + return subtitle.replace(/\*/g, ""); } - return c.subTitle; + return subtitle; }, }); - builder.field("notes"); + builder.field("notes", { extractor: (c: CipherViewLike) => CipherViewLikeUtils.getNotes(c) }); builder.field("login.username", { - extractor: (c: CipherView) => - c.type === CipherType.Login && c.login != null ? c.login.username : null, + extractor: (c: CipherViewLike) => { + const login = CipherViewLikeUtils.getLogin(c); + return login?.username ?? null; + }, + }); + builder.field("login.uris", { + boost: 2, + extractor: (c: CipherViewLike) => this.uriExtractor(c), + }); + builder.field("fields", { + extractor: (c: CipherViewLike) => this.fieldExtractor(c, false), + }); + builder.field("fields_joined", { + extractor: (c: CipherViewLike) => this.fieldExtractor(c, true), }); - builder.field("login.uris", { boost: 2, extractor: (c: CipherView) => this.uriExtractor(c) }); - builder.field("fields", { extractor: (c: CipherView) => this.fieldExtractor(c, false) }); - builder.field("fields_joined", { extractor: (c: CipherView) => this.fieldExtractor(c, true) }); builder.field("attachments", { - extractor: (c: CipherView) => this.attachmentExtractor(c, false), + extractor: (c: CipherViewLike) => this.attachmentExtractor(c, false), }); builder.field("attachments_joined", { - extractor: (c: CipherView) => this.attachmentExtractor(c, true), + extractor: (c: CipherViewLike) => this.attachmentExtractor(c, true), }); - builder.field("organizationid", { extractor: (c: CipherView) => c.organizationId }); + builder.field("organizationid", { extractor: (c: CipherViewLike) => c.organizationId }); ciphers = ciphers || []; ciphers.forEach((c) => builder.add(c)); const index = builder.build(); @@ -400,37 +412,44 @@ export class SearchService implements SearchServiceAbstraction { return await firstValueFrom(this.searchIsIndexing$(userId)); } - private fieldExtractor(c: CipherView, joined: boolean) { - if (!c.hasFields) { + private fieldExtractor(c: CipherViewLike, joined: boolean) { + const fields = CipherViewLikeUtils.getFields(c); + if (!fields || fields.length === 0) { return null; } - let fields: string[] = []; - c.fields.forEach((f) => { + let fieldStrings: string[] = []; + fields.forEach((f) => { if (f.name != null) { - fields.push(f.name); + fieldStrings.push(f.name); } - if (f.type === FieldType.Text && f.value != null) { - fields.push(f.value); + // For CipherListView, value is only populated for Text fields + // For CipherView, we check the type explicitly + if (f.value != null) { + const fieldType = (f as { type?: FieldType }).type; + if (fieldType === undefined || fieldType === FieldType.Text) { + fieldStrings.push(f.value); + } } }); - fields = fields.filter((f) => f.trim() !== ""); - if (fields.length === 0) { + fieldStrings = fieldStrings.filter((f) => f.trim() !== ""); + if (fieldStrings.length === 0) { return null; } - return joined ? fields.join(" ") : fields; + return joined ? fieldStrings.join(" ") : fieldStrings; } - private attachmentExtractor(c: CipherView, joined: boolean) { - if (!c.hasAttachments) { + private attachmentExtractor(c: CipherViewLike, joined: boolean) { + const attachmentNames = CipherViewLikeUtils.getAttachmentNames(c); + if (!attachmentNames || attachmentNames.length === 0) { return null; } let attachments: string[] = []; - c.attachments.forEach((a) => { - if (a != null && a.fileName != null) { - if (joined && a.fileName.indexOf(".") > -1) { - attachments.push(a.fileName.substr(0, a.fileName.lastIndexOf("."))); + attachmentNames.forEach((fileName) => { + if (fileName != null) { + if (joined && fileName.indexOf(".") > -1) { + attachments.push(fileName.substring(0, fileName.lastIndexOf("."))); } else { - attachments.push(a.fileName); + attachments.push(fileName); } } }); @@ -441,43 +460,39 @@ export class SearchService implements SearchServiceAbstraction { return joined ? attachments.join(" ") : attachments; } - private uriExtractor(c: CipherView) { - if (c.type !== CipherType.Login || c.login == null || !c.login.hasUris) { + private uriExtractor(c: CipherViewLike) { + if (CipherViewLikeUtils.getType(c) !== CipherType.Login) { + return null; + } + const login = CipherViewLikeUtils.getLogin(c); + if (!login?.uris?.length) { return null; } const uris: string[] = []; - c.login.uris.forEach((u) => { + login.uris.forEach((u) => { if (u.uri == null || u.uri === "") { return; } - // Match ports + // Extract port from URI const portMatch = u.uri.match(/:(\d+)(?:[/?#]|$)/); const port = portMatch?.[1]; - let uri = u.uri; - - if (u.hostname !== null) { - uris.push(u.hostname); + const hostname = CipherViewLikeUtils.getUriHostname(u); + if (hostname !== undefined) { + uris.push(hostname); if (port) { - uris.push(`${u.hostname}:${port}`); - uris.push(port); - } - return; - } else { - const slash = uri.indexOf("/"); - const hostPart = slash > -1 ? uri.substring(0, slash) : uri; - uris.push(hostPart); - if (port) { - uris.push(`${hostPart}`); + uris.push(`${hostname}:${port}`); uris.push(port); } } + // Add processed URI (strip protocol and query params for non-regex matches) + let uri = u.uri; if (u.match !== UriMatchStrategy.RegularExpression) { const protocolIndex = uri.indexOf("://"); if (protocolIndex > -1) { - uri = uri.substr(protocolIndex + 3); + uri = uri.substring(protocolIndex + 3); } const queryIndex = uri.search(/\?|&|#/); if (queryIndex > -1) { @@ -486,6 +501,7 @@ export class SearchService implements SearchServiceAbstraction { } uris.push(uri); }); + return uris.length > 0 ? uris : null; } diff --git a/libs/common/src/vault/utils/cipher-view-like-utils.spec.ts b/libs/common/src/vault/utils/cipher-view-like-utils.spec.ts index 56b94fcf3ce..2a7bfac2970 100644 --- a/libs/common/src/vault/utils/cipher-view-like-utils.spec.ts +++ b/libs/common/src/vault/utils/cipher-view-like-utils.spec.ts @@ -651,4 +651,198 @@ describe("CipherViewLikeUtils", () => { expect(CipherViewLikeUtils.decryptionFailure(cipherListView)).toBe(false); }); }); + + describe("getNotes", () => { + describe("CipherView", () => { + it("returns notes when present", () => { + const cipherView = createCipherView(); + cipherView.notes = "This is a test note"; + + expect(CipherViewLikeUtils.getNotes(cipherView)).toBe("This is a test note"); + }); + + it("returns undefined when notes are not present", () => { + const cipherView = createCipherView(); + cipherView.notes = undefined; + + expect(CipherViewLikeUtils.getNotes(cipherView)).toBeUndefined(); + }); + }); + + describe("CipherListView", () => { + it("returns notes when present", () => { + const cipherListView = { + type: "secureNote", + notes: "List view notes", + } as CipherListView; + + expect(CipherViewLikeUtils.getNotes(cipherListView)).toBe("List view notes"); + }); + + it("returns undefined when notes are not present", () => { + const cipherListView = { + type: "secureNote", + } as CipherListView; + + expect(CipherViewLikeUtils.getNotes(cipherListView)).toBeUndefined(); + }); + }); + }); + + describe("getFields", () => { + describe("CipherView", () => { + it("returns fields when present", () => { + const cipherView = createCipherView(); + cipherView.fields = [ + { name: "Field1", value: "Value1" } as any, + { name: "Field2", value: "Value2" } as any, + ]; + + const fields = CipherViewLikeUtils.getFields(cipherView); + + expect(fields).toHaveLength(2); + expect(fields?.[0].name).toBe("Field1"); + expect(fields?.[0].value).toBe("Value1"); + expect(fields?.[1].name).toBe("Field2"); + expect(fields?.[1].value).toBe("Value2"); + }); + + it("returns empty array when fields array is empty", () => { + const cipherView = createCipherView(); + cipherView.fields = []; + + expect(CipherViewLikeUtils.getFields(cipherView)).toEqual([]); + }); + }); + + describe("CipherListView", () => { + it("returns fields when present", () => { + const cipherListView = { + type: { login: {} }, + fields: [ + { name: "Username", value: "user@example.com" }, + { name: "API Key", value: "abc123" }, + ], + } as CipherListView; + + const fields = CipherViewLikeUtils.getFields(cipherListView); + + expect(fields).toHaveLength(2); + expect(fields?.[0].name).toBe("Username"); + expect(fields?.[0].value).toBe("user@example.com"); + expect(fields?.[1].name).toBe("API Key"); + expect(fields?.[1].value).toBe("abc123"); + }); + + it("returns empty array when fields array is empty", () => { + const cipherListView = { + type: "secureNote", + fields: [], + } as unknown as CipherListView; + + expect(CipherViewLikeUtils.getFields(cipherListView)).toEqual([]); + }); + + it("returns undefined when fields are not present", () => { + const cipherListView = { + type: "secureNote", + } as CipherListView; + + expect(CipherViewLikeUtils.getFields(cipherListView)).toBeUndefined(); + }); + }); + }); + + describe("getAttachmentNames", () => { + describe("CipherView", () => { + it("returns attachment filenames when present", () => { + const cipherView = createCipherView(); + const attachment1 = new AttachmentView(); + attachment1.id = "1"; + attachment1.fileName = "document.pdf"; + const attachment2 = new AttachmentView(); + attachment2.id = "2"; + attachment2.fileName = "image.png"; + const attachment3 = new AttachmentView(); + attachment3.id = "3"; + attachment3.fileName = "spreadsheet.xlsx"; + cipherView.attachments = [attachment1, attachment2, attachment3]; + + const attachmentNames = CipherViewLikeUtils.getAttachmentNames(cipherView); + + expect(attachmentNames).toEqual(["document.pdf", "image.png", "spreadsheet.xlsx"]); + }); + + it("filters out null and undefined filenames", () => { + const cipherView = createCipherView(); + const attachment1 = new AttachmentView(); + attachment1.id = "1"; + attachment1.fileName = "valid.pdf"; + const attachment2 = new AttachmentView(); + attachment2.id = "2"; + attachment2.fileName = null as any; + const attachment3 = new AttachmentView(); + attachment3.id = "3"; + attachment3.fileName = undefined; + const attachment4 = new AttachmentView(); + attachment4.id = "4"; + attachment4.fileName = "another.txt"; + cipherView.attachments = [attachment1, attachment2, attachment3, attachment4]; + + const attachmentNames = CipherViewLikeUtils.getAttachmentNames(cipherView); + + expect(attachmentNames).toEqual(["valid.pdf", "another.txt"]); + }); + + it("returns empty array when attachments have no filenames", () => { + const cipherView = createCipherView(); + const attachment1 = new AttachmentView(); + attachment1.id = "1"; + const attachment2 = new AttachmentView(); + attachment2.id = "2"; + cipherView.attachments = [attachment1, attachment2]; + + const attachmentNames = CipherViewLikeUtils.getAttachmentNames(cipherView); + + expect(attachmentNames).toEqual([]); + }); + + it("returns empty array for empty attachments array", () => { + const cipherView = createCipherView(); + cipherView.attachments = []; + + expect(CipherViewLikeUtils.getAttachmentNames(cipherView)).toEqual([]); + }); + }); + + describe("CipherListView", () => { + it("returns attachment names when present", () => { + const cipherListView = { + type: "secureNote", + attachmentNames: ["report.pdf", "photo.jpg", "data.csv"], + } as CipherListView; + + const attachmentNames = CipherViewLikeUtils.getAttachmentNames(cipherListView); + + expect(attachmentNames).toEqual(["report.pdf", "photo.jpg", "data.csv"]); + }); + + it("returns empty array when attachmentNames is empty", () => { + const cipherListView = { + type: "secureNote", + attachmentNames: [], + } as unknown as CipherListView; + + expect(CipherViewLikeUtils.getAttachmentNames(cipherListView)).toEqual([]); + }); + + it("returns undefined when attachmentNames is not present", () => { + const cipherListView = { + type: "secureNote", + } as CipherListView; + + expect(CipherViewLikeUtils.getAttachmentNames(cipherListView)).toBeUndefined(); + }); + }); + }); }); diff --git a/libs/common/src/vault/utils/cipher-view-like-utils.ts b/libs/common/src/vault/utils/cipher-view-like-utils.ts index 04adb8d4832..5359bfb958f 100644 --- a/libs/common/src/vault/utils/cipher-view-like-utils.ts +++ b/libs/common/src/vault/utils/cipher-view-like-utils.ts @@ -10,6 +10,7 @@ import { LoginUriView as LoginListUriView, } from "@bitwarden/sdk-internal"; +import { Utils } from "../../platform/misc/utils"; import { CipherType } from "../enums"; import { Cipher } from "../models/domain/cipher"; import { CardView } from "../models/view/card.view"; @@ -290,6 +291,71 @@ export class CipherViewLikeUtils { static decryptionFailure = (cipher: CipherViewLike): boolean => { return "decryptionFailure" in cipher ? cipher.decryptionFailure : false; }; + + /** + * Returns the notes from the cipher. + * + * @param cipher - The cipher to extract notes from (either `CipherView` or `CipherListView`) + * @returns The notes string if present, or `undefined` if not set + */ + static getNotes = (cipher: CipherViewLike): string | undefined => { + return cipher.notes; + }; + + /** + * Returns the fields from the cipher. + * + * @param cipher - The cipher to extract fields from (either `CipherView` or `CipherListView`) + * @returns Array of field objects with `name` and `value` properties, `undefined` if not set + */ + static getFields = ( + cipher: CipherViewLike, + ): { name?: string | null; value?: string | undefined }[] | undefined => { + if (this.isCipherListView(cipher)) { + return cipher.fields; + } + return cipher.fields; + }; + + /** + * Returns attachment filenames from the cipher. + * + * @param cipher - The cipher to extract attachment names from (either `CipherView` or `CipherListView`) + * @returns Array of attachment filenames, `undefined` if attachments are not present + */ + static getAttachmentNames = (cipher: CipherViewLike): string[] | undefined => { + if (this.isCipherListView(cipher)) { + return cipher.attachmentNames; + } + + return cipher.attachments + ?.map((a) => a.fileName) + .filter((name): name is string => name != null); + }; + + /** + * Extracts hostname from a login URI. + * + * @param uri - The URI object (either `LoginUriView` class or `LoginListUriView`) + * @returns The hostname if available, `undefined` otherwise + * + * @remarks + * - For `LoginUriView` (CipherView): Uses the built-in `hostname` getter + * - For `LoginListUriView` (CipherListView): Computes hostname using `Utils.getHostname()` + * - Returns `undefined` for RegularExpression match types or when hostname cannot be extracted + */ + static getUriHostname = (uri: LoginListUriView | LoginUriView): string | undefined => { + if ("hostname" in uri && typeof uri.hostname !== "undefined") { + return uri.hostname ?? undefined; + } + + if (uri.match !== UriMatchStrategy.RegularExpression && uri.uri) { + const hostname = Utils.getHostname(uri.uri); + return hostname === "" ? undefined : hostname; + } + + return undefined; + }; } /** From 8bd1e5a855700dc1f132ad8fbad2dd03d19364fa Mon Sep 17 00:00:00 2001 From: SmithThe4th Date: Fri, 13 Feb 2026 18:13:41 -0500 Subject: [PATCH 042/134] [PM-30580] Add encryptMany to SDK for batch cipher encryption (#18942) * Migrated encrypt many to the sdk * removed comment * updated sdk package --- .../default-cipher-encryption.service.spec.ts | 27 ++++++++++++++----- .../default-cipher-encryption.service.ts | 19 +++++-------- package-lock.json | 16 +++++------ package.json | 4 +-- 4 files changed, 37 insertions(+), 29 deletions(-) diff --git a/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts b/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts index a0ca4833b92..98b554b5762 100644 --- a/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts +++ b/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts @@ -95,6 +95,7 @@ describe("DefaultCipherEncryptionService", () => { vault: jest.fn().mockReturnValue({ ciphers: jest.fn().mockReturnValue({ encrypt: jest.fn(), + encrypt_list: jest.fn(), encrypt_cipher_for_rotation: jest.fn(), set_fido2_credentials: jest.fn(), decrypt: jest.fn(), @@ -280,10 +281,23 @@ describe("DefaultCipherEncryptionService", () => { name: "encrypted-name-3", } as unknown as Cipher; - mockSdkClient.vault().ciphers().encrypt.mockReturnValue({ - cipher: sdkCipher, - encryptedFor: userId, - }); + mockSdkClient + .vault() + .ciphers() + .encrypt_list.mockReturnValue([ + { + cipher: sdkCipher, + encryptedFor: userId, + }, + { + cipher: sdkCipher, + encryptedFor: userId, + }, + { + cipher: sdkCipher, + encryptedFor: userId, + }, + ]); jest .spyOn(Cipher, "fromSdkCipher") @@ -299,7 +313,8 @@ describe("DefaultCipherEncryptionService", () => { expect(results[1].cipher).toEqual(expectedCipher2); expect(results[2].cipher).toEqual(expectedCipher3); - expect(mockSdkClient.vault().ciphers().encrypt).toHaveBeenCalledTimes(3); + expect(mockSdkClient.vault().ciphers().encrypt_list).toHaveBeenCalledTimes(1); + expect(mockSdkClient.vault().ciphers().encrypt).not.toHaveBeenCalled(); expect(results[0].encryptedFor).toBe(userId); expect(results[1].encryptedFor).toBe(userId); @@ -311,7 +326,7 @@ describe("DefaultCipherEncryptionService", () => { expect(results).toBeDefined(); expect(results.length).toBe(0); - expect(mockSdkClient.vault().ciphers().encrypt).not.toHaveBeenCalled(); + expect(mockSdkClient.vault().ciphers().encrypt_list).not.toHaveBeenCalled(); }); }); diff --git a/libs/common/src/vault/services/default-cipher-encryption.service.ts b/libs/common/src/vault/services/default-cipher-encryption.service.ts index 588265846e0..45542091618 100644 --- a/libs/common/src/vault/services/default-cipher-encryption.service.ts +++ b/libs/common/src/vault/services/default-cipher-encryption.service.ts @@ -65,21 +65,14 @@ export class DefaultCipherEncryptionService implements CipherEncryptionService { using ref = sdk.take(); - const results: EncryptionContext[] = []; - - // TODO: https://bitwarden.atlassian.net/browse/PM-30580 - // Replace this loop with a native SDK encryptMany method for better performance. - for (const model of models) { - const sdkCipherView = this.toSdkCipherView(model, ref.value); - const encryptionContext = ref.value.vault().ciphers().encrypt(sdkCipherView); - - results.push({ + return ref.value + .vault() + .ciphers() + .encrypt_list(models.map((model) => this.toSdkCipherView(model, ref.value))) + .map((encryptionContext) => ({ cipher: Cipher.fromSdkCipher(encryptionContext.cipher)!, encryptedFor: uuidAsString(encryptionContext.encryptedFor) as UserId, - }); - } - - return results; + })); }), catchError((error: unknown) => { this.logService.error(`Failed to encrypt ciphers in batch: ${error}`); diff --git a/package-lock.json b/package-lock.json index dbdcd6d083d..789a63c07b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,8 +23,8 @@ "@angular/platform-browser": "20.3.16", "@angular/platform-browser-dynamic": "20.3.16", "@angular/router": "20.3.16", - "@bitwarden/commercial-sdk-internal": "0.2.0-main.522", - "@bitwarden/sdk-internal": "0.2.0-main.522", + "@bitwarden/commercial-sdk-internal": "0.2.0-main.527", + "@bitwarden/sdk-internal": "0.2.0-main.527", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", @@ -4936,9 +4936,9 @@ "link": true }, "node_modules/@bitwarden/commercial-sdk-internal": { - "version": "0.2.0-main.522", - "resolved": "https://registry.npmjs.org/@bitwarden/commercial-sdk-internal/-/commercial-sdk-internal-0.2.0-main.522.tgz", - "integrity": "sha512-2wAbg30cGlDhSj14LaK2/ISuT91XPVeNgL/PU+eoxLhAehGKjAXdvZN3PSwFaAuaMbEFzlESvqC1pzzO4p/1zw==", + "version": "0.2.0-main.527", + "resolved": "https://registry.npmjs.org/@bitwarden/commercial-sdk-internal/-/commercial-sdk-internal-0.2.0-main.527.tgz", + "integrity": "sha512-4C4lwOgA2v184G2axUR5Jdb4UMXMhF52a/3c0lAZYbD/8Nid6jziE89nCa9hdfdazuPgWXhVFa3gPrhLZ4uTUQ==", "license": "BITWARDEN SOFTWARE DEVELOPMENT KIT LICENSE AGREEMENT", "dependencies": { "type-fest": "^4.41.0" @@ -5041,9 +5041,9 @@ "link": true }, "node_modules/@bitwarden/sdk-internal": { - "version": "0.2.0-main.522", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.522.tgz", - "integrity": "sha512-E+YqqX/FvGF0vGx6sNJfYaMj88C+rVo51fQPMSHoOePdryFcKQSJX706Glv86OMLMXE7Ln5Lua8LJRftlF/EFQ==", + "version": "0.2.0-main.527", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.527.tgz", + "integrity": "sha512-dxPh4XjEGFDBASRBEd/JwUdoMAz10W/0QGygYkPwhKKGzJncfDEAgQ/KrT9wc36ycrDrOOspff7xs/vmmzI0+A==", "license": "GPL-3.0", "dependencies": { "type-fest": "^4.41.0" diff --git a/package.json b/package.json index bc1553c4622..7499a69f99c 100644 --- a/package.json +++ b/package.json @@ -161,8 +161,8 @@ "@angular/platform-browser": "20.3.16", "@angular/platform-browser-dynamic": "20.3.16", "@angular/router": "20.3.16", - "@bitwarden/commercial-sdk-internal": "0.2.0-main.522", - "@bitwarden/sdk-internal": "0.2.0-main.522", + "@bitwarden/commercial-sdk-internal": "0.2.0-main.527", + "@bitwarden/sdk-internal": "0.2.0-main.527", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", From 470f91ae57a6af12026d5b1539d12d3ae89b2910 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 04:10:07 -0600 Subject: [PATCH 043/134] [deps]: Update dtolnay/rust-toolchain digest to efa25f7 (#18997) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/lint.yml | 4 ++-- .github/workflows/test.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index efc8c25fc5e..b50db6e08b6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -100,13 +100,13 @@ jobs: persist-credentials: false - name: Install Rust - uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # stable + uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 # stable with: toolchain: stable components: rustfmt, clippy - name: Install Rust nightly - uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # stable + uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 # stable with: toolchain: nightly components: rustfmt diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a0f783bbb36..c2fd4b7c32b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -210,7 +210,7 @@ jobs: persist-credentials: false - name: Install rust - uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # stable + uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 # stable with: toolchain: stable components: llvm-tools From 8620a2d7e412119ec75a9d5e2df256f54fc96a87 Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 04:36:42 -0600 Subject: [PATCH 044/134] Autosync the updated translations (#19008) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/src/_locales/ar/messages.json | 11 +- apps/browser/src/_locales/az/messages.json | 9 +- apps/browser/src/_locales/be/messages.json | 11 +- apps/browser/src/_locales/bg/messages.json | 11 +- apps/browser/src/_locales/bn/messages.json | 11 +- apps/browser/src/_locales/bs/messages.json | 11 +- apps/browser/src/_locales/ca/messages.json | 11 +- apps/browser/src/_locales/cs/messages.json | 9 +- apps/browser/src/_locales/cy/messages.json | 11 +- apps/browser/src/_locales/da/messages.json | 11 +- apps/browser/src/_locales/de/messages.json | 15 +- apps/browser/src/_locales/el/messages.json | 11 +- apps/browser/src/_locales/en_GB/messages.json | 11 +- apps/browser/src/_locales/en_IN/messages.json | 11 +- apps/browser/src/_locales/es/messages.json | 13 +- apps/browser/src/_locales/et/messages.json | 11 +- apps/browser/src/_locales/eu/messages.json | 143 +++++++++--------- apps/browser/src/_locales/fa/messages.json | 11 +- apps/browser/src/_locales/fi/messages.json | 11 +- apps/browser/src/_locales/fil/messages.json | 11 +- apps/browser/src/_locales/fr/messages.json | 11 +- apps/browser/src/_locales/gl/messages.json | 11 +- apps/browser/src/_locales/he/messages.json | 11 +- apps/browser/src/_locales/hi/messages.json | 11 +- apps/browser/src/_locales/hr/messages.json | 11 +- apps/browser/src/_locales/hu/messages.json | 9 +- apps/browser/src/_locales/id/messages.json | 11 +- apps/browser/src/_locales/it/messages.json | 11 +- apps/browser/src/_locales/ja/messages.json | 11 +- apps/browser/src/_locales/ka/messages.json | 11 +- apps/browser/src/_locales/km/messages.json | 11 +- apps/browser/src/_locales/kn/messages.json | 11 +- apps/browser/src/_locales/ko/messages.json | 11 +- apps/browser/src/_locales/lt/messages.json | 11 +- apps/browser/src/_locales/lv/messages.json | 11 +- apps/browser/src/_locales/ml/messages.json | 11 +- apps/browser/src/_locales/mr/messages.json | 11 +- apps/browser/src/_locales/my/messages.json | 11 +- apps/browser/src/_locales/nb/messages.json | 11 +- apps/browser/src/_locales/ne/messages.json | 11 +- apps/browser/src/_locales/nl/messages.json | 11 +- apps/browser/src/_locales/nn/messages.json | 11 +- apps/browser/src/_locales/or/messages.json | 11 +- apps/browser/src/_locales/pl/messages.json | 15 +- apps/browser/src/_locales/pt_BR/messages.json | 11 +- apps/browser/src/_locales/pt_PT/messages.json | 11 +- apps/browser/src/_locales/ro/messages.json | 11 +- apps/browser/src/_locales/ru/messages.json | 11 +- apps/browser/src/_locales/si/messages.json | 11 +- apps/browser/src/_locales/sk/messages.json | 7 +- apps/browser/src/_locales/sl/messages.json | 11 +- apps/browser/src/_locales/sr/messages.json | 11 +- apps/browser/src/_locales/sv/messages.json | 11 +- apps/browser/src/_locales/ta/messages.json | 11 +- apps/browser/src/_locales/te/messages.json | 11 +- apps/browser/src/_locales/th/messages.json | 11 +- apps/browser/src/_locales/tr/messages.json | 9 +- apps/browser/src/_locales/uk/messages.json | 11 +- apps/browser/src/_locales/vi/messages.json | 11 +- apps/browser/src/_locales/zh_CN/messages.json | 11 +- apps/browser/src/_locales/zh_TW/messages.json | 57 ++++--- 61 files changed, 332 insertions(+), 515 deletions(-) diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 78cf90c3555..7334362d446 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 6a43475da32..6168f7cf2dd 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -573,13 +573,10 @@ "noItemsInArchiveDesc": { "message": "Arxivlənmiş elementlər burada görünəcək, ümumi axtarış nəticələrindən və avto-doldurma təkliflərindən xaric ediləcək." }, - "itemWasSentToArchive": { - "message": "Element arxivə göndərildi" + "itemArchiveToast": { + "message": "Element arxivləndi" }, - "itemWasUnarchived": { - "message": "Element arxivdən çıxarıldı" - }, - "itemUnarchived": { + "itemUnarchivedToast": { "message": "Element arxivdən çıxarıldı" }, "archiveItem": { diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 9f4a65e3072..ea569cabdf4 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index a46ad75065e..e5d68bce366 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Архивираните елементи ще се показват тук и ще бъдат изключени от общите резултати при търсене и от предложенията за автоматично попълване." }, - "itemWasSentToArchive": { - "message": "Елементът беше преместен в архива" + "itemArchiveToast": { + "message": "Елементът е преместен в архива" }, - "itemWasUnarchived": { - "message": "Елементът беше изваден от архива" - }, - "itemUnarchived": { - "message": "Елементът беше изваден от архива" + "itemUnarchivedToast": { + "message": "Елементът е изваден от архива" }, "archiveItem": { "message": "Архивиране на елемента" diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index b46d0664231..533b12ab0a5 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index e81fc637b5c..35c4177e5eb 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index 2bd53876953..8e82fc34be4 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index 1501c7d7c4a..ed1b37134e1 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -573,13 +573,10 @@ "noItemsInArchiveDesc": { "message": "Zde se zobrazí archivované položky a budou vyloučeny z obecných výsledků vyhledávání a návrhů automatického vyplňování." }, - "itemWasSentToArchive": { - "message": "Položka byla přesunuta do archivu" + "itemArchiveToast": { + "message": "Položka archivována" }, - "itemWasUnarchived": { - "message": "Položka byla odebrána z archivu" - }, - "itemUnarchived": { + "itemUnarchivedToast": { "message": "Položka byla odebrána z archivu" }, "archiveItem": { diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 6910fe2efb3..165cd05de8e 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index faf4fc855ec..615cc6a2a0b 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index ad5b45159df..8f2b023bc00 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archivierte Einträge werden hier angezeigt und von allgemeinen Suchergebnissen sowie Vorschlägen zum automatischen Ausfüllen ausgeschlossen." }, - "itemWasSentToArchive": { - "message": "Eintrag wurde archiviert" + "itemArchiveToast": { + "message": "Eintrag archiviert" }, - "itemWasUnarchived": { - "message": "Eintrag wird nicht mehr archiviert" - }, - "itemUnarchived": { - "message": "Eintrag wird nicht mehr archiviert" + "itemUnarchivedToast": { + "message": "Eintrag nicht mehr archiviert" }, "archiveItem": { "message": "Eintrag archivieren" @@ -5964,7 +5961,7 @@ "message": "Kartennummer" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "Fehler: Entschlüsselung nicht möglich" }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Deine Organisation verwendet keine Master-Passwörter mehr, um sich bei Bitwarden anzumelden. Verifiziere die Organisation und Domain, um fortzufahren." @@ -6128,7 +6125,7 @@ "message": "benutzer@bitwarden.com, benutzer@acme.com" }, "downloadBitwardenApps": { - "message": "Download Bitwarden apps" + "message": "Bitwarden-Apps herunterladen" }, "emailProtected": { "message": "E-Mail-Adresse geschützt" diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index 59f757008f2..68f7267825d 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Το στοιχείο στάλθηκε στην αρχειοθήκη" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Το στοιχείο επαναφέρθηκε από την αρχειοθήκη" - }, - "itemUnarchived": { - "message": "Το στοιχείο επαναφέρθηκε από την αρχειοθήκη" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Αρχειοθέτηση στοιχείου" diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index e34e20844e6..d61774df145 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index 9fd388a80d3..3622ffce241 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index ab5fad7e3af..131263ea4d9 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Los elementos archivados aparecerán aquí y se excluirán de los resultados de búsqueda generales y de sugerencias de autocompletado." }, - "itemWasSentToArchive": { - "message": "El elemento fue archivado" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "El elemento fue desarchivado" - }, - "itemUnarchived": { - "message": "El elemento fue desarchivado" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archivar elemento" @@ -6113,7 +6110,7 @@ "message": "Resize side navigation" }, "whoCanView": { - "message": "Quien puede ver" + "message": "Quién puede ver" }, "specificPeople": { "message": "Personas específicas" diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index e8efd12b1e2..cd78c444c89 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index e7fcd4998e0..3e4382a3d3b 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -3,14 +3,14 @@ "message": "Bitwarden" }, "appLogoLabel": { - "message": "Bitwarden logo" + "message": "Bitwardenen logoa" }, "extName": { "message": "Bitwarden pasahitz kudeatzailea", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "message": "Etxean, lanean edo bidean, Bitwardenek zure pasahitz, giltz orokor edo informazio delikatua erraz gordetzen du", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { @@ -23,19 +23,19 @@ "message": "Sortu kontua" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "Berria Bitwardenen?" }, "logInWithPasskey": { - "message": "Log in with passkey" + "message": "Sartu giltz orokorrarekin" }, "unlockWithPasskey": { - "message": "Unlock with passkey" + "message": "Ireki giltz orokorrarekin" }, "useSingleSignOn": { - "message": "Use single sign-on" + "message": "Erabili SSO" }, "yourOrganizationRequiresSingleSignOn": { - "message": "Your organization requires single sign-on." + "message": "Zure erakundeak SSO erabiltzera behartzen du." }, "welcomeBack": { "message": "Ongi etorri berriro ere" @@ -71,7 +71,7 @@ "message": "Pasahitz nagusia ahazten baduzu, pista batek pasahitza gogoratzen lagunduko dizu." }, "masterPassHintText": { - "message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.", + "message": "Zure pasahitza ahazten bazaizu, pasahitzaren pista emailez bidal dezakegu. Gehienez $CURRENT$/$MAXIMUM$ karaktere.", "placeholders": { "current": { "content": "$1", @@ -90,7 +90,7 @@ "message": "Pasahitz nagusirako pista (aukerakoa)" }, "passwordStrengthScore": { - "message": "Password strength score $SCORE$", + "message": "Pasahitzaren sendotasun puntuazioa $SCORE$", "placeholders": { "score": { "content": "$1", @@ -99,10 +99,10 @@ } }, "joinOrganization": { - "message": "Join organization" + "message": "Erakundearen kide egin" }, "joinOrganizationName": { - "message": "Join $ORGANIZATIONNAME$", + "message": "$ORGANIZATIONNAME$-ren kide egin", "placeholders": { "organizationName": { "content": "$1", @@ -111,7 +111,7 @@ } }, "finishJoiningThisOrganizationBySettingAMasterPassword": { - "message": "Finish joining this organization by setting a master password." + "message": "Bukatu erakunde honen kide egitea pasahitz nagusi bat ezarriz." }, "tab": { "message": "Fitxak" @@ -138,7 +138,7 @@ "message": "Kopiatu pasahitza" }, "copyPassphrase": { - "message": "Copy passphrase" + "message": "Kopiatu esaldi-gakoa" }, "copyNote": { "message": "Kopiatu oharra" @@ -159,28 +159,28 @@ "message": "Izena kopiatu" }, "copyCompany": { - "message": "Copy company" + "message": "Kopiatu enpresa" }, "copySSN": { - "message": "Copy Social Security number" + "message": "Kopiatu segurtasun sozialaren zenbakia" }, "copyPassportNumber": { - "message": "Copy passport number" + "message": "Kopiatu pasaporte zenbakia" }, "copyLicenseNumber": { - "message": "Copy license number" + "message": "Kopiatu lizentzia zenbakia" }, "copyPrivateKey": { - "message": "Copy private key" + "message": "Kopiatu gako pribatua" }, "copyPublicKey": { - "message": "Copy public key" + "message": "Kopiatu gako publikoa" }, "copyFingerprint": { - "message": "Copy fingerprint" + "message": "Kopiatu hatz-marka" }, "copyCustomField": { - "message": "Copy $FIELD$", + "message": "Kopiatu $FIELD$", "placeholders": { "field": { "content": "$1", @@ -189,7 +189,7 @@ } }, "copyWebsite": { - "message": "Copy website" + "message": "Kopiatu webgunea" }, "copyNotes": { "message": "Kopiatu oharrak" @@ -206,7 +206,7 @@ "message": "Auto-betetzea" }, "autoFillLogin": { - "message": "Autofill login" + "message": "Saio-hasiera autobetetzea" }, "autoFillCard": { "message": "Auto-bete txartela" @@ -261,16 +261,16 @@ "message": "Gehitu elementua" }, "accountEmail": { - "message": "Account email" + "message": "Kontuaren e-maila" }, "requestHint": { - "message": "Request hint" + "message": "Argibidea eskatu" }, "requestPasswordHint": { - "message": "Request password hint" + "message": "Pasahitz-laguntza eskatu" }, "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": { - "message": "Enter your account email address and your password hint will be sent to you" + "message": "Idatzi zure kontuaren e-maila eta pasahitzaren argibidea bidaliko dizugu" }, "getMasterPasswordHint": { "message": "Jaso pasahitz nagusiaren pista" @@ -297,10 +297,10 @@ "message": "Aldatu pasahitz nagusia" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "Web aplikaziora jarraitu?" }, "continueToWebAppDesc": { - "message": "Explore more features of your Bitwarden account on the web app." + "message": "Esploratu zure Bitwarden kontuaren funtzio gehiago web-aplikazioan." }, "continueToHelpCenter": { "message": "Continue to Help Center?" @@ -332,7 +332,7 @@ "message": "Itxi saioa" }, "aboutBitwarden": { - "message": "About Bitwarden" + "message": "Bitwardeni buruz" }, "about": { "message": "Honi buruz" @@ -398,10 +398,10 @@ } }, "newFolder": { - "message": "New folder" + "message": "Karpeta berria" }, "folderName": { - "message": "Folder name" + "message": "Karpetaren izena" }, "folderHintText": { "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" @@ -440,7 +440,7 @@ "message": "Sinkronizatu" }, "syncNow": { - "message": "Sync now" + "message": "Sinkronizatu orain" }, "lastSync": { "message": "Azken sinkronizazioa:" @@ -456,7 +456,7 @@ "message": "Automatikoki pasahitz sendo eta bakarrak sortzen ditu zure saio-hasieratarako." }, "bitWebVaultApp": { - "message": "Bitwarden web app" + "message": "Bitwarden web aplikazioa" }, "select": { "message": "Hautatu" @@ -489,11 +489,11 @@ "message": "Luzera" }, "include": { - "message": "Include", + "message": "Sartu", "description": "Card header for password generator include block" }, "uppercaseDescription": { - "message": "Include uppercase characters", + "message": "Sartu letra maiuskulak", "description": "Tooltip for the password generator uppercase character checkbox" }, "uppercaseLabel": { @@ -501,7 +501,7 @@ "description": "Label for the password generator uppercase character checkbox" }, "lowercaseDescription": { - "message": "Include lowercase characters", + "message": "Sartu letra minuskulak", "description": "Full description for the password generator lowercase character checkbox" }, "lowercaseLabel": { @@ -509,7 +509,7 @@ "description": "Label for the password generator lowercase character checkbox" }, "numbersDescription": { - "message": "Include numbers", + "message": "Sartu zenbakiak", "description": "Full description for the password generator numbers checkbox" }, "numbersLabel": { @@ -517,7 +517,7 @@ "description": "Label for the password generator numbers checkbox" }, "specialCharactersDescription": { - "message": "Include special characters", + "message": "Sartu karaktere bereziak", "description": "Full description for the password generator special characters checkbox" }, "numWords": { @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -607,16 +604,16 @@ "message": "Erakutsi" }, "viewAll": { - "message": "View all" + "message": "Ikusi denak" }, "showAll": { - "message": "Show all" + "message": "Dena erakutsi" }, "viewLess": { "message": "View less" }, "viewLogin": { - "message": "View login" + "message": "Ikusi saio-hasiera" }, "noItemsInList": { "message": "Ez dago erakusteko elementurik." @@ -946,16 +943,16 @@ "message": "Saioa amaitu da." }, "logIn": { - "message": "Log in" + "message": "Hasi saioa" }, "logInToBitwarden": { - "message": "Log in to Bitwarden" + "message": "Sartu Bitwardenera" }, "enterTheCodeSentToYourEmail": { - "message": "Enter the code sent to your email" + "message": "Sartu e-mailera bidali dizugun kodea" }, "enterTheCodeFromYourAuthenticatorApp": { - "message": "Enter the code from your authenticator app" + "message": "Sartu zure egiaztapenerako aplikazioko kodea" }, "pressYourYubiKeyToAuthenticate": { "message": "Press your YubiKey to authenticate" @@ -1344,11 +1341,11 @@ "message": "Export from" }, "exportVerb": { - "message": "Export", + "message": "Esportatu", "description": "The verb form of the word Export" }, "exportNoun": { - "message": "Export", + "message": "Esportatu", "description": "The noun form of the word Export" }, "importNoun": { @@ -1768,7 +1765,7 @@ "description": "Represents the message for allowing the user to enable the autofill overlay" }, "autofillSuggestionsSectionTitle": { - "message": "Autofill suggestions" + "message": "Autobetetzeko iradokizunak" }, "autofillSpotlightTitle": { "message": "Easily find autofill suggestions" @@ -2165,7 +2162,7 @@ "description": "Header for edit file send" }, "viewItemHeaderLogin": { - "message": "View Login", + "message": "Ikusi saio-hasiera", "description": "Header for view login item type" }, "viewItemHeaderCard": { @@ -2203,7 +2200,7 @@ "message": "Bildumak" }, "nCollections": { - "message": "$COUNT$ collections", + "message": "$COUNT$ bilduma", "placeholders": { "count": { "content": "$1", @@ -2928,7 +2925,7 @@ } }, "send": { - "message": "Send", + "message": "Bidali", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendDetails": { @@ -4019,7 +4016,7 @@ "message": "required" }, "search": { - "message": "Search" + "message": "Bilatu" }, "inputMinLength": { "message": "Input must be at least $COUNT$ characters long.", @@ -4346,7 +4343,7 @@ "message": "Select a folder" }, "selectImportCollection": { - "message": "Select a collection" + "message": "Bilduma bat aukeratu" }, "importTargetHintCollection": { "message": "Select this option if you want the imported file contents moved to a collection" @@ -4525,7 +4522,7 @@ "message": "Try again or look for an email from LastPass to verify it's you." }, "collection": { - "message": "Collection" + "message": "Bilduma" }, "lastPassYubikeyDesc": { "message": "Insert the YubiKey associated with your LastPass account into your computer's USB port, then touch its button." @@ -4686,7 +4683,7 @@ "message": "Passkey removed" }, "autofillSuggestions": { - "message": "Autofill suggestions" + "message": "Autobetetzeko proposamenak" }, "itemSuggestions": { "message": "Suggested items" @@ -4812,7 +4809,7 @@ "message": "No values to copy" }, "assignToCollections": { - "message": "Assign to collections" + "message": "Esleitu bildumetan" }, "copyEmail": { "message": "Copy email" @@ -4904,7 +4901,7 @@ } }, "new": { - "message": "New" + "message": "Berria" }, "removeItem": { "message": "Remove $NAME$", @@ -4942,10 +4939,10 @@ "message": "Additional information" }, "itemHistory": { - "message": "Item history" + "message": "Aldaketen historia" }, "lastEdited": { - "message": "Last edited" + "message": "Azken edizioa" }, "ownerYou": { "message": "Owner: You" @@ -5029,7 +5026,7 @@ "message": "Filters" }, "filterVault": { - "message": "Filter vault" + "message": "Iragazi kutxa gotorra" }, "filterApplied": { "message": "One filter applied" @@ -5066,13 +5063,13 @@ "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." }, "loginCredentials": { - "message": "Login credentials" + "message": "Saio-hasierako kredentzialak" }, "authenticatorKey": { "message": "Authenticator key" }, "autofillOptions": { - "message": "Autofill options" + "message": "Autobetetzeko aukerak" }, "websiteUri": { "message": "Website (URI)" @@ -5866,7 +5863,7 @@ "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, "generatorNudgeTitle": { - "message": "Quickly create passwords" + "message": "Sortu pasahitzak azkar" }, "generatorNudgeBodyOne": { "message": "Easily create strong and unique passwords by clicking on", @@ -5879,7 +5876,7 @@ "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." }, "generatorNudgeBodyAria": { - "message": "Easily create strong and unique passwords by clicking on the Generate password button to help you keep your logins secure.", + "message": "Sortu erraz pasahitz sendo eta bakarrak Sortu pasahitza botoian klik eginez, zure saio-hasierak seguru mantentzen laguntzeko.", "description": "Aria label for the body content of the generator nudge" }, "aboutThisSetting": { @@ -6107,7 +6104,7 @@ "message": "Items" }, "searchResults": { - "message": "Search results" + "message": "Bilaketaren emaitzak" }, "resizeSideNavigation": { "message": "Resize side navigation" diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index bca4ad20d52..5bb22dc6292 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 2997ed6c128..2f5b1ec4932 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Arkistoi kohde" diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index 11da450cc0f..abb06f0f19f 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index face33e0087..596315c4d3f 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Les éléments archivés apparaîtront ici et seront exclus des résultats de recherche généraux et des suggestions de remplissage automatique." }, - "itemWasSentToArchive": { - "message": "L'élément a été envoyé à l'archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "L'élément a été désarchivé" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archiver l'élément" diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index 69ef54f78eb..e710a489f9a 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index 22939259639..a76cbb711a9 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "פריטים בארכיון יופיעו כאן ויוחרגו מתוצאות חיפוש כללי והצעות למילוי אוטומטי." }, - "itemWasSentToArchive": { - "message": "הפריט נשלח לארכיון" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "הפריט שוחזר מהארכיב" - }, - "itemUnarchived": { - "message": "הפריט הוסר מהארכיון" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "העבר פריט לארכיון" diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 298f0312be7..d30cbd2cc6e 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index d7814a22da0..98fdee3b657 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Arhivirane stavke biti će prikazane ovdje i biti će izuzete iz rezultata općih pretraga i preporuka auto-ispune." }, - "itemWasSentToArchive": { - "message": "Stavka poslana u arhivu" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Stavka vraćena iz arhive" - }, - "itemUnarchived": { - "message": "Stavka vraćena iz arhive" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Arhiviraj stavku" diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index ec4b2d405bf..e6765219f15 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -573,15 +573,12 @@ "noItemsInArchiveDesc": { "message": "Az archivált elemek itt jelennek meg és kizárásra kerülnek az általános keresési eredményekből és az automatikus kitöltési javaslatokból." }, - "itemWasSentToArchive": { - "message": "Az elem az archivumba került." + "itemArchiveToast": { + "message": "Az elem archiválásra került." }, - "itemWasUnarchived": { + "itemUnarchivedToast": { "message": "Az elem visszavételre került az archivumból." }, - "itemUnarchived": { - "message": "Az elemek visszavéelre kerültek az archivumból." - }, "archiveItem": { "message": "Elem archiválása" }, diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index f364b2f7540..ccf35569f36 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Butir yang diarsipkan akan muncul di sini dan akan dikecualikan dari hasil pencarian umum dan saran isi otomatis." }, - "itemWasSentToArchive": { - "message": "Butir dikirim ke arsip" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Arsipkan butir" diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 9c4ce6a0369..42efa025207 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Gli elementi archiviati compariranno qui e saranno esclusi dai risultati di ricerca e suggerimenti di autoriempimento." }, - "itemWasSentToArchive": { - "message": "Elemento archiviato" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Elemento rimosso dall'archivio" - }, - "itemUnarchived": { - "message": "Elemento rimosso dall'archivio" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archivia elemento" diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index c8a963fc744..915308cec13 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "アーカイブされたアイテムはここに表示され、通常の検索結果および自動入力の候補から除外されます。" }, - "itemWasSentToArchive": { - "message": "アイテムはアーカイブに送信されました" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "アイテムはアーカイブから解除されました" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "アイテムをアーカイブ" diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index cb6129ed2bb..791664e6eec 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index 336e8783b75..c28007c3838 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index e97ce2a95a4..faef7703a66 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index 9f570d62abb..b4a04e75e43 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "보관된 항목은 여기에 표시되며 일반 검색 결과 및 자동 완성 제안에서 제외됩니다." }, - "itemWasSentToArchive": { - "message": "항목이 보관함으로 이동되었습니다" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "항목이 보관 해제되었습니다" - }, - "itemUnarchived": { - "message": "항목 보관 해제됨" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "항목 보관" diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index 6e105f044f3..68eb11aa234 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index 8c86d7040fe..6eaf545e390 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Šeit parādīsies arhivētie vienumi, un tie netiks iekļauti vispārējās meklēšanas iznākumos un automātiskās aizpildes ieteikumos." }, - "itemWasSentToArchive": { - "message": "Vienums tika ievietots arhīvā" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Vienums tika izņemts no arhīva" - }, - "itemUnarchived": { - "message": "Vienums tika izņemts no arhīva" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Arhivēt vienumu" diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index 61f69ffe22b..db48220ffbb 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index 5cc614c5df7..abf2f7db968 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index 336e8783b75..c28007c3838 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index ce6c8d5a7d4..4689cb23b7a 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index 336e8783b75..c28007c3838 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index 44522727429..044b3cfaa64 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Gearchiveerde items verschijnen hier en worden uitgesloten van algemene zoekresultaten en automatisch invulsuggesties." }, - "itemWasSentToArchive": { - "message": "Item naar archief verzonden" + "itemArchiveToast": { + "message": "Item gearchiveerd" }, - "itemWasUnarchived": { - "message": "Item uit het archief gehaald" - }, - "itemUnarchived": { - "message": "Item uit het archief gehaald" + "itemUnarchivedToast": { + "message": "Item gedearchiveerd" }, "archiveItem": { "message": "Item archiveren" diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index 336e8783b75..c28007c3838 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index 336e8783b75..c28007c3838 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index 44c7d9e6d47..44c7b5fb6dd 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -440,7 +440,7 @@ "message": "Synchronizacja" }, "syncNow": { - "message": "Sync now" + "message": "Synchronizuj teraz" }, "lastSync": { "message": "Ostatnia synchronizacja:" @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Zarchiwizowane elementy pojawią się tutaj i zostaną wykluczone z wyników wyszukiwania i sugestii autouzupełniania." }, - "itemWasSentToArchive": { - "message": "Element został przeniesiony do archiwum" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Element został usunięty z archiwum" - }, - "itemUnarchived": { - "message": "Element został usunięty z archiwum" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archiwizuj element" @@ -6128,7 +6125,7 @@ "message": "user@bitwarden.com , user@acme.com" }, "downloadBitwardenApps": { - "message": "Download Bitwarden apps" + "message": "Pobierz aplikacje Bitwarden" }, "emailProtected": { "message": "Email protected" diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 5ad95b480db..679173205b1 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Os itens arquivados aparecerão aqui e serão excluídos dos resultados gerais de busca e das sugestões de preenchimento automático." }, - "itemWasSentToArchive": { - "message": "O item foi enviado para o arquivo" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "O item foi desarquivado" - }, - "itemUnarchived": { - "message": "O item foi desarquivado" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Arquivar item" diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index 604bf054707..9094e04094d 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Os itens arquivados aparecerão aqui e serão excluídos dos resultados gerais da pesquisa e das sugestões de preenchimento automático." }, - "itemWasSentToArchive": { - "message": "O item foi movido para o arquivo" + "itemArchiveToast": { + "message": "Item arquivado" }, - "itemWasUnarchived": { - "message": "O item foi desarquivado" - }, - "itemUnarchived": { - "message": "O item foi desarquivado" + "itemUnarchivedToast": { + "message": "Item desarquivado" }, "archiveItem": { "message": "Arquivar item" diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 12706943e83..47f7ae9cae3 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index dab9a22f03a..d1fb3de89a6 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Архивированные элементы появятся здесь и будут исключены из общих результатов поиска и предложений автозаполнения." }, - "itemWasSentToArchive": { - "message": "Элемент был отправлен в архив" + "itemArchiveToast": { + "message": "Элемент архивирован" }, - "itemWasUnarchived": { - "message": "Элемент был разархивирован" - }, - "itemUnarchived": { - "message": "Элемент был разархивирован" + "itemUnarchivedToast": { + "message": "Элемент разархивирован" }, "archiveItem": { "message": "Архивировать элемент" diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index d228cdb512a..e70c620eaf8 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index db7efcd8b9f..e1886098a31 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -573,13 +573,10 @@ "noItemsInArchiveDesc": { "message": "Tu sa zobrazia archivované položky, ktoré budú vylúčené zo všeobecného vyhľadávania a z návrhov automatického vypĺňania." }, - "itemWasSentToArchive": { + "itemArchiveToast": { "message": "Položka bola archivovaná" }, - "itemWasUnarchived": { - "message": "Položka bola odobraná z archívu" - }, - "itemUnarchived": { + "itemUnarchivedToast": { "message": "Položka bola odobraná z archívu" }, "archiveItem": { diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index 07ee84ab810..100a04a3012 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index 0ad71788514..e91e003c8e0 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Архивиране ставке ће се овде појавити и бити искључени из општих резултата претраге и сугестија о ауто-пуњењу." }, - "itemWasSentToArchive": { - "message": "Ставка је послата у архиву" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Ставка враћена из архиве" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Архивирај ставку" diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 08cec673d27..484817b0210 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Arkiverade objekt kommer att visas här och kommer att uteslutas från allmänna sökresultat och förslag för autofyll." }, - "itemWasSentToArchive": { - "message": "Objektet skickades till arkivet" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Objektet har avarkiverats" - }, - "itemUnarchived": { - "message": "Objektet har avarkiverats" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Arkivera objekt" diff --git a/apps/browser/src/_locales/ta/messages.json b/apps/browser/src/_locales/ta/messages.json index 374c0968d2c..3e76c0ab0d1 100644 --- a/apps/browser/src/_locales/ta/messages.json +++ b/apps/browser/src/_locales/ta/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "காப்பகப்படுத்தப்பட்ட உருப்படிகள் இங்கே தோன்றும், மேலும் அவை பொதுவான தேடல் முடிவுகள் மற்றும் தானியங்குநிரப்பு பரிந்துரைகளிலிருந்து விலக்கப்படும்." }, - "itemWasSentToArchive": { - "message": "ஆவணம் காப்பகத்திற்கு அனுப்பப்பட்டது" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "காப்பகம் மீட்டெடுக்கப்பட்டது" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "உருப்படியைக் காப்பகப்படுத்து" diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index 336e8783b75..c28007c3838 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index 5af1c742f45..5ec728189a8 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "รายการที่จัดเก็บถาวรจะปรากฏที่นี่ และจะไม่ถูกรวมในผลการค้นหาทั่วไปหรือคำแนะนำการป้อนอัตโนมัติ" }, - "itemWasSentToArchive": { - "message": "ย้ายรายการไปที่จัดเก็บถาวรแล้ว" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "เลิกจัดเก็บถาวรรายการแล้ว" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "จัดเก็บรายการถาวร" diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 33f600fb7a7..7d5b31a9aba 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -573,13 +573,10 @@ "noItemsInArchiveDesc": { "message": "Arşivlenmiş kayıtlar burada görünecek ve genel arama sonuçlarından ile otomatik doldurma önerilerinden hariç tutulacaktır." }, - "itemWasSentToArchive": { - "message": "Kayıt arşive gönderildi" + "itemArchiveToast": { + "message": "Kayıt arşivlendi" }, - "itemWasUnarchived": { - "message": "Kayıt arşivden çıkarıldı" - }, - "itemUnarchived": { + "itemUnarchivedToast": { "message": "Kayıt arşivden çıkarıldı" }, "archiveItem": { diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index b703cfeefce..49a0c9de25b 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -573,13 +573,10 @@ "noItemsInArchiveDesc": { "message": "Архівовані записи з'являтимуться тут і будуть виключені з результатів звичайного пошуку та пропозицій автозаповнення." }, - "itemWasSentToArchive": { + "itemArchiveToast": { "message": "Запис архівовано" }, - "itemWasUnarchived": { - "message": "Запис розархівовано" - }, - "itemUnarchived": { + "itemUnarchivedToast": { "message": "Запис розархівовано" }, "archiveItem": { @@ -5964,7 +5961,7 @@ "message": "Номер картки" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "Помилка: неможливо розшифрувати" }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Ваша організація більше не використовує головні паролі для входу в Bitwarden. Щоб продовжити, підтвердіть організацію та домен." @@ -6128,7 +6125,7 @@ "message": "user@bitwarden.com , user@acme.com" }, "downloadBitwardenApps": { - "message": "Download Bitwarden apps" + "message": "Завантажити програми Bitwarden" }, "emailProtected": { "message": "Е-пошту захищено" diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index 0082ee1ece7..4f1165835cc 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Các mục đã lưu trữ sẽ hiển thị ở đây và sẽ bị loại khỏi kết quả tìm kiếm và gợi ý tự động điền." }, - "itemWasSentToArchive": { - "message": "Mục đã được chuyển vào kho lưu trữ" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Mục đã được bỏ lưu trữ" - }, - "itemUnarchived": { - "message": "Mục đã được bỏ lưu trữ" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Lưu trữ mục" diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index 860a8c09f27..c9dd30ab08e 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "已归档的项目将显示在此处,并将被排除在一般搜索结果和自动填充建议之外。" }, - "itemWasSentToArchive": { - "message": "项目已发送到归档" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "项目已取消归档" - }, - "itemUnarchived": { - "message": "项目已取消归档" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "归档项目" diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index 3f387d935d4..8da1b2ad08f 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -228,7 +228,7 @@ "message": "複製自訂欄位名稱" }, "noMatchingLogins": { - "message": "沒有符合的登入資料" + "message": "沒有相符的登入項目" }, "noCards": { "message": "沒有付款卡" @@ -252,7 +252,7 @@ "message": "登入您的密碼庫" }, "autoFillInfo": { - "message": "沒有可以在目前瀏覽器分頁自動填入的登入資訊。" + "message": "目前瀏覽器分頁沒有可自動填入的登入項目。" }, "addLogin": { "message": "新增登入資料" @@ -453,10 +453,10 @@ "description": "Short for 'credential generator'." }, "passGenInfo": { - "message": "為您的登入資料自動產生高強度且唯一的密碼。" + "message": "為您的登入項目自動產生高強度且唯一的密碼。" }, "bitWebVaultApp": { - "message": "Bitwarden 網頁應用程式" + "message": "Bitwarden Web 應用程式" }, "select": { "message": "選擇" @@ -468,16 +468,16 @@ "message": "產生密碼短語" }, "passwordGenerated": { - "message": "已產生密碼" + "message": "密碼已產生" }, "passphraseGenerated": { - "message": "已產生密碼" + "message": "密碼短語已產生" }, "usernameGenerated": { - "message": "已產生使用者名稱" + "message": "使用者名稱已產生" }, "emailGenerated": { - "message": "已產生電子郵件" + "message": "電子郵件已產生" }, "regeneratePassword": { "message": "重新產生密碼" @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "封存的項目會顯示在此處,且不會出現在一般搜尋結果或自動填入建議中。" }, - "itemWasSentToArchive": { - "message": "項目已移至封存" + "itemArchiveToast": { + "message": "項目已封存" }, - "itemWasUnarchived": { - "message": "已取消封存項目" - }, - "itemUnarchived": { - "message": "項目取消封存" + "itemUnarchivedToast": { + "message": "項目已取消封存" }, "archiveItem": { "message": "封存項目" @@ -598,7 +595,7 @@ "message": "需要進階版會員才能使用封存功能。" }, "itemRestored": { - "message": "已還原項目" + "message": "項目已還原" }, "edit": { "message": "編輯" @@ -1215,7 +1212,7 @@ "description": "Button text for saving login details as a new entry." }, "updateLoginAction": { - "message": "更新登入資料", + "message": "更新登入項目", "description": "Button text for updating an existing login entry." }, "unlockToSave": { @@ -1223,7 +1220,7 @@ "description": "User prompt to take action in order to save the login they just entered." }, "saveLogin": { - "message": "儲存登入資料", + "message": "儲存登入項目", "description": "Prompt asking the user if they want to save their login details." }, "updateLogin": { @@ -1231,7 +1228,7 @@ "description": "Prompt asking the user if they want to update an existing login entry." }, "loginSaveSuccess": { - "message": "登入資訊已儲存", + "message": "登入項目已儲存", "description": "Message displayed when login details are successfully saved." }, "loginUpdateSuccess": { @@ -1305,7 +1302,7 @@ "message": "解鎖" }, "additionalOptions": { - "message": "額外選項" + "message": "其他選項" }, "enableContextMenuItem": { "message": "顯示內容選單選項" @@ -1567,7 +1564,7 @@ "message": "提供密碼健全性、帳戶健康狀態及資料外洩報告,確保您的密碼庫安全。" }, "ppremiumSignUpTotp": { - "message": "為密碼庫中的登入資料產生 TOTP 驗證碼(2FA)。" + "message": "為密碼庫中的登入項目產生 TOTP 驗證碼(2FA)。" }, "ppremiumSignUpSupport": { "message": "優先客戶支援。" @@ -1801,7 +1798,7 @@ "message": "Bitwarden 如何保護您的資料免於網路釣魚攻擊?" }, "currentWebsite": { - "message": "目網站" + "message": "目前網站" }, "autofillAndAddWebsite": { "message": "自動填充並新增此網站" @@ -2091,7 +2088,7 @@ "message": "登入資料" }, "typeLogins": { - "message": "登入資料" + "message": "登入項目" }, "typeSecureNote": { "message": "安全筆記" @@ -2227,7 +2224,7 @@ "message": "身分" }, "logins": { - "message": "登入資料" + "message": "登入項目" }, "secureNotes": { "message": "安全筆記" @@ -2513,7 +2510,7 @@ "message": "項目已自動填入並且已儲存統一資源標識符(URI)" }, "autoFillSuccess": { - "message": "項目已自動填入 " + "message": "項目已自動填入" }, "insecurePageWarning": { "message": "警告:此為不安全的 HTTP 頁面,您送出的任何資訊都可能被他人查看並修改。此登入資料原本儲存在安全的(HTTPS)頁面上。" @@ -2773,10 +2770,10 @@ } }, "atRiskPassword": { - "message": "具有風險的密碼" + "message": "有風險的密碼" }, "atRiskPasswords": { - "message": "具有風險的密碼" + "message": "有風險的密碼" }, "atRiskPasswordDescSingleOrg": { "message": "$ORGANIZATION$ 要求你變更一組有風險的密碼。", @@ -2848,7 +2845,7 @@ "message": "更新你的設定,以便能快速自動填入密碼並產生新密碼" }, "reviewAtRiskLogins": { - "message": "檢視有風險的登入資訊" + "message": "檢視有風險的登入項目" }, "reviewAtRiskPasswords": { "message": "檢視有風險的密碼" @@ -5749,7 +5746,7 @@ "message": "設定生物辨識解鎖及自動填入,不需要輸入任何字元就可以登入。" }, "secureUser": { - "message": "升級您的登入體驗" + "message": "讓您的登入項目更升級" }, "secureUserBody": { "message": "使用密碼產生器來建立及儲存高強度、唯一的密碼,來保護您所有的帳號。" @@ -5964,7 +5961,7 @@ "message": "付款卡號碼" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "錯誤:無法解密" }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "您的組織已不再使用主密碼登入 Bitwarden。若要繼續,請驗證組織與網域。" From eb4b5721a6c64256342685f791c72c4ac89cde28 Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 10:36:54 +0000 Subject: [PATCH 045/134] Autosync the updated translations (#19007) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/af/messages.json | 8 ++-- apps/desktop/src/locales/ar/messages.json | 8 ++-- apps/desktop/src/locales/az/messages.json | 8 ++-- apps/desktop/src/locales/be/messages.json | 8 ++-- apps/desktop/src/locales/bg/messages.json | 8 ++-- apps/desktop/src/locales/bn/messages.json | 8 ++-- apps/desktop/src/locales/bs/messages.json | 8 ++-- apps/desktop/src/locales/ca/messages.json | 8 ++-- apps/desktop/src/locales/cs/messages.json | 6 +-- apps/desktop/src/locales/cy/messages.json | 8 ++-- apps/desktop/src/locales/da/messages.json | 8 ++-- apps/desktop/src/locales/de/messages.json | 10 ++--- apps/desktop/src/locales/el/messages.json | 8 ++-- apps/desktop/src/locales/en_GB/messages.json | 8 ++-- apps/desktop/src/locales/en_IN/messages.json | 8 ++-- apps/desktop/src/locales/eo/messages.json | 8 ++-- apps/desktop/src/locales/es/messages.json | 46 ++++++++++---------- apps/desktop/src/locales/et/messages.json | 8 ++-- apps/desktop/src/locales/eu/messages.json | 8 ++-- apps/desktop/src/locales/fa/messages.json | 8 ++-- apps/desktop/src/locales/fi/messages.json | 8 ++-- apps/desktop/src/locales/fil/messages.json | 8 ++-- apps/desktop/src/locales/fr/messages.json | 8 ++-- apps/desktop/src/locales/gl/messages.json | 8 ++-- apps/desktop/src/locales/he/messages.json | 8 ++-- apps/desktop/src/locales/hi/messages.json | 8 ++-- apps/desktop/src/locales/hr/messages.json | 8 ++-- apps/desktop/src/locales/hu/messages.json | 8 ++-- apps/desktop/src/locales/id/messages.json | 8 ++-- apps/desktop/src/locales/it/messages.json | 8 ++-- apps/desktop/src/locales/ja/messages.json | 8 ++-- apps/desktop/src/locales/ka/messages.json | 8 ++-- apps/desktop/src/locales/km/messages.json | 8 ++-- apps/desktop/src/locales/kn/messages.json | 8 ++-- apps/desktop/src/locales/ko/messages.json | 8 ++-- apps/desktop/src/locales/lt/messages.json | 8 ++-- apps/desktop/src/locales/lv/messages.json | 8 ++-- apps/desktop/src/locales/me/messages.json | 8 ++-- apps/desktop/src/locales/ml/messages.json | 8 ++-- apps/desktop/src/locales/mr/messages.json | 8 ++-- apps/desktop/src/locales/my/messages.json | 8 ++-- apps/desktop/src/locales/nb/messages.json | 8 ++-- apps/desktop/src/locales/ne/messages.json | 8 ++-- apps/desktop/src/locales/nl/messages.json | 8 ++-- apps/desktop/src/locales/nn/messages.json | 8 ++-- apps/desktop/src/locales/or/messages.json | 8 ++-- apps/desktop/src/locales/pl/messages.json | 10 ++--- apps/desktop/src/locales/pt_BR/messages.json | 8 ++-- apps/desktop/src/locales/pt_PT/messages.json | 8 ++-- apps/desktop/src/locales/ro/messages.json | 8 ++-- apps/desktop/src/locales/ru/messages.json | 8 ++-- apps/desktop/src/locales/si/messages.json | 8 ++-- apps/desktop/src/locales/sk/messages.json | 4 +- apps/desktop/src/locales/sl/messages.json | 8 ++-- apps/desktop/src/locales/sr/messages.json | 8 ++-- apps/desktop/src/locales/sv/messages.json | 8 ++-- apps/desktop/src/locales/ta/messages.json | 8 ++-- apps/desktop/src/locales/te/messages.json | 8 ++-- apps/desktop/src/locales/th/messages.json | 8 ++-- apps/desktop/src/locales/tr/messages.json | 6 +-- apps/desktop/src/locales/uk/messages.json | 8 ++-- apps/desktop/src/locales/vi/messages.json | 8 ++-- apps/desktop/src/locales/zh_CN/messages.json | 8 ++-- apps/desktop/src/locales/zh_TW/messages.json | 8 ++-- 64 files changed, 273 insertions(+), 273 deletions(-) diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json index ef221f96878..c0824c61d03 100644 --- a/apps/desktop/src/locales/af/messages.json +++ b/apps/desktop/src/locales/af/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json index bcac0529a8c..3e668c327b0 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index f94ff2417cf..4e5d414eb1c 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -4387,10 +4387,10 @@ "noItemsInArchiveDesc": { "message": "Arxivlənmiş elementlər burada görünəcək, ümumi axtarış nəticələrindən və avto-doldurma təkliflərindən xaric ediləcək." }, - "itemWasSentToArchive": { - "message": "Element arxivə göndərildi" + "itemArchiveToast": { + "message": "Element arxivləndi" }, - "itemWasUnarchived": { + "itemUnarchivedToast": { "message": "Element arxivdən çıxarıldı" }, "archiveItem": { @@ -4487,7 +4487,7 @@ "message": "Vaxt bitmə əməliyyatı" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "Xəta: Şifrəsi açıla bilmir" }, "sessionTimeoutHeader": { "message": "Sessiya vaxt bitməsi" diff --git a/apps/desktop/src/locales/be/messages.json b/apps/desktop/src/locales/be/messages.json index 3467fe20ae8..f2f9d0a736d 100644 --- a/apps/desktop/src/locales/be/messages.json +++ b/apps/desktop/src/locales/be/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index cab21191e37..ea0355ad7f6 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Архивираните елементи ще се показват тук и ще бъдат изключени от общите резултати при търсене и от предложенията за автоматично попълване." }, - "itemWasSentToArchive": { - "message": "Елементът беше преместен в архива" + "itemArchiveToast": { + "message": "Елементът е преместен в архива" }, - "itemWasUnarchived": { - "message": "Елементът беше изваден от архива" + "itemUnarchivedToast": { + "message": "Елементът е изваден от архива" }, "archiveItem": { "message": "Архивиране на елемента" diff --git a/apps/desktop/src/locales/bn/messages.json b/apps/desktop/src/locales/bn/messages.json index 544d88a72a6..6a211c93052 100644 --- a/apps/desktop/src/locales/bn/messages.json +++ b/apps/desktop/src/locales/bn/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/bs/messages.json b/apps/desktop/src/locales/bs/messages.json index 289554a237f..4ca3aa8ffc2 100644 --- a/apps/desktop/src/locales/bs/messages.json +++ b/apps/desktop/src/locales/bs/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index 7b8d32a798c..3b8562814fd 100644 --- a/apps/desktop/src/locales/ca/messages.json +++ b/apps/desktop/src/locales/ca/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index 478343b7e7d..75136c41831 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -4387,10 +4387,10 @@ "noItemsInArchiveDesc": { "message": "Zde se zobrazí archivované položky a budou vyloučeny z obecných výsledků vyhledávání a návrhů automatického vyplňování." }, - "itemWasSentToArchive": { - "message": "Položka byla přesunuta do archivu" + "itemArchiveToast": { + "message": "Položka archivována" }, - "itemWasUnarchived": { + "itemUnarchivedToast": { "message": "Položka byla odebrána z archivu" }, "archiveItem": { diff --git a/apps/desktop/src/locales/cy/messages.json b/apps/desktop/src/locales/cy/messages.json index 4feb0181431..46df0aca8c5 100644 --- a/apps/desktop/src/locales/cy/messages.json +++ b/apps/desktop/src/locales/cy/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index bd6a6f4379a..f6abcd51740 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index 205c8e95435..a2c346896ac 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archivierte Einträge werden hier angezeigt und von allgemeinen Suchergebnissen sowie Auto-Ausfüllen-Vorschlägen ausgeschlossen." }, - "itemWasSentToArchive": { - "message": "Eintrag wurde archiviert" + "itemArchiveToast": { + "message": "Eintrag archiviert" }, - "itemWasUnarchived": { - "message": "Eintrag wird nicht mehr archiviert" + "itemUnarchivedToast": { + "message": "Eintrag nicht mehr archiviert" }, "archiveItem": { "message": "Eintrag archivieren" @@ -4487,7 +4487,7 @@ "message": "Timeout-Aktion" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "Fehler: Entschlüsselung nicht möglich" }, "sessionTimeoutHeader": { "message": "Sitzungs-Timeout" diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index 97371668dca..624560f5888 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index 22b482ed04d..aaf1e12955c 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json index 42a63ff0db1..1dca7070bfc 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/eo/messages.json b/apps/desktop/src/locales/eo/messages.json index ae37e15d84c..cefd462e99f 100644 --- a/apps/desktop/src/locales/eo/messages.json +++ b/apps/desktop/src/locales/eo/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index d6210f940b5..d9fb17907aa 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -773,7 +773,7 @@ "message": "Añadir adjunto" }, "itemsTransferred": { - "message": "Items transferred" + "message": "Elementos transferidos" }, "fixEncryption": { "message": "Corregir cifrado" @@ -2089,7 +2089,7 @@ "message": "Elemento eliminado de forma permanente" }, "archivedItemRestored": { - "message": "Archived item restored" + "message": "Elemento archivado restaurado" }, "restoredItem": { "message": "Elemento restaurado" @@ -4009,7 +4009,7 @@ "message": "No, no lo tengo" }, "newDeviceVerificationNoticePageOneEmailAccessYes": { - "message": "Yes, I can reliably access my email" + "message": "Sí, puedo acceder a mi correo electrónico de forma fiable" }, "turnOnTwoStepLogin": { "message": "Turn on two-step login" @@ -4075,10 +4075,10 @@ "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, "vulnerablePassword": { - "message": "Vulnerable password." + "message": "Contraseña vulnerable." }, "changeNow": { - "message": "Change now" + "message": "Cambiar ahora" }, "missingWebsite": { "message": "Missing website" @@ -4109,7 +4109,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendsTitleNoSearchResults": { - "message": "No search results returned" + "message": "Ningún resultado de búsqueda devuelto" }, "sendsBodyNoItems": { "message": "Comparte archivos y datos de forma segura con cualquiera, en cualquier plataforma. Tu información permanecerá encriptada de extremo a extremo, limitando su exposición.", @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Los elementos archivados aparecerán aquí y se excluirán de los resultados de búsqueda generales y de sugerencias de autocompletado." }, - "itemWasSentToArchive": { - "message": "El elemento fue archivado" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "El elemento fue desarchivado" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archivar elemento" @@ -4403,7 +4403,7 @@ "message": "Desarchivar y guardar" }, "restartPremium": { - "message": "Restart Premium" + "message": "Reiniciar Premium" }, "premiumSubscriptionEnded": { "message": "Tu suscripción Premium ha terminado" @@ -4537,10 +4537,10 @@ "message": "Set an unlock method to change your timeout action" }, "upgrade": { - "message": "Upgrade" + "message": "Actualizar" }, "leaveConfirmationDialogTitle": { - "message": "Are you sure you want to leave?" + "message": "¿Estás seguro de que quieres salir?" }, "leaveConfirmationDialogContentOne": { "message": "By declining, your personal items will stay in your account, but you'll lose access to shared items and organization features." @@ -4558,10 +4558,10 @@ } }, "howToManageMyVault": { - "message": "How do I manage my vault?" + "message": "¿Cómo gestiono mi caja fuerte?" }, "transferItemsToOrganizationTitle": { - "message": "Transfer items to $ORGANIZATION$", + "message": "Transferir elementos a $ORGANIZATION$", "placeholders": { "organization": { "content": "$1", @@ -4579,13 +4579,13 @@ } }, "acceptTransfer": { - "message": "Accept transfer" + "message": "Aceptar transferencia" }, "declineAndLeave": { - "message": "Decline and leave" + "message": "Rechazar y salir" }, "whyAmISeeingThis": { - "message": "Why am I seeing this?" + "message": "¿Por qué estoy viendo esto?" }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", @@ -4595,25 +4595,25 @@ "message": "Email protected" }, "emails": { - "message": "Emails" + "message": "Correos electrónicos" }, "noAuth": { - "message": "Anyone with the link" + "message": "Cualquiera con el enlace" }, "anyOneWithPassword": { "message": "Anyone with a password set by you" }, "whoCanView": { - "message": "Who can view" + "message": "Quién puede ver" }, "specificPeople": { - "message": "Specific people" + "message": "Personas específicas" }, "emailVerificationDesc": { "message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send." }, "enterMultipleEmailsSeparatedByComma": { - "message": "Enter multiple emails by separating with a comma." + "message": "Introduce varios correos electrónicos separándolos con una coma." }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" diff --git a/apps/desktop/src/locales/et/messages.json b/apps/desktop/src/locales/et/messages.json index 8c9476cc69e..ba930db8961 100644 --- a/apps/desktop/src/locales/et/messages.json +++ b/apps/desktop/src/locales/et/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json index 75c33286fb7..e03da9ef685 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/fa/messages.json b/apps/desktop/src/locales/fa/messages.json index 4136edbde06..a443cc8c2e7 100644 --- a/apps/desktop/src/locales/fa/messages.json +++ b/apps/desktop/src/locales/fa/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "آیتم‌های بایگانی‌شده در اینجا نمایش داده می‌شوند و از نتایج جستجوی عمومی و پیشنهاد ها پر کردن خودکار حذف خواهند شد." }, - "itemWasSentToArchive": { - "message": "آیتم به بایگانی فرستاده شد" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "آیتم از بایگانی خارج شد" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "بایگانی آیتم" diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index b04b741bd3b..c7b51def9b2 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/fil/messages.json b/apps/desktop/src/locales/fil/messages.json index c503efc39f9..5835821f526 100644 --- a/apps/desktop/src/locales/fil/messages.json +++ b/apps/desktop/src/locales/fil/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index f04807aaeb9..e8d07e28d2d 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Les éléments archivés apparaîtront ici et seront exclus des résultats de recherche généraux et des suggestions de remplissage automatique." }, - "itemWasSentToArchive": { - "message": "L'élément a été envoyé à l'archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "L'élément a été désarchivé" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archiver l'élément" diff --git a/apps/desktop/src/locales/gl/messages.json b/apps/desktop/src/locales/gl/messages.json index 76904276732..6d0922bd680 100644 --- a/apps/desktop/src/locales/gl/messages.json +++ b/apps/desktop/src/locales/gl/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json index dbb2533e03e..763401ac6fe 100644 --- a/apps/desktop/src/locales/he/messages.json +++ b/apps/desktop/src/locales/he/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "פריטים בארכיון יופיעו כאן ויוחרגו מתוצאות חיפוש כללי והצעות למילוי אוטומטי." }, - "itemWasSentToArchive": { - "message": "הפריט נשלח לארכיון" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "הפריט הוסר מהארכיון" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "העבר פריט לארכיון" diff --git a/apps/desktop/src/locales/hi/messages.json b/apps/desktop/src/locales/hi/messages.json index 1bfc0674ffe..33b69ac1519 100644 --- a/apps/desktop/src/locales/hi/messages.json +++ b/apps/desktop/src/locales/hi/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json index 5ef663ab52b..2dc081fa3c7 100644 --- a/apps/desktop/src/locales/hr/messages.json +++ b/apps/desktop/src/locales/hr/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Arhivirane stavke biti će prikazane ovdje i biti će izuzete iz rezultata općih pretraga i preporuka auto-ispune." }, - "itemWasSentToArchive": { - "message": "Stavka poslana u arhivu" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Stavka vraćena iz arhive" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Arhiviraj stavku" diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index 5440b53e93d..3ec097c2a7a 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Az archivált elemek itt jelennek meg és kizárásra kerülnek az általános keresési eredményekből és az automatikus kitöltési javaslatokból." }, - "itemWasSentToArchive": { - "message": "Az elem az archivumba került." + "itemArchiveToast": { + "message": "Az elem archiválásra került." }, - "itemWasUnarchived": { - "message": "Az elem visszavéelre került az archivumból." + "itemUnarchivedToast": { + "message": "Az elem visszavételre került az archivumból." }, "archiveItem": { "message": "Elem archiválása" diff --git a/apps/desktop/src/locales/id/messages.json b/apps/desktop/src/locales/id/messages.json index f4de0deff33..7648f4bb99b 100644 --- a/apps/desktop/src/locales/id/messages.json +++ b/apps/desktop/src/locales/id/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index 9cd4783407d..eb2ade245a0 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Gli elementi archiviati appariranno qui e saranno esclusi dai risultati di ricerca e dal riempimento automatico." }, - "itemWasSentToArchive": { - "message": "Elemento archiviato" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Elemento estratto dall'archivio" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archivia elemento" diff --git a/apps/desktop/src/locales/ja/messages.json b/apps/desktop/src/locales/ja/messages.json index 1d46532a980..a9b05f728d8 100644 --- a/apps/desktop/src/locales/ja/messages.json +++ b/apps/desktop/src/locales/ja/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/ka/messages.json b/apps/desktop/src/locales/ka/messages.json index 4618fd024a9..68bba7fcb27 100644 --- a/apps/desktop/src/locales/ka/messages.json +++ b/apps/desktop/src/locales/ka/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/km/messages.json b/apps/desktop/src/locales/km/messages.json index 76904276732..6d0922bd680 100644 --- a/apps/desktop/src/locales/km/messages.json +++ b/apps/desktop/src/locales/km/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/kn/messages.json b/apps/desktop/src/locales/kn/messages.json index 3bb7f513701..3c6aced3a73 100644 --- a/apps/desktop/src/locales/kn/messages.json +++ b/apps/desktop/src/locales/kn/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json index 70ffd234941..8932f0efb48 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json index f703b1f5d53..a19856a776e 100644 --- a/apps/desktop/src/locales/lt/messages.json +++ b/apps/desktop/src/locales/lt/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index e82a4b91487..8a863256ed1 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Šeit parādīsies arhivētie vienumi, un tie netiks iekļauti vispārējās meklēšanas iznākumos un automātiskās aizpildes ieteikumos." }, - "itemWasSentToArchive": { - "message": "Vienums tika ievietots arhīvā" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Vienums tika izņemts no arhīva" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Arhivēt vienumu" diff --git a/apps/desktop/src/locales/me/messages.json b/apps/desktop/src/locales/me/messages.json index 25bb0cbc816..773a596a10d 100644 --- a/apps/desktop/src/locales/me/messages.json +++ b/apps/desktop/src/locales/me/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/ml/messages.json b/apps/desktop/src/locales/ml/messages.json index 43e0dc85fb0..3bddc3baa5b 100644 --- a/apps/desktop/src/locales/ml/messages.json +++ b/apps/desktop/src/locales/ml/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/mr/messages.json b/apps/desktop/src/locales/mr/messages.json index 76904276732..6d0922bd680 100644 --- a/apps/desktop/src/locales/mr/messages.json +++ b/apps/desktop/src/locales/mr/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/my/messages.json b/apps/desktop/src/locales/my/messages.json index ddc8bef0241..22f4a30329a 100644 --- a/apps/desktop/src/locales/my/messages.json +++ b/apps/desktop/src/locales/my/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/nb/messages.json b/apps/desktop/src/locales/nb/messages.json index 792c95eb1ec..b2b1631fe04 100644 --- a/apps/desktop/src/locales/nb/messages.json +++ b/apps/desktop/src/locales/nb/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/ne/messages.json b/apps/desktop/src/locales/ne/messages.json index 89c3d3ba231..b9eba55d8bd 100644 --- a/apps/desktop/src/locales/ne/messages.json +++ b/apps/desktop/src/locales/ne/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index e4b1d6d8abc..306bf31efe2 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Gearchiveerde items verschijnen hier en worden uitgesloten van algemene zoekresultaten en automatisch invulsuggesties." }, - "itemWasSentToArchive": { - "message": "Item naar archief verzonden" + "itemArchiveToast": { + "message": "Item gearchiveerd" }, - "itemWasUnarchived": { - "message": "Item uit het archief gehaald" + "itemUnarchivedToast": { + "message": "Item gedearchiveerd" }, "archiveItem": { "message": "Item archiveren" diff --git a/apps/desktop/src/locales/nn/messages.json b/apps/desktop/src/locales/nn/messages.json index 850696f8fcf..dc62d73a236 100644 --- a/apps/desktop/src/locales/nn/messages.json +++ b/apps/desktop/src/locales/nn/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/or/messages.json b/apps/desktop/src/locales/or/messages.json index b63a970e9c2..e8ea506a873 100644 --- a/apps/desktop/src/locales/or/messages.json +++ b/apps/desktop/src/locales/or/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index e2d9fbce4a2..0cee8d6683d 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Zarchiwizowane elementy pojawią się tutaj i zostaną wykluczone z wyników wyszukiwania i sugestii autouzupełniania." }, - "itemWasSentToArchive": { - "message": "Element został przeniesiony do archiwum" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Element został usunięty z archiwum" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archiwizuj element" @@ -4487,7 +4487,7 @@ "message": "Timeout action" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "Błąd: Nie można odszyfrować" }, "sessionTimeoutHeader": { "message": "Session timeout" diff --git a/apps/desktop/src/locales/pt_BR/messages.json b/apps/desktop/src/locales/pt_BR/messages.json index 3a055bdb03d..817e9de0c50 100644 --- a/apps/desktop/src/locales/pt_BR/messages.json +++ b/apps/desktop/src/locales/pt_BR/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Os itens arquivados aparecerão aqui e serão excluídos dos resultados gerais de busca e das sugestões de preenchimento automático." }, - "itemWasSentToArchive": { - "message": "O item foi enviado para o arquivo" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "O item foi desarquivado" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Arquivar item" diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index bb99678bde6..3076291a4a3 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Os itens arquivados aparecerão aqui e serão excluídos dos resultados gerais da pesquisa e das sugestões de preenchimento automático." }, - "itemWasSentToArchive": { - "message": "O item foi movido para o arquivo" + "itemArchiveToast": { + "message": "Item arquivado" }, - "itemWasUnarchived": { - "message": "O item foi desarquivado" + "itemUnarchivedToast": { + "message": "Item desarquivado" }, "archiveItem": { "message": "Arquivar item" diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json index 6b51cb9fecd..b8fc25b4105 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index 389f4b37dfd..089e815fefe 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Архивированные элементы появятся здесь и будут исключены из общих результатов поиска и предложений автозаполнения." }, - "itemWasSentToArchive": { - "message": "Элемент был отправлен в архив" + "itemArchiveToast": { + "message": "Элемент архивирован" }, - "itemWasUnarchived": { - "message": "Элемент был разархивирован" + "itemUnarchivedToast": { + "message": "Элемент разархивирован" }, "archiveItem": { "message": "Архивировать элемент" diff --git a/apps/desktop/src/locales/si/messages.json b/apps/desktop/src/locales/si/messages.json index 9e8a727ad5d..a1a84b8ba15 100644 --- a/apps/desktop/src/locales/si/messages.json +++ b/apps/desktop/src/locales/si/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index 471984785d8..2876361bbf0 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -4387,10 +4387,10 @@ "noItemsInArchiveDesc": { "message": "Tu sa zobrazia archivované položky, ktoré budú vylúčené zo všeobecného vyhľadávania a z návrhov automatického vypĺňania." }, - "itemWasSentToArchive": { + "itemArchiveToast": { "message": "Položka bola archivovaná" }, - "itemWasUnarchived": { + "itemUnarchivedToast": { "message": "Položka bola odobraná z archívu" }, "archiveItem": { diff --git a/apps/desktop/src/locales/sl/messages.json b/apps/desktop/src/locales/sl/messages.json index 23d9f18fadb..82e0a20b29e 100644 --- a/apps/desktop/src/locales/sl/messages.json +++ b/apps/desktop/src/locales/sl/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index 1a68810bca3..633d3123242 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Архивиране ставке ће се овде појавити и бити искључени из општих резултата претраге и сугестија о ауто-пуњењу." }, - "itemWasSentToArchive": { - "message": "Ставка је послата у архиву" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Ставка враћена из архиве" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Архивирај ставку" diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index 1dd66c409c0..ed9e62e8319 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Arkiverade objekt kommer att visas här och kommer att uteslutas från allmänna sökresultat och förslag för autofyll." }, - "itemWasSentToArchive": { - "message": "Objektet skickades till arkivet" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Objektet har avarkiverats" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Arkivera objekt" diff --git a/apps/desktop/src/locales/ta/messages.json b/apps/desktop/src/locales/ta/messages.json index 3a7fc795668..53e155874f6 100644 --- a/apps/desktop/src/locales/ta/messages.json +++ b/apps/desktop/src/locales/ta/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json index 76904276732..6d0922bd680 100644 --- a/apps/desktop/src/locales/te/messages.json +++ b/apps/desktop/src/locales/te/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/th/messages.json b/apps/desktop/src/locales/th/messages.json index bcff8d0849e..b7117a7ccad 100644 --- a/apps/desktop/src/locales/th/messages.json +++ b/apps/desktop/src/locales/th/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index 936a68516bd..96bb11c7a35 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -4387,10 +4387,10 @@ "noItemsInArchiveDesc": { "message": "Arşivlenmiş kayıtlar burada görünecek ve genel arama sonuçları ile otomatik doldurma önerilerinden hariç tutulacaktır." }, - "itemWasSentToArchive": { - "message": "Kayıt arşive gönderildi" + "itemArchiveToast": { + "message": "Kayıt arşivlendi" }, - "itemWasUnarchived": { + "itemUnarchivedToast": { "message": "Kayıt arşivden çıkarıldı" }, "archiveItem": { diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index 2b4cfcb7c22..abcdbea0b1f 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Архівовані записи з'являтимуться тут і будуть виключені з результатів звичайного пошуку та пропозицій автозаповнення." }, - "itemWasSentToArchive": { - "message": "Запис архівовано" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Запис розархівовано" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Архівувати запис" diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json index 56c9d9a5c6e..f06556568a1 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "Các mục đã lưu trữ sẽ hiển thị ở đây và sẽ bị loại khỏi kết quả tìm kiếm và gợi ý tự động điền." }, - "itemWasSentToArchive": { - "message": "Mục đã được chuyển vào kho lưu trữ" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Mục đã được bỏ lưu trữ" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Lưu trữ mục" diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index cf42adba294..9941e296da3 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "已归档的项目将显示在此处,并将被排除在一般搜索结果和自动填充建议之外。" }, - "itemWasSentToArchive": { - "message": "项目已发送到归档" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "项目已取消归档" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "归档项目" diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index 3157e929e8f..461eb031068 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -4387,11 +4387,11 @@ "noItemsInArchiveDesc": { "message": "封存的項目會顯示在此處,且不會出現在一般搜尋結果或自動填入建議中。" }, - "itemWasSentToArchive": { - "message": "項目已移至封存" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "項目取消封存" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "封存項目" From 22a6fb1e6d468b04f86e7297066ac2b9106524b9 Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 11:42:43 +0100 Subject: [PATCH 046/134] Autosync the updated translations (#19009) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/web/src/locales/af/messages.json | 24 +++++-- apps/web/src/locales/ar/messages.json | 24 +++++-- apps/web/src/locales/az/messages.json | 84 +++++++++++++----------- apps/web/src/locales/be/messages.json | 24 +++++-- apps/web/src/locales/bg/messages.json | 24 +++++-- apps/web/src/locales/bn/messages.json | 24 +++++-- apps/web/src/locales/bs/messages.json | 24 +++++-- apps/web/src/locales/ca/messages.json | 24 +++++-- apps/web/src/locales/cs/messages.json | 22 +++++-- apps/web/src/locales/cy/messages.json | 24 +++++-- apps/web/src/locales/da/messages.json | 24 +++++-- apps/web/src/locales/de/messages.json | 26 +++++--- apps/web/src/locales/el/messages.json | 24 +++++-- apps/web/src/locales/en_GB/messages.json | 24 +++++-- apps/web/src/locales/en_IN/messages.json | 24 +++++-- apps/web/src/locales/eo/messages.json | 24 +++++-- apps/web/src/locales/es/messages.json | 24 +++++-- apps/web/src/locales/et/messages.json | 24 +++++-- apps/web/src/locales/eu/messages.json | 24 +++++-- apps/web/src/locales/fa/messages.json | 24 +++++-- apps/web/src/locales/fi/messages.json | 24 +++++-- apps/web/src/locales/fil/messages.json | 24 +++++-- apps/web/src/locales/fr/messages.json | 24 +++++-- apps/web/src/locales/gl/messages.json | 24 +++++-- apps/web/src/locales/he/messages.json | 24 +++++-- apps/web/src/locales/hi/messages.json | 24 +++++-- apps/web/src/locales/hr/messages.json | 24 +++++-- apps/web/src/locales/hu/messages.json | 26 +++++--- apps/web/src/locales/id/messages.json | 24 +++++-- apps/web/src/locales/it/messages.json | 24 +++++-- apps/web/src/locales/ja/messages.json | 24 +++++-- apps/web/src/locales/ka/messages.json | 24 +++++-- apps/web/src/locales/km/messages.json | 24 +++++-- apps/web/src/locales/kn/messages.json | 24 +++++-- apps/web/src/locales/ko/messages.json | 24 +++++-- apps/web/src/locales/lv/messages.json | 24 +++++-- apps/web/src/locales/ml/messages.json | 24 +++++-- apps/web/src/locales/mr/messages.json | 24 +++++-- apps/web/src/locales/my/messages.json | 24 +++++-- apps/web/src/locales/nb/messages.json | 24 +++++-- apps/web/src/locales/ne/messages.json | 24 +++++-- apps/web/src/locales/nl/messages.json | 24 +++++-- apps/web/src/locales/nn/messages.json | 24 +++++-- apps/web/src/locales/or/messages.json | 24 +++++-- apps/web/src/locales/pl/messages.json | 24 +++++-- apps/web/src/locales/pt_BR/messages.json | 24 +++++-- apps/web/src/locales/pt_PT/messages.json | 24 +++++-- apps/web/src/locales/ro/messages.json | 24 +++++-- apps/web/src/locales/ru/messages.json | 24 +++++-- apps/web/src/locales/si/messages.json | 24 +++++-- apps/web/src/locales/sk/messages.json | 24 +++++-- apps/web/src/locales/sl/messages.json | 24 +++++-- apps/web/src/locales/sr_CS/messages.json | 24 +++++-- apps/web/src/locales/sr_CY/messages.json | 24 +++++-- apps/web/src/locales/sv/messages.json | 24 +++++-- apps/web/src/locales/ta/messages.json | 24 +++++-- apps/web/src/locales/te/messages.json | 24 +++++-- apps/web/src/locales/th/messages.json | 24 +++++-- apps/web/src/locales/tr/messages.json | 22 +++++-- apps/web/src/locales/uk/messages.json | 24 +++++-- apps/web/src/locales/vi/messages.json | 24 +++++-- apps/web/src/locales/zh_CN/messages.json | 24 +++++-- apps/web/src/locales/zh_TW/messages.json | 24 +++++-- 63 files changed, 1101 insertions(+), 471 deletions(-) diff --git a/apps/web/src/locales/af/messages.json b/apps/web/src/locales/af/messages.json index 7b6c7778d70..eb983fc3512 100644 --- a/apps/web/src/locales/af/messages.json +++ b/apps/web/src/locales/af/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -6429,6 +6438,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Die Bitwarden-gebruiker wat hierdie Send geskep het, het gekies om hul e-posadres te verberg. U moet verseker dat u die bron van hierdie skakel vertrou voordat u die inhoud gebruik of aflaai.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/ar/messages.json b/apps/web/src/locales/ar/messages.json index 65b8578a206..647c4602c17 100644 --- a/apps/web/src/locales/ar/messages.json +++ b/apps/web/src/locales/ar/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -6429,6 +6438,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index 6c2150ec158..fec06600593 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -48,7 +48,7 @@ "message": "Bu bəndə düzəliş etmə icazəniz yoxdur" }, "reviewAccessIntelligence": { - "message": "Review security reports to find and fix credential risks before they escalate." + "message": "Kimlik məlumatları riskləri artmazdan əvvəl onları tapıb düzəltmək üçün təhlükəsizlik hesabatlarını incələyin." }, "reviewAtRiskLoginsPrompt": { "message": "Risk altında olan girişi nəzərdən keçirin" @@ -269,7 +269,7 @@ } }, "numCriticalApplicationsMarkedSuccess": { - "message": "$COUNT$ applications marked critical", + "message": "$COUNT$ tətbiq kritik olaraq işarələndi", "placeholders": { "count": { "content": "$1", @@ -278,7 +278,7 @@ } }, "numApplicationsUnmarkedCriticalSuccess": { - "message": "$COUNT$ applications marked not critical", + "message": "$COUNT$ tətbiq kritik deyil olaraq işarələndi", "placeholders": { "count": { "content": "$1", @@ -287,7 +287,7 @@ } }, "markAppCountAsCritical": { - "message": "Mark $COUNT$ as critical", + "message": "$COUNT$ tətbiqi kritik olaraq işarələ", "placeholders": { "count": { "content": "$1", @@ -296,7 +296,7 @@ } }, "markAppCountAsNotCritical": { - "message": "Mark $COUNT$ as not critical", + "message": "$COUNT$ tətbiqi kritik deyil olaraq işarələ", "placeholders": { "count": { "content": "$1", @@ -1438,7 +1438,7 @@ "message": "Keçidə sahib olan hər kəs" }, "anyOneWithPassword": { - "message": "Sizin təyin etdiyiniz parola sahib hər kəs" + "message": "Ayarladığınız parola sahib hər kəs" }, "location": { "message": "Yerləşmə" @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Abunəliyiniz əvvəlki halına qaytarıldı." }, + "resubscribe": { + "message": "Təkrar abunə ol" + }, + "yourSubscriptionIsExpired": { + "message": "Abunəliyiniz bitib" + }, + "yourSubscriptionIsCanceled": { + "message": "Abunəliyiniz ləğv edilib" + }, "cancelConfirmation": { "message": "İmtina etmək istədiyinizə əminsiniz? Bu faktura dövrünün sonunda bu abunəliyin bütün özəlliklərinə erişimi itirəcəksiniz." }, @@ -5431,7 +5440,7 @@ "message": "Minimum söz sayı" }, "passwordTypePolicyOverride": { - "message": "Password type", + "message": "Parol növü", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -5704,7 +5713,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendCreatedDescriptionV2": { - "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "message": "Bu Send keçidini kopyala və paylaş. Send, keçidə sahib olan hər kəs üçün növbəti $TIME$ ərzində əlçatan olacaq.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "time": { @@ -5714,7 +5723,7 @@ } }, "sendCreatedDescriptionPassword": { - "message": "Bu Send keçidini kopyala və paylaş. Send, təyin etdiyiniz keçidə və parola sahib olan hər kəs üçün növbəti $TIME$ ərzində əlçatan olacaq.", + "message": "Bu Send keçidini kopyala və paylaş. Send, keçidə və ayarladığınız parola sahib olan hər kəs üçün növbəti $TIME$ ərzində əlçatan olacaq.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "time": { @@ -6429,6 +6438,10 @@ "message": "\"Send\"ə bax", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Bu \"Send\"i yaradan Bitwarden istifadəçisi e-poçt ünvanını gizlətməyi seçib. İstifadə etməzdən və ya endirməzdən əvvəl bu keçidin mənbəyinin etibarlı olduğuna əmin olmalısınız.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6596,7 +6609,7 @@ "message": "Yeni istifadəçiləri avtomatik qeydiyyata al" }, "resetPasswordAutoEnrollInviteWarning": { - "message": "Bu təşkilat, sizi \"parol sıfırlama\"da avtomatik olaraq qeydiyyata alan müəssisə siyasətinə sahibdir. Qeydiyyat, təşkilat administratorlarına ana parolunuzu dəyişdirmə icazəsi verəcək." + "message": "Bu təşkilat, sizi \"parol sıfırlama\"da avtomatik olaraq qeydiyyata alan Enterprise siyasətinə sahibdir. Qeydiyyat, təşkilat inzibatçılarına ana parolunuzu dəyişdirmə icazəsi verəcək." }, "resetPasswordOrgKeysError": { "message": "\"Təşkilat açarları\" cavabı boşdur" @@ -6674,10 +6687,10 @@ } }, "reinviteSuccessToast": { - "message": "1 invitation sent" + "message": "1 dəvət göndərilib" }, "bulkReinviteSentToast": { - "message": "$COUNT$ invitations sent", + "message": "$COUNT$ dəvət göndərilib", "placeholders": { "count": { "content": "$1", @@ -6703,7 +6716,7 @@ } }, "bulkReinviteProgressTitle": { - "message": "$COUNT$ of $TOTAL$ invitations sent...", + "message": "$COUNT$/$TOTAL$ dəvət göndərilib...", "placeholders": { "count": { "content": "$1", @@ -6716,10 +6729,10 @@ } }, "bulkReinviteProgressSubtitle": { - "message": "Keep this page open until all are sent." + "message": "Hamısı göndərilənə qədər bu səhifəni açıq saxlayın." }, "bulkReinviteFailuresTitle": { - "message": "$COUNT$ invitations didn't send", + "message": "$COUNT$ dəvət göndərilməyib", "placeholders": { "count": { "content": "$1", @@ -6728,10 +6741,10 @@ } }, "bulkReinviteFailureTitle": { - "message": "1 invitation didn't send" + "message": "1 dəvət göndərilməyib" }, "bulkReinviteFailureDescription": { - "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "message": "$COUNT$/$TOTAL$ üzvə dəvət göndərərkən xəta baş verdi. Yenidən göndərməyə çalışın, problem davam edərsə,", "placeholders": { "count": { "content": "$1", @@ -6744,7 +6757,7 @@ } }, "bulkResendInvitations": { - "message": "Try sending again" + "message": "Yenidən göndərməyə çalış" }, "bulkRemovedMessage": { "message": "Uğurla çıxarıldı" @@ -10185,7 +10198,7 @@ "message": "Tapşırıq təyin et" }, "allTasksAssigned": { - "message": "All tasks have been assigned" + "message": "Bütün tapşırıqlar təyin edilib" }, "assignSecurityTasksToMembers": { "message": "Parol dəyişdirmə bildirişlərini göndər" @@ -11896,13 +11909,10 @@ "noItemsInArchiveDesc": { "message": "Arxivlənmiş elementlər burada görünəcək, ümumi axtarış nəticələrindən və avto-doldurma təkliflərindən xaric ediləcək." }, - "itemWasSentToArchive": { - "message": "Element arxivə göndərildi" + "itemArchiveToast": { + "message": "Element arxivləndi" }, - "itemWasUnarchived": { - "message": "Element arxivdən çıxarıldı" - }, - "itemUnarchived": { + "itemUnarchivedToast": { "message": "Element arxivdən çıxarıldı" }, "bulkArchiveItems": { @@ -12583,7 +12593,7 @@ "message": "Davam etmək istədiyinizə əminsiniz?" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "Xəta: Şifrəsi açıla bilmir" }, "userVerificationFailed": { "message": "İstifadəçi doğrulaması uğursuz oldu." @@ -12891,22 +12901,22 @@ "message": "bir istifadəçi üçün" }, "upgradeToTeams": { - "message": "Upgrade to Teams" + "message": "\"Teams\"ə yüksəlt" }, "upgradeToEnterprise": { - "message": "Upgrade to Enterprise" + "message": "\"Enterprise\"a yüksəlt" }, "upgradeShareEvenMore": { - "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + "message": "Ailələr planı ilə daha çoxunu paylaşın, ya da Komandalar və ya Müəssisə planı ilə daha güclü və etibarlı parol təhlükəsizliyi əldə edin." }, "organizationUpgradeTaxInformationMessage": { - "message": "Prices exclude tax and are billed annually." + "message": "Qiymətə vergi daxil deyildir və illik hesablanır." }, "invoicePreviewErrorMessage": { - "message": "Encountered an error while generating the invoice preview." + "message": "Faktura önizləməsini yaradan zaman xəta ilə üzləşildi." }, "planProratedMembershipInMonths": { - "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "message": "Proporsional $PLAN$ üzvlüyü ($NUMOFMOONTHS$)", "placeholders": { "plan": { "content": "$1", @@ -12919,16 +12929,16 @@ } }, "premiumSubscriptionCredit": { - "message": "Premium subscription credit" + "message": "Premium abunəlik krediti" }, "enterpriseMembership": { - "message": "Enterprise membership" + "message": "Enterprise üzvlüyü" }, "teamsMembership": { - "message": "Teams membership" + "message": "Teams üzvlüyü" }, "plansUpdated": { - "message": "You've upgraded to $PLAN$!", + "message": "$PLAN$ planına yüksəltmisiniz!", "placeholders": { "plan": { "content": "$1", @@ -12937,6 +12947,6 @@ } }, "paymentMethodUpdateError": { - "message": "There was an error updating your payment method." + "message": "Ödəniş üsulunuzu güncəlləyərkən xəta baş verdi." } } diff --git a/apps/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index ef00d46b4c0..4d6fe96c6d2 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Падпіска была адноўлена." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Вы сапраўды хочаце скасаваць? Вы страціце доступ да ўсіх функцый падпіскі ў канцы гэтага плацежнага перыяду." }, @@ -6429,6 +6438,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Карыстальнік Bitwarden, які стварыў гэты Send, вырашыў схаваць сваю электронную пошту. Пераканайцеся, што гэта спасылка атрымана з надзейных крыніц і толькі пасля гэтага выкарыстоўвайце або пампуйце дадзенае змесціва.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index e32b2b3d5f1..7b5ee07efe1 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Абонаментът е подновен." }, + "resubscribe": { + "message": "Подновявана на абонамента" + }, + "yourSubscriptionIsExpired": { + "message": "Абонаментът Ви е изтекъл" + }, + "yourSubscriptionIsCanceled": { + "message": "Абонаментът Ви е прекратен" + }, "cancelConfirmation": { "message": "Уверени ли сте, че искате да прекратите абонамента? След края на последно платения период ще загубите всички допълнителни преимущества." }, @@ -6429,6 +6438,10 @@ "message": "Преглед на изпращането", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Потвърдете е-пощата си, за да видите това Изпращане", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Потребителят, който е създал това изпращане, е избрал да скрие адреса на своята е-поща. Уверете се, че източникът на тази връзка е достоверен, преди да я последвате или да свалите съдържание чрез нея.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Архивираните елементи ще се показват тук и ще бъдат изключени от общите резултати при търсене и от предложенията за автоматично попълване." }, - "itemWasSentToArchive": { - "message": "Елементът беше преместен в архива" + "itemArchiveToast": { + "message": "Елементът е преместен в архива" }, - "itemWasUnarchived": { - "message": "Елементът беше изваден от архива" - }, - "itemUnarchived": { - "message": "Елементът беше изваден от архива" + "itemUnarchivedToast": { + "message": "Елементът е изваден от архива" }, "bulkArchiveItems": { "message": "Елементите са архивирани" diff --git a/apps/web/src/locales/bn/messages.json b/apps/web/src/locales/bn/messages.json index 53f29d874e7..9935cc538a1 100644 --- a/apps/web/src/locales/bn/messages.json +++ b/apps/web/src/locales/bn/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -6429,6 +6438,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/bs/messages.json b/apps/web/src/locales/bs/messages.json index 1ae5523c9ac..dda9249b383 100644 --- a/apps/web/src/locales/bs/messages.json +++ b/apps/web/src/locales/bs/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -6429,6 +6438,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index 88e4b0569d6..642ae65228e 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "S'ha restablert la subscripció." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Esteu segur que voleu cancel·lar? Perdreu l'accés a totes les característiques d'aquesta subscripció al final d'aquest cicle de facturació." }, @@ -6429,6 +6438,10 @@ "message": "Veure Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "L'usuari Bitwarden que ha creat aquest Send ha decidit amagar la seua adreça de correu electrònic. Heu d’assegurar-vos que confieu en la font d’aquest enllaç abans d’utilitzar o descarregar el seu contingut.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index d85f3fada18..ae41eeebfef 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Předplatné bylo obnoveno." }, + "resubscribe": { + "message": "Obnovit odběr" + }, + "yourSubscriptionIsExpired": { + "message": "Vaše předplatné vypršelo" + }, + "yourSubscriptionIsCanceled": { + "message": "Vaše předplatné je zrušeno" + }, "cancelConfirmation": { "message": "Opravdu chcete zrušit předplatné? Na konci fakturačního období přijdete o veškeré výhody plynoucí z vybraného plánu." }, @@ -6429,6 +6438,10 @@ "message": "Zobrazit Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Ověřte svůj e-mail pro zobrazení tohoto Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Uživatel Bitwardenu, který vytvořil tento Send, se rozhodl skrýt svou e-mailovou adresu. Před použitím nebo stažením jeho obsahu byste se měli ujistit, že zdroji tohoto odkazu důvěřujete.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,13 +11909,10 @@ "noItemsInArchiveDesc": { "message": "Zde se zobrazí archivované položky a budou vyloučeny z obecných výsledků vyhledávání a návrhů automatického vyplňování." }, - "itemWasSentToArchive": { - "message": "Položka byla přesunuta do archivu" + "itemArchiveToast": { + "message": "Položka archivována" }, - "itemWasUnarchived": { - "message": "Položka byla odebrána z archivu" - }, - "itemUnarchived": { + "itemUnarchivedToast": { "message": "Položka byla odebrána z archivu" }, "bulkArchiveItems": { diff --git a/apps/web/src/locales/cy/messages.json b/apps/web/src/locales/cy/messages.json index c566ffaf831..10639a60017 100644 --- a/apps/web/src/locales/cy/messages.json +++ b/apps/web/src/locales/cy/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -6429,6 +6438,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index 42fbf0cf7c4..2943127dbb8 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Abonnementet er gentegnet." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Er du sikker på at du vil opsige? Du vil miste adgangen til alle abonnementsfunktionerne ved afslutningen af denne faktureringsperiode." }, @@ -6429,6 +6438,10 @@ "message": "Vis Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Bitwarden-brugeren, der oprettede denne Send, har valgt at skjule sin e-mailadresse. Du bør sikre dig, at du stoler på kilden til dette link, inden dets indhold downloades/benyttes.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index 89672d615dc..a89e10c8d45 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Das Abo wurde wieder aufgenommen." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Sind Sie sicher, dass Sie kündigen wollen? Am Ende dieses Abrechnungszyklus verlieren Sie den Zugriff auf alle Funktionen dieses Abos." }, @@ -6429,6 +6438,10 @@ "message": "Sends anzeigen", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Der Bitwarden Benutzer, der dieses Send erstellt hat, hat sich entschieden, seine E-Mail-Adresse zu verstecken. Du solltest sicherstellen, dass du der Quelle dieses Links vertraust, bevor du dessen Inhalt verwendest oder herunterlädst.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archivierte Einträge werden hier angezeigt und von allgemeinen Suchergebnissen und Auto-Ausfüllen-Vorschlägen ausgeschlossen." }, - "itemWasSentToArchive": { - "message": "Eintrag wurde ins Archiv verschoben" + "itemArchiveToast": { + "message": "Eintrag archiviert" }, - "itemWasUnarchived": { - "message": "Eintrag wird nicht mehr archiviert" - }, - "itemUnarchived": { - "message": "Eintrag wird nicht mehr archiviert" + "itemUnarchivedToast": { + "message": "Eintrag nicht mehr archiviert" }, "bulkArchiveItems": { "message": "Einträge archiviert" @@ -12583,7 +12593,7 @@ "message": "Bist du sicher, dass du fortfahren möchtest?" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "Fehler: Entschlüsselung nicht möglich" }, "userVerificationFailed": { "message": "Benutzerverifizierung fehlgeschlagen." diff --git a/apps/web/src/locales/el/messages.json b/apps/web/src/locales/el/messages.json index 0aef25ee9cb..e084687382a 100644 --- a/apps/web/src/locales/el/messages.json +++ b/apps/web/src/locales/el/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Η συνδρομή έχει αποκατασταθεί." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Είστε βέβαιοι ότι θέλετε να ακυρώσετε; Θα χάσετε την πρόσβαση σε όλες τις λειτουργίες αυτής της συνδρομής στο τέλος αυτού του κύκλου χρέωσης." }, @@ -6429,6 +6438,10 @@ "message": "Προβολή Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Ο χρήστης Bitwarden που δημιούργησε αυτό το send έχει επιλέξει να κρύψει τη διεύθυνση email του. Πρέπει να διασφαλίσετε ότι εμπιστεύεστε την πηγή αυτού του συνδέσμου πριν χρησιμοποιήσετε ή κατεβάσετε το περιεχόμενό του.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index 1617bfb4580..78242ac88dd 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription has expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is cancelled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -6429,6 +6438,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/en_IN/messages.json b/apps/web/src/locales/en_IN/messages.json index 8845ef9f042..5d1bf31a336 100644 --- a/apps/web/src/locales/en_IN/messages.json +++ b/apps/web/src/locales/en_IN/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription has expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is cancelled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -6429,6 +6438,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/eo/messages.json b/apps/web/src/locales/eo/messages.json index 1e289a324fe..1c9f0473adf 100644 --- a/apps/web/src/locales/eo/messages.json +++ b/apps/web/src/locales/eo/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "La abono estis reinstalita." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Ĉu vi certas, ke vi volas nuligi? Vi perdos aliron al ĉiuj funkcioj de ĉi tiu abono fine de ĉi tiu faktura ciklo." }, @@ -6429,6 +6438,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/es/messages.json b/apps/web/src/locales/es/messages.json index 2d85309144f..7e8f62b6a3c 100644 --- a/apps/web/src/locales/es/messages.json +++ b/apps/web/src/locales/es/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "La suscripción ha sido restablecida." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "¿Estás seguro de que quieres cancelar? Perderá el acceso a todas las funciones de esta suscripción al final de este ciclo de facturación." }, @@ -6429,6 +6438,10 @@ "message": "Ver Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "El usuario Bitwarden que creó este Send ha elegido ocultar su dirección de correo electrónico. Deberías asegurarte de que confías en la fuente de este enlace antes de usar o descargar su contenido.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/et/messages.json b/apps/web/src/locales/et/messages.json index 60a2690785c..e101d49d3cf 100644 --- a/apps/web/src/locales/et/messages.json +++ b/apps/web/src/locales/et/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Tellimus on uuesti aktiveeritud." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Oled kindel, et soovid tellimuse tühistada? Kaotad sellega arveperioodi lõpus kõik tellimisega kaasnevad eelised." }, @@ -6429,6 +6438,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Selle Sendi looja ei soovi oma e-posti aadressi avaldada. Palun veendu, et see pärineb usaldusväärsest allikast, enne kui asud selle sisu kasutama või faile alla laadima.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/eu/messages.json b/apps/web/src/locales/eu/messages.json index a75f95994c0..75f55a0d0f0 100644 --- a/apps/web/src/locales/eu/messages.json +++ b/apps/web/src/locales/eu/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Harpidetza berrezarri da." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Ziur zaude ezeztatu nahi duzula? Harpidetza honen ezaugarri guztiak galduko dituzu fakturazio ziklo honen amaieran." }, @@ -6429,6 +6438,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Send hau sortu duen Bitwarden erabiltzaileak emaileko helbidea ezkutatzea erabaki du. Ziurtatu lotura honen iturrian konfiantza duzula edukia erabili edo deskargatu aurretik.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/fa/messages.json b/apps/web/src/locales/fa/messages.json index 5c046211648..d1efb2a239f 100644 --- a/apps/web/src/locales/fa/messages.json +++ b/apps/web/src/locales/fa/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "اشتراک دوباره برقرار شده است." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "آیا مطمئنید که می‌خواهید لغو کنید؟ در پایان این چرخه صورتحساب، دسترسی به همه ویژگی‌های این اشتراک را از دست خواهید داد." }, @@ -6429,6 +6438,10 @@ "message": "مشاهده ارسال", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "کاربر Bitwarden که این ارسال را ایجاد کرده است انتخاب کرده که نشانی ایمیل خود را پنهان کند. قبل از استفاده یا دانلود محتوای این پیوند، باید مطمئن شوید که به منبع این پیوند اعتماد دارید.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/fi/messages.json b/apps/web/src/locales/fi/messages.json index 7cc931fd9f8..4c6b3ef62d8 100644 --- a/apps/web/src/locales/fi/messages.json +++ b/apps/web/src/locales/fi/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Tilaus palautettiin." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Haluatko varmasti irtisanoa tilauksen? Menetät pääsyn kaikkiin tilauksen tarjoamiin ominaisuuksiin kuluvan laskutuskauden lopussa." }, @@ -6429,6 +6438,10 @@ "message": "Näytä Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Sendin luonut Bitwarden-käyttäjä on piilottanut sähköpostiosoitteensa. Varmista, että linkin lähde on luotettava ennen kuin käytät tai lataat sen sisältöä.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/fil/messages.json b/apps/web/src/locales/fil/messages.json index 415793a4b98..e94c5b4fea6 100644 --- a/apps/web/src/locales/fil/messages.json +++ b/apps/web/src/locales/fil/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Naibalik na ang subscription." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Sigurado ka bang gusto mong kanselahin? Mawawala ang access mo sa lahat ng tampok ng subscription na ito sa pagtatapos ng siklo ng pagsingil na ito." }, @@ -6429,6 +6438,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Ang gumagamit ng Bitwarden na lumikha ng Send na ito ay piniling itago ang kanilang email address. Dapat mong tiyakin na pinagkakatiwalaan mo ang pinagmulan ng link na ito bago gamitin o i download ang nilalaman nito.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index eb9b2d1c915..e4183ff5692 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Votre abonnement a été rétabli." }, + "resubscribe": { + "message": "Se réabonner" + }, + "yourSubscriptionIsExpired": { + "message": "Votre abonnement a expiré" + }, + "yourSubscriptionIsCanceled": { + "message": "Votre abonnement est annulé" + }, "cancelConfirmation": { "message": "Êtes-vous sûr de vouloir annuler ? Vous perdrez l’accès à toutes les fonctionnalités de l’abonnement à la fin de ce cycle de facturation." }, @@ -6429,6 +6438,10 @@ "message": "Voir le Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Vérifiez votre courriel pour afficher ce Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "L'utilisateur de Bitwarden qui a créé ce Send a choisi de masquer son adresse électronique. Vous devriez vous assurer que vous faites confiance à la source de ce lien avant d'utiliser ou de télécharger son contenu.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Les éléments archivés apparaîtront ici et seront exclus des résultats de recherche généraux et des suggestions de remplissage automatique." }, - "itemWasSentToArchive": { - "message": "L'élément a été envoyé à l'archive" + "itemArchiveToast": { + "message": "Élément archivé" }, - "itemWasUnarchived": { - "message": "L'élément a été désarchivé" - }, - "itemUnarchived": { - "message": "L'élément a été désarchivé" + "itemUnarchivedToast": { + "message": "Élément désarchivé" }, "bulkArchiveItems": { "message": "Éléments archivés" diff --git a/apps/web/src/locales/gl/messages.json b/apps/web/src/locales/gl/messages.json index 8357f5d0747..de6c7782c6d 100644 --- a/apps/web/src/locales/gl/messages.json +++ b/apps/web/src/locales/gl/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -6429,6 +6438,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/he/messages.json b/apps/web/src/locales/he/messages.json index b075b91a860..8829ed90e65 100644 --- a/apps/web/src/locales/he/messages.json +++ b/apps/web/src/locales/he/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "המנוי הופעל מחדש." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "האם אתה בטוח שברצונך לבטל? ביטול המנוי יגרום לאיבוד כל האפשרויות השמורות למנויים בסיום מחזור החיוב הנוכחי." }, @@ -6429,6 +6438,10 @@ "message": "הצג סֵנְד", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "משתמש ה־Bitwarden שיצר את סֵנְד זה בחר להסתיר את כתובת הדוא\"ל שלו. עליך לוודא שאתה בוטח במקור של קישור זה לפני שימוש או הורדה של התוכן שלו.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "פריטים בארכיון יופיעו כאן ויוחרגו מתוצאות חיפוש כללי והצעות למילוי אוטומטי." }, - "itemWasSentToArchive": { - "message": "הפריט נשלח לארכיון" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "הפריט הוסר מהארכיון" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "הפריטים הועברו לארכיון" diff --git a/apps/web/src/locales/hi/messages.json b/apps/web/src/locales/hi/messages.json index 4e261d672bb..41d32fb9587 100644 --- a/apps/web/src/locales/hi/messages.json +++ b/apps/web/src/locales/hi/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -6429,6 +6438,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/hr/messages.json b/apps/web/src/locales/hr/messages.json index ced4fc03b1c..41eac503c59 100644 --- a/apps/web/src/locales/hr/messages.json +++ b/apps/web/src/locales/hr/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Pretplata je vraćena" }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Sigurno želiš otkazati? Izgubiti ćeš pristup svim ovim pretplatnim značajkama kad istekne rok naplate." }, @@ -6429,6 +6438,10 @@ "message": "Pogledaj Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Bitwarden korisnik koji je stvorio ovaj Send odabrao/la je sakriti svoju e-poštu. Koristi i/ili preuzmi ove podatke samo ako vjeruješ izvoru iz kojeg je primljena ova vezu.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Arhivirane stavke biti će prikazane ovdje i biti će izuzete iz rezultata općih pretraga i preporuka auto-ispune." }, - "itemWasSentToArchive": { - "message": "Stavka poslana u arhivu" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Stavka vraćena iz arhive" - }, - "itemUnarchived": { - "message": "Stavka vraćena iz arhive" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Stavke arhivirane" diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index f4c9144cc6d..9b8a98e5625 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Az előfizetés visszaállításra került." }, + "resubscribe": { + "message": "Feliratkozás ismét" + }, + "yourSubscriptionIsExpired": { + "message": "Az előfizetés lejárt." + }, + "yourSubscriptionIsCanceled": { + "message": "Az előfizetés törlésre került." + }, "cancelConfirmation": { "message": "Biztosan törlésre kerüljön? A számlázási időszak végén az összes előfizetési hozzáférés elveszik." }, @@ -6429,6 +6438,10 @@ "message": "Send megtekintése", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Ellenőrizzük az email címet a Send elem megtekintéséhez.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Az ezt a Send elemet létrehozó Bitwarden felhasználó úgy döntött, hogy elrejti email címét. Mielőtt felhasználnánk vagy letöltenénk a tartalmát, ellenőrizzük a hivatkozás megbízhatóságát.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -10906,7 +10919,7 @@ "message": "A biztonsági kockázatok azonosítása a tagok hozzáférésének ellenőrzésével" }, "onlyAvailableForEnterpriseOrganization": { - "message": "Vállalati tervre frissítéssel gyorsan megtekinthető a tagok hozzáférése a szervezetben." + "message": "Vállalati csomagra frissítéssel gyorsan megtekinthető a tagok hozzáférése a szervezetben." }, "date": { "message": "Dátum" @@ -11896,20 +11909,17 @@ "noItemsInArchiveDesc": { "message": "Az archivált elemek itt jelennek meg és kizárásra kerülnek az általános keresési eredményekből és az automatikus kitöltési javaslatokból." }, - "itemWasSentToArchive": { - "message": "Az elem az archivumba került." + "itemArchiveToast": { + "message": "Az elem archiválásra került." }, - "itemWasUnarchived": { + "itemUnarchivedToast": { "message": "Az elem visszavételre került az archivumból." }, - "itemUnarchived": { - "message": "Az elemek visszavéelre kerültek az archivumból." - }, "bulkArchiveItems": { "message": "Az elemek archiválásra kerültek." }, "bulkUnarchiveItems": { - "message": "Az elemek visszavéelre kerültek az archivumból." + "message": "Az elemek visszavételre kerültek az archivumból." }, "archiveItem": { "message": "Elem archiválása", diff --git a/apps/web/src/locales/id/messages.json b/apps/web/src/locales/id/messages.json index 270b8624c24..30746054e41 100644 --- a/apps/web/src/locales/id/messages.json +++ b/apps/web/src/locales/id/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Langganan telah dipulihkan." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Anda yakin ingin membatalkan? Anda akan kehilangan akses ke semua fitur langganan ini di akhir siklus penagihan ini." }, @@ -6429,6 +6438,10 @@ "message": "Lihat Kirim", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Pengguna Bitwarden yang membuat Send ini memilih untuk menyembunyikan alamat emailnya. Kamu harus yakin bahwa kamu mempercayai sumber dari link it sebelum mengunduh isinya.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/it/messages.json b/apps/web/src/locales/it/messages.json index c2fb3effdf0..1e89c1624f6 100644 --- a/apps/web/src/locales/it/messages.json +++ b/apps/web/src/locales/it/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "L'abbonamento è stato ripristinato." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Sei sicuro di voler annullare il tuo abbonamento? Alla fine di questo ciclo di fatturazione perderai l'accesso a tutte le funzionalità aggiuntive date dall'abbonamento." }, @@ -6429,6 +6438,10 @@ "message": "Visualizza Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "L'utente Bitwarden che ha creato questo Send ha nascosto il suo indirizzo email. Devi essere sicuro di fidarti della fonte di questo link prima di usare o scaricare il suo contenuto.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Gli elementi archiviati appariranno qui e saranno esclusi dai risultati di ricerca e dall'auto-riempimento." }, - "itemWasSentToArchive": { - "message": "Elemento archiviato" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Elemento estratto dall'archivio" - }, - "itemUnarchived": { - "message": "Elemento estratto dall'archivio" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Elementi archiviati" diff --git a/apps/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index cd78e0a269a..d86a7dce648 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "契約が再開されました。" }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "本当にキャンセルしますか?契約していたすべての追加機能が請求期間の終期で利用できなくなります。" }, @@ -6429,6 +6438,10 @@ "message": "Send を表示", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "この Send を作成した Bitwarden ユーザーが、自身のメールアドレスを非表示にしました。 コンテンツを使用またはダウンロードする前に、このリンクのソースが信頼できるかどうか確認する必要があります。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/ka/messages.json b/apps/web/src/locales/ka/messages.json index 6de5179793d..915ca5d6cba 100644 --- a/apps/web/src/locales/ka/messages.json +++ b/apps/web/src/locales/ka/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -6429,6 +6438,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/km/messages.json b/apps/web/src/locales/km/messages.json index b95256dfacd..bc0e3b74cb6 100644 --- a/apps/web/src/locales/km/messages.json +++ b/apps/web/src/locales/km/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -6429,6 +6438,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/kn/messages.json b/apps/web/src/locales/kn/messages.json index 7a378184a39..bd77a975193 100644 --- a/apps/web/src/locales/kn/messages.json +++ b/apps/web/src/locales/kn/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "ಚಂದಾದಾರಿಕೆಯನ್ನು ಪುನಃ ಸ್ಥಾಪಿಸಲಾಗಿದೆ." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "ನೀವು ರದ್ದು ಮಾಡಲು ಖಚಿತವಾಗಿ ಬಯಸುವಿರಾ? ಈ ಬಿಲ್ಲಿಂಗ್ ಚಕ್ರದ ಕೊನೆಯಲ್ಲಿ ಈ ಎಲ್ಲಾ ಚಂದಾದಾರಿಕೆಯ ವೈಶಿಷ್ಟ್ಯಗಳಿಗೆ ನೀವು ಪ್ರವೇಶವನ್ನು ಕಳೆದುಕೊಳ್ಳುತ್ತೀರಿ." }, @@ -6429,6 +6438,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "ಈ ಕಳುಹಿಸುವಿಕೆಯನ್ನು ರಚಿಸಿದ ಬಿಟ್‌ವಾರ್ಡೆನ್ ಬಳಕೆದಾರರು ತಮ್ಮ ಇಮೇಲ್ ವಿಳಾಸವನ್ನು ಮರೆಮಾಡಲು ಆಯ್ಕೆ ಮಾಡಿದ್ದಾರೆ. ಈ ಲಿಂಕ್‌ನ ವಿಷಯವನ್ನು ಬಳಸುವ ಅಥವಾ ಡೌನ್‌ಲೋಡ್ ಮಾಡುವ ಮೊದಲು ಅದರ ಮೂಲವನ್ನು ನೀವು ನಂಬಿದ್ದೀರಿ ಎಂದು ನೀವು ಖಚಿತಪಡಿಸಿಕೊಳ್ಳಬೇಕು.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/ko/messages.json b/apps/web/src/locales/ko/messages.json index ad9b80b9f6a..0af228dd821 100644 --- a/apps/web/src/locales/ko/messages.json +++ b/apps/web/src/locales/ko/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "구독을 복원했습니다." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "정말로 취소하시겠습니까? 청구 주기 후에 이 구독의 모든 기능에 대한 접근을 잃게 됩니다." }, @@ -6429,6 +6438,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "이 Send를 생성한 Bitwarden 사용자가 자신의 이메일 주소를 숨겼습니다. 이 링크에 접속하거나 내용을 다운로드하기 전에, 이 링크의 출처를 신뢰하는지 확인하십시오.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index 4875dc4fe31..f12dda40ea3 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Abonements tika atjaunots." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Vai tiešām atcelt? Tiks zaudēta piekļuve visām abonementa iespējām pēc pašreizējā norēķinu laika posma beigām." }, @@ -6429,6 +6438,10 @@ "message": "Apskatīt Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Bitwarden lietotājs, kurš izveidoja šo Send, ir izvēlējies slēpt savu e-pasta adresi. Ir jāpārliecinās par šīs saites avota uzticamību, pirms saturs tiek izmantots vai lejupielādēts.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Šeit parādīsies arhivētie vienumi, un tie netiks iekļauti vispārējās meklēšanas iznākumos un automātiskās aizpildes ieteikumos." }, - "itemWasSentToArchive": { - "message": "Vienums tika ievietots arhīvā" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Vienums tika izņemts no arhīva" - }, - "itemUnarchived": { - "message": "Vienums tika izņemts no arhīva" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Vienumi tika arhivēti" diff --git a/apps/web/src/locales/ml/messages.json b/apps/web/src/locales/ml/messages.json index f0173da95e5..e68e7d25b85 100644 --- a/apps/web/src/locales/ml/messages.json +++ b/apps/web/src/locales/ml/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -6429,6 +6438,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/mr/messages.json b/apps/web/src/locales/mr/messages.json index 35031280ec3..2abd88c1169 100644 --- a/apps/web/src/locales/mr/messages.json +++ b/apps/web/src/locales/mr/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -6429,6 +6438,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/my/messages.json b/apps/web/src/locales/my/messages.json index b95256dfacd..bc0e3b74cb6 100644 --- a/apps/web/src/locales/my/messages.json +++ b/apps/web/src/locales/my/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -6429,6 +6438,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/nb/messages.json b/apps/web/src/locales/nb/messages.json index 07db562d56b..de97b70a119 100644 --- a/apps/web/src/locales/nb/messages.json +++ b/apps/web/src/locales/nb/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Abonnementet har blitt gjeninnført." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Er du sikker på at du vil avbryte? Du vil miste tilgang til alle funksjonene til dette abonnementet etter den inneværende regningsperioden." }, @@ -6429,6 +6438,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Bitwarden-brukeren som opprettet denne sendingen, har valgt å skjule deres e-postadresse. Du bør forsikre deg om at du stoler på kilden til denne lenken før du bruker eller laster ned innholdet.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/ne/messages.json b/apps/web/src/locales/ne/messages.json index cd5e07cc33e..26c942795b6 100644 --- a/apps/web/src/locales/ne/messages.json +++ b/apps/web/src/locales/ne/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -6429,6 +6438,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index 6754c7a0c51..2748009dba8 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Het abonnement is opnieuw geactiveerd." }, + "resubscribe": { + "message": "Opnieuw abonneren" + }, + "yourSubscriptionIsExpired": { + "message": "Je abonnement is verlopen" + }, + "yourSubscriptionIsCanceled": { + "message": "Je abonnement is geannuleerd" + }, "cancelConfirmation": { "message": "Weet je zeker dat je wilt opzeggen? Je verliest toegang tot alle functionaliteiten van dit abonnement aan het einde van deze betalingscyclus." }, @@ -6429,6 +6438,10 @@ "message": "Send weergeven", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verifieer je e-mailadres om deze Send te bekijken", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "De Bitwarden-gebruiker die deze Send heeft gemaakt heeft ervoor gekozen het e-mailadres te verbergen. Je moet je ervan verzekeren dat je de bron van deze link vertrouwt voordat je de inhoud gebruikt of downloadt.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Gearchiveerde items verschijnen hier en worden uitgesloten van algemene zoekresultaten en automatisch invulsuggesties." }, - "itemWasSentToArchive": { - "message": "Item naar archief verzonden" + "itemArchiveToast": { + "message": "Item gearchiveerd" }, - "itemWasUnarchived": { - "message": "Item uit het archief gehaald" - }, - "itemUnarchived": { - "message": "Item uit het archief gehaald" + "itemUnarchivedToast": { + "message": "Item gedearchiveerd" }, "bulkArchiveItems": { "message": "Items gearchiveerd" diff --git a/apps/web/src/locales/nn/messages.json b/apps/web/src/locales/nn/messages.json index 7987b4077d1..78a638ee05a 100644 --- a/apps/web/src/locales/nn/messages.json +++ b/apps/web/src/locales/nn/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -6429,6 +6438,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/or/messages.json b/apps/web/src/locales/or/messages.json index b95256dfacd..bc0e3b74cb6 100644 --- a/apps/web/src/locales/or/messages.json +++ b/apps/web/src/locales/or/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -6429,6 +6438,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index 9736337ee0c..40ba151b7ad 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Subskrypcja została przywrócona." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Czy na pewno chcesz anulować? Dostęp do wszystkich funkcji związanych z tą subskrypcją zostanie wyłączony na koniec tego okresu rozliczeniowego." }, @@ -6429,6 +6438,10 @@ "message": "Zobacz Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Użytkownik Bitwarden, który utworzył wysyłkę, zdecydował ukryć swój adres e-mail. Przed użyciem lub pobraniem treści wysyłki upewnij się, że ufasz źródłu tego linku.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index f29637803f0..f78d39715cc 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "A assinatura foi restabelecida." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Você tem certeza que deseja cancelar? Você perderá o acesso a todos os recursos dessa assinatura no final deste ciclo de faturamento." }, @@ -6429,6 +6438,10 @@ "message": "Ver Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "O usuário Bitwarden que criou este Send optou por ocultar seu endereço de e-mail. Você deve certificar-se de que confia na fonte deste link antes de usar ou baixar seu conteúdo.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Os itens arquivados aparecerão aqui e serão excluídos dos resultados gerais de busca e das sugestões de preenchimento automático." }, - "itemWasSentToArchive": { - "message": "O item foi enviado para o arquivo" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "O item foi desarquivado" - }, - "itemUnarchived": { - "message": "O item foi desarquivado" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Itens arquivados" diff --git a/apps/web/src/locales/pt_PT/messages.json b/apps/web/src/locales/pt_PT/messages.json index e0626be7255..a20d884c321 100644 --- a/apps/web/src/locales/pt_PT/messages.json +++ b/apps/web/src/locales/pt_PT/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "A subscrição foi restabelecida." }, + "resubscribe": { + "message": "Renovar a subscrição" + }, + "yourSubscriptionIsExpired": { + "message": "A sua subscrição expirou" + }, + "yourSubscriptionIsCanceled": { + "message": "A sua subscrição foi cancelada" + }, "cancelConfirmation": { "message": "Tem a certeza de que pretende cancelar? Perderá o acesso a todas as funcionalidades desta subscrição no final deste ciclo de faturação." }, @@ -6429,6 +6438,10 @@ "message": "Ver Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Confirme o seu e-mail para ver este Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "O utilizador Bitwarden que criou este Send optou por ocultar o seu endereço de e-mail. Deve certificar-se de que confia na fonte deste link antes de utilizar ou descarregar o seu conteúdo.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Os itens arquivados aparecerão aqui e serão excluídos dos resultados gerais da pesquisa e das sugestões de preenchimento automático." }, - "itemWasSentToArchive": { - "message": "O item foi movido para o arquivo" + "itemArchiveToast": { + "message": "Item arquivado" }, - "itemWasUnarchived": { - "message": "O item foi desarquivado" - }, - "itemUnarchived": { - "message": "O item foi desarquivado" + "itemUnarchivedToast": { + "message": "Item desarquivado" }, "bulkArchiveItems": { "message": "Itens arquivados" diff --git a/apps/web/src/locales/ro/messages.json b/apps/web/src/locales/ro/messages.json index 0b567f2f969..6dca68fa932 100644 --- a/apps/web/src/locales/ro/messages.json +++ b/apps/web/src/locales/ro/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Abonamentul a fost restabilit." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Sigur doriți să anulați? Veți pierde accesul la toate funcționalitățile acestui abonament la sfârșitul acestui ciclu de facturare." }, @@ -6429,6 +6438,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Utilizatorul Bitwarden care a creat acest Send a ales să-și ascundă adresa de e-mail. Ar trebui să vă asigurați că aveți încredere în sursa acestui link înainte de utilizarea sau descărcarea conținutului acestuia.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index 5433c0ac312..13aec65ba3e 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Подписка восстановлена." }, + "resubscribe": { + "message": "Возобновить подписку" + }, + "yourSubscriptionIsExpired": { + "message": "Срок действия вашей подписки истек" + }, + "yourSubscriptionIsCanceled": { + "message": "Ваша подписка отменена" + }, "cancelConfirmation": { "message": "Вы действительно хотите отменить? Вы потеряете доступ ко всем возможностям этой подписки в конце этого платежного периода." }, @@ -6429,6 +6438,10 @@ "message": "Просмотр Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Подтвердите свой адрес email, чтобы просмотреть эту Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Пользователь Bitwarden, создавший эту Send, решил скрыть свой адрес email. Вы должны убедиться, что доверяете источнику этой ссылки, прежде чем использовать или скачивать ее содержимое.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Архивированные элементы появятся здесь и будут исключены из общих результатов поиска и предложений автозаполнения." }, - "itemWasSentToArchive": { - "message": "Элемент был отправлен в архив" + "itemArchiveToast": { + "message": "Элемент архивирован" }, - "itemWasUnarchived": { - "message": "Элемент был разархивирован" - }, - "itemUnarchived": { - "message": "Элемент был разархивирован" + "itemUnarchivedToast": { + "message": "Элемент разархивирован" }, "bulkArchiveItems": { "message": "Элементы архивированы" diff --git a/apps/web/src/locales/si/messages.json b/apps/web/src/locales/si/messages.json index ac073315340..ab7c7e30566 100644 --- a/apps/web/src/locales/si/messages.json +++ b/apps/web/src/locales/si/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -6429,6 +6438,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index dffa3975792..b1f38615a99 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Predplatné bolo obnovené." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Naozaj chcete zrušiť? Stratíte prístup k všetkým funkciám, ktoré vám predplatné ponúka na konci fakturačného obdobia." }, @@ -6429,6 +6438,10 @@ "message": "Zobraziť Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Používateľ Bitwardenu, ktorý vytvoril tento Send, skryl e-mailové adresy pred príjemcami. Mali by ste zvážiť, či dôverujete zdrojovému odkazu pred jeho použitím alebo stiahnutím jeho obsahu.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Tu sa zobrazia archivované položky, ktoré budú vylúčené zo všeobecného vyhľadávania a z návrhov automatického vypĺňania." }, - "itemWasSentToArchive": { - "message": "Položka bola archivovaná" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Položka bola odobraná z archívu" - }, - "itemUnarchived": { - "message": "Položka bola odobraná z archívu" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Položky archivované" diff --git a/apps/web/src/locales/sl/messages.json b/apps/web/src/locales/sl/messages.json index aef3a8ca3ac..db65260f9b1 100644 --- a/apps/web/src/locales/sl/messages.json +++ b/apps/web/src/locales/sl/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Naročnina je bila ponovno vzpostavljena." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Ste prepričani, da želite odpovedati? Ob koncu plačilnega obdobja boste izgubili dostop do vseh ugodnosti naročnine." }, @@ -6429,6 +6438,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/sr_CS/messages.json b/apps/web/src/locales/sr_CS/messages.json index 7e7bcbd97b1..fd18cb42a06 100644 --- a/apps/web/src/locales/sr_CS/messages.json +++ b/apps/web/src/locales/sr_CS/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -6429,6 +6438,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/sr_CY/messages.json b/apps/web/src/locales/sr_CY/messages.json index d457b16f44e..eb6484db8df 100644 --- a/apps/web/src/locales/sr_CY/messages.json +++ b/apps/web/src/locales/sr_CY/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Претплата је враћена." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Јесте ли сигурни да хоћете да откажете? На крају овог обрачунског циклуса изгубићете приступ свим функцијама ове претплате." }, @@ -6429,6 +6438,10 @@ "message": "Видети Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Bitwarden корисник који је створио овај „Send“ је изабрао да сакрије своју е-адресу. Требате да се осигурате да верујете извору ове везе пре употребе или преузимања његовог садржаја.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Архивиране ставке ће се овде појавити и бити искључени из општих резултата претраге и сугестија о ауто-пуњењу." }, - "itemWasSentToArchive": { - "message": "Ставка је послата у архиву" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Ставка враћена из архиве" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Ставке у архиви" diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index f4bab640104..3cb3116b684 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Abonnemanget har återupptagits." }, + "resubscribe": { + "message": "Prenumerera igen" + }, + "yourSubscriptionIsExpired": { + "message": "Din prenumeration har löpt ut" + }, + "yourSubscriptionIsCanceled": { + "message": "Din prenumeration är avbruten" + }, "cancelConfirmation": { "message": "Är du säker på att du vill avsluta? Du kommer förlora tillgång till alla funktioner som abonnemanget erbjuder vid slutet av den nuvarande faktureringsperioden." }, @@ -6429,6 +6438,10 @@ "message": "Visa Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Bitwarden-användaren som skapade denna Send har valt att dölja sin e-postadress. Du bör se till att du litar på källan till denna länk innan du använder eller hämtar innehållet.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Arkiverade objekt visas här och undantas från allmänna sökresultat och autofyllförslag." }, - "itemWasSentToArchive": { - "message": "Objektet skickades till arkivet" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Objektet har avarkiverats" - }, - "itemUnarchived": { - "message": "Objektet har avarkiverats" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Objekt arkiverade" diff --git a/apps/web/src/locales/ta/messages.json b/apps/web/src/locales/ta/messages.json index b93591182db..3789574201c 100644 --- a/apps/web/src/locales/ta/messages.json +++ b/apps/web/src/locales/ta/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "சந்தா மீட்டெடுக்கப்பட்டது." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "நீங்கள் ரத்துசெய்ய விரும்புகிறீர்களா? இந்த பில்லிங் சுழற்சியின் முடிவில் இந்தச் சந்தாவின் அனைத்து அம்சங்களுக்கான அணுகலையும் நீங்கள் இழப்பீர்கள்." }, @@ -6429,6 +6438,10 @@ "message": "Send ஐப் பார்க்கவும்", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "இந்த Send-ஐ உருவாக்கிய Bitwarden பயனர் தங்கள் மின்னஞ்சல் முகவரியை மறைக்கத் தேர்ந்தெடுத்துள்ளார். இதன் உள்ளடக்கத்தைப் பயன்படுத்துவதற்கு அல்லது பதிவிறக்குவதற்கு முன், இந்த இணைப்பின் மூலத்தை நீங்கள் நம்புகிறீர்கள் என்பதை உறுதிப்படுத்த வேண்டும்.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/te/messages.json b/apps/web/src/locales/te/messages.json index b95256dfacd..bc0e3b74cb6 100644 --- a/apps/web/src/locales/te/messages.json +++ b/apps/web/src/locales/te/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -6429,6 +6438,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/th/messages.json b/apps/web/src/locales/th/messages.json index e5e449038ac..eb6fa9011fc 100644 --- a/apps/web/src/locales/th/messages.json +++ b/apps/web/src/locales/th/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -6429,6 +6438,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/tr/messages.json b/apps/web/src/locales/tr/messages.json index 97a29756a89..53348da8697 100644 --- a/apps/web/src/locales/tr/messages.json +++ b/apps/web/src/locales/tr/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Abonelik sürdürüldü." }, + "resubscribe": { + "message": "Yeniden abone ol" + }, + "yourSubscriptionIsExpired": { + "message": "Aboneliğiniz sona erdi" + }, + "yourSubscriptionIsCanceled": { + "message": "Aboneliğiniz iptal edildi" + }, "cancelConfirmation": { "message": "İptal etmek istediğinden emin misin? Bu fatura döneminin sonunda bu aboneliğin tüm özelliklerine erişiminizi kaybedeceksiniz." }, @@ -6429,6 +6438,10 @@ "message": "Send'i görüntüle", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Bu Send'i görmek için e-posta adresinizi doğrulayın", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Bu Send'i oluşturan Bitwarden kullanıcısı e-posta adresini gizlemeyi seçti. Kullanmadan veya içeriğini indirmeden önce bu bağlantının kaynağının güvenilir olduğundan emin olun.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,13 +11909,10 @@ "noItemsInArchiveDesc": { "message": "Arşivlenmiş kayıtlar burada görünür ve genel arama sonuçları ile otomatik doldurma önerilerinden hariç tutulur." }, - "itemWasSentToArchive": { - "message": "Kayıt arşive gönderildi" + "itemArchiveToast": { + "message": "Kayıt arşivlendi" }, - "itemWasUnarchived": { - "message": "Kayıt arşivden çıkarıldı" - }, - "itemUnarchived": { + "itemUnarchivedToast": { "message": "Kayıt arşivden çıkarıldı" }, "bulkArchiveItems": { diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index 6fc3db5d64e..dcb5d26aa1f 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Передплату було відновлено." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Ви справді хочете скасувати? Ви втратите доступ до всіх можливостей, пов'язаних з нею після завершення поточного періоду передплати." }, @@ -6429,6 +6438,10 @@ "message": "Переглянути відправлення", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Користувач Bitwarden, який створив це відправлення, вирішив приховати свою адресу електронної пошти. Вам слід упевнитися в надійності джерела цього посилання перед його використанням чи завантаженням вмісту.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Архівовані записи з'являтимуться тут і будуть виключені з результатів звичайного пошуку та пропозицій автозаповнення." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" diff --git a/apps/web/src/locales/vi/messages.json b/apps/web/src/locales/vi/messages.json index d54ae1933a7..64d0703bc07 100644 --- a/apps/web/src/locales/vi/messages.json +++ b/apps/web/src/locales/vi/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Đã kích hoạt lại gói." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Bạn có chắc muốn hủy không? Bạn sẽ mất hết quyền truy cập tất cả các tính năng của thuê bao này khi kì thanh toán kết thúc." }, @@ -6429,6 +6438,10 @@ "message": "Xem Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Người dùng Bitwarden đã tạo Send này đã chọn ẩn địa chỉ email của họ. Bạn nên đảm bảo rằng bạn tin tưởng nguồn của liên kết này trước khi sử dụng hoặc tải xuống nội dung của nó.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "Các mục đã lưu trữ sẽ hiển thị ở đây và sẽ bị loại khỏi kết quả tìm kiếm và gợi ý tự động điền." }, - "itemWasSentToArchive": { - "message": "Mục đã được chuyển vào kho lưu trữ" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Mục đã được bỏ lưu trữ" - }, - "itemUnarchived": { - "message": "Mục đã được bỏ lưu trữ" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Các mục đã được lưu trữ" diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index 76b95446091..47c732a5a32 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "您的订阅已恢复。" }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "确定要取消吗?在本次计费周期结束后,您将无法使用此订阅的所有功能。" }, @@ -6429,6 +6438,10 @@ "message": "查看 Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "创建此 Send 的 Bitwarden 用户已选择隐藏他们的电子邮箱地址。在使用或下载其内容之前,您应确保信任此链接的来源。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "已归档的项目将显示在此处,并将被排除在一般搜索结果和自动填充建议之外。" }, - "itemWasSentToArchive": { - "message": "项目已发送到归档" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "项目已取消归档" - }, - "itemUnarchived": { - "message": "项目已取消归档" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "项目已归档" diff --git a/apps/web/src/locales/zh_TW/messages.json b/apps/web/src/locales/zh_TW/messages.json index 8056e49b08a..c006b37d612 100644 --- a/apps/web/src/locales/zh_TW/messages.json +++ b/apps/web/src/locales/zh_TW/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "已重新開始訂閱。" }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "您確定要取消訂閱嗎?在本次計費週期結束後,您將無法再使用此訂閱的所有功能。" }, @@ -6429,6 +6438,10 @@ "message": "檢視 Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "建立此 Send 的 Bitwarden 使用者已選擇隱藏他們的電子郵件地址。在使用或下載此連結的內容之前,應確保您信任此連結的來源。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -11896,14 +11909,11 @@ "noItemsInArchiveDesc": { "message": "封存的項目會顯示在此處,且不會出現在一般搜尋結果或自動填入建議中。" }, - "itemWasSentToArchive": { - "message": "項目已移至封存" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "已取消封存項目" - }, - "itemUnarchived": { - "message": "項目取消封存" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "項目已封存" From 460b9ccb6730a3879061b49ccbee46c02080fac7 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 16 Feb 2026 12:42:56 +0100 Subject: [PATCH 047/134] Fix high CPU usage on flatpak (#19006) --- apps/desktop/resources/com.bitwarden.desktop.devel.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/desktop/resources/com.bitwarden.desktop.devel.yaml b/apps/desktop/resources/com.bitwarden.desktop.devel.yaml index 0f6e3ea370d..b708c07206b 100644 --- a/apps/desktop/resources/com.bitwarden.desktop.devel.yaml +++ b/apps/desktop/resources/com.bitwarden.desktop.devel.yaml @@ -53,4 +53,8 @@ modules: - export TMPDIR="$XDG_RUNTIME_DIR/app/$FLATPAK_ID" - export ZYPAK_LD_PRELOAD="/app/bin/libprocess_isolation.so" - export PROCESS_ISOLATION_LD_PRELOAD="/app/bin/libprocess_isolation.so" - - exec zypak-wrapper /app/bin/bitwarden-app "$@" + - PARAMS="--enable-features=UseOzonePlatform,WaylandWindowDecorations --ozone-platform-hint=auto" + - if [ "$USE_X11" != "false" ]; then + - PARAMS="--ozone-platform=x11" + - fi + - exec zypak-wrapper /app/bin/bitwarden-app "$@" "$PARAMS" From c415beb653b16d87e6ea6f989550cce3a494dac3 Mon Sep 17 00:00:00 2001 From: John Harrington <84741727+harr1424@users.noreply.github.com> Date: Mon, 16 Feb 2026 15:53:46 -0700 Subject: [PATCH 048/134] add password specific header (#18988) --- .../src/app/tools/send/send-access/send-auth.component.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/tools/send/send-access/send-auth.component.ts b/apps/web/src/app/tools/send/send-access/send-auth.component.ts index 22ab04bd9dd..8c630ce5315 100644 --- a/apps/web/src/app/tools/send/send-access/send-auth.component.ts +++ b/apps/web/src/app/tools/send/send-access/send-auth.component.ts @@ -73,7 +73,6 @@ export class SendAuthComponent implements OnInit { ) {} ngOnInit() { - this.updatePageTitle(); void this.onSubmit(); } @@ -226,6 +225,10 @@ export class SendAuthComponent implements OnInit { pageTitle: { key: "verifyYourEmailToViewThisSend" }, }); } + } else if (authType === AuthType.Password) { + this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({ + pageTitle: { key: "sendAccessPasswordTitle" }, + }); } } } From 5623568a2f67c98deef45820bf98b3c02ea463a4 Mon Sep 17 00:00:00 2001 From: John Harrington <84741727+harr1424@users.noreply.github.com> Date: Mon, 16 Feb 2026 15:54:22 -0700 Subject: [PATCH 049/134] [PM-31620] Browser - Incorrect "Copy link" message when Send is shared with specific people (#18982) * add existing Send creation messages to browser * remove unused method and associated tests --- apps/browser/src/_locales/en/messages.json | 39 +++++++++ .../send-created/send-created.component.html | 8 +- .../send-created.component.spec.ts | 82 ++++++++++++++----- .../send-created/send-created.component.ts | 15 ++-- 4 files changed, 116 insertions(+), 28 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index e77550b01dc..a221dc4f338 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -3080,6 +3080,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." diff --git a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html index 94c1df46eea..38ef7a4f1df 100644 --- a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html +++ b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html @@ -20,7 +20,13 @@ {{ "createdSendSuccessfully" | i18n }}

    - {{ formatExpirationDate() }} + @let translationKey = + send.authType === AuthType.Email + ? "sendCreatedDescriptionEmail" + : send.authType === AuthType.Password + ? "sendCreatedDescriptionPassword" + : "sendCreatedDescriptionV2"; + {{ translationKey | i18n: formattedExpirationTime }}

    From e262441999e4e243f903c8a781fcefc7906fa60c Mon Sep 17 00:00:00 2001 From: Dave <3836813+enmande@users.noreply.github.com> Date: Tue, 17 Feb 2026 17:43:00 -0500 Subject: [PATCH 063/134] [PM-31088] saltForUser should emit salt from master password unlock data (#18976) * feat(salt-for-user) [PM-31088]: Add feature flag for saltForUser. * feat(salt-for-user) [PM-31088]: Flag saltForUser logic to return unlockdata.salt or emailToSalt. * test(salt-for-user) [PM-31088]: Update tests to include coverage for new behavior. --- libs/common/src/enums/feature-flag.enum.ts | 2 + .../services/master-password.service.spec.ts | 47 +++++++++++++++++-- .../services/master-password.service.ts | 30 ++++++++++-- 3 files changed, 72 insertions(+), 7 deletions(-) diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 05fded6bcaf..71b95ec6057 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -19,6 +19,7 @@ export enum FeatureFlag { PM23801_PrefetchPasswordPrelogin = "pm-23801-prefetch-password-prelogin", PM27086_UpdateAuthenticationApisForInputPassword = "pm-27086-update-authentication-apis-for-input-password", SafariAccountSwitching = "pm-5594-safari-account-switching", + PM31088_MasterPasswordServiceEmitSalt = "pm-31088-master-password-service-emit-salt", /* Autofill */ UseUndeterminedCipherScenarioTriggeringLogic = "undetermined-cipher-scenario-logic", @@ -143,6 +144,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.PM23801_PrefetchPasswordPrelogin]: FALSE, [FeatureFlag.PM27086_UpdateAuthenticationApisForInputPassword]: FALSE, [FeatureFlag.SafariAccountSwitching]: FALSE, + [FeatureFlag.PM31088_MasterPasswordServiceEmitSalt]: FALSE, /* Billing */ [FeatureFlag.TrialPaymentOptional]: FALSE, diff --git a/libs/common/src/key-management/master-password/services/master-password.service.spec.ts b/libs/common/src/key-management/master-password/services/master-password.service.spec.ts index f72ae0e7c5e..4a96dedf024 100644 --- a/libs/common/src/key-management/master-password/services/master-password.service.spec.ts +++ b/libs/common/src/key-management/master-password/services/master-password.service.spec.ts @@ -17,8 +17,11 @@ import { mockAccountServiceWith, } from "../../../../spec"; import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason"; +import { FeatureFlag } from "../../../enums/feature-flag.enum"; +import { ServerConfig } from "../../../platform/abstractions/config/server-config"; import { LogService } from "../../../platform/abstractions/log.service"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; +import { USER_SERVER_CONFIG } from "../../../platform/services/config/default-config.service"; import { UserId } from "../../../types/guid"; import { MasterKey, UserKey } from "../../../types/key"; import { KeyGenerationService } from "../../crypto"; @@ -92,14 +95,52 @@ describe("MasterPasswordService", () => { sut.saltForUser$(null as unknown as UserId); }).toThrow("userId is null or undefined."); }); + // Removable with unwinding of PM31088_MasterPasswordServiceEmitSalt it("throws when userid present but not in account service", async () => { await expect( firstValueFrom(sut.saltForUser$("00000000-0000-0000-0000-000000000001" as UserId)), ).rejects.toThrow("Cannot read properties of undefined (reading 'email')"); }); - it("returns salt", async () => { - const salt = await firstValueFrom(sut.saltForUser$(userId)); - expect(salt).toBeDefined(); + // Removable with unwinding of PM31088_MasterPasswordServiceEmitSalt + it("returns email-derived salt for legacy path", async () => { + const result = await firstValueFrom(sut.saltForUser$(userId)); + // mockAccountServiceWith defaults email to "email" + expect(result).toBe("email" as MasterPasswordSalt); + }); + + describe("saltForUser$ master password unlock data migration path", () => { + // Flagged with PM31088_MasterPasswordServiceEmitSalt PM-31088 + beforeEach(() => { + stateProvider.singleUser.getFake(userId, USER_SERVER_CONFIG).nextState({ + featureStates: { + [FeatureFlag.PM31088_MasterPasswordServiceEmitSalt]: true, + }, + } as unknown as ServerConfig); + }); + + // Unwinding should promote these tests as part of saltForUser suite. + it("returns salt from master password unlock data", async () => { + const expectedSalt = "custom-salt" as MasterPasswordSalt; + const unlockData = new MasterPasswordUnlockData( + expectedSalt, + new PBKDF2KdfConfig(600_000), + makeEncString().toSdk() as MasterKeyWrappedUserKey, + ); + stateProvider.singleUser + .getFake(userId, MASTER_PASSWORD_UNLOCK_KEY) + .nextState(unlockData.toJSON()); + + const result = await firstValueFrom(sut.saltForUser$(userId)); + expect(result).toBe(expectedSalt); + }); + + it("throws when master password unlock data is null", async () => { + stateProvider.singleUser.getFake(userId, MASTER_PASSWORD_UNLOCK_KEY).nextState(null); + + await expect(firstValueFrom(sut.saltForUser$(userId))).rejects.toThrow( + "Master password unlock data not found for user.", + ); + }); }); }); diff --git a/libs/common/src/key-management/master-password/services/master-password.service.ts b/libs/common/src/key-management/master-password/services/master-password.service.ts index 28d4f58d7dc..f1a074ff14c 100644 --- a/libs/common/src/key-management/master-password/services/master-password.service.ts +++ b/libs/common/src/key-management/master-password/services/master-password.service.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom, map, Observable } from "rxjs"; +import { firstValueFrom, iif, map, Observable, switchMap } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { assertNonNullish } from "@bitwarden/common/auth/utils"; @@ -12,8 +12,10 @@ import { KdfConfig } from "@bitwarden/key-management"; import { PureCrypto } from "@bitwarden/sdk-internal"; import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason"; +import { FeatureFlag, getFeatureFlagValue } from "../../../enums/feature-flag.enum"; import { LogService } from "../../../platform/abstractions/log.service"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; +import { USER_SERVER_CONFIG } from "../../../platform/services/config/default-config.service"; import { MASTER_PASSWORD_DISK, MASTER_PASSWORD_MEMORY, @@ -102,9 +104,29 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr saltForUser$(userId: UserId): Observable { assertNonNullish(userId, "userId"); - return this.accountService.accounts$.pipe( - map((accounts) => accounts[userId].email), - map((email) => this.emailToSalt(email)), + + // Note: We can't use the config service as an abstraction here because it creates a circular dependency: ConfigService -> ConfigApiService -> ApiService -> VaultTimeoutSettingsService -> KeyService -> MP service. + return this.stateProvider.getUser(userId, USER_SERVER_CONFIG).state$.pipe( + map((serverConfig) => + getFeatureFlagValue(serverConfig, FeatureFlag.PM31088_MasterPasswordServiceEmitSalt), + ), + switchMap((enabled) => + iif( + () => enabled, + this.masterPasswordUnlockData$(userId).pipe( + map((unlockData) => { + if (unlockData == null) { + throw new Error("Master password unlock data not found for user."); + } + return unlockData.salt; + }), + ), + this.accountService.accounts$.pipe( + map((accounts) => accounts[userId].email), + map((email) => this.emailToSalt(email)), + ), + ), + ), ); } From 24c3b8fb2bfb51abb3b7674b1e000577f71f289d Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:43:23 -0800 Subject: [PATCH 064/134] fix autofill on click behavior (#19046) --- .../autofill-vault-list-items.component.html | 2 +- .../vault-list-items-container.component.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/browser/src/vault/popup/components/vault/autofill-vault-list-items/autofill-vault-list-items.component.html b/apps/browser/src/vault/popup/components/vault/autofill-vault-list-items/autofill-vault-list-items.component.html index 38d60233200..8ea65e77c5e 100644 --- a/apps/browser/src/vault/popup/components/vault/autofill-vault-list-items/autofill-vault-list-items.component.html +++ b/apps/browser/src/vault/popup/components/vault/autofill-vault-list-items/autofill-vault-list-items.component.html @@ -6,8 +6,8 @@ (onRefresh)="refreshCurrentTab()" [description]="(showEmptyAutofillTip$ | async) ? ('autofillSuggestionsTip' | i18n) : undefined" isAutofillList + showAutofillButton [disableDescriptionMargin]="showEmptyAutofillTip$ | async" [groupByType]="groupByType()" - [showAutofillButton]="(clickItemsToAutofillVaultView$ | async) === false" [primaryActionAutofill]="clickItemsToAutofillVaultView$ | async" > diff --git a/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.ts b/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.ts index fb8d20c5cf6..331ea799169 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.ts +++ b/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.ts @@ -302,8 +302,9 @@ export class VaultListItemsContainerComponent implements AfterViewInit { if (this.currentUriIsBlocked()) { return false; } - return this.isAutofillList() - ? this.simplifiedItemActionEnabled() + + return this.simplifiedItemActionEnabled() + ? this.isAutofillList() : this.primaryActionAutofill(); }); From ff775c7bbc4867a7d42c8bfceaa5acce1c570f9b Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:43:37 -0800 Subject: [PATCH 065/134] fix click on "Fill" text (#19047) --- .../vault-list-items-container.component.html | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.html b/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.html index e9e89776dde..69c548540eb 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.html +++ b/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.html @@ -90,7 +90,13 @@ - + } @if (showAutofillBadge()) { From ec33ea4f3c661050f458677aa1ecd7773be234df Mon Sep 17 00:00:00 2001 From: Alex <55413326+AlexRubik@users.noreply.github.com> Date: Tue, 17 Feb 2026 21:29:41 -0700 Subject: [PATCH 066/134] [PM-27782] Update Access Intelligence loading state text (#18808) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [PM-27782] Update Access Intelligence loading state text Simplify the loading progress messages shown during Access Intelligence report generation to be more user-friendly and concise. Changes: - Add new i18n keys with simplified text - Update ProgressStepConfig to use new keys Progress message updates: - "Fetching member data..." → "Reviewing member data..." - "Analyzing password health..." → "Analyzing passwords..." - "Calculating risk scores..." → "Calculating risks..." - "Generating report data..." → "Generating reports..." - "Saving report..." → "Compiling insights..." - "Compiling insights..." → "Done!" * delete old messages * remove all "this might take a few minutes" --- apps/web/src/locales/en/messages.json | 37 +++++++++---------- .../shared/report-loading.component.html | 13 ++----- .../shared/report-loading.component.ts | 12 +++--- 3 files changed, 27 insertions(+), 35 deletions(-) diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 970244119f8..cc73a04b81b 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -4596,29 +4596,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/report-loading.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/report-loading.component.html index 0b5a63c8f03..c816861b623 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/report-loading.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/report-loading.component.html @@ -10,14 +10,9 @@ >
    - -
    - - {{ stepConfig[progressStep()].message | i18n }} - - - {{ "thisMightTakeFewMinutes" | i18n }} - -
    + + + {{ stepConfig[progressStep()].message | i18n }} +
    diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/report-loading.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/report-loading.component.ts index 45b28dae470..9df729b9645 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/report-loading.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/report-loading.component.ts @@ -6,12 +6,12 @@ import { ProgressModule } from "@bitwarden/components"; // Map of progress step to display config const ProgressStepConfig = Object.freeze({ - [ReportProgress.FetchingMembers]: { message: "fetchingMemberData", progress: 20 }, - [ReportProgress.AnalyzingPasswords]: { message: "analyzingPasswordHealth", progress: 40 }, - [ReportProgress.CalculatingRisks]: { message: "calculatingRiskScores", progress: 60 }, - [ReportProgress.GeneratingReport]: { message: "generatingReportData", progress: 80 }, - [ReportProgress.Saving]: { message: "savingReport", progress: 95 }, - [ReportProgress.Complete]: { message: "compilingInsights", progress: 100 }, + [ReportProgress.FetchingMembers]: { message: "reviewingMemberData", progress: 20 }, + [ReportProgress.AnalyzingPasswords]: { message: "analyzingPasswords", progress: 40 }, + [ReportProgress.CalculatingRisks]: { message: "calculatingRisks", progress: 60 }, + [ReportProgress.GeneratingReport]: { message: "generatingReports", progress: 80 }, + [ReportProgress.Saving]: { message: "compilingInsightsProgress", progress: 95 }, + [ReportProgress.Complete]: { message: "reportGenerationDone", progress: 100 }, } as const); // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush From 03340aee7102f4c296b0e83e732bff7d7f14cf1c Mon Sep 17 00:00:00 2001 From: Alex <55413326+AlexRubik@users.noreply.github.com> Date: Tue, 17 Feb 2026 21:31:08 -0700 Subject: [PATCH 067/134] [PM-31163] stabilize table column widths with fixed layout (#18708) * stabilize table column widths with fixed layout (PM-31163) Add layout="fixed" and explicit width classes to report tables to prevent column widths from shifting during virtual scroll. Files changed: - weak-passwords-report.component.html - reused-passwords-report.component.html - exposed-passwords-report.component.html - inactive-two-factor-report.component.html - unsecured-websites-report.component.html * use auto width for name column to fix width calculation (PM-31163) Remove tw-w-1/2 from name column headers. With layout="fixed", the explicit percentages didn't sum to 100%, causing inconsistent column widths. Before: | 48px | 50% | 25% | 25% | = 48px + 100% (overflow) After: | 48px | auto | 25% | 25% | = columns sum correctly Name column now uses auto to fill remaining space. * render headers in Admin Console to fix column widths (PM-31163) Admin Console reports had a very wide icon column because no headers were rendered. Without headers, table-layout: fixed uses data row content to determine column widths, causing inconsistent sizing. Root cause: Three reports had their entire block inside @if (!isAdminConsoleActive), so when isAdminConsoleActive=true (Admin Console), no headers were rendered at all. Before (broken): @if (!isAdminConsoleActive) { Icon Name Owner } After (fixed): Icon Name @if (!isAdminConsoleActive) { Owner } This matches the pattern already used by weak-passwords-report and exposed-passwords-report, which were working correctly. Files changed: - unsecured-websites-report.component.html - reused-passwords-report.component.html - inactive-two-factor-report.component.html Result: - Admin Console now renders headers with correct column widths - Icon column is 48px (tw-w-12) as expected - Owner column properly hidden in Admin Console view * truncate long item names to prevent column overflow - you can hover cursor for tooltip to see full name --- .../exposed-passwords-report.component.html | 12 ++--- .../inactive-two-factor-report.component.html | 46 ++++++++++-------- .../reused-passwords-report.component.html | 48 ++++++++++--------- .../unsecured-websites-report.component.html | 46 ++++++++++-------- .../weak-passwords-report.component.html | 10 ++-- 5 files changed, 87 insertions(+), 75 deletions(-) diff --git a/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.html b/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.html index 144396d6772..56316fcddee 100644 --- a/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.html +++ b/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.html @@ -43,16 +43,16 @@ > } } - + - + {{ "name" | i18n }} @if (!isAdminConsoleActive) { - + {{ "owner" | i18n }} } - + {{ "timesExposed" | i18n }} @@ -60,7 +60,7 @@ - + @if (!organization || canManageCipher(row)) { } @else { - {{ row.name }} + {{ row.name }} } @if (!organization && row.organizationId) { } } - - @if (!isAdminConsoleActive) { - - - {{ "name" | i18n }} - {{ "owner" | i18n }} - - - } + + + + {{ "name" | i18n }} + @if (!isAdminConsoleActive) { + {{ "owner" | i18n }} + } + + - + @if (!organization || canManageCipher(row)) { {{ row.name }} } @else { - {{ row.name }} + {{ row.name }} } @if (!organization && row.organizationId) { {{ row.subTitle }} - - @if (!organization) { - - } - + @if (!isAdminConsoleActive) { + + @if (!organization) { + + } + + } @if (cipherDocs.has(row.id)) { diff --git a/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.html b/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.html index f08af8bda01..66bd11e7bc3 100644 --- a/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.html +++ b/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.html @@ -45,20 +45,20 @@ > } } - - @if (!isAdminConsoleActive) { - - - {{ "name" | i18n }} - {{ "owner" | i18n }} - {{ "timesReused" | i18n }} - - } + + + + {{ "name" | i18n }} + @if (!isAdminConsoleActive) { + {{ "owner" | i18n }} + } + {{ "timesReused" | i18n }} + - + @if (!organization || canManageCipher(row)) { {{ row.name }} } @else { - {{ row.name }} + {{ row.name }} } @if (!organization && row.organizationId) { {{ row.subTitle }} - - @if (!organization) { - - - } - + @if (!isAdminConsoleActive) { + + @if (!organization) { + + + } + + } {{ "reusedXTimes" | i18n: passwordUseMap.get(row.login.password) }} diff --git a/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.html b/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.html index 810c1e384b0..553c3f2f04e 100644 --- a/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.html +++ b/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.html @@ -45,19 +45,19 @@ > } } - - @if (!isAdminConsoleActive) { - - - {{ "name" | i18n }} - {{ "owner" | i18n }} - - } + + + + {{ "name" | i18n }} + @if (!isAdminConsoleActive) { + {{ "owner" | i18n }} + } + - + @if (!organization || canManageCipher(row)) { {{ row.name }} } @else { - {{ row.name }} + {{ row.name }} } @if (!organization && row.organizationId) { {{ row.subTitle }} - - @if (!organization) { - - - } - + @if (!isAdminConsoleActive) { + + @if (!organization) { + + + } + + } } diff --git a/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.html b/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.html index 5a187427b5e..fd5b916e661 100644 --- a/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.html +++ b/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.html @@ -45,12 +45,12 @@ > } } - + - + {{ "name" | i18n }} @if (!isAdminConsoleActive) { - + {{ "owner" | i18n }} } @@ -62,7 +62,7 @@ - + @if (!organization || canManageCipher(row)) { {{ row.name }} } @else { - {{ row.name }} + {{ row.name }} } @if (!organization && row.organizationId) { Date: Wed, 18 Feb 2026 09:32:08 +0100 Subject: [PATCH 068/134] Fix non-relative imports (#19022) --- tsconfig.base.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tsconfig.base.json b/tsconfig.base.json index 17f8f6d44fc..995eac031fd 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -24,8 +24,8 @@ "@bitwarden/assets/svg": ["./libs/assets/src/svg/index.ts"], "@bitwarden/auth/angular": ["./libs/auth/src/angular"], "@bitwarden/auth/common": ["./libs/auth/src/common"], - "@bitwarden/auto-confirm": ["libs/auto-confirm/src/index.ts"], - "@bitwarden/auto-confirm/angular": ["libs/auto-confirm/src/angular"], + "@bitwarden/auto-confirm": ["./libs/auto-confirm/src/index.ts"], + "@bitwarden/auto-confirm/angular": ["./libs/auto-confirm/src/angular"], "@bitwarden/billing": ["./libs/billing/src"], "@bitwarden/bit-common/*": ["./bitwarden_license/bit-common/src/*"], "@bitwarden/browser/*": ["./apps/browser/src/*"], From cf5e19463937c72f48bc3e8275558181de854bd2 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Wed, 18 Feb 2026 06:57:29 -0600 Subject: [PATCH 069/134] [BRE-1621] Fix Appx Release (#19043) * Revert to electron-builder appx manifest template * Remove comments * Remove unnecessary namespaces * Re-include Tamil translation files * Reinstate bitwarden protocol handler * Set minimum version to Windows 10 2016 Anniversary Update * Fix spacing --- apps/desktop/custom-appx-manifest.xml | 25 ++++++++++++----------- apps/desktop/electron-builder.beta.json | 1 - apps/desktop/electron-builder.json | 1 - apps/desktop/scripts/appx-cross-build.ps1 | 3 ++- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/apps/desktop/custom-appx-manifest.xml b/apps/desktop/custom-appx-manifest.xml index 166b852588b..8a5c36e7da6 100644 --- a/apps/desktop/custom-appx-manifest.xml +++ b/apps/desktop/custom-appx-manifest.xml @@ -1,17 +1,9 @@ - + xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10" + xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"> + @@ -87,8 +80,9 @@ xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/re - + + @@ -106,6 +100,13 @@ xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/re + + + + Bitwarden + + + diff --git a/apps/desktop/electron-builder.beta.json b/apps/desktop/electron-builder.beta.json index 9c66b17aa1f..f0746e6d408 100644 --- a/apps/desktop/electron-builder.beta.json +++ b/apps/desktop/electron-builder.beta.json @@ -61,7 +61,6 @@ "appx": { "artifactName": "Bitwarden-Beta-${version}-${arch}.${ext}", "backgroundColor": "#175DDC", - "customManifestPath": "./custom-appx-manifest.xml", "applicationId": "BitwardenBeta", "identityName": "8bitSolutionsLLC.BitwardenBeta", "publisher": "CN=14D52771-DE3C-4886-B8BF-825BA7690418", diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index 151ce72182d..f876b7ff680 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -176,7 +176,6 @@ "appx": { "artifactName": "${productName}-${version}-${arch}.${ext}", "backgroundColor": "#175DDC", - "customManifestPath": "./custom-appx-manifest.xml", "applicationId": "bitwardendesktop", "identityName": "8bitSolutionsLLC.bitwardendesktop", "publisher": "CN=14D52771-DE3C-4886-B8BF-825BA7690418", diff --git a/apps/desktop/scripts/appx-cross-build.ps1 b/apps/desktop/scripts/appx-cross-build.ps1 index ef2ab09104c..c47567695ed 100755 --- a/apps/desktop/scripts/appx-cross-build.ps1 +++ b/apps/desktop/scripts/appx-cross-build.ps1 @@ -72,6 +72,7 @@ param( # Whether to build in release mode. $Release=$false ) + $ErrorActionPreference = "Stop" $PSNativeCommandUseErrorActionPreference = $true $startTime = Get-Date @@ -113,7 +114,7 @@ else { $builderConfig = Get-Content $electronConfigFile | ConvertFrom-Json $packageConfig = Get-Content package.json | ConvertFrom-Json -$manifestTemplate = Get-Content $builderConfig.appx.customManifestPath +$manifestTemplate = Get-Content ($builderConfig.appx.customManifestPath ?? "custom-appx-manifest.xml") $srcDir = Get-Location $assetsDir = Get-Item $builderConfig.directories.buildResources From 51731c1526470bb20a4eb46a46a15c73b6746599 Mon Sep 17 00:00:00 2001 From: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> Date: Wed, 18 Feb 2026 14:32:21 +0000 Subject: [PATCH 070/134] Bumped client version(s) --- apps/desktop/package.json | 2 +- apps/desktop/src/package-lock.json | 4 ++-- apps/desktop/src/package.json | 2 +- package-lock.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index cd2147d21e4..5718c752a7c 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/desktop", "description": "A secure and free password manager for all of your devices.", - "version": "2026.2.0", + "version": "2026.2.1", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index 0aa188eba2f..01c429ab3d0 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2026.2.0", + "version": "2026.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2026.2.0", + "version": "2026.2.1", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-napi": "file:../desktop_native/napi" diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index 0076981ab60..fac797b5344 100644 --- a/apps/desktop/src/package.json +++ b/apps/desktop/src/package.json @@ -2,7 +2,7 @@ "name": "@bitwarden/desktop", "productName": "Bitwarden", "description": "A secure and free password manager for all of your devices.", - "version": "2026.2.0", + "version": "2026.2.1", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/package-lock.json b/package-lock.json index 8d3c32c027d..bf532fba66a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -232,7 +232,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2026.2.0", + "version": "2026.2.1", "hasInstallScript": true, "license": "GPL-3.0" }, From 5161a232f52cd714ad64b5ff6b4b42d4e07a43c6 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Wed, 18 Feb 2026 09:06:10 -0600 Subject: [PATCH 071/134] [PM-29055] Remove pm-25379-use-new-organization-metadata-structure feature flag (#18848) Remove the fully-enabled feature flag and simplify the billing metadata API to always use the vNext endpoints. The legacy API path is removed since the server will no longer serve it. - Remove FeatureFlag.PM25379_UseNewOrganizationMetadataStructure enum and default - Delete legacy getOrganizationBillingMetadata() API method (old /billing/metadata path) - Rename vNext methods to remove VNext suffix - Simplify OrganizationMetadataService to always use cached vNext path - Remove ConfigService dependency from OrganizationMetadataService - Update tests to remove feature flag branching --- .../src/services/jslib-services.module.ts | 2 +- .../billing-api.service.abstraction.ts | 6 +- .../billing/services/billing-api.service.ts | 16 +- .../organization-metadata.service.spec.ts | 264 ++++++------------ .../organization-metadata.service.ts | 57 +--- libs/common/src/enums/feature-flag.enum.ts | 2 - 6 files changed, 98 insertions(+), 249 deletions(-) diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 9d407f0f310..02ec9833d6f 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1528,7 +1528,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: OrganizationMetadataServiceAbstraction, useClass: DefaultOrganizationMetadataService, - deps: [BillingApiServiceAbstraction, ConfigService, PlatformUtilsServiceAbstraction], + deps: [BillingApiServiceAbstraction, PlatformUtilsServiceAbstraction], }), safeProvider({ provide: BillingAccountProfileStateService, diff --git a/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts b/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts index dcb395ef85c..9868a57bd78 100644 --- a/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts +++ b/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts @@ -21,11 +21,7 @@ export abstract class BillingApiServiceAbstraction { organizationId: OrganizationId, ): Promise; - abstract getOrganizationBillingMetadataVNext( - organizationId: OrganizationId, - ): Promise; - - abstract getOrganizationBillingMetadataVNextSelfHost( + abstract getOrganizationBillingMetadataSelfHost( organizationId: OrganizationId, ): Promise; diff --git a/libs/common/src/billing/services/billing-api.service.ts b/libs/common/src/billing/services/billing-api.service.ts index ae6913e545c..834606426db 100644 --- a/libs/common/src/billing/services/billing-api.service.ts +++ b/libs/common/src/billing/services/billing-api.service.ts @@ -36,20 +36,6 @@ export class BillingApiService implements BillingApiServiceAbstraction { async getOrganizationBillingMetadata( organizationId: OrganizationId, - ): Promise { - const r = await this.apiService.send( - "GET", - "/organizations/" + organizationId + "/billing/metadata", - null, - true, - true, - ); - - return new OrganizationBillingMetadataResponse(r); - } - - async getOrganizationBillingMetadataVNext( - organizationId: OrganizationId, ): Promise { const r = await this.apiService.send( "GET", @@ -62,7 +48,7 @@ export class BillingApiService implements BillingApiServiceAbstraction { return new OrganizationBillingMetadataResponse(r); } - async getOrganizationBillingMetadataVNextSelfHost( + async getOrganizationBillingMetadataSelfHost( organizationId: OrganizationId, ): Promise { const r = await this.apiService.send( diff --git a/libs/common/src/billing/services/organization/organization-metadata.service.spec.ts b/libs/common/src/billing/services/organization/organization-metadata.service.spec.ts index a2b012eb161..998356cbc14 100644 --- a/libs/common/src/billing/services/organization/organization-metadata.service.spec.ts +++ b/libs/common/src/billing/services/organization/organization-metadata.service.spec.ts @@ -1,13 +1,11 @@ import { mock } from "jest-mock-extended"; -import { BehaviorSubject, firstValueFrom } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { OrganizationBillingMetadataResponse } from "@bitwarden/common/billing/models/response/organization-billing-metadata.response"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { newGuid } from "@bitwarden/guid"; -import { FeatureFlag } from "../../../enums/feature-flag.enum"; import { OrganizationId } from "../../../types/guid"; import { DefaultOrganizationMetadataService } from "./organization-metadata.service"; @@ -15,9 +13,7 @@ import { DefaultOrganizationMetadataService } from "./organization-metadata.serv describe("DefaultOrganizationMetadataService", () => { let service: DefaultOrganizationMetadataService; let billingApiService: jest.Mocked; - let configService: jest.Mocked; let platformUtilsService: jest.Mocked; - let featureFlagSubject: BehaviorSubject; const mockOrganizationId = newGuid() as OrganizationId; const mockOrganizationId2 = newGuid() as OrganizationId; @@ -34,182 +30,114 @@ describe("DefaultOrganizationMetadataService", () => { beforeEach(() => { billingApiService = mock(); - configService = mock(); platformUtilsService = mock(); - featureFlagSubject = new BehaviorSubject(false); - configService.getFeatureFlag$.mockReturnValue(featureFlagSubject.asObservable()); platformUtilsService.isSelfHost.mockReturnValue(false); - service = new DefaultOrganizationMetadataService( - billingApiService, - configService, - platformUtilsService, - ); + service = new DefaultOrganizationMetadataService(billingApiService, platformUtilsService); }); afterEach(() => { jest.resetAllMocks(); - featureFlagSubject.complete(); }); describe("getOrganizationMetadata$", () => { - describe("feature flag OFF", () => { - beforeEach(() => { - featureFlagSubject.next(false); - }); + it("calls getOrganizationBillingMetadata for cloud-hosted", async () => { + const mockResponse = createMockMetadataResponse(false, 10); + billingApiService.getOrganizationBillingMetadata.mockResolvedValue(mockResponse); - it("calls getOrganizationBillingMetadata when feature flag is off", async () => { - const mockResponse = createMockMetadataResponse(false, 10); - billingApiService.getOrganizationBillingMetadata.mockResolvedValue(mockResponse); + const result = await firstValueFrom(service.getOrganizationMetadata$(mockOrganizationId)); - const result = await firstValueFrom(service.getOrganizationMetadata$(mockOrganizationId)); - - expect(configService.getFeatureFlag$).toHaveBeenCalledWith( - FeatureFlag.PM25379_UseNewOrganizationMetadataStructure, - ); - expect(billingApiService.getOrganizationBillingMetadata).toHaveBeenCalledWith( - mockOrganizationId, - ); - expect(billingApiService.getOrganizationBillingMetadataVNext).not.toHaveBeenCalled(); - expect(result).toEqual(mockResponse); - }); - - it("does not cache metadata when feature flag is off", async () => { - const mockResponse1 = createMockMetadataResponse(false, 10); - const mockResponse2 = createMockMetadataResponse(false, 15); - billingApiService.getOrganizationBillingMetadata - .mockResolvedValueOnce(mockResponse1) - .mockResolvedValueOnce(mockResponse2); - - const result1 = await firstValueFrom(service.getOrganizationMetadata$(mockOrganizationId)); - const result2 = await firstValueFrom(service.getOrganizationMetadata$(mockOrganizationId)); - - expect(billingApiService.getOrganizationBillingMetadata).toHaveBeenCalledTimes(2); - expect(result1).toEqual(mockResponse1); - expect(result2).toEqual(mockResponse2); - }); + expect(billingApiService.getOrganizationBillingMetadata).toHaveBeenCalledWith( + mockOrganizationId, + ); + expect(result).toEqual(mockResponse); }); - describe("feature flag ON", () => { - beforeEach(() => { - featureFlagSubject.next(true); - }); + it("calls getOrganizationBillingMetadataSelfHost when isSelfHost is true", async () => { + platformUtilsService.isSelfHost.mockReturnValue(true); + const mockResponse = createMockMetadataResponse(true, 25); + billingApiService.getOrganizationBillingMetadataSelfHost.mockResolvedValue(mockResponse); - it("calls getOrganizationBillingMetadataVNext when feature flag is on", async () => { - const mockResponse = createMockMetadataResponse(true, 15); - billingApiService.getOrganizationBillingMetadataVNext.mockResolvedValue(mockResponse); + const result = await firstValueFrom(service.getOrganizationMetadata$(mockOrganizationId)); - const result = await firstValueFrom(service.getOrganizationMetadata$(mockOrganizationId)); - - expect(configService.getFeatureFlag$).toHaveBeenCalledWith( - FeatureFlag.PM25379_UseNewOrganizationMetadataStructure, - ); - expect(billingApiService.getOrganizationBillingMetadataVNext).toHaveBeenCalledWith( - mockOrganizationId, - ); - expect(billingApiService.getOrganizationBillingMetadata).not.toHaveBeenCalled(); - expect(result).toEqual(mockResponse); - }); - - it("caches metadata by organization ID when feature flag is on", async () => { - const mockResponse = createMockMetadataResponse(true, 10); - billingApiService.getOrganizationBillingMetadataVNext.mockResolvedValue(mockResponse); - - const result1 = await firstValueFrom(service.getOrganizationMetadata$(mockOrganizationId)); - const result2 = await firstValueFrom(service.getOrganizationMetadata$(mockOrganizationId)); - - expect(billingApiService.getOrganizationBillingMetadataVNext).toHaveBeenCalledTimes(1); - expect(result1).toEqual(mockResponse); - expect(result2).toEqual(mockResponse); - }); - - it("maintains separate cache entries for different organization IDs", async () => { - const mockResponse1 = createMockMetadataResponse(true, 10); - const mockResponse2 = createMockMetadataResponse(false, 20); - billingApiService.getOrganizationBillingMetadataVNext - .mockResolvedValueOnce(mockResponse1) - .mockResolvedValueOnce(mockResponse2); - - const result1 = await firstValueFrom(service.getOrganizationMetadata$(mockOrganizationId)); - const result2 = await firstValueFrom(service.getOrganizationMetadata$(mockOrganizationId2)); - const result3 = await firstValueFrom(service.getOrganizationMetadata$(mockOrganizationId)); - const result4 = await firstValueFrom(service.getOrganizationMetadata$(mockOrganizationId2)); - - expect(billingApiService.getOrganizationBillingMetadataVNext).toHaveBeenCalledTimes(2); - expect(billingApiService.getOrganizationBillingMetadataVNext).toHaveBeenNthCalledWith( - 1, - mockOrganizationId, - ); - expect(billingApiService.getOrganizationBillingMetadataVNext).toHaveBeenNthCalledWith( - 2, - mockOrganizationId2, - ); - expect(result1).toEqual(mockResponse1); - expect(result2).toEqual(mockResponse2); - expect(result3).toEqual(mockResponse1); - expect(result4).toEqual(mockResponse2); - }); - - it("calls getOrganizationBillingMetadataVNextSelfHost when feature flag is on and isSelfHost is true", async () => { - platformUtilsService.isSelfHost.mockReturnValue(true); - const mockResponse = createMockMetadataResponse(true, 25); - billingApiService.getOrganizationBillingMetadataVNextSelfHost.mockResolvedValue( - mockResponse, - ); - - const result = await firstValueFrom(service.getOrganizationMetadata$(mockOrganizationId)); - - expect(platformUtilsService.isSelfHost).toHaveBeenCalled(); - expect(billingApiService.getOrganizationBillingMetadataVNextSelfHost).toHaveBeenCalledWith( - mockOrganizationId, - ); - expect(billingApiService.getOrganizationBillingMetadataVNext).not.toHaveBeenCalled(); - expect(billingApiService.getOrganizationBillingMetadata).not.toHaveBeenCalled(); - expect(result).toEqual(mockResponse); - }); + expect(platformUtilsService.isSelfHost).toHaveBeenCalled(); + expect(billingApiService.getOrganizationBillingMetadataSelfHost).toHaveBeenCalledWith( + mockOrganizationId, + ); + expect(billingApiService.getOrganizationBillingMetadata).not.toHaveBeenCalled(); + expect(result).toEqual(mockResponse); }); - describe("shareReplay behavior", () => { - beforeEach(() => { - featureFlagSubject.next(true); - }); + it("caches metadata by organization ID", async () => { + const mockResponse = createMockMetadataResponse(true, 10); + billingApiService.getOrganizationBillingMetadata.mockResolvedValue(mockResponse); - it("does not call API multiple times when the same cached observable is subscribed to multiple times", async () => { - const mockResponse = createMockMetadataResponse(true, 10); - billingApiService.getOrganizationBillingMetadataVNext.mockResolvedValue(mockResponse); + const result1 = await firstValueFrom(service.getOrganizationMetadata$(mockOrganizationId)); + const result2 = await firstValueFrom(service.getOrganizationMetadata$(mockOrganizationId)); - const metadata$ = service.getOrganizationMetadata$(mockOrganizationId); + expect(billingApiService.getOrganizationBillingMetadata).toHaveBeenCalledTimes(1); + expect(result1).toEqual(mockResponse); + expect(result2).toEqual(mockResponse); + }); - const subscription1Promise = firstValueFrom(metadata$); - const subscription2Promise = firstValueFrom(metadata$); - const subscription3Promise = firstValueFrom(metadata$); + it("maintains separate cache entries for different organization IDs", async () => { + const mockResponse1 = createMockMetadataResponse(true, 10); + const mockResponse2 = createMockMetadataResponse(false, 20); + billingApiService.getOrganizationBillingMetadata + .mockResolvedValueOnce(mockResponse1) + .mockResolvedValueOnce(mockResponse2); - const [result1, result2, result3] = await Promise.all([ - subscription1Promise, - subscription2Promise, - subscription3Promise, - ]); + const result1 = await firstValueFrom(service.getOrganizationMetadata$(mockOrganizationId)); + const result2 = await firstValueFrom(service.getOrganizationMetadata$(mockOrganizationId2)); + const result3 = await firstValueFrom(service.getOrganizationMetadata$(mockOrganizationId)); + const result4 = await firstValueFrom(service.getOrganizationMetadata$(mockOrganizationId2)); - expect(billingApiService.getOrganizationBillingMetadataVNext).toHaveBeenCalledTimes(1); - expect(result1).toEqual(mockResponse); - expect(result2).toEqual(mockResponse); - expect(result3).toEqual(mockResponse); - }); + expect(billingApiService.getOrganizationBillingMetadata).toHaveBeenCalledTimes(2); + expect(billingApiService.getOrganizationBillingMetadata).toHaveBeenNthCalledWith( + 1, + mockOrganizationId, + ); + expect(billingApiService.getOrganizationBillingMetadata).toHaveBeenNthCalledWith( + 2, + mockOrganizationId2, + ); + expect(result1).toEqual(mockResponse1); + expect(result2).toEqual(mockResponse2); + expect(result3).toEqual(mockResponse1); + expect(result4).toEqual(mockResponse2); + }); + + it("does not call API multiple times when the same cached observable is subscribed to multiple times", async () => { + const mockResponse = createMockMetadataResponse(true, 10); + billingApiService.getOrganizationBillingMetadata.mockResolvedValue(mockResponse); + + const metadata$ = service.getOrganizationMetadata$(mockOrganizationId); + + const subscription1Promise = firstValueFrom(metadata$); + const subscription2Promise = firstValueFrom(metadata$); + const subscription3Promise = firstValueFrom(metadata$); + + const [result1, result2, result3] = await Promise.all([ + subscription1Promise, + subscription2Promise, + subscription3Promise, + ]); + + expect(billingApiService.getOrganizationBillingMetadata).toHaveBeenCalledTimes(1); + expect(result1).toEqual(mockResponse); + expect(result2).toEqual(mockResponse); + expect(result3).toEqual(mockResponse); }); }); describe("refreshMetadataCache", () => { - beforeEach(() => { - featureFlagSubject.next(true); - }); - - it("refreshes cached metadata when called with feature flag on", (done) => { + it("refreshes cached metadata when called", (done) => { const mockResponse1 = createMockMetadataResponse(true, 10); const mockResponse2 = createMockMetadataResponse(true, 20); let invocationCount = 0; - billingApiService.getOrganizationBillingMetadataVNext + billingApiService.getOrganizationBillingMetadata .mockResolvedValueOnce(mockResponse1) .mockResolvedValueOnce(mockResponse2); @@ -221,7 +149,7 @@ describe("DefaultOrganizationMetadataService", () => { expect(result).toEqual(mockResponse1); } else if (invocationCount === 2) { expect(result).toEqual(mockResponse2); - expect(billingApiService.getOrganizationBillingMetadataVNext).toHaveBeenCalledTimes(2); + expect(billingApiService.getOrganizationBillingMetadata).toHaveBeenCalledTimes(2); subscription.unsubscribe(); done(); } @@ -234,45 +162,13 @@ describe("DefaultOrganizationMetadataService", () => { }, 10); }); - it("does trigger refresh when feature flag is disabled", async () => { - featureFlagSubject.next(false); - - const mockResponse1 = createMockMetadataResponse(false, 10); - const mockResponse2 = createMockMetadataResponse(false, 20); - let invocationCount = 0; - - billingApiService.getOrganizationBillingMetadata - .mockResolvedValueOnce(mockResponse1) - .mockResolvedValueOnce(mockResponse2); - - const subscription = service.getOrganizationMetadata$(mockOrganizationId).subscribe({ - next: () => { - invocationCount++; - }, - }); - - // wait for initial invocation - await new Promise((resolve) => setTimeout(resolve, 10)); - - expect(invocationCount).toBe(1); - - service.refreshMetadataCache(); - - await new Promise((resolve) => setTimeout(resolve, 10)); - - expect(invocationCount).toBe(2); - expect(billingApiService.getOrganizationBillingMetadata).toHaveBeenCalledTimes(2); - - subscription.unsubscribe(); - }); - it("bypasses cache when refreshing metadata", (done) => { const mockResponse1 = createMockMetadataResponse(true, 10); const mockResponse2 = createMockMetadataResponse(true, 20); const mockResponse3 = createMockMetadataResponse(true, 30); let invocationCount = 0; - billingApiService.getOrganizationBillingMetadataVNext + billingApiService.getOrganizationBillingMetadata .mockResolvedValueOnce(mockResponse1) .mockResolvedValueOnce(mockResponse2) .mockResolvedValueOnce(mockResponse3); @@ -289,7 +185,7 @@ describe("DefaultOrganizationMetadataService", () => { service.refreshMetadataCache(); } else if (invocationCount === 3) { expect(result).toEqual(mockResponse3); - expect(billingApiService.getOrganizationBillingMetadataVNext).toHaveBeenCalledTimes(3); + expect(billingApiService.getOrganizationBillingMetadata).toHaveBeenCalledTimes(3); subscription.unsubscribe(); done(); } diff --git a/libs/common/src/billing/services/organization/organization-metadata.service.ts b/libs/common/src/billing/services/organization/organization-metadata.service.ts index 5ce87262c4b..149c4536df4 100644 --- a/libs/common/src/billing/services/organization/organization-metadata.service.ts +++ b/libs/common/src/billing/services/organization/organization-metadata.service.ts @@ -1,10 +1,8 @@ -import { BehaviorSubject, combineLatest, from, Observable, shareReplay, switchMap } from "rxjs"; +import { BehaviorSubject, from, Observable, shareReplay, switchMap } from "rxjs"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { FeatureFlag } from "../../../enums/feature-flag.enum"; -import { ConfigService } from "../../../platform/abstractions/config/config.service"; import { OrganizationId } from "../../../types/guid"; import { OrganizationMetadataServiceAbstraction } from "../../abstractions/organization-metadata.service.abstraction"; import { OrganizationBillingMetadataResponse } from "../../models/response/organization-billing-metadata.response"; @@ -17,7 +15,6 @@ export class DefaultOrganizationMetadataService implements OrganizationMetadataS constructor( private billingApiService: BillingApiServiceAbstraction, - private configService: ConfigService, private platformUtilsService: PlatformUtilsService, ) {} private refreshMetadataTrigger = new BehaviorSubject(undefined); @@ -28,50 +25,26 @@ export class DefaultOrganizationMetadataService implements OrganizationMetadataS }; getOrganizationMetadata$(orgId: OrganizationId): Observable { - return combineLatest([ - this.refreshMetadataTrigger, - this.configService.getFeatureFlag$(FeatureFlag.PM25379_UseNewOrganizationMetadataStructure), - ]).pipe( - switchMap(([_, featureFlagEnabled]) => - featureFlagEnabled - ? this.vNextGetOrganizationMetadataInternal$(orgId) - : this.getOrganizationMetadataInternal$(orgId), - ), - ); - } - - private vNextGetOrganizationMetadataInternal$( - orgId: OrganizationId, - ): Observable { - const cacheHit = this.metadataCache.get(orgId); - if (cacheHit) { - return cacheHit; - } - - const result = from(this.fetchMetadata(orgId, true)).pipe( - shareReplay({ bufferSize: 1, refCount: false }), - ); - - this.metadataCache.set(orgId, result); - return result; - } - - private getOrganizationMetadataInternal$( - organizationId: OrganizationId, - ): Observable { - return from(this.fetchMetadata(organizationId, false)).pipe( - shareReplay({ bufferSize: 1, refCount: false }), + return this.refreshMetadataTrigger.pipe( + switchMap(() => { + const cacheHit = this.metadataCache.get(orgId); + if (cacheHit) { + return cacheHit; + } + const result = from(this.fetchMetadata(orgId)).pipe( + shareReplay({ bufferSize: 1, refCount: false }), + ); + this.metadataCache.set(orgId, result); + return result; + }), ); } private async fetchMetadata( organizationId: OrganizationId, - featureFlagEnabled: boolean, ): Promise { - return featureFlagEnabled - ? this.platformUtilsService.isSelfHost() - ? await this.billingApiService.getOrganizationBillingMetadataVNextSelfHost(organizationId) - : await this.billingApiService.getOrganizationBillingMetadataVNext(organizationId) + return this.platformUtilsService.isSelfHost() + ? await this.billingApiService.getOrganizationBillingMetadataSelfHost(organizationId) : await this.billingApiService.getOrganizationBillingMetadata(organizationId); } } diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 71b95ec6057..d252f7dcda5 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -31,7 +31,6 @@ export enum FeatureFlag { /* Billing */ TrialPaymentOptional = "PM-8163-trial-payment", PM24032_NewNavigationPremiumUpgradeButton = "pm-24032-new-navigation-premium-upgrade-button", - PM25379_UseNewOrganizationMetadataStructure = "pm-25379-use-new-organization-metadata-structure", PM26793_FetchPremiumPriceFromPricingService = "pm-26793-fetch-premium-price-from-pricing-service", PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog = "pm-23713-premium-badge-opens-new-premium-upgrade-dialog", PM26462_Milestone_3 = "pm-26462-milestone-3", @@ -149,7 +148,6 @@ export const DefaultFeatureFlagValue = { /* Billing */ [FeatureFlag.TrialPaymentOptional]: FALSE, [FeatureFlag.PM24032_NewNavigationPremiumUpgradeButton]: FALSE, - [FeatureFlag.PM25379_UseNewOrganizationMetadataStructure]: FALSE, [FeatureFlag.PM26793_FetchPremiumPriceFromPricingService]: FALSE, [FeatureFlag.PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog]: FALSE, [FeatureFlag.PM26462_Milestone_3]: FALSE, From dda862a8c6924d7e202be91f032b6c9c5037d085 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Wed, 18 Feb 2026 09:39:58 -0600 Subject: [PATCH 072/134] Revert "Bumped client version(s)" (#19062) This reverts commit 51731c1526470bb20a4eb46a46a15c73b6746599. The desktop version was bumped erroneously, skipping 2026.2.0. --- apps/desktop/package.json | 2 +- apps/desktop/src/package-lock.json | 4 ++-- apps/desktop/src/package.json | 2 +- package-lock.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 5718c752a7c..cd2147d21e4 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/desktop", "description": "A secure and free password manager for all of your devices.", - "version": "2026.2.1", + "version": "2026.2.0", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index 01c429ab3d0..0aa188eba2f 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2026.2.1", + "version": "2026.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2026.2.1", + "version": "2026.2.0", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-napi": "file:../desktop_native/napi" diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index fac797b5344..0076981ab60 100644 --- a/apps/desktop/src/package.json +++ b/apps/desktop/src/package.json @@ -2,7 +2,7 @@ "name": "@bitwarden/desktop", "productName": "Bitwarden", "description": "A secure and free password manager for all of your devices.", - "version": "2026.2.1", + "version": "2026.2.0", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/package-lock.json b/package-lock.json index bf532fba66a..8d3c32c027d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -232,7 +232,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2026.2.1", + "version": "2026.2.0", "hasInstallScript": true, "license": "GPL-3.0" }, From 1ef8f257b0120fbef5f22e1828e2facea9a9913e Mon Sep 17 00:00:00 2001 From: Alex <55413326+AlexRubik@users.noreply.github.com> Date: Wed, 18 Feb 2026 09:00:36 -0700 Subject: [PATCH 073/134] [PM-31803] Fix Password Manager reports not displaying items with limited collection access (#18956) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When "Owners and admins can manage all collections and items" is OFF, Password Manager reports incorrectly filter out items from collections where the user has "Can view", "Can view except passwords", or "Can edit except passwords" access. The root cause is that all five PM report components filter ciphers using `(!this.organization && !edit) || !viewPassword`. Since PM reports run without an organization context (this.organization is undefined), this condition excludes any item where edit=false or viewPassword=false. These permission checks are unnecessary for PM reports because: 1. Personal vault items always have edit=true and viewPassword=true, so the checks never applied to them. 2. Organization items should appear in reports regardless of permission level — the user has collection access, and edit restrictions should only affect the item dialog, not report visibility. 3. Admin Console reports (which work correctly) skip this filtering because this.organization is always set, making the condition always false. This also explains why "Can edit except passwords" items only appeared in the Unsecured Websites report — it was the only report that didn't check !viewPassword. Removed the edit/viewPassword filter conditions from all five PM report components: - exposed-passwords-report - weak-passwords-report - reused-passwords-report - inactive-two-factor-report - unsecured-websites-report --- ...exposed-passwords-report.component.spec.ts | 15 +++++------- .../exposed-passwords-report.component.ts | 6 ++--- ...active-two-factor-report.component.spec.ts | 23 ++++++++----------- .../inactive-two-factor-report.component.ts | 6 ++--- .../reused-passwords-report.component.spec.ts | 14 +++++------ .../reused-passwords-report.component.ts | 6 ++--- ...nsecured-websites-report.component.spec.ts | 11 ++++----- .../unsecured-websites-report.component.ts | 7 +----- .../weak-passwords-report.component.spec.ts | 15 +++++------- .../pages/weak-passwords-report.component.ts | 11 ++------- 10 files changed, 41 insertions(+), 73 deletions(-) diff --git a/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.spec.ts b/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.spec.ts index e056ec44af5..81e4a78b491 100644 --- a/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.spec.ts +++ b/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.spec.ts @@ -122,19 +122,16 @@ describe("ExposedPasswordsReportComponent", () => { expect(component).toBeTruthy(); }); - it('should get only ciphers with exposed passwords that the user has "Can Edit" access to', async () => { - const expectedIdOne: any = "cbea34a8-bde4-46ad-9d19-b05001228ab2"; - const expectedIdTwo = "cbea34a8-bde4-46ad-9d19-b05001228cd3"; - + it("should get ciphers with exposed passwords regardless of edit access", async () => { jest.spyOn(auditService, "passwordLeaked").mockReturnValue(Promise.resolve(1234)); jest.spyOn(component as any, "getAllCiphers").mockReturnValue(Promise.resolve(cipherData)); await component.setCiphers(); - expect(component.ciphers.length).toEqual(2); - expect(component.ciphers[0].id).toEqual(expectedIdOne); - expect(component.ciphers[0].edit).toEqual(true); - expect(component.ciphers[1].id).toEqual(expectedIdTwo); - expect(component.ciphers[1].edit).toEqual(true); + const cipherIds = component.ciphers.map((c) => c.id); + expect(cipherIds).toContain("cbea34a8-bde4-46ad-9d19-b05001228ab1"); + expect(cipherIds).toContain("cbea34a8-bde4-46ad-9d19-b05001228ab2"); + expect(cipherIds).toContain("cbea34a8-bde4-46ad-9d19-b05001228cd3"); + expect(component.ciphers.length).toEqual(3); }); it("should call fullSync method of syncService", () => { diff --git a/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.ts b/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.ts index 51bdde3eda8..e39ef811d66 100644 --- a/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.ts @@ -64,14 +64,12 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple this.filterStatus = [0]; allCiphers.forEach((ciph) => { - const { type, login, isDeleted, edit, viewPassword } = ciph; + const { type, login, isDeleted } = ciph; if ( type !== CipherType.Login || login.password == null || login.password === "" || - isDeleted || - (!this.organization && !edit) || - !viewPassword + isDeleted ) { return; } diff --git a/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.spec.ts b/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.spec.ts index 12453ea3b88..07a772755f5 100644 --- a/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.spec.ts +++ b/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.spec.ts @@ -95,9 +95,7 @@ describe("InactiveTwoFactorReportComponent", () => { expect(component).toBeTruthy(); }); - it('should get only ciphers with domains in the 2fa directory that they have "Can Edit" access to', async () => { - const expectedIdOne: any = "cbea34a8-bde4-46ad-9d19-b05001228xy4"; - const expectedIdTwo: any = "cbea34a8-bde4-46ad-9d19-b05001227nm5"; + it("should get ciphers with domains in the 2fa directory regardless of edit access", async () => { component.services.set( "101domain.com", "https://help.101domain.com/account-management/account-security/enabling-disabling-two-factor-verification", @@ -110,11 +108,10 @@ describe("InactiveTwoFactorReportComponent", () => { jest.spyOn(component as any, "getAllCiphers").mockReturnValue(Promise.resolve(cipherData)); await component.setCiphers(); + const cipherIds = component.ciphers.map((c) => c.id); + expect(cipherIds).toContain("cbea34a8-bde4-46ad-9d19-b05001228xy4"); + expect(cipherIds).toContain("cbea34a8-bde4-46ad-9d19-b05001227nm5"); expect(component.ciphers.length).toEqual(2); - expect(component.ciphers[0].id).toEqual(expectedIdOne); - expect(component.ciphers[0].edit).toEqual(true); - expect(component.ciphers[1].id).toEqual(expectedIdTwo); - expect(component.ciphers[1].edit).toEqual(true); }); it("should call fullSync method of syncService", () => { @@ -197,7 +194,7 @@ describe("InactiveTwoFactorReportComponent", () => { expect(doc).toBe(""); }); - it("should return false if cipher does not have edit access and no organization", () => { + it("should return true for cipher without edit access", () => { component.organization = null; const cipher = createCipherView({ edit: false, @@ -206,11 +203,11 @@ describe("InactiveTwoFactorReportComponent", () => { }, }); const [doc, isInactive] = (component as any).isInactive2faCipher(cipher); - expect(isInactive).toBe(false); - expect(doc).toBe(""); + expect(isInactive).toBe(true); + expect(doc).toBe("https://example.com/2fa-doc"); }); - it("should return false if cipher does not have viewPassword", () => { + it("should return true for cipher without viewPassword", () => { const cipher = createCipherView({ viewPassword: false, login: { @@ -218,8 +215,8 @@ describe("InactiveTwoFactorReportComponent", () => { }, }); const [doc, isInactive] = (component as any).isInactive2faCipher(cipher); - expect(isInactive).toBe(false); - expect(doc).toBe(""); + expect(isInactive).toBe(true); + expect(doc).toBe("https://example.com/2fa-doc"); }); it("should check all uris and return true if any matches domain or host", () => { diff --git a/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.ts b/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.ts index 9d7de688f3e..cd892130518 100644 --- a/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.ts @@ -92,14 +92,12 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl let docFor2fa: string = ""; let isInactive2faCipher: boolean = false; - const { type, login, isDeleted, edit, viewPassword } = cipher; + const { type, login, isDeleted } = cipher; if ( type !== CipherType.Login || (login.totp != null && login.totp !== "") || !login.hasUris || - isDeleted || - (!this.organization && !edit) || - !viewPassword + isDeleted ) { return [docFor2fa, isInactive2faCipher]; } diff --git a/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.spec.ts b/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.spec.ts index 1b7006d0c68..8f08d06e27b 100644 --- a/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.spec.ts +++ b/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.spec.ts @@ -109,17 +109,15 @@ describe("ReusedPasswordsReportComponent", () => { expect(component).toBeTruthy(); }); - it('should get ciphers with reused passwords that the user has "Can Edit" access to', async () => { - const expectedIdOne: any = "cbea34a8-bde4-46ad-9d19-b05001228ab2"; - const expectedIdTwo = "cbea34a8-bde4-46ad-9d19-b05001228cd3"; + it("should get ciphers with reused passwords regardless of edit access", async () => { jest.spyOn(component as any, "getAllCiphers").mockReturnValue(Promise.resolve(cipherData)); await component.setCiphers(); - expect(component.ciphers.length).toEqual(2); - expect(component.ciphers[0].id).toEqual(expectedIdOne); - expect(component.ciphers[0].edit).toEqual(true); - expect(component.ciphers[1].id).toEqual(expectedIdTwo); - expect(component.ciphers[1].edit).toEqual(true); + const cipherIds = component.ciphers.map((c) => c.id); + expect(cipherIds).toContain("cbea34a8-bde4-46ad-9d19-b05001228ab1"); + expect(cipherIds).toContain("cbea34a8-bde4-46ad-9d19-b05001228ab2"); + expect(cipherIds).toContain("cbea34a8-bde4-46ad-9d19-b05001228cd3"); + expect(component.ciphers.length).toEqual(3); }); it("should call fullSync method of syncService", () => { diff --git a/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.ts b/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.ts index 0a81b19d4ff..7d24e61f276 100644 --- a/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.ts @@ -71,14 +71,12 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem this.filterStatus = [0]; ciphers.forEach((ciph) => { - const { type, login, isDeleted, edit, viewPassword } = ciph; + const { type, login, isDeleted } = ciph; if ( type !== CipherType.Login || login.password == null || login.password === "" || - isDeleted || - (!this.organization && !edit) || - !viewPassword + isDeleted ) { return; } diff --git a/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.spec.ts b/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.spec.ts index 2107e0c8df7..f116faf114f 100644 --- a/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.spec.ts +++ b/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.spec.ts @@ -118,17 +118,14 @@ describe("UnsecuredWebsitesReportComponent", () => { expect(component).toBeTruthy(); }); - it('should get only unsecured ciphers that the user has "Can Edit" access to', async () => { - const expectedIdOne: any = "cbea34a8-bde4-46ad-9d19-b05001228ab2"; - const expectedIdTwo = "cbea34a8-bde4-46ad-9d19-b05001228cd3"; + it("should get unsecured ciphers regardless of edit access", async () => { jest.spyOn(component as any, "getAllCiphers").mockReturnValue(Promise.resolve(cipherData)); await component.setCiphers(); + const cipherIds = component.ciphers.map((c) => c.id); + expect(cipherIds).toContain("cbea34a8-bde4-46ad-9d19-b05001228ab2"); + expect(cipherIds).toContain("cbea34a8-bde4-46ad-9d19-b05001228cd3"); expect(component.ciphers.length).toEqual(2); - expect(component.ciphers[0].id).toEqual(expectedIdOne); - expect(component.ciphers[0].edit).toEqual(true); - expect(component.ciphers[1].id).toEqual(expectedIdTwo); - expect(component.ciphers[1].edit).toEqual(true); }); it("should call fullSync method of syncService", () => { diff --git a/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.ts b/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.ts index 4a2c0677574..8399395d273 100644 --- a/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.ts @@ -71,12 +71,7 @@ export class UnsecuredWebsitesReportComponent extends CipherReportComponent impl * @param cipher Current cipher with unsecured uri */ private cipherContainsUnsecured(cipher: CipherView): boolean { - if ( - cipher.type !== CipherType.Login || - !cipher.login.hasUris || - cipher.isDeleted || - (!this.organization && !cipher.edit) - ) { + if (cipher.type !== CipherType.Login || !cipher.login.hasUris || cipher.isDeleted) { return false; } diff --git a/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.spec.ts b/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.spec.ts index a63723dc688..f9aca0aa378 100644 --- a/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.spec.ts +++ b/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.spec.ts @@ -114,10 +114,7 @@ describe("WeakPasswordsReportComponent", () => { expect(component).toBeTruthy(); }); - it('should get only ciphers with weak passwords that the user has "Can Edit" access to', async () => { - const expectedIdOne: any = "cbea34a8-bde4-46ad-9d19-b05001228ab2"; - const expectedIdTwo = "cbea34a8-bde4-46ad-9d19-b05001228cd3"; - + it("should get ciphers with weak passwords regardless of edit access", async () => { jest.spyOn(passwordStrengthService, "getPasswordStrength").mockReturnValue({ password: "123", score: 0, @@ -125,11 +122,11 @@ describe("WeakPasswordsReportComponent", () => { jest.spyOn(component as any, "getAllCiphers").mockReturnValue(Promise.resolve(cipherData)); await component.setCiphers(); - expect(component.ciphers.length).toEqual(2); - expect(component.ciphers[0].id).toEqual(expectedIdOne); - expect(component.ciphers[0].edit).toEqual(true); - expect(component.ciphers[1].id).toEqual(expectedIdTwo); - expect(component.ciphers[1].edit).toEqual(true); + const cipherIds = component.ciphers.map((c) => c.id); + expect(cipherIds).toContain("cbea34a8-bde4-46ad-9d19-b05001228ab1"); + expect(cipherIds).toContain("cbea34a8-bde4-46ad-9d19-b05001228ab2"); + expect(cipherIds).toContain("cbea34a8-bde4-46ad-9d19-b05001228cd3"); + expect(component.ciphers.length).toEqual(3); }); it("should call fullSync method of syncService", () => { diff --git a/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.ts b/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.ts index bb5400346fd..6cde01f2d92 100644 --- a/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.ts @@ -103,15 +103,8 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen } protected determineWeakPasswordScore(ciph: CipherView): ReportResult | null { - const { type, login, isDeleted, edit, viewPassword } = ciph; - if ( - type !== CipherType.Login || - login.password == null || - login.password === "" || - isDeleted || - (!this.organization && !edit) || - !viewPassword - ) { + const { type, login, isDeleted } = ciph; + if (type !== CipherType.Login || login.password == null || login.password === "" || isDeleted) { return; } From bc6b1c3b831778b64e75988e104dbec361113a4c Mon Sep 17 00:00:00 2001 From: John Harrington <84741727+harr1424@users.noreply.github.com> Date: Wed, 18 Feb 2026 09:34:57 -0700 Subject: [PATCH 074/134] [PM-32242] Error message is incorrectly formatted for password protected Send (#18991) * re-work error display to match design specs * fix password auth in attemptV1Access * fix locales file (formatting) --- .../send/send-access/send-auth.component.ts | 27 ++++++++++++++++--- apps/web/src/locales/en/messages.json | 8 ++++-- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/apps/web/src/app/tools/send/send-access/send-auth.component.ts b/apps/web/src/app/tools/send/send-access/send-auth.component.ts index 8c630ce5315..92c3d445333 100644 --- a/apps/web/src/app/tools/send/send-access/send-auth.component.ts +++ b/apps/web/src/app/tools/send/send-access/send-auth.component.ts @@ -104,7 +104,27 @@ export class SendAuthComponent implements OnInit { } catch (e) { if (e instanceof ErrorResponse) { if (e.statusCode === 401) { + if (this.sendAuthType() === AuthType.Password) { + // Password was already required, so this is an invalid password error + const passwordControl = this.sendAccessForm.get("password"); + if (passwordControl) { + passwordControl.setErrors({ + invalidPassword: { message: this.i18nService.t("sendPasswordInvalidAskOwner") }, + }); + passwordControl.markAsTouched(); + } + } + // Set auth type to Password (either first time or refresh) this.sendAuthType.set(AuthType.Password); + } else if (e.statusCode === 400 && this.sendAuthType() === AuthType.Password) { + // Server returns 400 for SendAccessResult.PasswordInvalid + const passwordControl = this.sendAccessForm.get("password"); + if (passwordControl) { + passwordControl.setErrors({ + invalidPassword: { message: this.i18nService.t("sendPasswordInvalidAskOwner") }, + }); + passwordControl.markAsTouched(); + } } else if (e.statusCode === 404) { this.unavailable.set(true); } else { @@ -175,11 +195,10 @@ export class SendAuthComponent implements OnInit { this.sendAuthType.set(AuthType.Password); this.updatePageTitle(); } else if (passwordHashB64Invalid(response.error)) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("invalidSendPassword"), + this.sendAccessForm.controls.password?.setErrors({ + invalidPassword: { message: this.i18nService.t("sendPasswordInvalidAskOwner") }, }); + this.sendAccessForm.controls.password?.markAsTouched(); } else if (sendIdInvalid(response.error)) { this.unavailable.set(true); } else { diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index cc73a04b81b..4731be36ef5 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -12946,8 +12946,13 @@ "paymentMethodUpdateError": { "message": "There was an error updating your payment method." }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "sendExpiresOn": { "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "time": { "content": "$1", @@ -12957,7 +12962,6 @@ "content": "$2", "example": "Jan 1, 1970" } - }, - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + } } } From f7f06267ee22e7ba06fc8ef02c8c854810529ada Mon Sep 17 00:00:00 2001 From: Mike Amirault Date: Wed, 18 Feb 2026 11:50:52 -0500 Subject: [PATCH 075/134] [PM-31347] Add missing messages resulting in empty toast on invalid export master password (#19037) --- apps/browser/src/_locales/en/messages.json | 3 +++ apps/desktop/src/locales/en/messages.json | 3 +++ 2 files changed, 6 insertions(+) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index a221dc4f338..5ed97ce0f07 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -6173,5 +6173,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index f444265877d..85ef3d94001 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -4617,5 +4617,8 @@ }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } From 935bf3655cd8ac0c62eff3956d116df0739044b6 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 18 Feb 2026 18:08:16 +0100 Subject: [PATCH 076/134] Update sdk to 546 (#19056) --- package-lock.json | 16 ++++++++-------- package.json | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8d3c32c027d..fb1111a82b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,8 +23,8 @@ "@angular/platform-browser": "20.3.16", "@angular/platform-browser-dynamic": "20.3.16", "@angular/router": "20.3.16", - "@bitwarden/commercial-sdk-internal": "0.2.0-main.527", - "@bitwarden/sdk-internal": "0.2.0-main.527", + "@bitwarden/commercial-sdk-internal": "0.2.0-main.546", + "@bitwarden/sdk-internal": "0.2.0-main.546", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", @@ -4936,9 +4936,9 @@ "link": true }, "node_modules/@bitwarden/commercial-sdk-internal": { - "version": "0.2.0-main.527", - "resolved": "https://registry.npmjs.org/@bitwarden/commercial-sdk-internal/-/commercial-sdk-internal-0.2.0-main.527.tgz", - "integrity": "sha512-4C4lwOgA2v184G2axUR5Jdb4UMXMhF52a/3c0lAZYbD/8Nid6jziE89nCa9hdfdazuPgWXhVFa3gPrhLZ4uTUQ==", + "version": "0.2.0-main.546", + "resolved": "https://registry.npmjs.org/@bitwarden/commercial-sdk-internal/-/commercial-sdk-internal-0.2.0-main.546.tgz", + "integrity": "sha512-3lIQSb1yYSpDqhgT2uqHjPC88yVL7rWR08i0XD0BQJMFfN0FcB378r2Fq6d5TMXLPEYZ8PR62BCDB+tYKM7FPw==", "license": "BITWARDEN SOFTWARE DEVELOPMENT KIT LICENSE AGREEMENT", "dependencies": { "type-fest": "^4.41.0" @@ -5041,9 +5041,9 @@ "link": true }, "node_modules/@bitwarden/sdk-internal": { - "version": "0.2.0-main.527", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.527.tgz", - "integrity": "sha512-dxPh4XjEGFDBASRBEd/JwUdoMAz10W/0QGygYkPwhKKGzJncfDEAgQ/KrT9wc36ycrDrOOspff7xs/vmmzI0+A==", + "version": "0.2.0-main.546", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.546.tgz", + "integrity": "sha512-KGPyP1pr7aIBaJ9Knibpfjydo/27Rlve77X4ENmDIwrSJ9FB3o2B6D3UXpNNVyXKt2Ii1C+rNT7ezMRO25Qs4A==", "license": "GPL-3.0", "dependencies": { "type-fest": "^4.41.0" diff --git a/package.json b/package.json index 7499a69f99c..c18112989fe 100644 --- a/package.json +++ b/package.json @@ -161,8 +161,8 @@ "@angular/platform-browser": "20.3.16", "@angular/platform-browser-dynamic": "20.3.16", "@angular/router": "20.3.16", - "@bitwarden/commercial-sdk-internal": "0.2.0-main.527", - "@bitwarden/sdk-internal": "0.2.0-main.527", + "@bitwarden/commercial-sdk-internal": "0.2.0-main.546", + "@bitwarden/sdk-internal": "0.2.0-main.546", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", From c086df14e7c27433eda798a10abad4a25c1635bb Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Wed, 18 Feb 2026 12:56:53 -0500 Subject: [PATCH 077/134] chore(ownership): Move account-fingerprint to KM ownership --- .../organizations/settings/organization-settings.module.ts | 2 +- apps/web/src/app/auth/settings/account/profile.component.ts | 2 +- .../account-fingerprint/account-fingerprint.component.html | 0 .../account-fingerprint/account-fingerprint.component.ts | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename apps/web/src/app/{shared/components => key-management}/account-fingerprint/account-fingerprint.component.html (100%) rename apps/web/src/app/{shared/components => key-management}/account-fingerprint/account-fingerprint.component.ts (96%) diff --git a/apps/web/src/app/admin-console/organizations/settings/organization-settings.module.ts b/apps/web/src/app/admin-console/organizations/settings/organization-settings.module.ts index 27a6226f964..13467e222d2 100644 --- a/apps/web/src/app/admin-console/organizations/settings/organization-settings.module.ts +++ b/apps/web/src/app/admin-console/organizations/settings/organization-settings.module.ts @@ -4,9 +4,9 @@ import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/pre import { ItemModule } from "@bitwarden/components"; import { DangerZoneComponent } from "../../../auth/settings/account/danger-zone.component"; +import { AccountFingerprintComponent } from "../../../key-management/account-fingerprint/account-fingerprint.component"; import { HeaderModule } from "../../../layouts/header/header.module"; import { SharedModule } from "../../../shared"; -import { AccountFingerprintComponent } from "../../../shared/components/account-fingerprint/account-fingerprint.component"; import { AccountComponent } from "./account.component"; import { OrganizationSettingsRoutingModule } from "./organization-settings-routing.module"; diff --git a/apps/web/src/app/auth/settings/account/profile.component.ts b/apps/web/src/app/auth/settings/account/profile.component.ts index fd96f343b3a..24e8a370e2a 100644 --- a/apps/web/src/app/auth/settings/account/profile.component.ts +++ b/apps/web/src/app/auth/settings/account/profile.component.ts @@ -18,8 +18,8 @@ import { DialogService, ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; import { DynamicAvatarComponent } from "../../../components/dynamic-avatar.component"; +import { AccountFingerprintComponent } from "../../../key-management/account-fingerprint/account-fingerprint.component"; import { SharedModule } from "../../../shared"; -import { AccountFingerprintComponent } from "../../../shared/components/account-fingerprint/account-fingerprint.component"; import { ChangeAvatarDialogComponent } from "./change-avatar-dialog.component"; diff --git a/apps/web/src/app/shared/components/account-fingerprint/account-fingerprint.component.html b/apps/web/src/app/key-management/account-fingerprint/account-fingerprint.component.html similarity index 100% rename from apps/web/src/app/shared/components/account-fingerprint/account-fingerprint.component.html rename to apps/web/src/app/key-management/account-fingerprint/account-fingerprint.component.html diff --git a/apps/web/src/app/shared/components/account-fingerprint/account-fingerprint.component.ts b/apps/web/src/app/key-management/account-fingerprint/account-fingerprint.component.ts similarity index 96% rename from apps/web/src/app/shared/components/account-fingerprint/account-fingerprint.component.ts rename to apps/web/src/app/key-management/account-fingerprint/account-fingerprint.component.ts index eb84868dca1..ca9042e802e 100644 --- a/apps/web/src/app/shared/components/account-fingerprint/account-fingerprint.component.ts +++ b/apps/web/src/app/key-management/account-fingerprint/account-fingerprint.component.ts @@ -4,7 +4,7 @@ import { Component, Input, OnInit } from "@angular/core"; import { KeyService } from "@bitwarden/key-management"; -import { SharedModule } from "../../shared.module"; +import { SharedModule } from "../../shared/shared.module"; // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection From 5444869456317fc77f43d4639155838f0ce890b9 Mon Sep 17 00:00:00 2001 From: Isaac Ivins Date: Wed, 18 Feb 2026 13:20:08 -0500 Subject: [PATCH 078/134] PM-31733: Sends Drawer Persisting On Side Nav Change (#18762) * using activeDrawerRef with onDestroy * improved refs type checking - removed cdr --- .../app/tools/send-v2/send-v2.component.ts | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/app/tools/send-v2/send-v2.component.ts b/apps/desktop/src/app/tools/send-v2/send-v2.component.ts index 271418ae5b2..fc058c1a17f 100644 --- a/apps/desktop/src/app/tools/send-v2/send-v2.component.ts +++ b/apps/desktop/src/app/tools/send-v2/send-v2.component.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Component, computed, inject, signal, viewChild } from "@angular/core"; +import { Component, computed, DestroyRef, inject, signal, viewChild } from "@angular/core"; import { toSignal } from "@angular/core/rxjs-interop"; import { combineLatest, map, switchMap, lastValueFrom } from "rxjs"; @@ -20,7 +20,7 @@ import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.s import { SendType } from "@bitwarden/common/tools/send/types/send-type"; import { SendId } from "@bitwarden/common/types/guid"; import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; -import { ButtonModule, DialogService, ToastService } from "@bitwarden/components"; +import { ButtonModule, DialogRef, DialogService, ToastService } from "@bitwarden/components"; import { NewSendDropdownV2Component, SendItemsService, @@ -28,6 +28,7 @@ import { SendListState, SendAddEditDialogComponent, DefaultSendFormConfigService, + SendItemDialogResult, } from "@bitwarden/send-ui"; import { DesktopPremiumUpgradePromptService } from "../../../services/desktop-premium-upgrade-prompt.service"; @@ -84,6 +85,9 @@ export class SendV2Component { private dialogService = inject(DialogService); private toastService = inject(ToastService); private logService = inject(LogService); + private destroyRef = inject(DestroyRef); + + private activeDrawerRef?: DialogRef; protected readonly useDrawerEditMode = toSignal( this.configService.getFeatureFlag$(FeatureFlag.DesktopUiMigrationMilestone2), @@ -128,6 +132,12 @@ export class SendV2Component { { initialValue: null }, ); + constructor() { + this.destroyRef.onDestroy(() => { + this.activeDrawerRef?.close(); + }); + } + protected readonly selectedSendType = computed(() => { const action = this.action(); @@ -143,11 +153,12 @@ export class SendV2Component { if (this.useDrawerEditMode()) { const formConfig = await this.sendFormConfigService.buildConfig("add", undefined, type); - const dialogRef = SendAddEditDialogComponent.openDrawer(this.dialogService, { + this.activeDrawerRef = SendAddEditDialogComponent.openDrawer(this.dialogService, { formConfig, }); - await lastValueFrom(dialogRef.closed); + await lastValueFrom(this.activeDrawerRef.closed); + this.activeDrawerRef = null; } else { this.action.set(Action.Add); this.sendId.set(null); @@ -173,11 +184,12 @@ export class SendV2Component { if (this.useDrawerEditMode()) { const formConfig = await this.sendFormConfigService.buildConfig("edit", sendId as SendId); - const dialogRef = SendAddEditDialogComponent.openDrawer(this.dialogService, { + this.activeDrawerRef = SendAddEditDialogComponent.openDrawer(this.dialogService, { formConfig, }); - await lastValueFrom(dialogRef.closed); + await lastValueFrom(this.activeDrawerRef.closed); + this.activeDrawerRef = null; } else { if (sendId === this.sendId() && this.action() === Action.Edit) { return; From ab595900196800944cdc6fa8f286eaa03e2038a0 Mon Sep 17 00:00:00 2001 From: Stephon Brown Date: Wed, 18 Feb 2026 14:32:08 -0500 Subject: [PATCH 079/134] [PM-29823] Add Tests for Updates (#19040) * refactor: Remove direct self-hosted org creation from OrganizationPlansComponent * tests: Add comprehensive test suite for OrganizationPlansComponent --- .../organization-plans.component.spec.ts | 2199 +++++++++++++++++ .../organization-plans.component.ts | 27 +- 2 files changed, 2200 insertions(+), 26 deletions(-) create mode 100644 apps/web/src/app/billing/organizations/organization-plans.component.spec.ts diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.spec.ts b/apps/web/src/app/billing/organizations/organization-plans.component.spec.ts new file mode 100644 index 00000000000..aa4cbdab40e --- /dev/null +++ b/apps/web/src/app/billing/organizations/organization-plans.component.spec.ts @@ -0,0 +1,2199 @@ +// These are disabled until we can migrate to signals and remove the use of @Input properties that are used within the mocked child components +/* eslint-disable @angular-eslint/prefer-output-emitter-ref */ +/* eslint-disable @angular-eslint/prefer-signals */ +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from "@angular/core"; +import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing"; +import { FormBuilder, Validators } from "@angular/forms"; +import { Router } from "@angular/router"; +import { BehaviorSubject, of } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; +import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { ToastService } from "@bitwarden/components"; +import { KeyService } from "@bitwarden/key-management"; +import { + PreviewInvoiceClient, + SubscriberBillingClient, +} from "@bitwarden/web-vault/app/billing/clients"; + +import { OrganizationInformationComponent } from "../../admin-console/organizations/create/organization-information.component"; +import { EnterBillingAddressComponent, EnterPaymentMethodComponent } from "../payment/components"; +import { SecretsManagerSubscribeComponent } from "../shared"; +import { OrganizationSelfHostingLicenseUploaderComponent } from "../shared/self-hosting-license-uploader/organization-self-hosting-license-uploader.component"; + +import { OrganizationPlansComponent } from "./organization-plans.component"; + +// Mocked Child Components +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: "app-org-info", + template: "", + standalone: true, +}) +class MockOrgInfoComponent { + @Input() formGroup: any; + @Input() createOrganization = true; + @Input() isProvider = false; + @Input() acceptingSponsorship = false; + @Output() changedBusinessOwned = new EventEmitter(); +} + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: "sm-subscribe", + template: "", + standalone: true, +}) +class MockSmSubscribeComponent { + @Input() formGroup: any; + @Input() selectedPlan: any; + @Input() upgradeOrganization = false; +} + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: "app-enter-payment-method", + template: "", + standalone: true, +}) +class MockEnterPaymentMethodComponent { + @Input() group: any; + + static getFormGroup() { + const fb = new FormBuilder(); + return fb.group({ + type: fb.control("card"), + bankAccount: fb.group({ + routingNumber: fb.control(""), + accountNumber: fb.control(""), + accountHolderName: fb.control(""), + accountHolderType: fb.control(""), + }), + billingAddress: fb.group({ + country: fb.control("US"), + postalCode: fb.control(""), + }), + }); + } + + tokenize = jest.fn().mockResolvedValue({ + token: "mock_token", + type: "card", + }); +} + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: "app-enter-billing-address", + template: "", + standalone: true, +}) +class MockEnterBillingAddressComponent { + @Input() group: any; + @Input() scenario: any; + + static getFormGroup() { + return new FormBuilder().group({ + country: ["US", Validators.required], + postalCode: ["", Validators.required], + taxId: [""], + line1: [""], + line2: [""], + city: [""], + state: [""], + }); + } +} + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: "organization-self-hosting-license-uploader", + template: "", + standalone: true, +}) +class MockOrganizationSelfHostingLicenseUploaderComponent { + @Output() onLicenseFileUploaded = new EventEmitter(); +} + +// Test Helper Functions + +/** + * Sets up mock encryption keys and org key services + */ +const setupMockEncryptionKeys = ( + mockKeyService: jest.Mocked, + mockEncryptService: jest.Mocked, +) => { + mockKeyService.makeOrgKey.mockResolvedValue([{ encryptedString: "mock-key" }, {} as any] as any); + + mockEncryptService.encryptString.mockResolvedValue({ + encryptedString: "mock-collection", + } as any); + + mockKeyService.makeKeyPair.mockResolvedValue([ + "public-key", + { encryptedString: "private-key" }, + ] as any); +}; + +/** + * Sets up a mock payment method component that returns a successful tokenization + */ +const setupMockPaymentMethodComponent = ( + component: OrganizationPlansComponent, + token = "mock_token", + type = "card", +) => { + component["enterPaymentMethodComponent"] = { + tokenize: jest.fn().mockResolvedValue({ token, type }), + } as any; +}; + +/** + * Patches billing address form with standard test values + */ +const patchBillingAddress = ( + component: OrganizationPlansComponent, + overrides: Partial<{ + country: string; + postalCode: string; + line1: string; + line2: string; + city: string; + state: string; + taxId: string; + }> = {}, +) => { + component.billingFormGroup.controls.billingAddress.patchValue({ + country: "US", + postalCode: "12345", + line1: "123 Street", + line2: "", + city: "City", + state: "CA", + taxId: "", + ...overrides, + }); +}; + +/** + * Sets up a mock organization for upgrade scenarios + */ +const setupMockUpgradeOrganization = ( + mockOrganizationApiService: jest.Mocked, + organizationsSubject: BehaviorSubject, + orgConfig: { + id?: string; + productTierType?: ProductTierType; + hasPaymentSource?: boolean; + planType?: PlanType; + seats?: number; + maxStorageGb?: number; + hasPublicAndPrivateKeys?: boolean; + useSecretsManager?: boolean; + smSeats?: number; + smServiceAccounts?: number; + } = {}, +) => { + const { + id = "org-123", + productTierType = ProductTierType.Free, + hasPaymentSource = true, + planType = PlanType.Free, + seats = 5, + maxStorageGb, + hasPublicAndPrivateKeys = true, + useSecretsManager = false, + smSeats, + smServiceAccounts, + } = orgConfig; + + const mockOrganization = { + id, + name: "Test Org", + productTierType, + seats, + maxStorageGb, + hasPublicAndPrivateKeys, + useSecretsManager, + } as Organization; + + organizationsSubject.next([mockOrganization]); + + mockOrganizationApiService.getBilling.mockResolvedValue({ + paymentSource: hasPaymentSource ? { type: "card" } : null, + } as any); + + mockOrganizationApiService.getSubscription.mockResolvedValue({ + planType, + smSeats, + smServiceAccounts, + } as any); + + return mockOrganization; +}; + +/** + * Patches organization form with basic test values + */ +const patchOrganizationForm = ( + component: OrganizationPlansComponent, + values: { + name?: string; + billingEmail?: string; + productTier?: ProductTierType; + plan?: PlanType; + additionalSeats?: number; + additionalStorage?: number; + }, +) => { + component.formGroup.patchValue({ + name: "Test Org", + billingEmail: "test@example.com", + productTier: ProductTierType.Free, + plan: PlanType.Free, + additionalSeats: 0, + additionalStorage: 0, + ...values, + }); +}; + +/** + * Returns plan details + * + */ + +const createMockPlans = (): PlanResponse[] => { + return [ + { + type: PlanType.Free, + productTier: ProductTierType.Free, + name: "Free", + isAnnual: true, + upgradeSortOrder: 1, + displaySortOrder: 1, + PasswordManager: { + basePrice: 0, + seatPrice: 0, + maxSeats: 2, + baseSeats: 2, + hasAdditionalSeatsOption: false, + hasAdditionalStorageOption: false, + hasPremiumAccessOption: false, + baseStorageGb: 0, + }, + SecretsManager: null, + } as PlanResponse, + { + type: PlanType.FamiliesAnnually, + productTier: ProductTierType.Families, + name: "Families", + isAnnual: true, + upgradeSortOrder: 2, + displaySortOrder: 2, + PasswordManager: { + basePrice: 40, + seatPrice: 0, + maxSeats: 6, + baseSeats: 6, + hasAdditionalSeatsOption: false, + hasAdditionalStorageOption: true, + hasPremiumAccessOption: false, + baseStorageGb: 1, + additionalStoragePricePerGb: 4, + }, + SecretsManager: null, + } as PlanResponse, + { + type: PlanType.TeamsAnnually, + productTier: ProductTierType.Teams, + name: "Teams", + isAnnual: true, + canBeUsedByBusiness: true, + upgradeSortOrder: 3, + displaySortOrder: 3, + PasswordManager: { + basePrice: 0, + seatPrice: 48, + hasAdditionalSeatsOption: true, + hasAdditionalStorageOption: true, + hasPremiumAccessOption: true, + baseStorageGb: 1, + additionalStoragePricePerGb: 4, + premiumAccessOptionPrice: 40, + }, + SecretsManager: { + basePrice: 0, + seatPrice: 72, + hasAdditionalSeatsOption: true, + hasAdditionalServiceAccountOption: true, + baseServiceAccount: 50, + additionalPricePerServiceAccount: 6, + }, + } as PlanResponse, + { + type: PlanType.EnterpriseAnnually, + productTier: ProductTierType.Enterprise, + name: "Enterprise", + isAnnual: true, + canBeUsedByBusiness: true, + trialPeriodDays: 7, + upgradeSortOrder: 4, + displaySortOrder: 4, + PasswordManager: { + basePrice: 0, + seatPrice: 72, + hasAdditionalSeatsOption: true, + hasAdditionalStorageOption: true, + hasPremiumAccessOption: true, + baseStorageGb: 1, + additionalStoragePricePerGb: 4, + premiumAccessOptionPrice: 40, + }, + SecretsManager: { + basePrice: 0, + seatPrice: 144, + hasAdditionalSeatsOption: true, + hasAdditionalServiceAccountOption: true, + baseServiceAccount: 200, + additionalPricePerServiceAccount: 6, + }, + } as PlanResponse, + ]; +}; + +describe("OrganizationPlansComponent", () => { + let component: OrganizationPlansComponent; + let fixture: ComponentFixture; + + // Mock services + let mockApiService: jest.Mocked; + let mockI18nService: jest.Mocked; + let mockPlatformUtilsService: jest.Mocked; + let mockKeyService: jest.Mocked; + let mockEncryptService: jest.Mocked; + let mockRouter: jest.Mocked; + let mockSyncService: jest.Mocked; + let mockPolicyService: jest.Mocked; + let mockOrganizationService: jest.Mocked; + let mockMessagingService: jest.Mocked; + let mockOrganizationApiService: jest.Mocked; + let mockProviderApiService: jest.Mocked; + let mockToastService: jest.Mocked; + let mockAccountService: jest.Mocked; + let mockSubscriberBillingClient: jest.Mocked; + let mockPreviewInvoiceClient: jest.Mocked; + let mockConfigService: jest.Mocked; + + // Mock data + let mockPasswordManagerPlans: PlanResponse[]; + let mockOrganization: Organization; + let activeAccountSubject: BehaviorSubject; + let organizationsSubject: BehaviorSubject; + + beforeEach(async () => { + jest.clearAllMocks(); + + // Mock the static getFormGroup methods to return forms without validators + jest + .spyOn(EnterPaymentMethodComponent, "getFormGroup") + .mockReturnValue(MockEnterPaymentMethodComponent.getFormGroup() as any); + jest + .spyOn(EnterBillingAddressComponent, "getFormGroup") + .mockReturnValue(MockEnterBillingAddressComponent.getFormGroup() as any); + + // Initialize mock services + mockApiService = { + getPlans: jest.fn(), + postProviderCreateOrganization: jest.fn(), + refreshIdentityToken: jest.fn(), + } as any; + + mockI18nService = { + t: jest.fn((key: string) => key), + } as any; + + mockPlatformUtilsService = { + isSelfHost: jest.fn().mockReturnValue(false), + } as any; + + mockKeyService = { + makeOrgKey: jest.fn(), + makeKeyPair: jest.fn(), + orgKeys$: jest.fn().mockReturnValue(of({})), + providerKeys$: jest.fn().mockReturnValue(of({})), + } as any; + + mockEncryptService = { + encryptString: jest.fn(), + wrapSymmetricKey: jest.fn(), + } as any; + + mockRouter = { + navigate: jest.fn(), + } as any; + + mockSyncService = { + fullSync: jest.fn().mockResolvedValue(undefined), + } as any; + + mockPolicyService = { + policyAppliesToUser$: jest.fn().mockReturnValue(of(false)), + } as any; + + // Setup subjects for observables + activeAccountSubject = new BehaviorSubject({ + id: "user-id", + email: "test@example.com", + }); + organizationsSubject = new BehaviorSubject([]); + + mockAccountService = { + activeAccount$: activeAccountSubject.asObservable(), + } as any; + + mockOrganizationService = { + organizations$: jest.fn().mockReturnValue(organizationsSubject.asObservable()), + } as any; + + mockMessagingService = { + send: jest.fn(), + } as any; + + mockOrganizationApiService = { + getBilling: jest.fn(), + getSubscription: jest.fn(), + create: jest.fn(), + createLicense: jest.fn(), + upgrade: jest.fn(), + updateKeys: jest.fn(), + } as any; + + mockProviderApiService = { + getProvider: jest.fn(), + } as any; + + mockToastService = { + showToast: jest.fn(), + } as any; + + mockSubscriberBillingClient = { + getBillingAddress: jest.fn().mockResolvedValue({ + country: "US", + postalCode: "12345", + }), + updatePaymentMethod: jest.fn().mockResolvedValue(undefined), + } as any; + + mockPreviewInvoiceClient = { + previewTaxForOrganizationSubscriptionPurchase: jest.fn().mockResolvedValue({ + tax: 5.0, + total: 50.0, + }), + } as any; + + mockConfigService = { + getFeatureFlag: jest.fn().mockResolvedValue(true), + } as any; + + // Setup mock plan data + mockPasswordManagerPlans = createMockPlans(); + + mockApiService.getPlans.mockResolvedValue({ + data: mockPasswordManagerPlans, + } as any); + + await TestBed.configureTestingModule({ + providers: [ + { provide: ApiService, useValue: mockApiService }, + { provide: I18nService, useValue: mockI18nService }, + { provide: PlatformUtilsService, useValue: mockPlatformUtilsService }, + { provide: KeyService, useValue: mockKeyService }, + { provide: EncryptService, useValue: mockEncryptService }, + { provide: Router, useValue: mockRouter }, + { provide: SyncService, useValue: mockSyncService }, + { provide: PolicyService, useValue: mockPolicyService }, + { provide: OrganizationService, useValue: mockOrganizationService }, + { provide: MessagingService, useValue: mockMessagingService }, + FormBuilder, // Use real FormBuilder + { provide: OrganizationApiServiceAbstraction, useValue: mockOrganizationApiService }, + { provide: ProviderApiServiceAbstraction, useValue: mockProviderApiService }, + { provide: ToastService, useValue: mockToastService }, + { provide: AccountService, useValue: mockAccountService }, + { provide: SubscriberBillingClient, useValue: mockSubscriberBillingClient }, + { provide: PreviewInvoiceClient, useValue: mockPreviewInvoiceClient }, + { provide: ConfigService, useValue: mockConfigService }, + ], + }) + // Override the component to replace child components with mocks and provide mock services + .overrideComponent(OrganizationPlansComponent, { + remove: { + imports: [ + OrganizationInformationComponent, + SecretsManagerSubscribeComponent, + EnterPaymentMethodComponent, + EnterBillingAddressComponent, + OrganizationSelfHostingLicenseUploaderComponent, + ], + providers: [PreviewInvoiceClient, SubscriberBillingClient], + }, + add: { + imports: [ + MockOrgInfoComponent, + MockSmSubscribeComponent, + MockEnterPaymentMethodComponent, + MockEnterBillingAddressComponent, + MockOrganizationSelfHostingLicenseUploaderComponent, + ], + providers: [ + { provide: PreviewInvoiceClient, useValue: mockPreviewInvoiceClient }, + { provide: SubscriberBillingClient, useValue: mockSubscriberBillingClient }, + ], + }, + }) + .compileComponents(); + + fixture = TestBed.createComponent(OrganizationPlansComponent); + component = fixture.componentInstance; + }); + + describe("component creation", () => { + it("should create", () => { + expect(component).toBeTruthy(); + }); + + it("should initialize with default values", () => { + expect(component.loading).toBe(true); + expect(component.showFree).toBe(true); + expect(component.showCancel).toBe(false); + expect(component.productTier).toBe(ProductTierType.Free); + }); + }); + + describe("ngOnInit", () => { + describe("create organization flow", () => { + it("should load plans from API", async () => { + fixture.detectChanges(); + await fixture.whenStable(); + + expect(mockApiService.getPlans).toHaveBeenCalled(); + expect(component.passwordManagerPlans).toEqual(mockPasswordManagerPlans); + expect(component.loading).toBe(false); + }); + + it("should set required validators on name and billing email", async () => { + fixture.detectChanges(); + await fixture.whenStable(); + + component.formGroup.controls.name.setValue(""); + component.formGroup.controls.billingEmail.setValue(""); + + expect(component.formGroup.controls.name.hasError("required")).toBe(true); + expect(component.formGroup.controls.billingEmail.hasError("required")).toBe(true); + }); + + it("should not load organization data for create flow", async () => { + fixture.detectChanges(); + await fixture.whenStable(); + + expect(mockOrganizationApiService.getBilling).not.toHaveBeenCalled(); + expect(mockOrganizationApiService.getSubscription).not.toHaveBeenCalled(); + }); + }); + + describe("upgrade organization flow", () => { + beforeEach(() => { + mockOrganization = setupMockUpgradeOrganization( + mockOrganizationApiService, + organizationsSubject, + { + planType: PlanType.FamiliesAnnually2025, + }, + ); + + component.organizationId = mockOrganization.id; + }); + + it("should load existing organization data", async () => { + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component.organization).toEqual(mockOrganization); + expect(mockOrganizationApiService.getBilling).toHaveBeenCalledWith(mockOrganization.id); + expect(mockOrganizationApiService.getSubscription).toHaveBeenCalledWith( + mockOrganization.id, + ); + expect(mockSubscriberBillingClient.getBillingAddress).toHaveBeenCalledWith({ + type: "organization", + data: mockOrganization, + }); + // Verify the form was updated + expect(component.billingFormGroup.controls.billingAddress.value.country).toBe("US"); + expect(component.billingFormGroup.controls.billingAddress.value.postalCode).toBe("12345"); + }); + + it("should not add validators for name and billingEmail in upgrade flow", async () => { + fixture.detectChanges(); + await fixture.whenStable(); + + component.formGroup.controls.name.setValue(""); + component.formGroup.controls.billingEmail.setValue(""); + + // In upgrade flow, these should not be required + expect(component.formGroup.controls.name.hasError("required")).toBe(false); + expect(component.formGroup.controls.billingEmail.hasError("required")).toBe(false); + }); + }); + + describe("feature flags", () => { + it("should use FamiliesAnnually when PM26462_Milestone_3 is enabled", async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(true); + + fixture.detectChanges(); + await fixture.whenStable(); + + const familyPlan = component["_familyPlan"]; + expect(familyPlan).toBe(PlanType.FamiliesAnnually); + }); + + it("should use FamiliesAnnually2025 when feature flag is disabled", async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(false); + + fixture.detectChanges(); + await fixture.whenStable(); + + const familyPlan = component["_familyPlan"]; + expect(familyPlan).toBe(PlanType.FamiliesAnnually2025); + }); + }); + }); + + describe("organization creation validation flow", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should prevent submission with invalid form data", async () => { + component.formGroup.patchValue({ + name: "", + billingEmail: "invalid-email", + additionalStorage: -1, + additionalSeats: 200000, + }); + + await component.submit(); + + expect(mockOrganizationApiService.create).not.toHaveBeenCalled(); + expect(component.formGroup.invalid).toBe(true); + }); + + it("should allow submission with valid form data", async () => { + patchOrganizationForm(component, { + name: "Valid Organization", + billingEmail: "valid@example.com", + productTier: ProductTierType.Free, + plan: PlanType.Free, + }); + + setupMockEncryptionKeys(mockKeyService, mockEncryptService); + mockOrganizationApiService.create.mockResolvedValue({ + id: "new-org-id", + } as any); + + await component.submit(); + + expect(mockOrganizationApiService.create).toHaveBeenCalled(); + }); + }); + + describe("plan selection flow", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should configure form appropriately when switching between product tiers", () => { + // Start with Families plan with unsupported features + component.productTier = ProductTierType.Families; + component.formGroup.controls.additionalSeats.setValue(10); + component.formGroup.controls.additionalStorage.setValue(5); + component.changedProduct(); + + // Families doesn't support additional seats + expect(component.formGroup.controls.additionalSeats.value).toBe(0); + expect(component.formGroup.controls.plan.value).toBe(PlanType.FamiliesAnnually); + + // Switch to Teams plan which supports additional seats + component.productTier = ProductTierType.Teams; + component.changedProduct(); + + expect(component.formGroup.controls.plan.value).toBe(PlanType.TeamsAnnually); + // Teams initializes with 1 seat by default + expect(component.formGroup.controls.additionalSeats.value).toBeGreaterThan(0); + + // Switch to Free plan which doesn't support additional storage + component.formGroup.controls.additionalStorage.setValue(10); + component.productTier = ProductTierType.Free; + component.changedProduct(); + + expect(component.formGroup.controls.additionalStorage.value).toBe(0); + }); + }); + + describe("subscription pricing flow", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should calculate total price based on selected plan options", () => { + // Select Teams plan and configure options + component.productTier = ProductTierType.Teams; + component.changedProduct(); + component.formGroup.controls.additionalSeats.setValue(5); + component.formGroup.controls.additionalStorage.setValue(10); + component.formGroup.controls.premiumAccessAddon.setValue(true); + + const pmSubtotal = component.passwordManagerSubtotal; + // Verify pricing includes all selected options + expect(pmSubtotal).toBeGreaterThan(0); + expect(pmSubtotal).toBe(5 * 48 + 10 * 4 + 40); // seats + storage + premium + }); + + it("should calculate pricing with Secrets Manager addon", () => { + component.productTier = ProductTierType.Teams; + component.plan = PlanType.TeamsAnnually; + + // Enable Secrets Manager with additional options + component.secretsManagerForm.patchValue({ + enabled: true, + userSeats: 3, + additionalServiceAccounts: 10, + }); + + const smSubtotal = component.secretsManagerSubtotal; + expect(smSubtotal).toBeGreaterThan(0); + + // Disable Secrets Manager + component.secretsManagerForm.patchValue({ + enabled: false, + }); + + expect(component.secretsManagerSubtotal).toBe(0); + }); + }); + + describe("tax calculation", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should calculate tax after debounce period", fakeAsync(() => { + component.productTier = ProductTierType.Teams; + component.changedProduct(); + component.formGroup.controls.additionalSeats.setValue(1); + component.billingFormGroup.controls.billingAddress.patchValue({ + country: "US", + postalCode: "12345", + }); + + tick(1500); // Wait for debounce (1000ms) + + expect( + mockPreviewInvoiceClient.previewTaxForOrganizationSubscriptionPurchase, + ).toHaveBeenCalled(); + expect(component["estimatedTax"]).toBe(5.0); + })); + + it("should not calculate tax with invalid billing address", fakeAsync(() => { + component.billingFormGroup.controls.billingAddress.patchValue({ + country: "", + postalCode: "", + }); + + tick(1500); + + expect( + mockPreviewInvoiceClient.previewTaxForOrganizationSubscriptionPurchase, + ).not.toHaveBeenCalled(); + })); + }); + + describe("submit", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should create organization successfully", async () => { + patchOrganizationForm(component, { + name: "New Org", + billingEmail: "test@example.com", + }); + + setupMockEncryptionKeys(mockKeyService, mockEncryptService); + mockOrganizationApiService.create.mockResolvedValue({ + id: "new-org-id", + } as any); + + await component.submit(); + + expect(mockOrganizationApiService.create).toHaveBeenCalled(); + expect(mockToastService.showToast).toHaveBeenCalledWith({ + variant: "success", + title: "organizationCreated", + message: "organizationReadyToGo", + }); + expect(mockSyncService.fullSync).toHaveBeenCalledWith(true); + }); + + it("should emit onSuccess after successful creation", async () => { + const onSuccessSpy = jest.spyOn(component.onSuccess, "emit"); + + patchOrganizationForm(component, { + name: "New Org", + billingEmail: "test@example.com", + }); + + setupMockEncryptionKeys(mockKeyService, mockEncryptService); + mockOrganizationApiService.create.mockResolvedValue({ + id: "new-org-id", + } as any); + + await component.submit(); + + expect(onSuccessSpy).toHaveBeenCalledWith({ + organizationId: "new-org-id", + }); + }); + + it("should handle payment method validation failure", async () => { + patchOrganizationForm(component, { + name: "New Org", + billingEmail: "test@example.com", + productTier: ProductTierType.Teams, + plan: PlanType.TeamsAnnually, + additionalSeats: 5, + }); + + patchBillingAddress(component); + setupMockEncryptionKeys(mockKeyService, mockEncryptService); + + // Mock payment method component to return null (failure) + component["enterPaymentMethodComponent"] = { + tokenize: jest.fn().mockResolvedValue(null), + } as any; + + await component.submit(); + + // Should not create organization if payment method validation fails + expect(mockOrganizationApiService.create).not.toHaveBeenCalled(); + }); + + it("should block submission when single org policy applies", async () => { + mockPolicyService.policyAppliesToUser$.mockReturnValue(of(true)); + + // Need to reinitialize after changing policy mock + const policyFixture = TestBed.createComponent(OrganizationPlansComponent); + const policyComponent = policyFixture.componentInstance; + policyFixture.detectChanges(); + await policyFixture.whenStable(); + + policyComponent.formGroup.patchValue({ + name: "Test", + billingEmail: "test@example.com", + }); + + await policyComponent.submit(); + + expect(mockOrganizationApiService.create).not.toHaveBeenCalled(); + }); + }); + + describe("provider flow", () => { + beforeEach(() => { + component.providerId = "provider-123"; + }); + + it("should load provider data", async () => { + mockProviderApiService.getProvider.mockResolvedValue({ + id: "provider-123", + name: "Test Provider", + } as any); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(mockProviderApiService.getProvider).toHaveBeenCalledWith("provider-123"); + expect(component.provider).toBeDefined(); + }); + + it("should default to Teams Annual plan for providers", async () => { + mockProviderApiService.getProvider.mockResolvedValue({} as any); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component.plan).toBe(PlanType.TeamsAnnually); + }); + + it("should require clientOwnerEmail for provider flow", async () => { + mockProviderApiService.getProvider.mockResolvedValue({} as any); + + fixture.detectChanges(); + await fixture.whenStable(); + + const clientOwnerEmailControl = component.formGroup.controls.clientOwnerEmail; + clientOwnerEmailControl.setValue(""); + + expect(clientOwnerEmailControl.hasError("required")).toBe(true); + }); + + it("should set businessOwned to true for provider flow", async () => { + mockProviderApiService.getProvider.mockResolvedValue({} as any); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component.formGroup.controls.businessOwned.value).toBe(true); + }); + }); + + describe("self-hosted flow", () => { + beforeEach(async () => { + mockPlatformUtilsService.isSelfHost.mockReturnValue(true); + }); + + it("should render organization self-hosted license and not load plans", async () => { + mockPlatformUtilsService.isSelfHost.mockReturnValue(true); + const selfHostedFixture = TestBed.createComponent(OrganizationPlansComponent); + const selfHostedComponent = selfHostedFixture.componentInstance; + + expect(selfHostedComponent.selfHosted).toBe(true); + expect(mockApiService.getPlans).not.toHaveBeenCalled(); + }); + + it("should handle license file upload success", async () => { + const successSpy = jest.spyOn(component.onSuccess, "emit"); + + await component["onLicenseFileUploaded"]("uploaded-org-id"); + + expect(mockToastService.showToast).toHaveBeenCalledWith({ + variant: "success", + title: "organizationCreated", + message: "organizationReadyToGo", + }); + + expect(successSpy).toHaveBeenCalledWith({ + organizationId: "uploaded-org-id", + }); + + expect(mockMessagingService.send).toHaveBeenCalledWith("organizationCreated", { + organizationId: "uploaded-org-id", + }); + }); + + it("should navigate after license upload if not in trial or sponsorship flow", async () => { + component.acceptingSponsorship = false; + component["isInTrialFlow"] = false; + + await component["onLicenseFileUploaded"]("uploaded-org-id"); + + expect(mockRouter.navigate).toHaveBeenCalledWith(["/organizations/uploaded-org-id"]); + }); + + it("should not navigate after license upload if accepting sponsorship", async () => { + component.acceptingSponsorship = true; + + await component["onLicenseFileUploaded"]("uploaded-org-id"); + + expect(mockRouter.navigate).not.toHaveBeenCalled(); + }); + + it("should emit trial success after license upload in trial flow", async () => { + component["isInTrialFlow"] = true; + + fixture.detectChanges(); + await fixture.whenStable(); + + const trialSpy = jest.spyOn(component.onTrialBillingSuccess, "emit"); + + await component["onLicenseFileUploaded"]("uploaded-org-id"); + + expect(trialSpy).toHaveBeenCalled(); + }); + }); + + describe("policy enforcement", () => { + it("should check single org policy", async () => { + mockPolicyService.policyAppliesToUser$.mockReturnValue(of(true)); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component.singleOrgPolicyAppliesToActiveUser).toBe(true); + }); + + it("should not block provider flow with single org policy", async () => { + mockPolicyService.policyAppliesToUser$.mockReturnValue(of(true)); + component.providerId = "provider-123"; + mockProviderApiService.getProvider.mockResolvedValue({} as any); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component.singleOrgPolicyBlock).toBe(false); + }); + }); + + describe("business ownership change flow", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should automatically upgrade to business-compatible plan when marking as business-owned", () => { + // Start with a personal plan + component.formGroup.controls.businessOwned.setValue(false); + component.productTier = ProductTierType.Families; + component.plan = PlanType.FamiliesAnnually; + + // Mark as business-owned + component.formGroup.controls.businessOwned.setValue(true); + component.changedOwnedBusiness(); + + // Should automatically switch to Teams (lowest business plan) + expect(component.formGroup.controls.productTier.value).toBe(ProductTierType.Teams); + expect(component.formGroup.controls.plan.value).toBe(PlanType.TeamsAnnually); + + // Unchecking businessOwned should not force a downgrade + component.formGroup.controls.businessOwned.setValue(false); + component.changedOwnedBusiness(); + + expect(component.formGroup.controls.productTier.value).toBe(ProductTierType.Teams); + }); + }); + + describe("business organization plan selection flow", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should restrict available plans based on business ownership and upgrade context", () => { + // Upgrade flow (showFree = false) should exclude Free plan + component.showFree = false; + let products = component.selectableProducts; + expect(products.find((p) => p.type === PlanType.Free)).toBeUndefined(); + + // Create flow (showFree = true) should include Free plan + component.showFree = true; + products = component.selectableProducts; + expect(products.find((p) => p.type === PlanType.Free)).toBeDefined(); + + // Business organizations should only see business-compatible plans + component.formGroup.controls.businessOwned.setValue(true); + products = component.selectableProducts; + const nonFreeBusinessPlans = products.filter((p) => p.type !== PlanType.Free); + nonFreeBusinessPlans.forEach((plan) => { + expect(plan.canBeUsedByBusiness).toBe(true); + }); + }); + }); + + describe("accepting sponsorship flow", () => { + beforeEach(() => { + component.acceptingSponsorship = true; + }); + + it("should configure Families plan with full discount when accepting sponsorship", async () => { + fixture.detectChanges(); + await fixture.whenStable(); + + // Only Families plan should be available + const products = component.selectableProducts; + expect(products.length).toBe(1); + expect(products[0].productTier).toBe(ProductTierType.Families); + + // Full discount should be applied making the base price free + component.productTier = ProductTierType.Families; + component.plan = PlanType.FamiliesAnnually; + + const subtotal = component.passwordManagerSubtotal; + expect(subtotal).toBe(0); // Discount covers the full base price + expect(component.discount).toBe(products[0].PasswordManager.basePrice); + }); + }); + + describe("upgrade flow", () => { + it("should successfully upgrade organization", async () => { + setupMockUpgradeOrganization(mockOrganizationApiService, organizationsSubject, { + maxStorageGb: 2, + }); + + const upgradeFixture = TestBed.createComponent(OrganizationPlansComponent); + const upgradeComponent = upgradeFixture.componentInstance; + upgradeComponent.organizationId = "org-123"; + upgradeComponent.currentPlan = mockPasswordManagerPlans[0]; // Free plan + + upgradeFixture.detectChanges(); + await upgradeFixture.whenStable(); + + upgradeComponent.productTier = ProductTierType.Teams; + upgradeComponent.plan = PlanType.TeamsAnnually; + upgradeComponent.formGroup.controls.additionalSeats.setValue(5); + + mockOrganizationApiService.upgrade.mockResolvedValue(undefined); + + await upgradeComponent.submit(); + + expect(mockOrganizationApiService.upgrade).toHaveBeenCalledWith( + "org-123", + expect.objectContaining({ + planType: PlanType.TeamsAnnually, + additionalSeats: 5, + }), + ); + + expect(mockToastService.showToast).toHaveBeenCalledWith({ + variant: "success", + title: null, + message: "organizationUpgraded", + }); + }); + + it("should handle upgrade requiring payment method", async () => { + setupMockUpgradeOrganization(mockOrganizationApiService, organizationsSubject, { + hasPaymentSource: false, + maxStorageGb: 2, + }); + + const upgradeFixture = TestBed.createComponent(OrganizationPlansComponent); + const upgradeComponent = upgradeFixture.componentInstance; + upgradeComponent.organizationId = "org-123"; + upgradeComponent.showFree = false; // Required for upgradeRequiresPaymentMethod + + upgradeFixture.detectChanges(); + await upgradeFixture.whenStable(); + + expect(upgradeComponent.upgradeRequiresPaymentMethod).toBe(true); + }); + }); + + describe("billing form display flow", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should show appropriate billing fields based on plan type", () => { + // Personal plans (Free, Families) should not require tax ID + component.productTier = ProductTierType.Free; + expect(component["showTaxIdField"]).toBe(false); + + component.productTier = ProductTierType.Families; + expect(component["showTaxIdField"]).toBe(false); + + // Business plans (Teams, Enterprise) should show tax ID field + component.productTier = ProductTierType.Teams; + expect(component["showTaxIdField"]).toBe(true); + + component.productTier = ProductTierType.Enterprise; + expect(component["showTaxIdField"]).toBe(true); + }); + }); + + describe("secrets manager handling flow", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should prefill SM seats from existing subscription", async () => { + mockOrganization = { + id: "org-123", + name: "Test Org", + productTierType: ProductTierType.Teams, + useSecretsManager: true, + } as Organization; + + organizationsSubject.next([mockOrganization]); + + mockOrganizationApiService.getBilling.mockResolvedValue({ + paymentSource: { type: "card" }, + } as any); + + mockOrganizationApiService.getSubscription.mockResolvedValue({ + planType: PlanType.TeamsAnnually, + smSeats: 5, + smServiceAccounts: 75, + } as any); + + const upgradeFixture = TestBed.createComponent(OrganizationPlansComponent); + const upgradeComponent = upgradeFixture.componentInstance; + upgradeComponent.organizationId = "org-123"; + upgradeComponent.currentPlan = mockPasswordManagerPlans[2]; // Teams plan + + upgradeFixture.detectChanges(); + await upgradeFixture.whenStable(); + + upgradeComponent.changedProduct(); + + expect(upgradeComponent.secretsManagerForm.controls.enabled.value).toBe(true); + expect(upgradeComponent.secretsManagerForm.controls.userSeats.value).toBe(5); + expect(upgradeComponent.secretsManagerForm.controls.additionalServiceAccounts.value).toBe(25); + }); + + it("should enable SM by default when enableSecretsManagerByDefault is true", async () => { + const smFixture = TestBed.createComponent(OrganizationPlansComponent); + const smComponent = smFixture.componentInstance; + smComponent.enableSecretsManagerByDefault = true; + smComponent.productTier = ProductTierType.Teams; + + smFixture.detectChanges(); + await smFixture.whenStable(); + + expect(smComponent.secretsManagerForm.value.enabled).toBe(true); + expect(smComponent.secretsManagerForm.value.userSeats).toBe(1); + expect(smComponent.secretsManagerForm.value.additionalServiceAccounts).toBe(0); + }); + + it("should trigger tax recalculation when SM form changes", fakeAsync(() => { + component.productTier = ProductTierType.Teams; + component.changedProduct(); + component.billingFormGroup.controls.billingAddress.patchValue({ + country: "US", + postalCode: "90210", + }); + + // Clear previous calls + jest.clearAllMocks(); + + // Change SM form + component.secretsManagerForm.patchValue({ + enabled: true, + userSeats: 3, + }); + + tick(1500); // Wait for debounce + + expect( + mockPreviewInvoiceClient.previewTaxForOrganizationSubscriptionPurchase, + ).toHaveBeenCalled(); + })); + }); + + describe("form update helpers flow", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should handle premium addon access based on plan features", () => { + // Plan without premium access option should set addon to true (meaning it's included) + component.productTier = ProductTierType.Families; + component.changedProduct(); + + expect(component.formGroup.controls.premiumAccessAddon.value).toBe(true); + + // Plan with premium access option should set addon to false (user can opt-in) + component.productTier = ProductTierType.Teams; + component.changedProduct(); + + expect(component.formGroup.controls.premiumAccessAddon.value).toBe(false); + }); + + it("should handle additional storage for upgrade with existing data", async () => { + mockOrganization = { + id: "org-123", + name: "Test Org", + productTierType: ProductTierType.Free, + maxStorageGb: 5, + } as Organization; + + organizationsSubject.next([mockOrganization]); + + mockOrganizationApiService.getBilling.mockResolvedValue({ + paymentSource: { type: "card" }, + } as any); + + mockOrganizationApiService.getSubscription.mockResolvedValue({ + planType: PlanType.Free, + } as any); + + const upgradeFixture = TestBed.createComponent(OrganizationPlansComponent); + const upgradeComponent = upgradeFixture.componentInstance; + upgradeComponent.organizationId = "org-123"; + upgradeComponent.currentPlan = mockPasswordManagerPlans[0]; // Free plan with 0 GB base + + upgradeFixture.detectChanges(); + await upgradeFixture.whenStable(); + + upgradeComponent.productTier = ProductTierType.Teams; + upgradeComponent.changedProduct(); + + expect(upgradeComponent.formGroup.controls.additionalStorage.value).toBe(5); + }); + + it("should reset additional storage when plan doesn't support it", () => { + component.formGroup.controls.additionalStorage.setValue(10); + component.productTier = ProductTierType.Free; + component.changedProduct(); + + expect(component.formGroup.controls.additionalStorage.value).toBe(0); + }); + + it("should handle additional seats for various scenarios", () => { + // Plan without additional seats option should reset to 0 + component.formGroup.controls.additionalSeats.setValue(10); + component.productTier = ProductTierType.Families; + component.changedProduct(); + + expect(component.formGroup.controls.additionalSeats.value).toBe(0); + + // Default to 1 seat for new org with seats option + component.productTier = ProductTierType.Teams; + component.changedProduct(); + + expect(component.formGroup.controls.additionalSeats.value).toBeGreaterThanOrEqual(1); + }); + + it("should prefill seats from current plan when upgrading from non-seats plan", async () => { + mockOrganization = { + id: "org-123", + name: "Test Org", + productTierType: ProductTierType.Free, + seats: 2, + } as Organization; + + organizationsSubject.next([mockOrganization]); + + mockOrganizationApiService.getBilling.mockResolvedValue({ + paymentSource: { type: "card" }, + } as any); + + mockOrganizationApiService.getSubscription.mockResolvedValue({ + planType: PlanType.Free, + } as any); + + const upgradeFixture = TestBed.createComponent(OrganizationPlansComponent); + const upgradeComponent = upgradeFixture.componentInstance; + upgradeComponent.organizationId = "org-123"; + upgradeComponent.currentPlan = mockPasswordManagerPlans[0]; // Free plan (no additional seats) + + upgradeFixture.detectChanges(); + await upgradeFixture.whenStable(); + + upgradeComponent.productTier = ProductTierType.Teams; + upgradeComponent.changedProduct(); + + // Should use base seats from current plan + expect(upgradeComponent.formGroup.controls.additionalSeats.value).toBe(2); + }); + }); + + describe("provider creation flow", () => { + beforeEach(() => { + component.providerId = "provider-123"; + mockProviderApiService.getProvider.mockResolvedValue({ + id: "provider-123", + name: "Test Provider", + } as any); + }); + + it("should create organization through provider with wrapped key", async () => { + fixture.detectChanges(); + await fixture.whenStable(); + + patchOrganizationForm(component, { + name: "Provider Client Org", + billingEmail: "client@example.com", + productTier: ProductTierType.Teams, + plan: PlanType.TeamsAnnually, + additionalSeats: 5, + }); + component.formGroup.patchValue({ + clientOwnerEmail: "owner@client.com", + }); + + patchBillingAddress(component); + + const mockOrgKey = {} as any; + const mockProviderKey = {} as any; + + mockKeyService.makeOrgKey.mockResolvedValue([ + { encryptedString: "mock-key" }, + mockOrgKey, + ] as any); + + mockEncryptService.encryptString.mockResolvedValue({ + encryptedString: "mock-collection", + } as any); + + mockKeyService.makeKeyPair.mockResolvedValue([ + "public-key", + { encryptedString: "private-key" }, + ] as any); + + mockKeyService.providerKeys$.mockReturnValue(of({ "provider-123": mockProviderKey })); + + mockEncryptService.wrapSymmetricKey.mockResolvedValue({ + encryptedString: "wrapped-key", + } as any); + + mockApiService.postProviderCreateOrganization.mockResolvedValue({ + organizationId: "provider-org-id", + } as any); + + setupMockPaymentMethodComponent(component); + + await component.submit(); + + expect(mockApiService.postProviderCreateOrganization).toHaveBeenCalledWith( + "provider-123", + expect.objectContaining({ + clientOwnerEmail: "owner@client.com", + }), + ); + + expect(mockEncryptService.wrapSymmetricKey).toHaveBeenCalledWith(mockOrgKey, mockProviderKey); + }); + }); + + describe("upgrade with missing keys flow", () => { + beforeEach(async () => { + mockOrganization = { + id: "org-123", + name: "Test Org", + productTierType: ProductTierType.Free, + seats: 5, + hasPublicAndPrivateKeys: false, // Missing keys + } as Organization; + + organizationsSubject.next([mockOrganization]); + + mockOrganizationApiService.getBilling.mockResolvedValue({ + paymentSource: { type: "card" }, + } as any); + + mockOrganizationApiService.getSubscription.mockResolvedValue({ + planType: PlanType.Free, + } as any); + + component.organizationId = "org-123"; + + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should backfill organization keys during upgrade", async () => { + component.productTier = ProductTierType.Teams; + component.plan = PlanType.TeamsAnnually; + component.formGroup.controls.additionalSeats.setValue(5); + + const mockOrgShareKey = {} as any; + mockKeyService.orgKeys$.mockReturnValue(of({ "org-123": mockOrgShareKey })); + + mockKeyService.makeKeyPair.mockResolvedValue([ + "public-key", + { encryptedString: "private-key" }, + ] as any); + + mockOrganizationApiService.upgrade.mockResolvedValue(undefined); + + await component.submit(); + + expect(mockOrganizationApiService.upgrade).toHaveBeenCalledWith( + "org-123", + expect.objectContaining({ + keys: expect.any(Object), + }), + ); + }); + }); + + describe("trial flow", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should emit onTrialBillingSuccess when in trial flow", async () => { + component["isInTrialFlow"] = true; + const trialSpy = jest.spyOn(component.onTrialBillingSuccess, "emit"); + + component.formGroup.patchValue({ + name: "Trial Org", + billingEmail: "trial@example.com", + productTier: ProductTierType.Enterprise, + plan: PlanType.EnterpriseAnnually, + additionalSeats: 10, + }); + + component.billingFormGroup.controls.billingAddress.patchValue({ + country: "US", + postalCode: "12345", + line1: "123 Street", + city: "City", + state: "CA", + }); + + mockKeyService.makeOrgKey.mockResolvedValue([ + { encryptedString: "mock-key" }, + {} as any, + ] as any); + + mockEncryptService.encryptString.mockResolvedValue({ + encryptedString: "mock-collection", + } as any); + + mockKeyService.makeKeyPair.mockResolvedValue([ + "public-key", + { encryptedString: "private-key" }, + ] as any); + + mockOrganizationApiService.create.mockResolvedValue({ + id: "trial-org-id", + } as any); + + component["enterPaymentMethodComponent"] = { + tokenize: jest.fn().mockResolvedValue({ + token: "mock_token", + type: "card", + }), + } as any; + + await component.submit(); + + expect(trialSpy).toHaveBeenCalledWith({ + orgId: "trial-org-id", + subLabelText: expect.stringContaining("annual"), + }); + }); + + it("should not navigate away when in trial flow", async () => { + component["isInTrialFlow"] = true; + + component.formGroup.patchValue({ + name: "Trial Org", + billingEmail: "trial@example.com", + productTier: ProductTierType.Free, + plan: PlanType.Free, + }); + + mockKeyService.makeOrgKey.mockResolvedValue([ + { encryptedString: "mock-key" }, + {} as any, + ] as any); + + mockEncryptService.encryptString.mockResolvedValue({ + encryptedString: "mock-collection", + } as any); + + mockKeyService.makeKeyPair.mockResolvedValue([ + "public-key", + { encryptedString: "private-key" }, + ] as any); + + mockOrganizationApiService.create.mockResolvedValue({ + id: "trial-org-id", + } as any); + + await component.submit(); + + expect(mockRouter.navigate).not.toHaveBeenCalled(); + }); + }); + + describe("upgrade prefill flow", () => { + it("should prefill Families plan for Free tier upgrade", async () => { + mockOrganization = { + id: "org-123", + name: "Test Org", + productTierType: ProductTierType.Free, + } as Organization; + + organizationsSubject.next([mockOrganization]); + + mockOrganizationApiService.getBilling.mockResolvedValue({ + paymentSource: null, + } as any); + + mockOrganizationApiService.getSubscription.mockResolvedValue({ + planType: PlanType.Free, + } as any); + + const upgradeFixture = TestBed.createComponent(OrganizationPlansComponent); + const upgradeComponent = upgradeFixture.componentInstance; + upgradeComponent.organizationId = "org-123"; + upgradeComponent.currentPlan = mockPasswordManagerPlans[0]; // Free + + upgradeFixture.detectChanges(); + await upgradeFixture.whenStable(); + + expect(upgradeComponent.plan).toBe(PlanType.FamiliesAnnually); + expect(upgradeComponent.productTier).toBe(ProductTierType.Families); + }); + + it("should prefill Teams plan for Families tier upgrade when TeamsStarter unavailable", async () => { + mockOrganization = { + id: "org-123", + name: "Test Org", + productTierType: ProductTierType.Families, + } as Organization; + + organizationsSubject.next([mockOrganization]); + + mockOrganizationApiService.getBilling.mockResolvedValue({ + paymentSource: { type: "card" }, + } as any); + + mockOrganizationApiService.getSubscription.mockResolvedValue({ + planType: PlanType.FamiliesAnnually, + } as any); + + const upgradeFixture = TestBed.createComponent(OrganizationPlansComponent); + const upgradeComponent = upgradeFixture.componentInstance; + upgradeComponent.organizationId = "org-123"; + upgradeComponent.currentPlan = mockPasswordManagerPlans[1]; // Families + + upgradeFixture.detectChanges(); + await upgradeFixture.whenStable(); + + expect(upgradeComponent.plan).toBe(PlanType.TeamsAnnually); + expect(upgradeComponent.productTier).toBe(ProductTierType.Teams); + }); + + it("should use upgradeSortOrder for sequential plan upgrades", async () => { + mockOrganization = { + id: "org-123", + name: "Test Org", + productTierType: ProductTierType.Teams, + } as Organization; + + organizationsSubject.next([mockOrganization]); + + mockOrganizationApiService.getBilling.mockResolvedValue({ + paymentSource: { type: "card" }, + } as any); + + mockOrganizationApiService.getSubscription.mockResolvedValue({ + planType: PlanType.TeamsAnnually, + } as any); + + const upgradeFixture = TestBed.createComponent(OrganizationPlansComponent); + const upgradeComponent = upgradeFixture.componentInstance; + upgradeComponent.organizationId = "org-123"; + upgradeComponent.currentPlan = mockPasswordManagerPlans[2]; // Teams + + upgradeFixture.detectChanges(); + await upgradeFixture.whenStable(); + + expect(upgradeComponent.plan).toBe(PlanType.EnterpriseAnnually); + expect(upgradeComponent.productTier).toBe(ProductTierType.Enterprise); + }); + + it("should not prefill for Enterprise tier (no upgrade available)", async () => { + mockOrganization = { + id: "org-123", + name: "Test Org", + productTierType: ProductTierType.Enterprise, + } as Organization; + + organizationsSubject.next([mockOrganization]); + + mockOrganizationApiService.getBilling.mockResolvedValue({ + paymentSource: { type: "card" }, + } as any); + + mockOrganizationApiService.getSubscription.mockResolvedValue({ + planType: PlanType.EnterpriseAnnually, + } as any); + + const upgradeFixture = TestBed.createComponent(OrganizationPlansComponent); + const upgradeComponent = upgradeFixture.componentInstance; + upgradeComponent.organizationId = "org-123"; + upgradeComponent.currentPlan = mockPasswordManagerPlans[3]; // Enterprise + + upgradeFixture.detectChanges(); + await upgradeFixture.whenStable(); + + // Should not change from default Free + expect(upgradeComponent.productTier).toBe(ProductTierType.Free); + }); + }); + + describe("plan filtering logic", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should check if provider is qualified for 2020 plans", () => { + component.providerId = "provider-123"; + component["provider"] = { + id: "provider-123", + creationDate: "2023-01-01", // Before cutoff + } as any; + + const isQualified = component["isProviderQualifiedFor2020Plan"](); + + expect(isQualified).toBe(true); + }); + + it("should not qualify provider created after 2020 plan cutoff", () => { + component.providerId = "provider-123"; + component["provider"] = { + id: "provider-123", + creationDate: "2023-12-01", // After cutoff (2023-11-06) + } as any; + + const isQualified = component["isProviderQualifiedFor2020Plan"](); + + expect(isQualified).toBe(false); + }); + + it("should return false if provider has no creation date", () => { + component.providerId = "provider-123"; + component["provider"] = { + id: "provider-123", + creationDate: null, + } as any; + + const isQualified = component["isProviderQualifiedFor2020Plan"](); + + expect(isQualified).toBe(false); + }); + + it("should exclude upgrade-ineligible plans", async () => { + mockOrganization = { + id: "org-123", + name: "Test Org", + productTierType: ProductTierType.Teams, + } as Organization; + + organizationsSubject.next([mockOrganization]); + + mockOrganizationApiService.getBilling.mockResolvedValue({ + paymentSource: { type: "card" }, + } as any); + + mockOrganizationApiService.getSubscription.mockResolvedValue({ + planType: PlanType.TeamsAnnually, + } as any); + + const upgradeFixture = TestBed.createComponent(OrganizationPlansComponent); + const upgradeComponent = upgradeFixture.componentInstance; + upgradeComponent.organizationId = "org-123"; + upgradeComponent.currentPlan = mockPasswordManagerPlans[2]; // Teams + + upgradeFixture.detectChanges(); + await upgradeFixture.whenStable(); + + const products = upgradeComponent.selectableProducts; + + // Should not include plans with lower or equal upgradeSortOrder + expect(products.find((p) => p.type === PlanType.Free)).toBeUndefined(); + expect(products.find((p) => p.type === PlanType.FamiliesAnnually)).toBeUndefined(); + expect(products.find((p) => p.type === PlanType.TeamsAnnually)).toBeUndefined(); + }); + }); + + describe("helper calculation methods", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should calculate monthly seat price correctly", () => { + const annualPlan = mockPasswordManagerPlans[2]; // Teams Annual - 48/year + const monthlyPrice = component.seatPriceMonthly(annualPlan); + + expect(monthlyPrice).toBe(4); // 48 / 12 + }); + + it("should calculate monthly storage price correctly", () => { + const annualPlan = mockPasswordManagerPlans[2]; // 4/GB/year + const monthlyPrice = component.additionalStoragePriceMonthly(annualPlan); + + expect(monthlyPrice).toBeCloseTo(0.333, 2); // 4 / 12 + }); + + it("should generate billing sublabel text for annual plan", () => { + component.productTier = ProductTierType.Teams; + component.plan = PlanType.TeamsAnnually; + + const sublabel = component["billingSubLabelText"](); + + expect(sublabel).toContain("annual"); + expect(sublabel).toContain("$48"); // Seat price + expect(sublabel).toContain("yr"); + }); + + it("should generate billing sublabel text for plan with base price", () => { + component.productTier = ProductTierType.Families; + component.plan = PlanType.FamiliesAnnually; + + const sublabel = component["billingSubLabelText"](); + + expect(sublabel).toContain("annual"); + expect(sublabel).toContain("$40"); // Base price + }); + }); + + describe("template rendering and UI visibility", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should control form visibility based on loading state", () => { + // Initially not loading after setup + expect(component.loading).toBe(false); + + // When loading + component.loading = true; + expect(component.loading).toBe(true); + + // When not loading + component.loading = false; + expect(component.loading).toBe(false); + }); + + it("should determine createOrganization based on organizationId", () => { + // Create flow - no organizationId + expect(component.createOrganization).toBe(true); + + // Upgrade flow - has organizationId + component.organizationId = "org-123"; + expect(component.createOrganization).toBe(false); + }); + + it("should calculate passwordManagerSubtotal correctly for paid plans", () => { + component.productTier = ProductTierType.Teams; + component.changedProduct(); + component.formGroup.controls.additionalSeats.setValue(5); + + const subtotal = component.passwordManagerSubtotal; + + expect(typeof subtotal).toBe("number"); + expect(subtotal).toBeGreaterThan(0); + }); + + it("should show payment description based on plan type", () => { + component.productTier = ProductTierType.Teams; + component.changedProduct(); + + const paymentDesc = component.paymentDesc; + + expect(typeof paymentDesc).toBe("string"); + expect(paymentDesc.length).toBeGreaterThan(0); + }); + + it("should display tax ID field for business plans", () => { + component.productTier = ProductTierType.Free; + expect(component["showTaxIdField"]).toBe(false); + + component.productTier = ProductTierType.Families; + expect(component["showTaxIdField"]).toBe(false); + + component.productTier = ProductTierType.Teams; + expect(component["showTaxIdField"]).toBe(true); + + component.productTier = ProductTierType.Enterprise; + expect(component["showTaxIdField"]).toBe(true); + }); + + it("should show single org policy block when applicable", () => { + component.singleOrgPolicyAppliesToActiveUser = false; + expect(component.singleOrgPolicyBlock).toBe(false); + + component.singleOrgPolicyAppliesToActiveUser = true; + expect(component.singleOrgPolicyBlock).toBe(true); + + // But not when has provider + component.providerId = "provider-123"; + expect(component.singleOrgPolicyBlock).toBe(false); + }); + + it("should determine upgrade requires payment method correctly", async () => { + // Create flow - no organization + expect(component.upgradeRequiresPaymentMethod).toBe(false); + + // Create new component with organization setup + const mockOrg = setupMockUpgradeOrganization( + mockOrganizationApiService, + organizationsSubject, + { + productTierType: ProductTierType.Free, + hasPaymentSource: false, + }, + ); + + const upgradeFixture = TestBed.createComponent(OrganizationPlansComponent); + const upgradeComponent = upgradeFixture.componentInstance; + upgradeComponent.organizationId = mockOrg.id; + upgradeComponent.showFree = false; + + upgradeFixture.detectChanges(); + await upgradeFixture.whenStable(); + + expect(upgradeComponent.upgradeRequiresPaymentMethod).toBe(true); + }); + }); + + describe("user interactions and form controls", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should update component state when product tier changes", () => { + component.productTier = ProductTierType.Free; + + // Simulate changing product tier + component.productTier = ProductTierType.Teams; + component.formGroup.controls.productTier.setValue(ProductTierType.Teams); + component.changedProduct(); + + expect(component.productTier).toBe(ProductTierType.Teams); + expect(component.formGroup.controls.plan.value).toBe(PlanType.TeamsAnnually); + }); + + it("should update plan when changedOwnedBusiness is called", () => { + component.formGroup.controls.businessOwned.setValue(false); + component.productTier = ProductTierType.Families; + + component.formGroup.controls.businessOwned.setValue(true); + component.changedOwnedBusiness(); + + // Should switch to a business-compatible plan + expect(component.formGroup.controls.productTier.value).toBe(ProductTierType.Teams); + }); + + it("should emit onCanceled when cancel is called", () => { + const cancelSpy = jest.spyOn(component.onCanceled, "emit"); + + component["cancel"](); + + expect(cancelSpy).toHaveBeenCalled(); + }); + + it("should update form value when additional seats changes", () => { + component.productTier = ProductTierType.Teams; + component.changedProduct(); + + component.formGroup.controls.additionalSeats.setValue(10); + + expect(component.formGroup.controls.additionalSeats.value).toBe(10); + }); + + it("should update form value when additional storage changes", () => { + component.productTier = ProductTierType.Teams; + component.changedProduct(); + + component.formGroup.controls.additionalStorage.setValue(5); + + expect(component.formGroup.controls.additionalStorage.value).toBe(5); + }); + + it("should mark form as invalid when required fields are empty", () => { + component.formGroup.controls.name.setValue(""); + component.formGroup.controls.billingEmail.setValue(""); + component.formGroup.markAllAsTouched(); + + expect(component.formGroup.invalid).toBe(true); + }); + + it("should mark form as valid when all required fields are filled correctly", () => { + patchOrganizationForm(component, { + name: "Valid Org", + billingEmail: "valid@example.com", + }); + + expect(component.formGroup.valid).toBe(true); + }); + + it("should calculate subtotals based on form values", () => { + component.productTier = ProductTierType.Teams; + component.changedProduct(); + component.formGroup.controls.additionalSeats.setValue(5); + component.formGroup.controls.additionalStorage.setValue(10); + + const subtotal = component.passwordManagerSubtotal; + + // Should include cost of seats and storage + expect(subtotal).toBeGreaterThan(0); + }); + + it("should enable Secrets Manager form when plan supports it", () => { + // Free plan doesn't offer Secrets Manager + component.productTier = ProductTierType.Free; + component.formGroup.controls.productTier.setValue(ProductTierType.Free); + component.changedProduct(); + expect(component.planOffersSecretsManager).toBe(false); + + // Teams plan offers Secrets Manager + component.productTier = ProductTierType.Teams; + component.formGroup.controls.productTier.setValue(ProductTierType.Teams); + component.changedProduct(); + expect(component.planOffersSecretsManager).toBe(true); + expect(component.secretsManagerForm.disabled).toBe(false); + }); + + it("should update Secrets Manager subtotal when values change", () => { + component.productTier = ProductTierType.Teams; + component.changedProduct(); + + component.secretsManagerForm.patchValue({ + enabled: false, + }); + expect(component.secretsManagerSubtotal).toBe(0); + + component.secretsManagerForm.patchValue({ + enabled: true, + userSeats: 3, + additionalServiceAccounts: 10, + }); + + const smSubtotal = component.secretsManagerSubtotal; + expect(smSubtotal).toBeGreaterThan(0); + }); + }); + + describe("payment method and billing flow", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should update payment method during upgrade when required", async () => { + mockOrganization = { + id: "org-123", + name: "Test Org", + productTierType: ProductTierType.Free, + seats: 5, + hasPublicAndPrivateKeys: true, + } as Organization; + + organizationsSubject.next([mockOrganization]); + + mockOrganizationApiService.getBilling.mockResolvedValue({ + paymentSource: null, // No existing payment source + } as any); + + mockOrganizationApiService.getSubscription.mockResolvedValue({ + planType: PlanType.Free, + } as any); + + const upgradeFixture = TestBed.createComponent(OrganizationPlansComponent); + const upgradeComponent = upgradeFixture.componentInstance; + upgradeComponent.organizationId = "org-123"; + upgradeComponent.showFree = false; // Triggers upgradeRequiresPaymentMethod + + upgradeFixture.detectChanges(); + await upgradeFixture.whenStable(); + + upgradeComponent.productTier = ProductTierType.Teams; + upgradeComponent.plan = PlanType.TeamsAnnually; + upgradeComponent.formGroup.controls.additionalSeats.setValue(5); + + upgradeComponent.billingFormGroup.controls.billingAddress.patchValue({ + country: "US", + postalCode: "12345", + line1: "123 Street", + city: "City", + state: "CA", + }); + + upgradeComponent["enterPaymentMethodComponent"] = { + tokenize: jest.fn().mockResolvedValue({ + token: "new_token", + type: "card", + }), + } as any; + + mockOrganizationApiService.upgrade.mockResolvedValue(undefined); + + await upgradeComponent.submit(); + + expect(mockSubscriberBillingClient.updatePaymentMethod).toHaveBeenCalledWith( + { type: "organization", data: mockOrganization }, + { token: "new_token", type: "card" }, + { country: "US", postalCode: "12345" }, + ); + + expect(mockOrganizationApiService.upgrade).toHaveBeenCalled(); + }); + + it("should validate billing form for paid plans during creation", async () => { + component.formGroup.patchValue({ + name: "New Org", + billingEmail: "test@example.com", + productTier: ProductTierType.Teams, + plan: PlanType.TeamsAnnually, + additionalSeats: 5, + }); + + // Invalid billing form - explicitly mark as invalid since we removed validators from mock forms + component.billingFormGroup.controls.billingAddress.patchValue({ + country: "", + postalCode: "", + }); + + await component.submit(); + + expect(mockOrganizationApiService.create).not.toHaveBeenCalled(); + expect(component.billingFormGroup.invalid).toBe(true); + }); + + it("should not require billing validation for Free plan", async () => { + component.formGroup.patchValue({ + name: "Free Org", + billingEmail: "test@example.com", + productTier: ProductTierType.Free, + plan: PlanType.Free, + }); + + // Leave billing form empty + component.billingFormGroup.reset(); + + mockKeyService.makeOrgKey.mockResolvedValue([ + { encryptedString: "mock-key" }, + {} as any, + ] as any); + + mockEncryptService.encryptString.mockResolvedValue({ + encryptedString: "mock-collection", + } as any); + + mockKeyService.makeKeyPair.mockResolvedValue([ + "public-key", + { encryptedString: "private-key" }, + ] as any); + + mockOrganizationApiService.create.mockResolvedValue({ + id: "free-org-id", + } as any); + + await component.submit(); + + expect(mockOrganizationApiService.create).toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.ts b/apps/web/src/app/billing/organizations/organization-plans.component.ts index 3364ce2cbea..73fea30fa83 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.ts +++ b/apps/web/src/app/billing/organizations/organization-plans.component.ts @@ -113,8 +113,6 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { // eslint-disable-next-line @angular-eslint/prefer-signals @Input() currentPlan: PlanResponse; - selectedFile: File; - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals // eslint-disable-next-line @angular-eslint/prefer-signals @Input() @@ -675,9 +673,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { const collectionCt = collection.encryptedString; const orgKeys = await this.keyService.makeKeyPair(orgKey[1]); - orgId = this.selfHosted - ? await this.createSelfHosted(key, collectionCt, orgKeys) - : await this.createCloudHosted(key, collectionCt, orgKeys, orgKey[1], activeUserId); + orgId = await this.createCloudHosted(key, collectionCt, orgKeys, orgKey[1], activeUserId); this.toastService.showToast({ variant: "success", @@ -953,27 +949,6 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { } } - private async createSelfHosted(key: string, collectionCt: string, orgKeys: [string, EncString]) { - if (!this.selectedFile) { - throw new Error(this.i18nService.t("selectFile")); - } - - const fd = new FormData(); - fd.append("license", this.selectedFile); - fd.append("key", key); - fd.append("collectionName", collectionCt); - const response = await this.organizationApiService.createLicense(fd); - const orgId = response.id; - - await this.apiService.refreshIdentityToken(); - - // Org Keys live outside of the OrganizationLicense - add the keys to the org here - const request = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString); - await this.organizationApiService.updateKeys(orgId, request); - - return orgId; - } - private billingSubLabelText(): string { const selectedPlan = this.selectedPlan; const price = From 6dea7504a6105d5d34525e4d85eccf375cd7ef3e Mon Sep 17 00:00:00 2001 From: Mike Amirault Date: Wed, 18 Feb 2026 14:49:51 -0500 Subject: [PATCH 080/134] [PM-26732] Remove Chromium ABE importer feature flag (#19039) --- libs/common/src/enums/feature-flag.enum.ts | 2 - .../default-import-metadata.service.ts | 47 +-------- .../services/import-metadata.service.spec.ts | 95 +------------------ 3 files changed, 6 insertions(+), 138 deletions(-) diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index d252f7dcda5..5160e6aa542 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -53,7 +53,6 @@ export enum FeatureFlag { /* Tools */ UseSdkPasswordGenerators = "pm-19976-use-sdk-password-generators", - ChromiumImporterWithABE = "pm-25855-chromium-importer-abe", SendUIRefresh = "pm-28175-send-ui-refresh", SendEmailOTP = "pm-19051-send-email-verification", @@ -120,7 +119,6 @@ export const DefaultFeatureFlagValue = { /* Tools */ [FeatureFlag.UseSdkPasswordGenerators]: FALSE, - [FeatureFlag.ChromiumImporterWithABE]: FALSE, [FeatureFlag.SendUIRefresh]: FALSE, [FeatureFlag.SendEmailOTP]: FALSE, diff --git a/libs/importer/src/services/default-import-metadata.service.ts b/libs/importer/src/services/default-import-metadata.service.ts index 393c498e118..a9e767178aa 100644 --- a/libs/importer/src/services/default-import-metadata.service.ts +++ b/libs/importer/src/services/default-import-metadata.service.ts @@ -1,11 +1,9 @@ -import { combineLatest, map, Observable } from "rxjs"; +import { map, Observable } from "rxjs"; -import { ClientType, DeviceType } from "@bitwarden/common/enums"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { SemanticLogger } from "@bitwarden/common/tools/log"; import { SystemServiceProvider } from "@bitwarden/common/tools/providers"; -import { DataLoader, ImporterMetadata, Importers, ImportersMetadata, Loader } from "../metadata"; +import { ImporterMetadata, Importers, ImportersMetadata } from "../metadata"; import { ImportType } from "../models/import-options"; import { availableLoaders } from "../util"; @@ -15,13 +13,8 @@ export class DefaultImportMetadataService implements ImportMetadataServiceAbstra protected importers: ImportersMetadata = Importers; private logger: SemanticLogger; - private chromiumWithABE$: Observable; - constructor(protected system: SystemServiceProvider) { this.logger = system.log({ type: "ImportMetadataService" }); - this.chromiumWithABE$ = this.system.configService.getFeatureFlag$( - FeatureFlag.ChromiumImporterWithABE, - ); } async init(): Promise { @@ -30,13 +23,13 @@ export class DefaultImportMetadataService implements ImportMetadataServiceAbstra metadata$(type$: Observable): Observable { const client = this.system.environment.getClientType(); - const capabilities$ = combineLatest([type$, this.chromiumWithABE$]).pipe( - map(([type, enabled]) => { + const capabilities$ = type$.pipe( + map((type) => { if (!this.importers) { return { type, loaders: [] }; } - const loaders = this.availableLoaders(this.importers, type, client, enabled); + const loaders = availableLoaders(this.importers, type, client); if (!loaders || loaders.length === 0) { return { type, loaders: [] }; @@ -55,34 +48,4 @@ export class DefaultImportMetadataService implements ImportMetadataServiceAbstra return capabilities$; } - - /** Determine the available loaders for the given import type and client, considering feature flags and environments */ - private availableLoaders( - importers: ImportersMetadata, - type: ImportType, - client: ClientType, - withABESupport: boolean, - ): DataLoader[] | undefined { - let loaders = availableLoaders(importers, type, client); - - if (withABESupport) { - return loaders; - } - - // Special handling for Brave and Chrome CSV imports on Windows Desktop - if (type === "bravecsv" || type === "chromecsv") { - try { - const device = this.system.environment.getDevice(); - const isWindowsDesktop = device === DeviceType.WindowsDesktop; - if (isWindowsDesktop) { - // Exclude the Chromium loader if on Windows Desktop without ABE support - loaders = loaders?.filter((loader) => loader !== Loader.chromium); - } - } catch { - loaders = loaders?.filter((loader) => loader !== Loader.chromium); - } - } - - return loaders; - } } diff --git a/libs/importer/src/services/import-metadata.service.spec.ts b/libs/importer/src/services/import-metadata.service.spec.ts index e16965a69f8..d6c0ff64d87 100644 --- a/libs/importer/src/services/import-metadata.service.spec.ts +++ b/libs/importer/src/services/import-metadata.service.spec.ts @@ -1,9 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; -import { BehaviorSubject, Subject, firstValueFrom } from "rxjs"; +import { Subject, firstValueFrom } from "rxjs"; import { ClientType } from "@bitwarden/client-type"; -import { DeviceType } from "@bitwarden/common/enums"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SystemServiceProvider } from "@bitwarden/common/tools/providers"; @@ -17,13 +15,10 @@ describe("ImportMetadataService", () => { let systemServiceProvider: MockProxy; beforeEach(() => { - const configService = mock(); - const environment = mock(); environment.getClientType.mockReturnValue(ClientType.Desktop); systemServiceProvider = mock({ - configService, environment, log: jest.fn().mockReturnValue({ debug: jest.fn() }), }); @@ -34,7 +29,6 @@ describe("ImportMetadataService", () => { describe("metadata$", () => { let typeSubject: Subject; let mockLogger: { debug: jest.Mock }; - let featureFlagSubject: BehaviorSubject; const environment = mock(); environment.getClientType.mockReturnValue(ClientType.Desktop); @@ -42,13 +36,8 @@ describe("ImportMetadataService", () => { beforeEach(() => { typeSubject = new Subject(); mockLogger = { debug: jest.fn() }; - featureFlagSubject = new BehaviorSubject(false); - - const configService = mock(); - configService.getFeatureFlag$.mockReturnValue(featureFlagSubject); systemServiceProvider = mock({ - configService, environment, log: jest.fn().mockReturnValue(mockLogger), }); @@ -78,7 +67,6 @@ describe("ImportMetadataService", () => { afterEach(() => { typeSubject.complete(); - featureFlagSubject.complete(); }); it("should emit metadata when type$ emits", async () => { @@ -129,86 +117,5 @@ describe("ImportMetadataService", () => { "capabilities updated", ); }); - - it("should update when feature flag changes", async () => { - environment.getDevice.mockReturnValue(DeviceType.WindowsDesktop); - const testType: ImportType = "bravecsv"; // Use bravecsv which supports chromium loader - const emissions: ImporterMetadata[] = []; - - const subscription = sut.metadata$(typeSubject).subscribe((metadata) => { - emissions.push(metadata); - }); - - typeSubject.next(testType); - featureFlagSubject.next(true); - - // Wait for emissions - await new Promise((resolve) => setTimeout(resolve, 0)); - - expect(emissions).toHaveLength(2); - // Disable ABE - chromium loader should be excluded - expect(emissions[0].loaders).not.toContain(Loader.chromium); - // Enabled ABE - chromium loader should be included - expect(emissions[1].loaders).toContain(Loader.chromium); - - subscription.unsubscribe(); - }); - - it("should exclude chromium loader when ABE is disabled and on Windows Desktop", async () => { - environment.getDevice.mockReturnValue(DeviceType.WindowsDesktop); - const testType: ImportType = "bravecsv"; // bravecsv supports both file and chromium loaders - featureFlagSubject.next(false); - - const metadataPromise = firstValueFrom(sut.metadata$(typeSubject)); - typeSubject.next(testType); - - const result = await metadataPromise; - - expect(result.loaders).not.toContain(Loader.chromium); - expect(result.loaders).toContain(Loader.file); - }); - - it("should exclude chromium loader when ABE is disabled and getDevice throws error", async () => { - environment.getDevice.mockImplementation(() => { - throw new Error("Device detection failed"); - }); - const testType: ImportType = "bravecsv"; - featureFlagSubject.next(false); - - const metadataPromise = firstValueFrom(sut.metadata$(typeSubject)); - typeSubject.next(testType); - - const result = await metadataPromise; - - expect(result.loaders).not.toContain(Loader.chromium); - expect(result.loaders).toContain(Loader.file); - }); - - it("should include chromium loader when ABE is disabled and not on Windows Desktop", async () => { - environment.getDevice.mockReturnValue(DeviceType.MacOsDesktop); - const testType: ImportType = "bravecsv"; // bravecsv supports both file and chromium loaders - featureFlagSubject.next(false); - - const metadataPromise = firstValueFrom(sut.metadata$(typeSubject)); - typeSubject.next(testType); - - const result = await metadataPromise; - - expect(result.loaders).toContain(Loader.chromium); - expect(result.loaders).toContain(Loader.file); - }); - - it("should include chromium loader when ABE is enabled regardless of device", async () => { - environment.getDevice.mockReturnValue(DeviceType.MacOsDesktop); - const testType: ImportType = "bravecsv"; // bravecsv supports both file and chromium loaders - featureFlagSubject.next(true); - - const metadataPromise = firstValueFrom(sut.metadata$(typeSubject)); - typeSubject.next(testType); - - const result = await metadataPromise; - - expect(result.loaders).toContain(Loader.chromium); - }); }); }); From bca2ebaca9b53b519e08877e9bf1c25a8d7d3883 Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Wed, 18 Feb 2026 16:22:50 -0500 Subject: [PATCH 081/134] [PM-30122] allow no folders inside browser folder settings (#19041) --- .../src/vault/popup/settings/folders.component.spec.ts | 3 ++- apps/browser/src/vault/popup/settings/folders.component.ts | 7 ------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/apps/browser/src/vault/popup/settings/folders.component.spec.ts b/apps/browser/src/vault/popup/settings/folders.component.spec.ts index 678e6d3f10e..7e08cc684a1 100644 --- a/apps/browser/src/vault/popup/settings/folders.component.spec.ts +++ b/apps/browser/src/vault/popup/settings/folders.component.spec.ts @@ -94,11 +94,12 @@ describe("FoldersComponent", () => { fixture.detectChanges(); }); - it("removes the last option in the folder array", (done) => { + it("should show all folders", (done) => { component.folders$.subscribe((folders) => { expect(folders).toEqual([ { id: "1", name: "Folder 1" }, { id: "2", name: "Folder 2" }, + { id: "0", name: "No Folder" }, ]); done(); }); diff --git a/apps/browser/src/vault/popup/settings/folders.component.ts b/apps/browser/src/vault/popup/settings/folders.component.ts index b70c17bd6a5..a38f6630949 100644 --- a/apps/browser/src/vault/popup/settings/folders.component.ts +++ b/apps/browser/src/vault/popup/settings/folders.component.ts @@ -53,13 +53,6 @@ export class FoldersComponent { this.folders$ = this.activeUserId$.pipe( filter((userId): userId is UserId => userId !== null), switchMap((userId) => this.folderService.folderViews$(userId)), - map((folders) => { - // Remove the last folder, which is the "no folder" option folder - if (folders.length > 0) { - return folders.slice(0, folders.length - 1); - } - return folders; - }), ); } From 263ec9412433f1c87360b5810184e4e43fd3d5d2 Mon Sep 17 00:00:00 2001 From: John Harrington <84741727+harr1424@users.noreply.github.com> Date: Wed, 18 Feb 2026 14:59:34 -0700 Subject: [PATCH 082/134] [PM-32161] Remove all emails when email list field is cleared and send is saved (#18959) * add new validation criteria to prevent authType.Email with an empty emails field * simplify validation logic --- apps/browser/src/_locales/en/messages.json | 4 +- apps/desktop/src/locales/en/messages.json | 3 ++ apps/web/src/locales/en/messages.json | 3 ++ .../send-details.component.spec.ts | 53 +++++++++++++++++++ .../send-details/send-details.component.ts | 22 +++++++- 5 files changed, 83 insertions(+), 2 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 5ed97ce0f07..cc99e0abe18 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -6160,10 +6160,12 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, - "downloadBitwardenApps": { "message": "Download Bitwarden apps" }, diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 85ef3d94001..3f005db0ba8 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -4615,6 +4615,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 4731be36ef5..ba59184a9f9 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -12866,6 +12866,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, diff --git a/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.spec.ts b/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.spec.ts index f816c9d5ce4..43b2bc7bcd5 100644 --- a/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.spec.ts +++ b/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.spec.ts @@ -127,4 +127,57 @@ describe("SendDetailsComponent", () => { expect(emailsControl?.validator).toBeNull(); expect(passwordControl?.validator).toBeNull(); }); + + it("should show validation error when emails are cleared while authType is Email", () => { + // Set authType to Email with valid emails + component.sendDetailsForm.patchValue({ + authType: AuthType.Email, + emails: "test@example.com", + }); + expect(component.sendDetailsForm.get("emails")?.valid).toBe(true); + + // Clear emails - should trigger validation error + component.sendDetailsForm.patchValue({ emails: "" }); + expect(component.sendDetailsForm.get("emails")?.valid).toBe(false); + expect(component.sendDetailsForm.get("emails")?.hasError("emailsRequiredForEmailAuth")).toBe( + true, + ); + }); + + it("should clear validation error when authType is changed from Email after clearing emails", () => { + // Set authType to Email and then clear emails + component.sendDetailsForm.patchValue({ + authType: AuthType.Email, + emails: "test@example.com", + }); + component.sendDetailsForm.patchValue({ emails: "" }); + expect(component.sendDetailsForm.get("emails")?.valid).toBe(false); + + // Change authType to None - emails field should become valid (no longer required) + component.sendDetailsForm.patchValue({ authType: AuthType.None }); + expect(component.sendDetailsForm.get("emails")?.valid).toBe(true); + }); + + it("should force user to change authType by blocking form submission when emails are cleared", () => { + // Set up a send with email verification + component.sendDetailsForm.patchValue({ + name: "Test Send", + authType: AuthType.Email, + emails: "user@example.com", + }); + expect(component.sendDetailsForm.valid).toBe(true); + + // User clears emails field + component.sendDetailsForm.patchValue({ emails: "" }); + + // Form should now be invalid, preventing save + expect(component.sendDetailsForm.valid).toBe(false); + expect(component.sendDetailsForm.get("emails")?.hasError("emailsRequiredForEmailAuth")).toBe( + true, + ); + + // User must change authType to continue + component.sendDetailsForm.patchValue({ authType: AuthType.None }); + expect(component.sendDetailsForm.valid).toBe(true); + }); }); diff --git a/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.ts b/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.ts index ac1453a925c..78681a70a00 100644 --- a/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.ts +++ b/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.ts @@ -224,7 +224,10 @@ export class SendDetailsComponent implements OnInit { } else if (type === AuthType.Email) { passwordControl.setValue(null); passwordControl.clearValidators(); - emailsControl.setValidators([Validators.required, this.emailListValidator()]); + emailsControl.setValidators([ + this.emailsRequiredForEmailAuthValidator(), + this.emailListValidator(), + ]); } else { emailsControl.setValue(null); emailsControl.clearValidators(); @@ -317,6 +320,23 @@ export class SendDetailsComponent implements OnInit { }; } + emailsRequiredForEmailAuthValidator(): ValidatorFn { + return (control: FormControl): ValidationErrors | null => { + const authType = this.sendDetailsForm?.get("authType")?.value; + const emails = control.value; + + if (authType === AuthType.Email && (!emails || emails.trim() === "")) { + return { + emailsRequiredForEmailAuth: { + message: this.i18nService.t("emailsRequiredChangeAccessType"), + }, + }; + } + + return null; + }; + } + generatePassword = async () => { const on$ = new BehaviorSubject({ source: "send", type: Type.password }); const account$ = this.accountService.activeAccount$.pipe( From f8b5e15a44c1f5770f9057e1d7fd9be7feb8d4fc Mon Sep 17 00:00:00 2001 From: John Harrington <84741727+harr1424@users.noreply.github.com> Date: Wed, 18 Feb 2026 15:08:57 -0700 Subject: [PATCH 083/134] [PM-31731] [Defect] No error is returned when entering an invalid email + an invalid verification code (#18913) * share i18n key for both invalid email and invalid otp submission * claude review --- apps/browser/src/_locales/en/messages.json | 3 +++ apps/cli/src/locales/en/messages.json | 3 +++ apps/desktop/src/locales/en/messages.json | 3 +++ .../app/tools/send/send-access/send-auth.component.ts | 11 ++++++++++- apps/web/src/locales/en/messages.json | 3 +++ 5 files changed, 22 insertions(+), 1 deletion(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index cc99e0abe18..fbfaa17a87d 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copied", "description": "Value has been copied to the clipboard.", diff --git a/apps/cli/src/locales/en/messages.json b/apps/cli/src/locales/en/messages.json index 18079bd2409..824b03b99cf 100644 --- a/apps/cli/src/locales/en/messages.json +++ b/apps/cli/src/locales/en/messages.json @@ -35,6 +35,9 @@ "invalidVerificationCode": { "message": "Invalid verification code." }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "masterPassRequired": { "message": "Master password is required." }, diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 3f005db0ba8..97a38235fd7 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continue" }, diff --git a/apps/web/src/app/tools/send/send-access/send-auth.component.ts b/apps/web/src/app/tools/send/send-access/send-auth.component.ts index 92c3d445333..994bd7f3ee3 100644 --- a/apps/web/src/app/tools/send/send-access/send-auth.component.ts +++ b/apps/web/src/app/tools/send/send-access/send-auth.component.ts @@ -52,6 +52,7 @@ export class SendAuthComponent implements OnInit { authType = AuthType; private expiredAuthAttempts = 0; + private otpSubmitted = false; readonly loading = signal(false); readonly error = signal(false); @@ -184,12 +185,20 @@ export class SendAuthComponent implements OnInit { this.updatePageTitle(); } else if (emailAndOtpRequired(response.error)) { this.enterOtp.set(true); + if (this.otpSubmitted) { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("invalidEmailOrVerificationCode"), + }); + } + this.otpSubmitted = true; this.updatePageTitle(); } else if (otpInvalid(response.error)) { this.toastService.showToast({ variant: "error", title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("invalidVerificationCode"), + message: this.i18nService.t("invalidEmailOrVerificationCode"), }); } else if (passwordHashB64Required(response.error)) { this.sendAuthType.set(AuthType.Password); diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index ba59184a9f9..b257a68052d 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -7397,6 +7397,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, From c90b4ded33feb26ea69799ead93989cb989d4a82 Mon Sep 17 00:00:00 2001 From: Meteoni-San <141850520+Meteony@users.noreply.github.com> Date: Wed, 18 Feb 2026 23:22:38 +0100 Subject: [PATCH 084/134] Revert "Inform user if Desktop client already running (#17846)" as per user feedback (#18897) This reverts commit a199744e2456fde1863dba0d89320ac659d04e32. Co-authored-by: neuronull <9162534+neuronull@users.noreply.github.com> --- apps/desktop/src/main/window.main.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/desktop/src/main/window.main.ts b/apps/desktop/src/main/window.main.ts index 2872154aa44..b4ced4471fa 100644 --- a/apps/desktop/src/main/window.main.ts +++ b/apps/desktop/src/main/window.main.ts @@ -4,7 +4,7 @@ import { once } from "node:events"; import * as path from "path"; import * as url from "url"; -import { app, BrowserWindow, dialog, ipcMain, nativeTheme, screen, session } from "electron"; +import { app, BrowserWindow, ipcMain, nativeTheme, screen, session } from "electron"; import { concatMap, firstValueFrom, pairwise } from "rxjs"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -127,7 +127,6 @@ export class WindowMain { if (!isMacAppStore()) { const gotTheLock = app.requestSingleInstanceLock(); if (!gotTheLock) { - dialog.showErrorBox("Error", "An instance of Bitwarden Desktop is already running."); app.quit(); return; } else { From d1250cf5a4449501bd1c3d0c7b0c8cab5f16d129 Mon Sep 17 00:00:00 2001 From: Jackson Engstrom Date: Wed, 18 Feb 2026 14:34:17 -0800 Subject: [PATCH 085/134] [PM-26704] Vault List Item Ordering for Extension (#18853) * shows all/filtered ciphers in allItems instead of the ones that haven't been bubbled up into autofill or favorites * removes remainingCiphers$ remnants * updates loading$ observable logic * updates loading$ test --- .../components/vault/vault.component.html | 2 +- .../popup/components/vault/vault.component.ts | 1 - .../vault-popup-items.service.spec.ts | 35 ++----------------- .../services/vault-popup-items.service.ts | 22 +----------- 4 files changed, 4 insertions(+), 56 deletions(-) diff --git a/apps/browser/src/vault/popup/components/vault/vault.component.html b/apps/browser/src/vault/popup/components/vault/vault.component.html index 28abb92b8a9..2f43d29d776 100644 --- a/apps/browser/src/vault/popup/components/vault/vault.component.html +++ b/apps/browser/src/vault/popup/components/vault/vault.component.html @@ -127,7 +127,7 @@ { }); }); - describe("remainingCiphers$", () => { - beforeEach(() => { - searchService.isSearchable.mockImplementation(async (text) => text.length > 2); - }); - - it("should exclude autofill and favorite ciphers", (done) => { - service.remainingCiphers$.subscribe((ciphers) => { - // 2 autofill ciphers, 2 favorite ciphers = 6 remaining ciphers to show - expect(ciphers.length).toBe(6); - done(); - }); - }); - - it("should filter remainingCiphers$ down to search term", (done) => { - const cipherList = Object.values(allCiphers); - const searchText = "Login"; - - searchService.searchCiphers.mockImplementation(async () => { - return cipherList.filter((cipher) => { - return cipher.name.includes(searchText); - }); - }); - - service.remainingCiphers$.subscribe((ciphers) => { - // There are 6 remaining ciphers but only 2 with "Login" in the name - expect(ciphers.length).toBe(2); - done(); - }); - }); - }); - describe("emptyVault$", () => { it("should return true if there are no ciphers", (done) => { cipherServiceMock.cipherListViews$.mockReturnValue(of([])); @@ -493,8 +462,8 @@ describe("VaultPopupItemsService", () => { // Start tracking loading$ emissions tracked = new ObservableTracker(service.loading$); - // Track remainingCiphers$ to make cipher observables active - trackedCiphers = new ObservableTracker(service.remainingCiphers$); + // Track favoriteCiphers$ to make cipher observables active + trackedCiphers = new ObservableTracker(service.favoriteCiphers$); }); it("should initialize with true first", async () => { diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts index 016fa330a38..0055d683f22 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts @@ -2,7 +2,6 @@ import { inject, Injectable, NgZone } from "@angular/core"; import { toObservable } from "@angular/core/rxjs-interop"; import { combineLatest, - concatMap, distinctUntilChanged, distinctUntilKeyChanged, filter, @@ -242,31 +241,12 @@ export class VaultPopupItemsService { shareReplay({ refCount: false, bufferSize: 1 }), ); - /** - * List of all remaining ciphers that are not currently suggested for autofill or marked as favorite. - * Ciphers are sorted by name. - */ - remainingCiphers$: Observable = this.favoriteCiphers$.pipe( - concatMap( - ( - favoriteCiphers, // concatMap->of is used to make withLatestFrom lazy to avoid race conditions with autoFillCiphers$ - ) => - of(favoriteCiphers).pipe(withLatestFrom(this._filteredCipherList$, this.autoFillCiphers$)), - ), - map(([favoriteCiphers, ciphers, autoFillCiphers]) => - ciphers.filter( - (cipher) => !autoFillCiphers.includes(cipher) && !favoriteCiphers.includes(cipher), - ), - ), - shareReplay({ refCount: false, bufferSize: 1 }), - ); - /** * Observable that indicates whether the service is currently loading ciphers. */ loading$: Observable = merge( this._ciphersLoading$.pipe(map(() => true)), - this.remainingCiphers$.pipe(map(() => false)), + this.favoriteCiphers$.pipe(map(() => false)), ).pipe(startWith(true), distinctUntilChanged(), shareReplay({ refCount: false, bufferSize: 1 })); /** Observable that indicates whether there is search text present. From 1efd74daafd8d4488ef3fb3cb1f512bd5a04b85c Mon Sep 17 00:00:00 2001 From: Leslie Xiong Date: Wed, 18 Feb 2026 17:59:18 -0500 Subject: [PATCH 086/134] fixed berry styles for dark mode (#19068) --- libs/components/src/berry/berry.component.ts | 4 ++-- libs/components/src/berry/berry.stories.ts | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/libs/components/src/berry/berry.component.ts b/libs/components/src/berry/berry.component.ts index 8e58b888f39..a6544b75f6e 100644 --- a/libs/components/src/berry/berry.component.ts +++ b/libs/components/src/berry/berry.component.ts @@ -38,7 +38,7 @@ export class BerryComponent { }); protected readonly textColor = computed(() => { - return this.variant() === "contrast" ? "tw-text-fg-dark" : "tw-text-fg-white"; + return this.variant() === "contrast" ? "tw-text-fg-heading" : "tw-text-fg-contrast"; }); protected readonly padding = computed(() => { @@ -67,7 +67,7 @@ export class BerryComponent { warning: "tw-bg-bg-warning", danger: "tw-bg-bg-danger", accentPrimary: "tw-bg-fg-accent-primary-strong", - contrast: "tw-bg-bg-white", + contrast: "tw-bg-bg-primary", }; return [ diff --git a/libs/components/src/berry/berry.stories.ts b/libs/components/src/berry/berry.stories.ts index 0b71e7259d8..56ee87d9ce3 100644 --- a/libs/components/src/berry/berry.stories.ts +++ b/libs/components/src/berry/berry.stories.ts @@ -75,7 +75,9 @@ export const statusType: Story = { - +
    + +
    `, }), @@ -153,8 +155,8 @@ export const AllVariants: Story = { -
    - Contrast: +
    + Contrast: From c9b821262c5f1589571645e44dddd02ec8bb51b1 Mon Sep 17 00:00:00 2001 From: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Date: Wed, 18 Feb 2026 17:08:33 -0600 Subject: [PATCH 087/134] [PM-30927] Fix lock component initialization bug (#18822) --- .../lock/components/lock.component.spec.ts | 148 +++++++++++++++++- .../src/lock/components/lock.component.ts | 29 ++-- 2 files changed, 161 insertions(+), 16 deletions(-) diff --git a/libs/key-management-ui/src/lock/components/lock.component.spec.ts b/libs/key-management-ui/src/lock/components/lock.component.spec.ts index 47c4d14fc98..915f8a2d30e 100644 --- a/libs/key-management-ui/src/lock/components/lock.component.spec.ts +++ b/libs/key-management-ui/src/lock/components/lock.component.spec.ts @@ -1,4 +1,4 @@ -import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { mock } from "jest-mock-extended"; @@ -605,4 +605,150 @@ describe("LockComponent", () => { expect(component.activeUnlockOption).toBe(UnlockOption.Biometrics); }); }); + + describe("listenForUnlockOptionsChanges", () => { + const mockActiveAccount: Account = { + id: userId, + email: "test@example.com", + name: "Test User", + } as Account; + + const mockUnlockOptions: UnlockOptions = { + masterPassword: { enabled: true }, + pin: { enabled: false }, + biometrics: { enabled: false, biometricsStatus: BiometricsStatus.Available }, + prf: { enabled: false }, + }; + + beforeEach(() => { + (component as any).loading = false; + component.activeAccount = mockActiveAccount; + component.activeUnlockOption = null; + component.unlockOptions = null; + mockLockComponentService.getAvailableUnlockOptions$.mockReturnValue(of(mockUnlockOptions)); + }); + + it("skips polling when loading is true", fakeAsync(() => { + (component as any).loading = true; + + component["listenForUnlockOptionsChanges"](); + tick(0); + + expect(mockLockComponentService.getAvailableUnlockOptions$).not.toHaveBeenCalled(); + })); + + it("skips polling when activeAccount is null", fakeAsync(() => { + component.activeAccount = null; + + component["listenForUnlockOptionsChanges"](); + tick(0); + + expect(mockLockComponentService.getAvailableUnlockOptions$).not.toHaveBeenCalled(); + })); + + it("fetches unlock options when loading is false and activeAccount exists", fakeAsync(() => { + component["listenForUnlockOptionsChanges"](); + tick(0); + + expect(mockLockComponentService.getAvailableUnlockOptions$).toHaveBeenCalledWith(userId); + expect(component.unlockOptions).toEqual(mockUnlockOptions); + })); + + it("calls getAvailableUnlockOptions$ at 1000ms intervals", fakeAsync(() => { + component["listenForUnlockOptionsChanges"](); + + // Initial timer fire at 0ms + tick(0); + expect(mockLockComponentService.getAvailableUnlockOptions$).toHaveBeenCalledTimes(1); + + // First poll at 1000ms + tick(1000); + expect(mockLockComponentService.getAvailableUnlockOptions$).toHaveBeenCalledTimes(2); + + // Second poll at 2000ms + tick(1000); + expect(mockLockComponentService.getAvailableUnlockOptions$).toHaveBeenCalledTimes(3); + })); + + it("calls setDefaultActiveUnlockOption when activeUnlockOption is null", fakeAsync(() => { + component.activeUnlockOption = null; + const setDefaultSpy = jest.spyOn(component as any, "setDefaultActiveUnlockOption"); + + component["listenForUnlockOptionsChanges"](); + tick(0); + + expect(setDefaultSpy).toHaveBeenCalledWith(mockUnlockOptions); + })); + + it("does NOT call setDefaultActiveUnlockOption when activeUnlockOption is already set", fakeAsync(() => { + component.activeUnlockOption = UnlockOption.MasterPassword; + component.unlockOptions = mockUnlockOptions; + + const setDefaultSpy = jest.spyOn(component as any, "setDefaultActiveUnlockOption"); + + component["listenForUnlockOptionsChanges"](); + tick(0); + + expect(setDefaultSpy).not.toHaveBeenCalled(); + })); + + it("calls setDefaultActiveUnlockOption when biometrics becomes enabled", fakeAsync(() => { + component.activeUnlockOption = UnlockOption.MasterPassword; + + // Start with biometrics disabled + component.unlockOptions = { + masterPassword: { enabled: true }, + pin: { enabled: false }, + biometrics: { enabled: false, biometricsStatus: BiometricsStatus.Available }, + prf: { enabled: false }, + }; + + // Mock response with biometrics enabled + const newUnlockOptions: UnlockOptions = { + masterPassword: { enabled: true }, + pin: { enabled: false }, + biometrics: { enabled: true, biometricsStatus: BiometricsStatus.Available }, + prf: { enabled: false }, + }; + + mockLockComponentService.getAvailableUnlockOptions$.mockReturnValue(of(newUnlockOptions)); + + const setDefaultSpy = jest.spyOn(component as any, "setDefaultActiveUnlockOption"); + const handleBioSpy = jest.spyOn(component as any, "handleBiometricsUnlockEnabled"); + + component["listenForUnlockOptionsChanges"](); + tick(0); + + expect(setDefaultSpy).toHaveBeenCalledWith(newUnlockOptions); + expect(handleBioSpy).toHaveBeenCalled(); + })); + + it("does NOT call setDefaultActiveUnlockOption when biometrics was already enabled", fakeAsync(() => { + component.activeUnlockOption = UnlockOption.MasterPassword; + + // Start with biometrics already enabled + component.unlockOptions = { + masterPassword: { enabled: true }, + pin: { enabled: false }, + biometrics: { enabled: true, biometricsStatus: BiometricsStatus.Available }, + prf: { enabled: false }, + }; + + // Mock response with biometrics still enabled + const newUnlockOptions: UnlockOptions = { + masterPassword: { enabled: true }, + pin: { enabled: false }, + biometrics: { enabled: true, biometricsStatus: BiometricsStatus.Available }, + prf: { enabled: false }, + }; + mockLockComponentService.getAvailableUnlockOptions$.mockReturnValue(of(newUnlockOptions)); + + const setDefaultSpy = jest.spyOn(component as any, "setDefaultActiveUnlockOption"); + + component["listenForUnlockOptionsChanges"](); + tick(0); + + expect(setDefaultSpy).not.toHaveBeenCalled(); + })); + }); }); diff --git a/libs/key-management-ui/src/lock/components/lock.component.ts b/libs/key-management-ui/src/lock/components/lock.component.ts index 9900aa6e827..5686e4b334a 100644 --- a/libs/key-management-ui/src/lock/components/lock.component.ts +++ b/libs/key-management-ui/src/lock/components/lock.component.ts @@ -202,7 +202,8 @@ export class LockComponent implements OnInit, OnDestroy { timer(0, 1000) .pipe( mergeMap(async () => { - if (this.activeAccount?.id != null) { + // Only perform polling after the component has loaded. This prevents multiple sources setting the default active unlock option on initialization. + if (this.loading === false && this.activeAccount?.id != null) { const prevBiometricsEnabled = this.unlockOptions?.biometrics.enabled; this.unlockOptions = await firstValueFrom( @@ -210,7 +211,6 @@ export class LockComponent implements OnInit, OnDestroy { ); if (this.activeUnlockOption == null) { - this.loading = false; await this.setDefaultActiveUnlockOption(this.unlockOptions); } else if (!prevBiometricsEnabled && this.unlockOptions?.biometrics.enabled) { await this.setDefaultActiveUnlockOption(this.unlockOptions); @@ -275,19 +275,18 @@ export class LockComponent implements OnInit, OnDestroy { this.lockComponentService.getAvailableUnlockOptions$(activeAccount.id), ); - const canUseBiometrics = [ - BiometricsStatus.Available, - ...BIOMETRIC_UNLOCK_TEMPORARY_UNAVAILABLE_STATUSES, - ].includes(await this.biometricService.getBiometricsStatusForUser(activeAccount.id)); - if ( - !this.unlockOptions?.masterPassword.enabled && - !this.unlockOptions?.pin.enabled && - !canUseBiometrics - ) { - // User has no available unlock options, force logout. This happens for TDE users without a masterpassword, that don't have a persistent unlock method set. - this.logService.warning("[LockComponent] User cannot unlock again. Logging out!"); - await this.logoutService.logout(activeAccount.id); - return; + // The canUseBiometrics query is an expensive operation. Only call if both PIN and master password unlock are unavailable. + if (!this.unlockOptions?.masterPassword.enabled && !this.unlockOptions?.pin.enabled) { + const canUseBiometrics = [ + BiometricsStatus.Available, + ...BIOMETRIC_UNLOCK_TEMPORARY_UNAVAILABLE_STATUSES, + ].includes(await this.biometricService.getBiometricsStatusForUser(activeAccount.id)); + if (!canUseBiometrics) { + // User has no available unlock options, force logout. This happens for TDE users without a masterpassword, that don't have a persistent unlock method set. + this.logService.warning("[LockComponent] User cannot unlock again. Logging out!"); + await this.logoutService.logout(activeAccount.id); + return; + } } await this.setDefaultActiveUnlockOption(this.unlockOptions); From 6498ec42f8e2433369e6686c5b658a4d8d8aa835 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 19 Feb 2026 14:04:43 +0100 Subject: [PATCH 088/134] [BEEEP] Add util functions for uint8 array conversion (#18451) * Add util functions for uint8 array conversion * Use polyfill instead of old functionality * Replace last usage of old functions --- libs/common/src/platform/misc/utils.spec.ts | 140 +++++++++++++++++++- libs/common/src/platform/misc/utils.ts | 76 ++++++++++- 2 files changed, 213 insertions(+), 3 deletions(-) diff --git a/libs/common/src/platform/misc/utils.spec.ts b/libs/common/src/platform/misc/utils.spec.ts index 664c6e22b3a..032b03fc3e2 100644 --- a/libs/common/src/platform/misc/utils.spec.ts +++ b/libs/common/src/platform/misc/utils.spec.ts @@ -417,6 +417,142 @@ describe("Utils Service", () => { // }); }); + describe("fromArrayToHex(...)", () => { + const originalIsNode = Utils.isNode; + + afterEach(() => { + Utils.isNode = originalIsNode; + }); + + runInBothEnvironments("should convert a Uint8Array to a hex string", () => { + const arr = new Uint8Array([0x00, 0x01, 0x02, 0x0a, 0xff]); + const hexString = Utils.fromArrayToHex(arr); + expect(hexString).toBe("0001020aff"); + }); + + runInBothEnvironments("should return null for null input", () => { + const hexString = Utils.fromArrayToHex(null); + expect(hexString).toBeNull(); + }); + + runInBothEnvironments("should return empty string for an empty Uint8Array", () => { + const arr = new Uint8Array([]); + const hexString = Utils.fromArrayToHex(arr); + expect(hexString).toBe(""); + }); + }); + + describe("fromArrayToB64(...)", () => { + const originalIsNode = Utils.isNode; + + afterEach(() => { + Utils.isNode = originalIsNode; + }); + + runInBothEnvironments("should convert a Uint8Array to a b64 string", () => { + const arr = new Uint8Array(asciiHelloWorldArray); + const b64String = Utils.fromArrayToB64(arr); + expect(b64String).toBe(b64HelloWorldString); + }); + + runInBothEnvironments("should return null for null input", () => { + const b64String = Utils.fromArrayToB64(null); + expect(b64String).toBeNull(); + }); + + runInBothEnvironments("should return empty string for an empty Uint8Array", () => { + const arr = new Uint8Array([]); + const b64String = Utils.fromArrayToB64(arr); + expect(b64String).toBe(""); + }); + }); + + describe("fromArrayToUrlB64(...)", () => { + const originalIsNode = Utils.isNode; + + afterEach(() => { + Utils.isNode = originalIsNode; + }); + + runInBothEnvironments("should convert a Uint8Array to a URL-safe b64 string", () => { + // Input that produces +, /, and = in standard base64 + const arr = new Uint8Array([251, 255, 254]); + const urlB64String = Utils.fromArrayToUrlB64(arr); + // Standard b64 would be "+//+" with padding, URL-safe removes padding and replaces chars + expect(urlB64String).not.toContain("+"); + expect(urlB64String).not.toContain("/"); + expect(urlB64String).not.toContain("="); + }); + + runInBothEnvironments("should return null for null input", () => { + const urlB64String = Utils.fromArrayToUrlB64(null); + expect(urlB64String).toBeNull(); + }); + + runInBothEnvironments("should return empty string for an empty Uint8Array", () => { + const arr = new Uint8Array([]); + const urlB64String = Utils.fromArrayToUrlB64(arr); + expect(urlB64String).toBe(""); + }); + }); + + describe("fromArrayToByteString(...)", () => { + const originalIsNode = Utils.isNode; + + afterEach(() => { + Utils.isNode = originalIsNode; + }); + + runInBothEnvironments("should convert a Uint8Array to a byte string", () => { + const arr = new Uint8Array(asciiHelloWorldArray); + const byteString = Utils.fromArrayToByteString(arr); + expect(byteString).toBe(asciiHelloWorld); + }); + + runInBothEnvironments("should return null for null input", () => { + const byteString = Utils.fromArrayToByteString(null); + expect(byteString).toBeNull(); + }); + + runInBothEnvironments("should return empty string for an empty Uint8Array", () => { + const arr = new Uint8Array([]); + const byteString = Utils.fromArrayToByteString(arr); + expect(byteString).toBe(""); + }); + }); + + describe("fromArrayToUtf8(...)", () => { + const originalIsNode = Utils.isNode; + + afterEach(() => { + Utils.isNode = originalIsNode; + }); + + runInBothEnvironments("should convert a Uint8Array to a UTF-8 string", () => { + const arr = new Uint8Array(asciiHelloWorldArray); + const utf8String = Utils.fromArrayToUtf8(arr); + expect(utf8String).toBe(asciiHelloWorld); + }); + + runInBothEnvironments("should return null for null input", () => { + const utf8String = Utils.fromArrayToUtf8(null); + expect(utf8String).toBeNull(); + }); + + runInBothEnvironments("should return empty string for an empty Uint8Array", () => { + const arr = new Uint8Array([]); + const utf8String = Utils.fromArrayToUtf8(arr); + expect(utf8String).toBe(""); + }); + + runInBothEnvironments("should handle multi-byte UTF-8 characters", () => { + // "日本" in UTF-8 bytes + const arr = new Uint8Array([0xe6, 0x97, 0xa5, 0xe6, 0x9c, 0xac]); + const utf8String = Utils.fromArrayToUtf8(arr); + expect(utf8String).toBe("日本"); + }); + }); + describe("Base64 and ArrayBuffer round trip conversions", () => { const originalIsNode = Utils.isNode; @@ -447,10 +583,10 @@ describe("Utils Service", () => { "should correctly round trip convert from base64 to ArrayBuffer and back", () => { // Convert known base64 string to ArrayBuffer - const bufferFromB64 = Utils.fromB64ToArray(b64HelloWorldString).buffer; + const bufferFromB64 = Utils.fromB64ToArray(b64HelloWorldString); // Convert the ArrayBuffer back to a base64 string - const roundTrippedB64String = Utils.fromBufferToB64(bufferFromB64); + const roundTrippedB64String = Utils.fromArrayToB64(bufferFromB64); // Compare the original base64 string with the round-tripped base64 string expect(roundTrippedB64String).toBe(b64HelloWorldString); diff --git a/libs/common/src/platform/misc/utils.ts b/libs/common/src/platform/misc/utils.ts index bdbfc4ea17b..c2d8871c2c9 100644 --- a/libs/common/src/platform/misc/utils.ts +++ b/libs/common/src/platform/misc/utils.ts @@ -8,6 +8,8 @@ import { Observable, of, switchMap } from "rxjs"; import { getHostname, parse } from "tldts"; import { Merge } from "type-fest"; +import "core-js/proposals/array-buffer-base64"; + // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { KeyService } from "@bitwarden/key-management"; @@ -129,6 +131,78 @@ export class Utils { return arr; } + /** + * Converts a Uint8Array to a hexadecimal string. + * @param arr - The Uint8Array to convert. + * @returns The hexadecimal string representation, or null if the input is null. + */ + static fromArrayToHex(arr: Uint8Array | null): string | null { + if (arr == null) { + return null; + } + + // @ts-expect-error - polyfilled by core-js + return arr.toHex(); + } + + /** + * Converts a Uint8Array to a Base64 encoded string. + * @param arr - The Uint8Array to convert. + * @returns The Base64 encoded string, or null if the input is null. + */ + static fromArrayToB64(arr: Uint8Array | null): string | null { + if (arr == null) { + return null; + } + + // @ts-expect-error - polyfilled by core-js + return arr.toBase64({ alphabet: "base64" }); + } + + /** + * Converts a Uint8Array to a URL-safe Base64 encoded string. + * @param arr - The Uint8Array to convert. + * @returns The URL-safe Base64 encoded string, or null if the input is null. + */ + static fromArrayToUrlB64(arr: Uint8Array | null): string | null { + if (arr == null) { + return null; + } + + // @ts-expect-error - polyfilled by core-js + return arr.toBase64({ alphabet: "base64url" }); + } + + /** + * Converts a Uint8Array to a byte string (each byte as a character). + * @param arr - The Uint8Array to convert. + * @returns The byte string representation, or null if the input is null. + */ + static fromArrayToByteString(arr: Uint8Array | null): string | null { + if (arr == null) { + return null; + } + + let byteString = ""; + for (let i = 0; i < arr.length; i++) { + byteString += String.fromCharCode(arr[i]); + } + return byteString; + } + + /** + * Converts a Uint8Array to a UTF-8 decoded string. + * @param arr - The Uint8Array containing UTF-8 encoded bytes. + * @returns The decoded UTF-8 string, or null if the input is null. + */ + static fromArrayToUtf8(arr: Uint8Array | null): string | null { + if (arr == null) { + return null; + } + + return BufferLib.from(arr).toString("utf8"); + } + /** * Convert binary data into a Base64 string. * @@ -302,7 +376,7 @@ export class Utils { } static fromUtf8ToUrlB64(utfStr: string): string { - return Utils.fromBufferToUrlB64(Utils.fromUtf8ToArray(utfStr)); + return Utils.fromArrayToUrlB64(Utils.fromUtf8ToArray(utfStr)); } static fromB64ToUtf8(b64Str: string): string { From e66a1f37b5a31513bef5d26b7c145b01eaa40a07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= Date: Thu, 19 Feb 2026 08:45:24 -0500 Subject: [PATCH 089/134] Extract urlOriginsMatch utility and refactor senderIsInternal (#19076) Adds urlOriginsMatch to @bitwarden/platform, which compares two URLs by scheme, host, and port. Uses `protocol + "//" + host` rather than `URL.origin` because non-special schemes (e.g. chrome-extension://) return the opaque string "null" from .origin, making equality comparison unreliable. URLs without a host (file:, data:) are explicitly rejected to prevent hostless schemes from comparing equal. Refactors senderIsInternal to delegate to urlOriginsMatch and to derive the extension URL via BrowserApi.getRuntimeURL("") rather than inline chrome/browser API detection. Adds full test coverage for senderIsInternal. The previous string-based comparison used startsWith after stripping trailing slashes, which was safe in senderIsInternal where inputs are tightly constrained. As a general utility accepting arbitrary URLs, startsWith can produce false positives (e.g. "https://example.com" matching "https://example.com.evil.com"). Structural host comparison is the correct contract for unrestricted input. --- .../src/platform/browser/browser-api.spec.ts | 100 ++++++++++++++++++ .../src/platform/browser/browser-api.ts | 36 ++++--- libs/platform/src/index.ts | 1 + libs/platform/src/util.spec.ts | 54 ++++++++++ libs/platform/src/util.ts | 53 ++++++++++ 5 files changed, 227 insertions(+), 17 deletions(-) create mode 100644 libs/platform/src/util.spec.ts create mode 100644 libs/platform/src/util.ts diff --git a/apps/browser/src/platform/browser/browser-api.spec.ts b/apps/browser/src/platform/browser/browser-api.spec.ts index f7561b2b50b..d8a8fe52570 100644 --- a/apps/browser/src/platform/browser/browser-api.spec.ts +++ b/apps/browser/src/platform/browser/browser-api.spec.ts @@ -1,5 +1,7 @@ import { mock } from "jest-mock-extended"; +import { LogService } from "@bitwarden/logging"; + import { BrowserApi } from "./browser-api"; type ChromeSettingsGet = chrome.types.ChromeSetting["get"]; @@ -29,6 +31,104 @@ describe("BrowserApi", () => { }); }); + describe("senderIsInternal", () => { + const EXTENSION_ORIGIN = "chrome-extension://id"; + + beforeEach(() => { + jest.spyOn(BrowserApi, "getRuntimeURL").mockReturnValue(`${EXTENSION_ORIGIN}/`); + }); + + it("returns false when sender is undefined", () => { + const result = BrowserApi.senderIsInternal(undefined); + + expect(result).toBe(false); + }); + + it("returns false when sender has no origin", () => { + const result = BrowserApi.senderIsInternal({ id: "abc" } as any); + + expect(result).toBe(false); + }); + + it("returns false when the extension URL cannot be determined", () => { + jest.spyOn(BrowserApi, "getRuntimeURL").mockReturnValue(""); + + const result = BrowserApi.senderIsInternal({ origin: EXTENSION_ORIGIN }); + + expect(result).toBe(false); + }); + + it.each([ + ["an external origin", "https://evil.com"], + ["a subdomain of the extension origin", "chrome-extension://id.evil.com"], + ["a file: URL (opaque origin)", "file:///home/user/page.html"], + ["a data: URL (opaque origin)", "data:text/html,

    hi

    "], + ])("returns false when sender origin is %s", (_, senderOrigin) => { + const result = BrowserApi.senderIsInternal({ origin: senderOrigin }); + + expect(result).toBe(false); + }); + + it("returns false when sender is from a non-top-level frame", () => { + const result = BrowserApi.senderIsInternal({ origin: EXTENSION_ORIGIN, frameId: 5 }); + + expect(result).toBe(false); + }); + + it("returns true when sender origin matches and no frameId is present (popup)", () => { + const result = BrowserApi.senderIsInternal({ origin: EXTENSION_ORIGIN }); + + expect(result).toBe(true); + }); + + it("returns true when sender origin matches and frameId is 0 (top-level frame)", () => { + const result = BrowserApi.senderIsInternal({ origin: EXTENSION_ORIGIN, frameId: 0 }); + + expect(result).toBe(true); + }); + + it("calls logger.warning when sender has no origin", () => { + const logger = mock(); + + BrowserApi.senderIsInternal({} as any, logger); + + expect(logger.warning).toHaveBeenCalledWith(expect.stringContaining("no origin")); + }); + + it("calls logger.warning when the extension URL cannot be determined", () => { + jest.spyOn(BrowserApi, "getRuntimeURL").mockReturnValue(""); + const logger = mock(); + + BrowserApi.senderIsInternal({ origin: EXTENSION_ORIGIN }, logger); + + expect(logger.warning).toHaveBeenCalledWith(expect.stringContaining("extension URL")); + }); + + it("calls logger.warning when origin does not match", () => { + const logger = mock(); + + BrowserApi.senderIsInternal({ origin: "https://evil.com" }, logger); + + expect(logger.warning).toHaveBeenCalledWith(expect.stringContaining("does not match")); + }); + + it("calls logger.warning when sender is from a non-top-level frame", () => { + const logger = mock(); + + BrowserApi.senderIsInternal({ origin: EXTENSION_ORIGIN, frameId: 5 }, logger); + + expect(logger.warning).toHaveBeenCalledWith(expect.stringContaining("top-level frame")); + }); + + it("calls logger.info when sender is confirmed internal", () => { + const logger = mock(); + + BrowserApi.senderIsInternal({ origin: EXTENSION_ORIGIN }, logger); + + expect(logger.info).toHaveBeenCalledWith(expect.stringContaining("internal")); + }); + }); + describe("getWindow", () => { it("will get the current window if a window id is not provided", () => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. diff --git a/apps/browser/src/platform/browser/browser-api.ts b/apps/browser/src/platform/browser/browser-api.ts index feefd527636..1b0f7639d1d 100644 --- a/apps/browser/src/platform/browser/browser-api.ts +++ b/apps/browser/src/platform/browser/browser-api.ts @@ -6,7 +6,7 @@ import { BrowserClientVendors } from "@bitwarden/common/autofill/constants"; import { BrowserClientVendor } from "@bitwarden/common/autofill/types"; import { DeviceType } from "@bitwarden/common/enums"; import { LogService } from "@bitwarden/logging"; -import { isBrowserSafariApi } from "@bitwarden/platform"; +import { isBrowserSafariApi, urlOriginsMatch } from "@bitwarden/platform"; import { TabMessage } from "../../types/tab-messages"; import { BrowserPlatformUtilsService } from "../services/platform-utils/browser-platform-utils.service"; @@ -34,12 +34,20 @@ export class BrowserApi { } /** - * Helper method that attempts to distinguish whether a message sender is internal to the extension or not. + * Returns `true` if the message sender appears to originate from within this extension. * - * Currently this is done through source origin matching, and frameId checking (only top-level frames are internal). - * @param sender a message sender - * @param logger an optional logger to log validation results - * @returns whether or not the sender appears to be internal to the extension + * Returns `false` when: + * - `sender` is absent or has no `origin` property + * - The extension's own URL cannot be determined at runtime + * - The sender's origin does not match the extension's origin (compared by scheme, host, and port; + * senders without a host such as `file:` or `data:` URLs are always rejected) + * - The message comes from a sub-frame rather than the top-level frame + * + * Note: this is a best-effort check that relies on the browser correctly populating `sender.origin`. + * + * @param sender - The message sender to validate. `undefined` or a sender without `origin` returns `false`. + * @param logger - Optional logger; rejections are reported at `warning` level, acceptance at `info`. + * @returns `true` if the sender appears to be internal to the extension; `false` otherwise. */ static senderIsInternal( sender: chrome.runtime.MessageSender | undefined, @@ -49,28 +57,22 @@ export class BrowserApi { logger?.warning("[BrowserApi] Message sender has no origin"); return false; } - const extensionUrl = - (typeof chrome !== "undefined" && chrome.runtime?.getURL("")) || - (typeof browser !== "undefined" && browser.runtime?.getURL("")) || - ""; + // Empty path yields the extension's base URL; coalesce to empty string so the guard below fires on a missing runtime. + const extensionUrl = BrowserApi.getRuntimeURL("") ?? ""; if (!extensionUrl) { logger?.warning("[BrowserApi] Unable to determine extension URL"); return false; } - // Normalize both URLs by removing trailing slashes - const normalizedOrigin = sender.origin.replace(/\/$/, "").toLowerCase(); - const normalizedExtensionUrl = extensionUrl.replace(/\/$/, "").toLowerCase(); - - if (!normalizedOrigin.startsWith(normalizedExtensionUrl)) { + if (!urlOriginsMatch(extensionUrl, sender.origin)) { logger?.warning( - `[BrowserApi] Message sender origin (${normalizedOrigin}) does not match extension URL (${normalizedExtensionUrl})`, + `[BrowserApi] Message sender origin (${sender.origin}) does not match extension URL (${extensionUrl})`, ); return false; } - // We only send messages from the top-level frame, but frameId is only set if tab is set, which for popups it is not. + // frameId is absent for popups, so use an 'in' check rather than direct comparison. if ("frameId" in sender && sender.frameId !== 0) { logger?.warning("[BrowserApi] Message sender is not from the top-level frame"); return false; diff --git a/libs/platform/src/index.ts b/libs/platform/src/index.ts index 3fabe3fad1a..9c9dac0c684 100644 --- a/libs/platform/src/index.ts +++ b/libs/platform/src/index.ts @@ -1,2 +1,3 @@ export * from "./services/browser-service"; export * from "./background-sync"; +export * from "./util"; diff --git a/libs/platform/src/util.spec.ts b/libs/platform/src/util.spec.ts new file mode 100644 index 00000000000..fda563db7ea --- /dev/null +++ b/libs/platform/src/util.spec.ts @@ -0,0 +1,54 @@ +import { urlOriginsMatch } from "./util"; + +describe("urlOriginsMatch", () => { + it.each([ + ["string/string, same origin", "https://example.com", "https://example.com"], + ["URL/URL, same origin", new URL("https://example.com"), new URL("https://example.com")], + ["string canonical, URL suspect", "https://example.com", new URL("https://example.com/path")], + ["URL canonical, string suspect", new URL("https://example.com/path"), "https://example.com"], + [ + "paths and query differ but origin same", + "https://example.com/foo", + "https://example.com/bar?baz=1", + ], + ["explicit default port matches implicit", "https://example.com", "https://example.com:443"], + [ + "non-special scheme with matching host", + "chrome-extension://abc123/popup.html", + "chrome-extension://abc123/bg.js", + ], + ])("returns true when %s", (_, canonical, suspect) => { + expect(urlOriginsMatch(canonical as string | URL, suspect as string | URL)).toBe(true); + }); + + it.each([ + ["hosts differ", "https://example.com", "https://evil.com"], + ["schemes differ", "https://example.com", "http://example.com"], + ["ports differ", "https://example.com:8080", "https://example.com:9090"], + [ + "suspect is a subdomain of the canonical host", + "https://example.com", + "https://sub.example.com", + ], + ["non-special scheme hosts differ", "chrome-extension://abc123/", "chrome-extension://xyz789/"], + ])("returns false when %s", (_, canonical, suspect) => { + expect(urlOriginsMatch(canonical, suspect)).toBe(false); + }); + + it.each([ + ["canonical is an invalid string", "not a url", "https://example.com"], + ["suspect is an invalid string", "https://example.com", "not a url"], + ])("returns false when %s", (_, canonical, suspect) => { + expect(urlOriginsMatch(canonical, suspect)).toBe(false); + }); + + it.each([ + ["canonical is a file: URL", "file:///home/user/a.txt", "https://example.com"], + ["suspect is a file: URL", "https://example.com", "file:///home/user/a.txt"], + ["both are file: URLs", "file:///home/user/a.txt", "file:///home/other/b.txt"], + ["canonical is a data: URL", "data:text/plain,foo", "https://example.com"], + ["suspect is a data: URL", "https://example.com", "data:text/plain,foo"], + ])("returns false when %s (no host)", (_, canonical, suspect) => { + expect(urlOriginsMatch(canonical, suspect)).toBe(false); + }); +}); diff --git a/libs/platform/src/util.ts b/libs/platform/src/util.ts new file mode 100644 index 00000000000..b59e713fba3 --- /dev/null +++ b/libs/platform/src/util.ts @@ -0,0 +1,53 @@ +function toURL(input: string | URL): URL | null { + if (input instanceof URL) { + return input; + } + try { + return new URL(input); + } catch { + return null; + } +} + +function effectiveOrigin(url: URL): string | null { + // The URL spec returns "null" for .origin on non-special schemes + // (e.g. chrome-extension://) so we build the origin from protocol + host instead. + // An empty host means no meaningful origin can be compared (file:, data:, etc.). + if (!url.host) { + return null; + } + return `${url.protocol}//${url.host}`; +} + +/** + * Compares two URLs to determine whether the suspect URL originates from the + * same host as the canonical URL. + * + * Both arguments accept either a string or an existing {@link URL} object. + * + * Returns `false` when: + * - Either argument cannot be parsed as a valid URL + * - Either URL has no host (e.g. `file:`, `data:` schemes), since URLs without + * a meaningful host cannot be distinguished by origin + * + * @param canonical - The reference URL whose origin acts as the baseline. + * @param suspect - The URL being tested against the canonical origin. + * @returns `true` if both URLs share the same scheme, host, and port; `false` otherwise. + */ +export function urlOriginsMatch(canonical: string | URL, suspect: string | URL): boolean { + const canonicalUrl = toURL(canonical); + const suspectUrl = toURL(suspect); + + if (!canonicalUrl || !suspectUrl) { + return false; + } + + const canonicalOrigin = effectiveOrigin(canonicalUrl); + const suspectOrigin = effectiveOrigin(suspectUrl); + + if (!canonicalOrigin || !suspectOrigin) { + return false; + } + + return canonicalOrigin === suspectOrigin; +} From c8ba23e28df4ae76c601a21eedeb985e8642bf32 Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Thu, 19 Feb 2026 09:57:52 -0500 Subject: [PATCH 090/134] [PM-26378] Auto confirm events (#19025) * add notification handler for auto confirm * add missing state check * fix test * isolate angular specific code from shared lib code * clean up * use autoconfirm method * add event logging for auto confirm * update copy --- .../settings/admin-settings.component.spec.ts | 13 +++++++++ .../settings/admin-settings.component.ts | 27 +++++++++++++++---- .../organizations/manage/events.component.ts | 1 + apps/web/src/app/core/event.service.ts | 19 +++++++++++++ apps/web/src/locales/en/messages.json | 24 +++++++++++++++++ .../default-auto-confirm.service.spec.ts | 15 +++++++++++ .../src/enums/event-system-user.enum.ts | 1 + libs/common/src/enums/event-type.enum.ts | 5 ++++ 8 files changed, 100 insertions(+), 5 deletions(-) diff --git a/apps/browser/src/vault/popup/settings/admin-settings.component.spec.ts b/apps/browser/src/vault/popup/settings/admin-settings.component.spec.ts index f7b4e7b473a..2a9ebdcddf6 100644 --- a/apps/browser/src/vault/popup/settings/admin-settings.component.spec.ts +++ b/apps/browser/src/vault/popup/settings/admin-settings.component.spec.ts @@ -7,6 +7,8 @@ import { of } from "rxjs"; import { NudgesService, NudgeType } from "@bitwarden/angular/vault"; import { AutoConfirmState, AutomaticUserConfirmationService } from "@bitwarden/auto-confirm"; import { PopOutComponent } from "@bitwarden/browser/platform/popup/components/pop-out.component"; +import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { mockAccountServiceWith } from "@bitwarden/common/spec"; @@ -52,6 +54,8 @@ describe("AdminSettingsComponent", () => { let autoConfirmService: MockProxy; let nudgesService: MockProxy; let mockDialogService: MockProxy; + let eventCollectionService: MockProxy; + let organizationService: MockProxy; const userId = "test-user-id" as UserId; const mockAutoConfirmState: AutoConfirmState = { @@ -64,10 +68,14 @@ describe("AdminSettingsComponent", () => { autoConfirmService = mock(); nudgesService = mock(); mockDialogService = mock(); + eventCollectionService = mock(); + organizationService = mock(); autoConfirmService.configuration$.mockReturnValue(of(mockAutoConfirmState)); autoConfirmService.upsert.mockResolvedValue(undefined); nudgesService.showNudgeSpotlight$.mockReturnValue(of(false)); + eventCollectionService.collect.mockResolvedValue(undefined); + organizationService.organizations$.mockReturnValue(of([])); await TestBed.configureTestingModule({ imports: [AdminSettingsComponent], @@ -77,6 +85,11 @@ describe("AdminSettingsComponent", () => { { provide: AutomaticUserConfirmationService, useValue: autoConfirmService }, { provide: DialogService, useValue: mockDialogService }, { provide: NudgesService, useValue: nudgesService }, + { provide: EventCollectionService, useValue: eventCollectionService }, + { + provide: InternalOrganizationServiceAbstraction, + useValue: organizationService, + }, { provide: I18nService, useValue: { t: (key: string) => key } }, ], }) diff --git a/apps/browser/src/vault/popup/settings/admin-settings.component.ts b/apps/browser/src/vault/popup/settings/admin-settings.component.ts index 52da4318047..99cb5a814c1 100644 --- a/apps/browser/src/vault/popup/settings/admin-settings.component.ts +++ b/apps/browser/src/vault/popup/settings/admin-settings.component.ts @@ -20,8 +20,11 @@ import { import { PopOutComponent } from "@bitwarden/browser/platform/popup/components/pop-out.component"; import { PopupHeaderComponent } from "@bitwarden/browser/platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "@bitwarden/browser/platform/popup/layout/popup-page.component"; +import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { EventType } from "@bitwarden/common/enums"; import { BitIconButtonComponent, CardComponent, @@ -69,6 +72,8 @@ export class AdminSettingsComponent implements OnInit { private destroyRef: DestroyRef, private dialogService: DialogService, private nudgesService: NudgesService, + private eventCollectionService: EventCollectionService, + private organizationService: InternalOrganizationServiceAbstraction, ) {} async ngOnInit() { @@ -88,14 +93,26 @@ export class AdminSettingsComponent implements OnInit { } return of(false); }), - withLatestFrom(this.autoConfirmService.configuration$(userId)), - switchMap(([newValue, existingState]) => - this.autoConfirmService.upsert(userId, { + withLatestFrom( + this.autoConfirmService.configuration$(userId), + this.organizationService.organizations$(userId), + ), + switchMap(async ([newValue, existingState, organizations]) => { + await this.autoConfirmService.upsert(userId, { ...existingState, enabled: newValue, showBrowserNotification: false, - }), - ), + }); + + // Auto-confirm users can only belong to one organization + const organization = organizations[0]; + if (organization?.id) { + const eventType = newValue + ? EventType.Organization_AutoConfirmEnabled_Admin + : EventType.Organization_AutoConfirmDisabled_Admin; + await this.eventCollectionService.collect(eventType, undefined, true, organization.id); + } + }), takeUntilDestroyed(this.destroyRef), ) .subscribe(); diff --git a/apps/web/src/app/admin-console/organizations/manage/events.component.ts b/apps/web/src/app/admin-console/organizations/manage/events.component.ts index 62f6539cc16..fffe1c06ab8 100644 --- a/apps/web/src/app/admin-console/organizations/manage/events.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/events.component.ts @@ -44,6 +44,7 @@ const EVENT_SYSTEM_USER_TO_TRANSLATION: Record = { [EventSystemUser.SCIM]: null, // SCIM acronym not able to be translated so just display SCIM [EventSystemUser.DomainVerification]: "domainVerification", [EventSystemUser.PublicApi]: "publicApi", + [EventSystemUser.BitwardenPortal]: "system", }; // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush diff --git a/apps/web/src/app/core/event.service.ts b/apps/web/src/app/core/event.service.ts index 47f4344ec36..006014b9fed 100644 --- a/apps/web/src/app/core/event.service.ts +++ b/apps/web/src/app/core/event.service.ts @@ -355,6 +355,13 @@ export class EventService { this.getShortId(ev.organizationUserId), ); break; + case EventType.OrganizationUser_AutomaticallyConfirmed: + msg = this.i18nService.t("automaticallyConfirmedUserId", this.formatOrgUserId(ev)); + humanReadableMsg = this.i18nService.t( + "automaticallyConfirmedUserId", + this.getShortId(ev.organizationUserId), + ); + break; // Org case EventType.Organization_Updated: msg = humanReadableMsg = this.i18nService.t("editedOrgSettings"); @@ -458,6 +465,18 @@ export class EventService { case EventType.Organization_ItemOrganization_Declined: msg = humanReadableMsg = this.i18nService.t("userDeclinedTransfer"); break; + case EventType.Organization_AutoConfirmEnabled_Admin: + msg = humanReadableMsg = this.i18nService.t("autoConfirmEnabledByAdmin"); + break; + case EventType.Organization_AutoConfirmDisabled_Admin: + msg = humanReadableMsg = this.i18nService.t("autoConfirmDisabledByAdmin"); + break; + case EventType.Organization_AutoConfirmEnabled_Portal: + msg = humanReadableMsg = this.i18nService.t("autoConfirmEnabledByPortal"); + break; + case EventType.Organization_AutoConfirmDisabled_Portal: + msg = humanReadableMsg = this.i18nService.t("autoConfirmDisabledByPortal"); + break; // Policies case EventType.Policy_Updated: { diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index b257a68052d..c45d7e5d630 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -6142,6 +6151,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, diff --git a/libs/auto-confirm/src/services/default-auto-confirm.service.spec.ts b/libs/auto-confirm/src/services/default-auto-confirm.service.spec.ts index 0ea3ca9c23a..2b098d3c231 100644 --- a/libs/auto-confirm/src/services/default-auto-confirm.service.spec.ts +++ b/libs/auto-confirm/src/services/default-auto-confirm.service.spec.ts @@ -439,6 +439,21 @@ describe("DefaultAutomaticUserConfirmationService", () => { expect(organizationUserApiService.postOrganizationUserAutoConfirm).not.toHaveBeenCalled(); }); + it("should return early when auto-confirm is disabled in configuration", async () => { + const disabledConfig = new AutoConfirmState(); + disabledConfig.enabled = false; + await stateProvider.setUserState( + AUTO_CONFIRM_STATE, + { [mockUserId]: disabledConfig }, + mockUserId, + ); + + await service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganizationId); + + expect(apiService.getUserPublicKey).not.toHaveBeenCalled(); + expect(organizationUserApiService.postOrganizationUserAutoConfirm).not.toHaveBeenCalled(); + }); + it("should build confirm request with organization and public key", async () => { await service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganizationId); diff --git a/libs/common/src/enums/event-system-user.enum.ts b/libs/common/src/enums/event-system-user.enum.ts index f4abbb1e3e9..e5e92ee7ef1 100644 --- a/libs/common/src/enums/event-system-user.enum.ts +++ b/libs/common/src/enums/event-system-user.enum.ts @@ -5,4 +5,5 @@ export enum EventSystemUser { SCIM = 1, DomainVerification = 2, PublicApi = 3, + BitwardenPortal = 5, } diff --git a/libs/common/src/enums/event-type.enum.ts b/libs/common/src/enums/event-type.enum.ts index 4750c881f06..e1bda61b111 100644 --- a/libs/common/src/enums/event-type.enum.ts +++ b/libs/common/src/enums/event-type.enum.ts @@ -60,6 +60,7 @@ export enum EventType { OrganizationUser_RejectedAuthRequest = 1514, OrganizationUser_Deleted = 1515, OrganizationUser_Left = 1516, + OrganizationUser_AutomaticallyConfirmed = 1517, Organization_Updated = 1600, Organization_PurgedVault = 1601, @@ -81,6 +82,10 @@ export enum EventType { Organization_CollectionManagement_AllowAdminAccessToAllCollectionItemsDisabled = 1617, Organization_ItemOrganization_Accepted = 1618, Organization_ItemOrganization_Declined = 1619, + Organization_AutoConfirmEnabled_Admin = 1620, + Organization_AutoConfirmDisabled_Admin = 1621, + Organization_AutoConfirmEnabled_Portal = 1622, + Organization_AutoConfirmDisabled_Portal = 1623, Policy_Updated = 1700, From 4f256fee6d79a0d30f7bcc0cded8431047442437 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 19 Feb 2026 09:26:18 -0600 Subject: [PATCH 091/134] [PM-29087] [PM-29088] Remove FF: `pm-26793-fetch-premium-price-from-pricing-service` - Logic + Flag (#18946) * refactor(billing): remove PM-26793 feature flag from subscription pricing service * test(billing): update subscription pricing tests for PM-26793 feature flag removal * chore: remove PM-26793 feature flag from keys --- .../subscription-pricing.service.spec.ts | 72 +++---------------- .../services/subscription-pricing.service.ts | 70 ++++++------------ libs/common/src/enums/feature-flag.enum.ts | 2 - 3 files changed, 30 insertions(+), 114 deletions(-) diff --git a/libs/common/src/billing/services/subscription-pricing.service.spec.ts b/libs/common/src/billing/services/subscription-pricing.service.spec.ts index 33bfcebeb58..e96c0d4b74c 100644 --- a/libs/common/src/billing/services/subscription-pricing.service.spec.ts +++ b/libs/common/src/billing/services/subscription-pricing.service.spec.ts @@ -231,6 +231,7 @@ describe("DefaultSubscriptionPricingService", () => { }, storage: { price: 4, + provided: 1, }, } as PremiumPlanResponse; @@ -350,7 +351,7 @@ describe("DefaultSubscriptionPricingService", () => { billingApiService.getPlans.mockResolvedValue(mockPlansResponse); billingApiService.getPremiumPlan.mockResolvedValue(mockPremiumPlanResponse); - configService.getFeatureFlag$.mockReturnValue(of(false)); // Default to false (use hardcoded value) + configService.getFeatureFlag$.mockReturnValue(of(false)); setupEnvironmentService(environmentService); service = new DefaultSubscriptionPricingService( @@ -915,7 +916,7 @@ describe("DefaultSubscriptionPricingService", () => { const testError = new Error("Premium plan API error"); errorBillingApiService.getPlans.mockResolvedValue(mockPlansResponse); errorBillingApiService.getPremiumPlan.mockRejectedValue(testError); - errorConfigService.getFeatureFlag$.mockReturnValue(of(true)); // Enable feature flag to use premium plan API + errorConfigService.getFeatureFlag$.mockReturnValue(of(false)); setupEnvironmentService(errorEnvironmentService); const errorService = new DefaultSubscriptionPricingService( @@ -959,71 +960,16 @@ describe("DefaultSubscriptionPricingService", () => { expect(getPlansResponse).toHaveBeenCalledTimes(1); }); - it("should share premium plan API response between multiple subscriptions when feature flag is enabled", () => { - // Create a new mock to avoid conflicts with beforeEach setup - const newBillingApiService = mock(); - const newConfigService = mock(); - const newEnvironmentService = mock(); - - newBillingApiService.getPlans.mockResolvedValue(mockPlansResponse); - newBillingApiService.getPremiumPlan.mockResolvedValue(mockPremiumPlanResponse); - newConfigService.getFeatureFlag$.mockReturnValue(of(true)); - setupEnvironmentService(newEnvironmentService); - - const getPremiumPlanSpy = jest.spyOn(newBillingApiService, "getPremiumPlan"); - - // Create a new service instance with the feature flag enabled - const newService = new DefaultSubscriptionPricingService( - newBillingApiService, - newConfigService, - i18nService, - logService, - newEnvironmentService, - ); + it("should share premium plan API response between multiple subscriptions", () => { + const getPremiumPlanSpy = jest.spyOn(billingApiService, "getPremiumPlan"); // Subscribe to the premium pricing tier multiple times - newService.getPersonalSubscriptionPricingTiers$().subscribe(); - newService.getPersonalSubscriptionPricingTiers$().subscribe(); + service.getPersonalSubscriptionPricingTiers$().subscribe(); + service.getPersonalSubscriptionPricingTiers$().subscribe(); // API should only be called once due to shareReplay on premiumPlanResponse$ expect(getPremiumPlanSpy).toHaveBeenCalledTimes(1); }); - - it("should use hardcoded premium price when feature flag is disabled", (done) => { - // Create a new mock to test from scratch - const newBillingApiService = mock(); - const newConfigService = mock(); - const newEnvironmentService = mock(); - - newBillingApiService.getPlans.mockResolvedValue(mockPlansResponse); - newBillingApiService.getPremiumPlan.mockResolvedValue({ - seat: { price: 999 }, // Different price to verify hardcoded value is used - storage: { price: 999 }, - } as PremiumPlanResponse); - newConfigService.getFeatureFlag$.mockReturnValue(of(false)); - setupEnvironmentService(newEnvironmentService); - - // Create a new service instance with the feature flag disabled - const newService = new DefaultSubscriptionPricingService( - newBillingApiService, - newConfigService, - i18nService, - logService, - newEnvironmentService, - ); - - // Subscribe with feature flag disabled - newService.getPersonalSubscriptionPricingTiers$().subscribe((tiers) => { - const premiumTier = tiers.find( - (tier) => tier.id === PersonalSubscriptionPricingTierIds.Premium, - ); - - // Should use hardcoded value of 10, not the API response value of 999 - expect(premiumTier!.passwordManager.annualPrice).toBe(10); - expect(premiumTier!.passwordManager.annualPricePerAdditionalStorageGB).toBe(4); - done(); - }); - }); }); describe("Self-hosted environment behavior", () => { @@ -1035,7 +981,7 @@ describe("DefaultSubscriptionPricingService", () => { const getPlansSpy = jest.spyOn(selfHostedBillingApiService, "getPlans"); const getPremiumPlanSpy = jest.spyOn(selfHostedBillingApiService, "getPremiumPlan"); - selfHostedConfigService.getFeatureFlag$.mockReturnValue(of(true)); + selfHostedConfigService.getFeatureFlag$.mockReturnValue(of(false)); setupEnvironmentService(selfHostedEnvironmentService, Region.SelfHosted); const selfHostedService = new DefaultSubscriptionPricingService( @@ -1061,7 +1007,7 @@ describe("DefaultSubscriptionPricingService", () => { const selfHostedConfigService = mock(); const selfHostedEnvironmentService = mock(); - selfHostedConfigService.getFeatureFlag$.mockReturnValue(of(true)); + selfHostedConfigService.getFeatureFlag$.mockReturnValue(of(false)); setupEnvironmentService(selfHostedEnvironmentService, Region.SelfHosted); const selfHostedService = new DefaultSubscriptionPricingService( diff --git a/libs/common/src/billing/services/subscription-pricing.service.ts b/libs/common/src/billing/services/subscription-pricing.service.ts index 9ba76d348d0..73f3dc1bc77 100644 --- a/libs/common/src/billing/services/subscription-pricing.service.ts +++ b/libs/common/src/billing/services/subscription-pricing.service.ts @@ -33,16 +33,6 @@ import { } from "../types/subscription-pricing-tier"; export class DefaultSubscriptionPricingService implements SubscriptionPricingServiceAbstraction { - /** - * Fallback premium pricing used when the feature flag is disabled. - * These values represent the legacy pricing model and will not reflect - * server-side price changes. They are retained for backward compatibility - * during the feature flag rollout period. - */ - private static readonly FALLBACK_PREMIUM_SEAT_PRICE = 10; - private static readonly FALLBACK_PREMIUM_STORAGE_PRICE = 4; - private static readonly FALLBACK_PREMIUM_PROVIDED_STORAGE_GB = 1; - constructor( private billingApiService: BillingApiServiceAbstraction, private configService: ConfigService, @@ -123,45 +113,27 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer shareReplay({ bufferSize: 1, refCount: false }), ); - private premium$: Observable = this.configService - .getFeatureFlag$(FeatureFlag.PM26793_FetchPremiumPriceFromPricingService) - .pipe( - take(1), // Lock behavior at first subscription to prevent switching data sources mid-stream - switchMap((fetchPremiumFromPricingService) => - fetchPremiumFromPricingService - ? this.premiumPlanResponse$.pipe( - map((premiumPlan) => ({ - seat: premiumPlan.seat?.price, - storage: premiumPlan.storage?.price, - provided: premiumPlan.storage?.provided, - })), - ) - : of({ - seat: DefaultSubscriptionPricingService.FALLBACK_PREMIUM_SEAT_PRICE, - storage: DefaultSubscriptionPricingService.FALLBACK_PREMIUM_STORAGE_PRICE, - provided: DefaultSubscriptionPricingService.FALLBACK_PREMIUM_PROVIDED_STORAGE_GB, - }), - ), - map((premiumPrices) => ({ - id: PersonalSubscriptionPricingTierIds.Premium, - name: this.i18nService.t("premium"), - description: this.i18nService.t("advancedOnlineSecurity"), - availableCadences: [SubscriptionCadenceIds.Annually], - passwordManager: { - type: "standalone", - annualPrice: premiumPrices.seat, - annualPricePerAdditionalStorageGB: premiumPrices.storage, - providedStorageGB: premiumPrices.provided, - features: [ - this.featureTranslations.builtInAuthenticator(), - this.featureTranslations.secureFileStorage(), - this.featureTranslations.emergencyAccess(), - this.featureTranslations.breachMonitoring(), - this.featureTranslations.andMoreFeatures(), - ], - }, - })), - ); + private premium$: Observable = this.premiumPlanResponse$.pipe( + map((premiumPlan) => ({ + id: PersonalSubscriptionPricingTierIds.Premium, + name: this.i18nService.t("premium"), + description: this.i18nService.t("advancedOnlineSecurity"), + availableCadences: [SubscriptionCadenceIds.Annually], + passwordManager: { + type: "standalone", + annualPrice: premiumPlan.seat?.price, + annualPricePerAdditionalStorageGB: premiumPlan.storage?.price, + providedStorageGB: premiumPlan.storage?.provided, + features: [ + this.featureTranslations.builtInAuthenticator(), + this.featureTranslations.secureFileStorage(), + this.featureTranslations.emergencyAccess(), + this.featureTranslations.breachMonitoring(), + this.featureTranslations.andMoreFeatures(), + ], + }, + })), + ); private families$: Observable = this.organizationPlansResponse$.pipe( diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 5160e6aa542..b7fad43ebbf 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -31,7 +31,6 @@ export enum FeatureFlag { /* Billing */ TrialPaymentOptional = "PM-8163-trial-payment", PM24032_NewNavigationPremiumUpgradeButton = "pm-24032-new-navigation-premium-upgrade-button", - PM26793_FetchPremiumPriceFromPricingService = "pm-26793-fetch-premium-price-from-pricing-service", PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog = "pm-23713-premium-badge-opens-new-premium-upgrade-dialog", PM26462_Milestone_3 = "pm-26462-milestone-3", PM23341_Milestone_2 = "pm-23341-milestone-2", @@ -146,7 +145,6 @@ export const DefaultFeatureFlagValue = { /* Billing */ [FeatureFlag.TrialPaymentOptional]: FALSE, [FeatureFlag.PM24032_NewNavigationPremiumUpgradeButton]: FALSE, - [FeatureFlag.PM26793_FetchPremiumPriceFromPricingService]: FALSE, [FeatureFlag.PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog]: FALSE, [FeatureFlag.PM26462_Milestone_3]: FALSE, [FeatureFlag.PM23341_Milestone_2]: FALSE, From d0ccb9cd31b0bb8cfad93dc0470dbdfb9252df96 Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Thu, 19 Feb 2026 11:12:03 -0600 Subject: [PATCH 092/134] [PM-32013] Empty state incorrectly rendered (#19033) --- .../applications.component.html | 4 +- .../applications.component.ts | 30 ++-- .../chip-select/chip-select.component.spec.ts | 156 +++++++++++++++++- .../src/chip-select/chip-select.component.ts | 11 ++ 4 files changed, 179 insertions(+), 22 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.html index 27864fa2f87..743f8ff1b68 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.html @@ -84,9 +84,9 @@ class="tw-mb-10" > - @if (emptyTableExplanation()) { + @if (this.dataSource.filteredData?.length === 0) {
    - {{ emptyTableExplanation() }} + {{ "noApplicationsMatchTheseFilters" | i18n }}
    }
    diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.ts index 0020106ba7d..962628584d3 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.ts @@ -117,7 +117,6 @@ export class ApplicationsComponent implements OnInit { icon: " ", }, ]); - protected readonly emptyTableExplanation = signal(""); readonly allSelectedAppsAreCritical = computed(() => { if (!this.dataSource.filteredData || this.selectedUrls().size == 0) { @@ -174,6 +173,9 @@ export class ApplicationsComponent implements OnInit { })); this.dataSource.data = tableDataWithIcon; this.totalApplicationsCount.set(report.reportData.length); + this.criticalApplicationsCount.set( + report.reportData.filter((app) => app.isMarkedAsCritical).length, + ); } else { this.dataSource.data = []; } @@ -183,16 +185,6 @@ export class ApplicationsComponent implements OnInit { }, }); - this.dataService.criticalReportResults$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({ - next: (criticalReport) => { - if (criticalReport != null) { - this.criticalApplicationsCount.set(criticalReport.reportData.length); - } else { - this.criticalApplicationsCount.set(0); - } - }, - }); - combineLatest([ this.searchControl.valueChanges.pipe(startWith("")), this.selectedFilterObservable, @@ -219,12 +211,6 @@ export class ApplicationsComponent implements OnInit { } }); this.selectedUrls.set(filteredUrls); - - if (this.dataSource?.filteredData?.length === 0) { - this.emptyTableExplanation.set(this.i18nService.t("noApplicationsMatchTheseFilters")); - } else { - this.emptyTableExplanation.set(""); - } }); } @@ -240,7 +226,7 @@ export class ApplicationsComponent implements OnInit { .saveCriticalApplications(Array.from(this.selectedUrls())) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ - next: () => { + next: (response) => { this.toastService.showToast({ variant: "success", title: "", @@ -248,6 +234,9 @@ export class ApplicationsComponent implements OnInit { }); this.selectedUrls.set(new Set()); this.updatingCriticalApps.set(false); + this.criticalApplicationsCount.set( + response?.data?.summaryData?.totalCriticalApplicationCount ?? 0, + ); }, error: () => { this.toastService.showToast({ @@ -267,7 +256,7 @@ export class ApplicationsComponent implements OnInit { .removeCriticalApplications(appsToUnmark) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ - next: () => { + next: (response) => { this.toastService.showToast({ message: this.i18nService.t( "numApplicationsUnmarkedCriticalSuccess", @@ -277,6 +266,9 @@ export class ApplicationsComponent implements OnInit { }); this.selectedUrls.set(new Set()); this.updatingCriticalApps.set(false); + this.criticalApplicationsCount.set( + response?.data?.summaryData?.totalCriticalApplicationCount ?? 0, + ); }, error: () => { this.toastService.showToast({ diff --git a/libs/components/src/chip-select/chip-select.component.spec.ts b/libs/components/src/chip-select/chip-select.component.spec.ts index 3a66b799652..bfabb5ea95e 100644 --- a/libs/components/src/chip-select/chip-select.component.spec.ts +++ b/libs/components/src/chip-select/chip-select.component.spec.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, signal } from "@angular/core"; +import { ChangeDetectionStrategy, Component, signal, computed } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { FormControl } from "@angular/forms"; import { By } from "@angular/platform-browser"; @@ -502,3 +502,157 @@ class TestAppComponent { readonly disabled = signal(false); readonly fullWidth = signal(false); } + +describe("ChipSelectComponentWithDynamicOptions", () => { + let component: ChipSelectComponent; + let fixture: ComponentFixture; + + const getChipButton = () => + fixture.debugElement.query(By.css("[data-fvw-target]"))?.nativeElement as HTMLButtonElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TestAppWithDynamicOptionsComponent, NoopAnimationsModule], + providers: [{ provide: I18nService, useValue: mockI18nService }], + }).compileComponents(); + + fixture = TestBed.createComponent(TestAppWithDynamicOptionsComponent); + fixture.detectChanges(); + + component = fixture.debugElement.query(By.directive(ChipSelectComponent)).componentInstance; + + fixture.componentInstance.firstCounter.set(0); + fixture.componentInstance.secondCounter.set(0); + + fixture.detectChanges(); + }); + + describe("User-Facing Behavior", () => { + it("should update available options when they change", () => { + const first = 5; + const second = 10; + + const testApp = fixture.componentInstance; + testApp.firstCounter.set(first); + testApp.secondCounter.set(second); + fixture.detectChanges(); + + getChipButton().click(); + fixture.detectChanges(); + + const menuItems = Array.from(document.querySelectorAll("[bitMenuItem]")); + expect(menuItems.some((el) => el.textContent?.includes(`Option - ${first}`))).toBe(true); + expect(menuItems.some((el) => el.textContent?.includes(`Option - ${second}`))).toBe(true); + }); + }); + + describe("Form Integration Behavior", () => { + it("should display selected option when form control value is set", () => { + const testApp = fixture.componentInstance; + testApp.firstCounter.set(1); + testApp.secondCounter.set(2); + + component.writeValue("opt2"); // select second menu option which has dynamic label + fixture.detectChanges(); + + const button = getChipButton(); + expect(button.textContent?.trim()).toContain("Option - 2"); // verify that the label reflects the dynamic value + + // change the dynamic values and verify that the menu still shows the correct labels for the options + // it should also keep opt2 selected since it's the same value, just with an updated label + const first = 10; + const second = 20; + + testApp.firstCounter.set(first); + testApp.secondCounter.set(second); + fixture.detectChanges(); + + // again, verify that the label reflects the dynamic value + expect(button.textContent?.trim()).toContain(`Option - ${second}`); + + // click the button to open the menu + getChipButton().click(); + fixture.detectChanges(); + + // verify that the menu items also reflect the updated dynamic values + const menuItems = Array.from(document.querySelectorAll("[bitMenuItem]")); + expect(menuItems.some((el) => el.textContent?.includes(`Option - ${first}`))).toBe(true); + expect(menuItems.some((el) => el.textContent?.includes(`Option - ${second}`))).toBe(true); + }); + + it("should find and display nested option when form control value is set", () => { + const testApp = fixture.componentInstance; + testApp.firstCounter.set(1); + testApp.secondCounter.set(2); + + component.writeValue("child1"); // select a child menu item + fixture.detectChanges(); + + const button = getChipButton(); + // verify that the label reflects the dynamic value for the child option + expect(button.textContent?.trim()).toContain("Child - 1"); + + const first = 10; + const second = 20; + + testApp.firstCounter.set(first); + testApp.secondCounter.set(second); + fixture.detectChanges(); + + // again, verify that the label reflects the dynamic value + expect(button.textContent?.trim()).toContain(`Child - ${first}`); + }); + + it("should clear selection when form control value is set to null", () => { + const testApp = fixture.componentInstance; + testApp.firstCounter.set(1); + testApp.secondCounter.set(2); + + component.writeValue("opt1"); + fixture.detectChanges(); + + expect(getChipButton().textContent).toContain("Option - 1"); + + component.writeValue(null as any); + fixture.detectChanges(); + expect(getChipButton().textContent).toContain("Select an option"); + }); + }); +}); /* end of ChipSelectComponentWithDynamicOptions tests */ +@Component({ + selector: "test-app-with-dynamic-options", + template: ` + + `, + imports: [ChipSelectComponent], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +class TestAppWithDynamicOptionsComponent { + readonly firstCounter = signal(1); + readonly secondCounter = signal(2); + readonly options = computed(() => { + const first = this.firstCounter(); + const second = this.secondCounter(); + return [ + { label: `Option - ${first}`, value: "opt1", icon: "bwi-folder" }, + { label: `Option - ${second}`, value: "opt2" }, + { + label: "Parent Option", + value: "parent", + children: [ + { label: `Child - ${first}`, value: "child1" }, + { label: `Child - ${second}`, value: "child2" }, + ], + }, + ]; + }); + + readonly disabled = signal(false); + readonly fullWidth = signal(false); +} diff --git a/libs/components/src/chip-select/chip-select.component.ts b/libs/components/src/chip-select/chip-select.component.ts index 50e462dc815..1e988960472 100644 --- a/libs/components/src/chip-select/chip-select.component.ts +++ b/libs/components/src/chip-select/chip-select.component.ts @@ -106,8 +106,19 @@ export class ChipSelectComponent implements ControlValueAccessor { constructor() { // Initialize the root tree whenever options change effect(() => { + const currentSelection = this.selectedOption; + + // when the options change, clear the childParentMap + this.childParentMap.clear(); + this.initializeRootTree(this.options()); + // when the options change, we need to change our selectedOption + // to reflect the changed options. + if (currentSelection?.value != null) { + this.selectedOption = this.findOption(this.rootTree, currentSelection.value); + } + // If there's a pending value, apply it now that options are available if (this.pendingValue !== undefined) { this.selectedOption = this.findOption(this.rootTree, this.pendingValue); From 46a2af38a0db5b72f9b5125788d7588d2ca74f59 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Thu, 19 Feb 2026 09:29:54 -0800 Subject: [PATCH 093/134] [PM-31974] - Vault Welcome dialog (#18960) * premium upgrade prompt and onboarding dialog * finalize onboard vault dialog * vault welcome dialog no ext * finish welcome dialog prompt * revert changes to unified upgrade prompt service * rename component * rename feature flag * add welcome dialog service * fix tests * fix footer position in welcome dialog * present dialog in order * fix tests * fix padding --- apps/browser/src/_locales/en/messages.json | 3 + .../vault-welcome-dialog.component.html | 27 ++++ .../vault-welcome-dialog.component.spec.ts | 87 +++++++++++++ .../vault-welcome-dialog.component.ts | 69 ++++++++++ .../services/web-vault-prompt.service.spec.ts | 21 ++- .../services/web-vault-prompt.service.ts | 11 +- .../services/welcome-dialog.service.spec.ts | 123 ++++++++++++++++++ .../vault/services/welcome-dialog.service.ts | 72 ++++++++++ .../web/src/images/welcome-dialog-graphic.png | Bin 0 -> 95551 bytes apps/web/src/locales/en/messages.json | 12 ++ libs/common/src/enums/feature-flag.enum.ts | 2 + libs/state/src/core/state-definitions.ts | 3 + 12 files changed, 425 insertions(+), 5 deletions(-) create mode 100644 apps/web/src/app/vault/components/vault-welcome-dialog/vault-welcome-dialog.component.html create mode 100644 apps/web/src/app/vault/components/vault-welcome-dialog/vault-welcome-dialog.component.spec.ts create mode 100644 apps/web/src/app/vault/components/vault-welcome-dialog/vault-welcome-dialog.component.ts create mode 100644 apps/web/src/app/vault/services/welcome-dialog.service.spec.ts create mode 100644 apps/web/src/app/vault/services/welcome-dialog.service.ts create mode 100644 apps/web/src/images/welcome-dialog-graphic.png diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index fbfaa17a87d..51ca51960d7 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" diff --git a/apps/web/src/app/vault/components/vault-welcome-dialog/vault-welcome-dialog.component.html b/apps/web/src/app/vault/components/vault-welcome-dialog/vault-welcome-dialog.component.html new file mode 100644 index 00000000000..3304fa3e3cc --- /dev/null +++ b/apps/web/src/app/vault/components/vault-welcome-dialog/vault-welcome-dialog.component.html @@ -0,0 +1,27 @@ + +
    + +
    +
    +

    + {{ "vaultWelcomeDialogTitle" | i18n }} +

    +

    + {{ "vaultWelcomeDialogDescription" | i18n }} +

    +
    +
    +
    +
    + + +
    +
    diff --git a/apps/web/src/app/vault/components/vault-welcome-dialog/vault-welcome-dialog.component.spec.ts b/apps/web/src/app/vault/components/vault-welcome-dialog/vault-welcome-dialog.component.spec.ts new file mode 100644 index 00000000000..bc0142b374d --- /dev/null +++ b/apps/web/src/app/vault/components/vault-welcome-dialog/vault-welcome-dialog.component.spec.ts @@ -0,0 +1,87 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { BehaviorSubject } from "rxjs"; + +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { DialogRef } from "@bitwarden/components"; +import { StateProvider } from "@bitwarden/state"; + +import { + VaultWelcomeDialogComponent, + VaultWelcomeDialogResult, +} from "./vault-welcome-dialog.component"; + +describe("VaultWelcomeDialogComponent", () => { + let component: VaultWelcomeDialogComponent; + let fixture: ComponentFixture; + + const mockUserId = "user-123" as UserId; + const activeAccount$ = new BehaviorSubject({ + id: mockUserId, + } as Account); + const setUserState = jest.fn().mockResolvedValue([mockUserId, true]); + const close = jest.fn(); + + beforeEach(async () => { + jest.clearAllMocks(); + + await TestBed.configureTestingModule({ + imports: [VaultWelcomeDialogComponent], + providers: [ + { provide: AccountService, useValue: { activeAccount$ } }, + { provide: StateProvider, useValue: { setUserState } }, + { provide: DialogRef, useValue: { close } }, + { provide: I18nService, useValue: { t: (key: string) => key } }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(VaultWelcomeDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + describe("onDismiss", () => { + it("should set acknowledged state and close with Dismissed result", async () => { + await component["onDismiss"](); + + expect(setUserState).toHaveBeenCalledWith( + expect.objectContaining({ key: "vaultWelcomeDialogAcknowledged" }), + true, + mockUserId, + ); + expect(close).toHaveBeenCalledWith(VaultWelcomeDialogResult.Dismissed); + }); + + it("should throw if no active account", async () => { + activeAccount$.next(null); + + await expect(component["onDismiss"]()).rejects.toThrow("Null or undefined account"); + + expect(setUserState).not.toHaveBeenCalled(); + }); + }); + + describe("onPrimaryCta", () => { + it("should set acknowledged state and close with GetStarted result", async () => { + activeAccount$.next({ id: mockUserId } as Account); + + await component["onPrimaryCta"](); + + expect(setUserState).toHaveBeenCalledWith( + expect.objectContaining({ key: "vaultWelcomeDialogAcknowledged" }), + true, + mockUserId, + ); + expect(close).toHaveBeenCalledWith(VaultWelcomeDialogResult.GetStarted); + }); + + it("should throw if no active account", async () => { + activeAccount$.next(null); + + await expect(component["onPrimaryCta"]()).rejects.toThrow("Null or undefined account"); + + expect(setUserState).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/web/src/app/vault/components/vault-welcome-dialog/vault-welcome-dialog.component.ts b/apps/web/src/app/vault/components/vault-welcome-dialog/vault-welcome-dialog.component.ts new file mode 100644 index 00000000000..d43ea5165f7 --- /dev/null +++ b/apps/web/src/app/vault/components/vault-welcome-dialog/vault-welcome-dialog.component.ts @@ -0,0 +1,69 @@ +import { CommonModule } from "@angular/common"; +import { ChangeDetectionStrategy, Component, inject } from "@angular/core"; +import { firstValueFrom } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { + ButtonModule, + DialogModule, + DialogRef, + DialogService, + TypographyModule, + CenterPositionStrategy, +} from "@bitwarden/components"; +import { StateProvider, UserKeyDefinition, VAULT_WELCOME_DIALOG_DISK } from "@bitwarden/state"; + +export const VaultWelcomeDialogResult = { + Dismissed: "dismissed", + GetStarted: "getStarted", +} as const; + +export type VaultWelcomeDialogResult = + (typeof VaultWelcomeDialogResult)[keyof typeof VaultWelcomeDialogResult]; + +const VAULT_WELCOME_DIALOG_ACKNOWLEDGED_KEY = new UserKeyDefinition( + VAULT_WELCOME_DIALOG_DISK, + "vaultWelcomeDialogAcknowledged", + { + deserializer: (value) => value, + clearOn: [], + }, +); + +@Component({ + selector: "app-vault-welcome-dialog", + templateUrl: "./vault-welcome-dialog.component.html", + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [CommonModule, DialogModule, ButtonModule, TypographyModule, JslibModule], +}) +export class VaultWelcomeDialogComponent { + private accountService = inject(AccountService); + private stateProvider = inject(StateProvider); + + constructor(private dialogRef: DialogRef) {} + + protected async onDismiss(): Promise { + await this.setAcknowledged(); + this.dialogRef.close(VaultWelcomeDialogResult.Dismissed); + } + + protected async onPrimaryCta(): Promise { + await this.setAcknowledged(); + this.dialogRef.close(VaultWelcomeDialogResult.GetStarted); + } + + private async setAcknowledged(): Promise { + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + await this.stateProvider.setUserState(VAULT_WELCOME_DIALOG_ACKNOWLEDGED_KEY, true, userId); + } + + static open(dialogService: DialogService): DialogRef { + return dialogService.open(VaultWelcomeDialogComponent, { + disableClose: true, + positionStrategy: new CenterPositionStrategy(), + }); + } +} diff --git a/apps/web/src/app/vault/services/web-vault-prompt.service.spec.ts b/apps/web/src/app/vault/services/web-vault-prompt.service.spec.ts index a224b8e7c4b..eb72c80fe04 100644 --- a/apps/web/src/app/vault/services/web-vault-prompt.service.spec.ts +++ b/apps/web/src/app/vault/services/web-vault-prompt.service.spec.ts @@ -7,7 +7,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { UserId } from "@bitwarden/common/types/guid"; import { DialogRef, DialogService } from "@bitwarden/components"; @@ -21,6 +21,7 @@ import { import { UnifiedUpgradePromptService } from "../../billing/individual/upgrade/services"; import { WebVaultPromptService } from "./web-vault-prompt.service"; +import { WelcomeDialogService } from "./welcome-dialog.service"; describe("WebVaultPromptService", () => { let service: WebVaultPromptService; @@ -38,20 +39,33 @@ describe("WebVaultPromptService", () => { ); const upsertAutoConfirm = jest.fn().mockResolvedValue(undefined); const organizations$ = jest.fn().mockReturnValue(of([])); - const displayUpgradePromptConditionally = jest.fn().mockResolvedValue(undefined); + const displayUpgradePromptConditionally = jest.fn().mockResolvedValue(false); const enforceOrganizationDataOwnership = jest.fn().mockResolvedValue(undefined); + const conditionallyShowWelcomeDialog = jest.fn().mockResolvedValue(false); const logError = jest.fn(); + let activeAccount$: BehaviorSubject; + + function createAccount(overrides: Partial = {}): Account { + return { + id: mockUserId, + creationDate: new Date(), + ...overrides, + } as Account; + } + beforeEach(() => { jest.clearAllMocks(); + activeAccount$ = new BehaviorSubject(createAccount()); + TestBed.configureTestingModule({ providers: [ WebVaultPromptService, { provide: UnifiedUpgradePromptService, useValue: { displayUpgradePromptConditionally } }, { provide: VaultItemsTransferService, useValue: { enforceOrganizationDataOwnership } }, { provide: PolicyService, useValue: { policies$ } }, - { provide: AccountService, useValue: { activeAccount$: of({ id: mockUserId }) } }, + { provide: AccountService, useValue: { activeAccount$ } }, { provide: AutomaticUserConfirmationService, useValue: { configuration$: configurationAutoConfirm$, upsert: upsertAutoConfirm }, @@ -60,6 +74,7 @@ describe("WebVaultPromptService", () => { { provide: ConfigService, useValue: { getFeatureFlag$ } }, { provide: DialogService, useValue: { open } }, { provide: LogService, useValue: { error: logError } }, + { provide: WelcomeDialogService, useValue: { conditionallyShowWelcomeDialog } }, ], }); diff --git a/apps/web/src/app/vault/services/web-vault-prompt.service.ts b/apps/web/src/app/vault/services/web-vault-prompt.service.ts index 1774bfcc085..4c4e7a3fe78 100644 --- a/apps/web/src/app/vault/services/web-vault-prompt.service.ts +++ b/apps/web/src/app/vault/services/web-vault-prompt.service.ts @@ -20,6 +20,8 @@ import { } from "../../admin-console/organizations/policies"; import { UnifiedUpgradePromptService } from "../../billing/individual/upgrade/services"; +import { WelcomeDialogService } from "./welcome-dialog.service"; + @Injectable() export class WebVaultPromptService { private unifiedUpgradePromptService = inject(UnifiedUpgradePromptService); @@ -31,6 +33,7 @@ export class WebVaultPromptService { private configService = inject(ConfigService); private dialogService = inject(DialogService); private logService = inject(LogService); + private welcomeDialogService = inject(WelcomeDialogService); private userId$ = this.accountService.activeAccount$.pipe(getUserId); @@ -46,9 +49,13 @@ export class WebVaultPromptService { async conditionallyPromptUser() { const userId = await firstValueFrom(this.userId$); - void this.unifiedUpgradePromptService.displayUpgradePromptConditionally(); + if (await this.unifiedUpgradePromptService.displayUpgradePromptConditionally()) { + return; + } - void this.vaultItemTransferService.enforceOrganizationDataOwnership(userId); + await this.vaultItemTransferService.enforceOrganizationDataOwnership(userId); + + await this.welcomeDialogService.conditionallyShowWelcomeDialog(); this.checkForAutoConfirm(); } diff --git a/apps/web/src/app/vault/services/welcome-dialog.service.spec.ts b/apps/web/src/app/vault/services/welcome-dialog.service.spec.ts new file mode 100644 index 00000000000..752514ca066 --- /dev/null +++ b/apps/web/src/app/vault/services/welcome-dialog.service.spec.ts @@ -0,0 +1,123 @@ +import { TestBed } from "@angular/core/testing"; +import { BehaviorSubject, of } from "rxjs"; + +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { DialogRef, DialogService } from "@bitwarden/components"; +import { StateProvider } from "@bitwarden/state"; + +import { VaultWelcomeDialogComponent } from "../components/vault-welcome-dialog/vault-welcome-dialog.component"; + +import { WelcomeDialogService } from "./welcome-dialog.service"; + +describe("WelcomeDialogService", () => { + let service: WelcomeDialogService; + + const mockUserId = "user-123" as UserId; + + const getFeatureFlag = jest.fn().mockResolvedValue(false); + const getUserState$ = jest.fn().mockReturnValue(of(false)); + const mockDialogOpen = jest.spyOn(VaultWelcomeDialogComponent, "open"); + + let activeAccount$: BehaviorSubject; + + function createAccount(overrides: Partial = {}): Account { + return { + id: mockUserId, + creationDate: new Date(), + ...overrides, + } as Account; + } + + beforeEach(() => { + jest.clearAllMocks(); + mockDialogOpen.mockReset(); + + activeAccount$ = new BehaviorSubject(createAccount()); + + TestBed.configureTestingModule({ + providers: [ + WelcomeDialogService, + { provide: AccountService, useValue: { activeAccount$ } }, + { provide: ConfigService, useValue: { getFeatureFlag } }, + { provide: DialogService, useValue: {} }, + { provide: StateProvider, useValue: { getUserState$ } }, + ], + }); + + service = TestBed.inject(WelcomeDialogService); + }); + + describe("conditionallyShowWelcomeDialog", () => { + it("should not show dialog when no active account", async () => { + activeAccount$.next(null); + + await service.conditionallyShowWelcomeDialog(); + + expect(mockDialogOpen).not.toHaveBeenCalled(); + }); + + it("should not show dialog when feature flag is disabled", async () => { + getFeatureFlag.mockResolvedValueOnce(false); + + await service.conditionallyShowWelcomeDialog(); + + expect(getFeatureFlag).toHaveBeenCalledWith(FeatureFlag.PM29437_WelcomeDialog); + expect(mockDialogOpen).not.toHaveBeenCalled(); + }); + + it("should not show dialog when account has no creation date", async () => { + activeAccount$.next(createAccount({ creationDate: undefined })); + getFeatureFlag.mockResolvedValueOnce(true); + + await service.conditionallyShowWelcomeDialog(); + + expect(mockDialogOpen).not.toHaveBeenCalled(); + }); + + it("should not show dialog when account is older than 30 days", async () => { + const overThirtyDaysAgo = new Date(Date.now() - 1000 * 60 * 60 * 24 * 30 - 1000); + activeAccount$.next(createAccount({ creationDate: overThirtyDaysAgo })); + getFeatureFlag.mockResolvedValueOnce(true); + + await service.conditionallyShowWelcomeDialog(); + + expect(mockDialogOpen).not.toHaveBeenCalled(); + }); + + it("should not show dialog when user has already acknowledged it", async () => { + activeAccount$.next(createAccount({ creationDate: new Date() })); + getFeatureFlag.mockResolvedValueOnce(true); + getUserState$.mockReturnValueOnce(of(true)); + + await service.conditionallyShowWelcomeDialog(); + + expect(mockDialogOpen).not.toHaveBeenCalled(); + }); + + it("should show dialog for new user who has not acknowledged", async () => { + activeAccount$.next(createAccount({ creationDate: new Date() })); + getFeatureFlag.mockResolvedValueOnce(true); + getUserState$.mockReturnValueOnce(of(false)); + mockDialogOpen.mockReturnValue({ closed: of(undefined) } as DialogRef); + + await service.conditionallyShowWelcomeDialog(); + + expect(mockDialogOpen).toHaveBeenCalled(); + }); + + it("should show dialog for account created exactly 30 days ago", async () => { + const exactlyThirtyDaysAgo = new Date(Date.now() - 1000 * 60 * 60 * 24 * 30); + activeAccount$.next(createAccount({ creationDate: exactlyThirtyDaysAgo })); + getFeatureFlag.mockResolvedValueOnce(true); + getUserState$.mockReturnValueOnce(of(false)); + mockDialogOpen.mockReturnValue({ closed: of(undefined) } as DialogRef); + + await service.conditionallyShowWelcomeDialog(); + + expect(mockDialogOpen).toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/web/src/app/vault/services/welcome-dialog.service.ts b/apps/web/src/app/vault/services/welcome-dialog.service.ts new file mode 100644 index 00000000000..25b24b6df2d --- /dev/null +++ b/apps/web/src/app/vault/services/welcome-dialog.service.ts @@ -0,0 +1,72 @@ +import { inject, Injectable } from "@angular/core"; +import { firstValueFrom, map } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { DialogService } from "@bitwarden/components"; +import { StateProvider, UserKeyDefinition, VAULT_WELCOME_DIALOG_DISK } from "@bitwarden/state"; + +import { VaultWelcomeDialogComponent } from "../components/vault-welcome-dialog/vault-welcome-dialog.component"; + +const VAULT_WELCOME_DIALOG_ACKNOWLEDGED_KEY = new UserKeyDefinition( + VAULT_WELCOME_DIALOG_DISK, + "vaultWelcomeDialogAcknowledged", + { + deserializer: (value) => value, + clearOn: [], + }, +); + +const THIRTY_DAY_MS = 1000 * 60 * 60 * 24 * 30; + +@Injectable({ providedIn: "root" }) +export class WelcomeDialogService { + private accountService = inject(AccountService); + private configService = inject(ConfigService); + private dialogService = inject(DialogService); + private stateProvider = inject(StateProvider); + + /** + * Conditionally shows the welcome dialog to new users. + * + * @returns true if the dialog was shown, false otherwise + */ + async conditionallyShowWelcomeDialog() { + const account = await firstValueFrom(this.accountService.activeAccount$); + if (!account) { + return; + } + + const enabled = await this.configService.getFeatureFlag(FeatureFlag.PM29437_WelcomeDialog); + if (!enabled) { + return; + } + + const createdAt = account.creationDate; + if (!createdAt) { + return; + } + + const ageMs = Date.now() - createdAt.getTime(); + const isNewUser = ageMs >= 0 && ageMs <= THIRTY_DAY_MS; + if (!isNewUser) { + return; + } + + const acknowledged = await firstValueFrom( + this.stateProvider + .getUserState$(VAULT_WELCOME_DIALOG_ACKNOWLEDGED_KEY, account.id) + .pipe(map((v) => v ?? false)), + ); + + if (acknowledged) { + return; + } + + const dialogRef = VaultWelcomeDialogComponent.open(this.dialogService); + await firstValueFrom(dialogRef.closed); + + return; + } +} diff --git a/apps/web/src/images/welcome-dialog-graphic.png b/apps/web/src/images/welcome-dialog-graphic.png new file mode 100644 index 0000000000000000000000000000000000000000..fd2a12c52727b3b98438173de4a6d813bec1ac02 GIT binary patch literal 95551 zcmeFZ_g7Qt7B`ME%3P7+2!hmc6r~sGH7Wv15vf7CgdU3Yngn$K=_*pBO7DcwYhodZ zbU|7OMTqng0wlDMykYL0x$htFet-C`b@qG08_ntOtpgo ztBjLh-0m9PWnx04uu`9%V>%N!`tbf;^UyOJGb~ZIZecQ1H#gUau}U7iKM~VKh(RQM z;xs$^FU|)K?!A?1yS5n09>%q}={9YxY#i5?TXvjfOzN*A zDi;WB(GnjkTfaaGY~tJJ`lB`<0Hcr*m)C-S4jOAV)mBw_P$wjUf`l)fXa4)CNwc-Q zL0T)46HS+g(GtsAj!p0ZWn34*UPAO#PerZY{=SjPu%MkjJ^Nv@?|gT7JSSVKemh(+$%3cZ4dXlV{o(@`_(%)kP z{xgH9+=*Fp9@chNzwH-J0xb@5gKC{xQ`s#-UjY_H+9_5h+=82_{`aBqDNhkZ zyA?pqd@5uP;#O@8@aZi1Z4rP|5J>+u_RQad8ybt@({9^Cw8waAt95~(-Gh8XS|&@k zDY-ouYs>H>pyqd3rcr^B!{5h=1H~7={0Iqk8y?JlR+H+*Frk8il0rAZJbf@V=C>O zW1%ypz**AI(0_*5Uh~JME)GCzbr-AeDLRo#fm-d2{}s~Cc8&VL-6*S3ZU|XXO4qiQ*Ia{Zvi- zbv!QmrTg#COrGzec>g}Rlk^+sKVn^Oa@OhZFPIed|2v5fy8n9h58p;b|M!x=sPsQ_ z{YO~-Ctd#`#Q*lme+cpaomw#$V`|>U^9+5Jp81bP`dEd%g=|vlsAi2as`#JE`-=nr zPnEa$x&LtWqk_eN^rF5c~2z;AKpJU&)zW%oi$_^x^0G&E*lMy8UcYR}>WA-K16-hgKxByYEwH*VY zr3&LuA57mho{b#f9iaKlwFTN~%qjD-q3<=067yC*-?ysfT-=x&I+2*b26Qp|O?SQ@ zo*Cg}f{XlProZ30@RE%!1-<>%DzVJbszmM0xvHu@lx0z{SESaMXEFUkP;|KrcOe47 zHDJVs+b`K`thft4A4^yg0Xy9|R-YZObNhp}!o|gU;+vIfEzC7cK&wN7J#XyITs+NWhkDvl4u*siQKc>z#RxWXVUP zqLBSec1xI-822uv&`hMJy$hk0w|eze0m(lHIk6EL@n?6G_08YUZ{MyQ+kXCUe`nIg zw4|D>`7%tRN8w*4kw)eh@K6uy(%RQ&1JIqHr;cVn@$v~JT@{oX{w7N{o4yVr$SEjX zA*A`w=0B@kdNU$mSy?-7?gSzKogqo^mYn)-_46F;pzpzM39 zw0BGunrA^TTMyL;fBo`w;Jc`RQbkh%mjsLMLbC{Xp;)Kk><3E*t<9vPzX(0K%}1o6 z3_!Rke+5OOl$3ifwbLejq1v`Pv8*7U32;M>{9kjb0~f29T!bLbUVy>j~WepTJ6 z{+y#P{c&Paie3nCUC7sL60iKO2ByOCbPOGMbTWosu~M~!Z>Fd=OqDf42cp)!>r3hlLeUWV8Wc`)h)VHk- zKwDb}FA!nSNB`DB8;PSAo(;6qc=@g-ISEOdDtfsUPt=%KCd_XH*>*Mm*3iCh%j?P5 zb+gakh;o-L6J?sjj6Su|KD}9S&u+f}&epfs@v*DF32Tb~F51|vD8)u5ZLj5J;->^= z2H3urXbF6jW^7?Y)|X$#M4P!AUcx)8A`N+%bEB;$07hrQf6d}!m8lRrzfh9tL+o3- z0xQWS?gq=6%>-3>{I1fw4*a*TR^}3E<=Gh2m-c(tIEI>96pa#a%`8B2_t89}^WH>Y z#@Z+D1_05zS!vK-BgER`Df1-vFWpfdPZwIZ(}dki2=t9f4P9*2- zJnK6|=?F!EI@4@iR1Z~%#U+zc^Px6(EUB-3SGGl^gjz@{U!!l0f>| zj^q`RVYwq^!oyN7)}@`M;(iwrdDz0E6A?fuaEv^j(k=ykA53?O+)-a&kU=&pxdw4L z1hG{Ao1%MiYDz-u5wV3<)!A1zX`U94icA&~AOBiWQ^~c=aB!?HD2(W+fVzO+dv_s7 z?)3@Y=o;dSYsk4fH4Nk9sb6B7gfYFwp6QSe`F)ac;{+kGQvl;jK< za->0WNE?>zj3x~4>Djzzo^TpG&&NO}M zxcbjI#f2e4^}%O)GTgK0e?wXU7*^K;OH_(Daj`s+npM=VL&{w#m6-KP_u4i%OcL z@?bkXt)xl`P@uEQ+IkDh+uhFEBe-pONf^uAGQ(FAvp1Q09AX6ozAAxeBLYOkm`a8J z`x@tG&u(C_W_NV&cp2-%gD>!s$tk~>xh;Nt#ua=!=JjXCBqK7Z47C$LvLQlkY!4S@ zx~HQXT8>GZcJfY#^2S*BN%uKY6;Hl!Up_>sqgEnV7eXP1oNT)%{wi=*@!sEoq$AIR zneC%k-6AVlHg#^(h)?rxhBZE5^OUV??+<;odKbI{huOHfO-JCpNnauRKLN1)b^7I^ zBEsdgPtDq924=P)tID8AFG=0lSVAl?@|Lx_wUb*hrA`|+S492-I`f!S;Qe2WVPe`i zTLii+#lCLNk^Ngmm0O!>62q)G4z2SG_DT4RZ93nnuV}kgdx)XmFR10j=1`78H)s(H zv)R6JwObkRvzBw;I8QV+{m>b7^(Et12CyNQ50{`*p|NLKEo()bfPG z5t<%P87$2>fi#B%VK$YgIVCcr;~c0bfH;oGpD^r5S$=$kPrI0!YanK@ypLv3pd9sI zi@bfoLVS!@P;B{(Na>@>StWaEw+#I{H@3wZ&5$i3OwUbk+V4vc!iU_evN*j<__K&T z4#J_Oz*sU6wD{H{ENY`fL17%5=5QEEJ0ZE{ahK)OOR2{asGSmbAMaPWZ)j3w%jTDq ze({X@XO$8PTK37Lc*EJ1ZNsDnEoxfg^9Tt8X1x)eYGr+)-VG57!-dChU%C ztPp`N3P{6`UR8LD44NH~p%kX9Ep zZ%#Py>*a;|Z-i{$ML3fkbNuGNQVQ+Fz zM*5_oV=sZu&Bm7Gw-)BxD})!;1RoPNTI>bbl8i_-bgH4ksBbGFxjegEx9kP)W7dE2 zHueSnZNC|Eh_RUWz=U6DMwrq(iH_&19!u&r-4@D#tfC5=x-=gF=%0C4HWNIX_y~kc zJYDpca#~7P0G&G{D;u5z@Huk>Jw1)XZy>)RBS3uWU>Im4Vw)zXAp6?x2!zt6BwaUF z5!>ooEDilh!xlExKw_I~d?e=1bXx!8o^_rTYcQ=b?|xAF#{Vd{oZ6_z9bEXr-oVC2 zBXFXN^i?wiojtpYR9Sl6RTN;U*FP6=S0ViZ$xJvB7C4xBHG?|g272bGqE9BvXs8^Y z1P{0;5TToWhbe$#yl|4LQFLa z7p=JX>u3}s8-6EAUzn0cKDAbk`&CO%f4gq-y{DUaAN@|VtiWn={;|Ct5NsRLqr6Lu zJz`l%h3!``nBxOd|N6dRSr?mlmDCSP!MqHaz8WhOVY^ptmpx}Gndao|E?RrEmQiRv z$JsLT%x?OGzz_0@^m2;e;{!Q|ie8AmXM4N}$I3wW_II%6y^EXE?VduM5(lwtqfDn- zHMmN@szoxOPdmM^T$#Y^lQYGmhThTP$9X-nC90(zlO`WSt;enT5p;M39U z?SC~DFGpHw7X|hn2H|%PG(Es@i?_9xOHa+$KZH%`cli?nJST1mvi0?jn`e-ca> ziY@=Jk3rO`N3MN4b1;NeZ#wW*nQ)k>4~nGc*I!YFZXW|TgO5j^`D#-p3;oPKoIee# z>TF7)Mt8U+7MKD1eJ0WwMc0l$;-#IZ&D^LkUy7 z9WHb0a&w9Q%)u$Z;&fDww3|k?fgWp4i#{RF+NArcAi5IJrh9ES9usdE6T8?vm z%UynbDZXFRhq;TGt3Zc4i`g@@jesF+4FIXdm(>9Zbl(h)?Rcxrt|co3+n8@Xu2nBY zB2{(I1F;iG(&oX;9PMkcCiLWJ_Fw{bY$$27W2>YxoTb@f;dk&9CB%k{`WiU;9nYgY z`%F7AtZ(B9sDmO;cQDd#UX6)VG` z+S~wY1rN5X5*WSPHBQbW>0W!E9`h&_@*aUafB5%CqG!U0*7)0y>sP3&O?bHQ)BbOs~uy*7z8C`@G&rCzd0;a0=RbCUg9zQEsGG4lI+$0ko z=^JdPZ4h9)oeJRATp!1HPdM=8%Bz7QxEfIR?TkFPn40b~+_b2fwRy(&#rt+Z{;0D6 z@(>7wFc-x5czWm>schj$csg?qu7)mhg*KnY`*Y;4OCJ-5(05=CwtF-V-3Ygc#FS(oG7#_?@caj*P!({ik*3AP^mhE0!(o7Y|&e&LCbYl5#kw0Z#9U^Kh{80ke zv9Xu|Q${(=aHWjp1TC#1h87@B&z_0SH5WW5`{81|Bg8Z{yZi8}`R|?bggvrl(7!#>nSJRa!EC%kPmW$(cZiLC(O|$7JjK}gwovrg@TkTmNj?GI z#FP_~9>I)$M0av@{2eYHycq;|Wl;shZaisl5+BIH2`=aF#~kb%!<43`tcyy#b#46? z$4Ge8Avo-Vw~bHx;dtoM>MbYQ=dSu?_22Ap{l|NPqMT4KoP~>n?zztOiT=~P|vt>*iAbn$B_o441?lMA7HW;}X;~%*RRLWc^jaWv>QKAY=H=)FZYHm6xTnx)&ocoqd+HJV z#9zSgpa2m}UUT>Gh;z$>-mGn3o66KrEvRmBh%_0Um{|6fRaa9djNACaa7w9jZPZ>W zc-Gz@Vw?k=67&1|iN(89Gy0<`XJrdqhPDQ$YYnO_8~N?5&++<(_o)*Gd?CP`LyR(( zbSs}5a6)rO;5gdAPc&PwTv%T*Tl8_2`iG3T6(vlfMvz%}t*P zzSl`)M!g2KIQ!dt-|d{o2)nhMI8ky2lN;tjO=to#p?&HLqZ_LE{+KWlmcA^rvu)K* z95h!vb)GV785zm2vC7K&bu;k+<&so7Ki{bS`wJBd<1dnzc=58qty0lWVlgZ%pvC${ zIVH8DxAS#W8ZLC^Vj;WskH-2b1)ulU=3M5>v7BEa3|~ zeel^OLU_dSLcBz6a_tuGkW%(MvBf@c9X0T%Azj9XuWF(i zw?Q_@08mo_2@OM9%1CM&y~o$sqW}C6%MO;!WoS3iuLU@$2obT`!fUSRh zT8VyqedOSQHbiOe+V4swmAGeSV*GvL+!mbK;t3trMVXH{A@50(IE({^wmLiQp6UVJs0XDsPOJ4U za${5V-kS8zfI<{phTc{J(X|a;v@ntOg}h$C2+wd0X^`(LC@U{|3%U*EUgpV(cm%~e zv!a(XB$MO^)oxiCL=X817^5nb_{#A;9J3PEsMSfPli+Q)guKU1{l&7~m?csn)te*9_#Lzno*gK2=ZZ z=~A-+g?0+Sn;o=0lW@bMAG&Tl4(P`)egFKBMM*EjO)f@npITPh8)E_8)Po@W7p)W& z4FxJG92OvA7vDlTB>QZXV94=i2>N7yhQ}^fFf0Pi%%P_8NQ2Z$ZSJ}vm1t(HE;3&8 zB$X{o5k0b;(d`1X75x559lB66{O#N8miJy@@IXEml;Ybp@UsOKpu=TQqVaoL9$~dC zS0G}uOBCf@e|8c-OEWu7dANInQ%VfdyBW?5AE0$>t(CB}PQXq$q;~1HU^-?=ml1)h z^%~#CM#ly74~}8|%x*zuvhhuG&;$Yn?-P#|LIMWW*2xGbbT9Qt1V>ycLhZf@rM@{P zeC)Sa+%6mte`s?-O>`$7_gja|2t)-ykjMF<_*GPb+1oRU`tLz(W)? zNY@ZHlG?=W5aUq?RVcQ<%*x2Ll`vb0laQ&?kin+N`h}q3V#Oct)V+e;6u4N!IxxUg z1W8}ucGY$LM_%VV6GIdot*tc5v>s_zs~(*fkJw90-aC{$&fx?f_mY4PHvCfF(UB*s zx4$V1Een3c(q6SangVh zX~5SZ3kc8hyd?lL6bKr9GdwYu--6j7ZpKN}=KKoxwjP$S_nq12wx?N6~F@#1IGcUgouBQ~2v+dJBwG>!#trXdO@8K@zr+0M4Z0R5-% zo(w`ktWX?zePS_fbR^(G+AwXwn69Fxrnd9Z>PY>y(C(Pbi5o4FL-XhB)*l1JSA(^K z4Ei#ai+EI5wY6QtIJgvKTEDTUU@)daBcQe7r(ZBm*VyyFrvWcTH1YN~2SN2x*Ph-n z5+?Q)s|#zIkmOF zAl!*Mq?m@mX|E>?e~1)Kr|xSUG(#c2$hr4&Me922Mi!&&>Ki{=m$JaS#P3?3i))H< zW4c&Oq0f$CTf5W6ix<6ukAFfN-`+ysgo>u=BZ4>Ety-Xv{*@_en^5q|C(;i{%i^bw zz=zDGsWmF6v9hO*je+(a$HirWwwcK_T2m$8WjFcY#jvJMEuy5zT;S@Cv%S* z$1|MNCdLfX2KiXgD#5e;OyxSRPEj6&0c;DVM^6FUKD#4S@kjbn{dZ4fILMc}ytob; zoQ`Au&h^NqAy{J&P$Bh5vsjgB(ocED-$sMBQZzg}egy(K@mraXML)bH(1L?droc}- zX?x!bTgq;6u+}(qC`qIIJ@JN**eC6%mw6T~25#aFXZ^4q*6?T=XHD8c;!U%O_~7buK=$Cf(Yav!|DEA{kgLeS?l+fo^3GJ}1m_yLHZgR#{BkUDzas`%=`%dIr0!pMHLB z6qF7_9PTc4$3nMG5YWX2x!bq-@xXIB>LZe7w}%OH{YbDgCMO1CYN*R#^imB=3M2(pEGgVJXhF@UF{M&+eL& zvsyBXE5E9&nvmX5_NCn^u4E?9i~|K(DZ}P>k@$z+r16Q6ZV|vnQrPYqC`91aqnIy3 zP^iM}&aELGk>i4@lGiP1hSB!paG{hrTE%<6g}axfkX*3ylX4YZKT0wjuI9?SEdqvC zw&HDkQS#6eWdPsHm)iWqiOEr3@ObqA-mvscPVH5@FdJ;dRtF$i^QfG$w;SJCKSXZU z@N|@UyAcN7&M#A;jDB1jUbz1>4^z!6D`^wZ23%Sdvu)Gs#l zdA*xN@~193&>@kOfczAcFWqhIxqT4CPtT;?v=ND0wh0{r8#me%8#~)#L0z^lQVOGoEv%^#d8D!0eXtCpW~==C@v<@Vm($&C z$~U#T<`tI)K5ELEW;ShV-nv?2GM!5>7zaPU{bb_Kw{H+pIn1PuHEI1`$1oXJp`2j z8Uj32Qb1T!-TPd$hCq6)LsO}Mp5O=<>(+5k<=0NquqG$jE4TM)Nvdi|uU^J4bhjbcd5Zvs3ER5@poti-RHnt$~-H zKOs4p(t1KsU+5{zi(}@Pl#%YBb{x{KMWc@<_PdW^M>E4?V@){!n=B{LqZ8qHIAoD^ zSkVo>lBXzdDdQlNiwl?q6T3jP=`g=L&7I12+c5`w^y8t{Ef(cdyBX}gY(NI(m1{M- z#aP{xcwREGAWYkWFU%035foc3;r-bN2MzL1v*GUz(v@lL?w%fTZ2`+mHS)d(pU8e| z9&Q-L30rQg;I7)sVg?8979QsEfzsiGRnF9Ft1Nzje%h9RQpfdrfpi)cLyO^+M zF2`^pe%aI3J~O~Cx?=9IWtan75=?` zh<%QBa&Fk%A@kSjb4++}z1@!q)ZwPPyAPhX(HapMwzpqvy#Go+ERrCSWO2+4RJ$uWM7ry?t{&D&yyTZC#B4FZ3Fq(t#u4ebGGk zZEPYE**Sb8vc(ZkM61oGDfX4-l_{EHC$+b1z=y+~ibJ}Unp%CsXtF^LG)7ZhsrC9&qXHV2u)M_GeIA1WRVKQ^d}I0+RebKY zJmcNYDtN&wTT?7~Hwz3{{ZM*vM1R8Td$JN>l4&MflN1!@2zD!GCildDh8+RG1W>-+ z)qz~}%2&Mpqv>X(%<)Zi_O3k{9yD^7PsviA+3~pK^|z75KEf0j#%VVrzq@^Et8zkS z+vGw9b9e;>!-hd?>+3~u6o3&mW9788gf=(5{fKrg*qWBvOJ(^`f5Wt%k0Tv(L4LLn zw2MiGHw*$Yqi$n_v7cB3c@5Gf|| zW7-ToiB8ObL96x!1qAx-zhtPTHnv|AQ6gEFEqeDcp36zH;I*sWT~WfMI@~|_&pc90erc7t69_ktP`YybF(IC7!P$kKpNWK4|8dNAvEew>veu8qk=rD?b;--Y)0JKqfjuP&#^8T91 z?PH{7hoi@hjg4MGHY~B{iynLSS2;H|HFxt5s>-9Fx6Ktj$XSYhyHYJWV0<&)U@%Kj z!Uo&@BuLOM_tcHQB7BlN_bNF#d0=b*Yf%c89=Vx4oHnzZR#|#y>ZjNn+nN+euPqb!KrLZQ9aLkPMD+GR`_MnqSsa}FM3AdO3t{dntjg_Qx&4MLn=AzJ9mi( zP3_Ev=c#3Rm)tWFxhIn|$DQkqKKRJ+g@Oo+nTP_Bp9VcPw%(b2k7D1sKyeE zSPhZcQuofmW`^^uO#@P(#49X=&fwelo6GaEcC}2w)6TMMrzMvG!I{JT6NY;^%Rdx` zduFwhuADCnv^$)kYIZh6xY`Dh_jfo=F4!BDn6Iu?2MoUQjXbeIIL;TIdd!kG$Uit7 zO5)wv$VsGBVyg;@IR3(Gz-YikNO<7cf8lkROuMnr#X)jM2eb1}clC!Sq~B;KR9n3I#ok z72?qo4C+>r$juGzeMrI$QNdb|$7W^(VT@WP4|u#bsikw3>h9?ie=9FsjYggW@K4x5 zbo%WYi3UE{o?C*#dV&Fg8qeq_a&EJoUMMAH==K+W!a?d#@)r*E8|PzV7LXBJJr4T; z4HLVw$3mfKW@kMCsJnSD;V^)ae73ZOE{^&2UR~BPQ<@AU+06ZhI($QyK{*^99nxt| z6t&`I)$2CSPRl1Po^XZQL?=>Y( z8E!9O+FD*vz{>zx?LJH@Ouvb*d295u*sry@xpc+L_x;FaS~lPME-T#Dtb$i>U^AY% z9Z%>9f*`74qDOjt71P>6ToTTxmCX~kw+rE`#D}UK=Ji%hoZDzp7!}P}4 zP$M(tOPS3|KU;SmPqe|oad^5PQN)CgWw8s#x%sXjOWj$`~#H1o*y z*e*G6uo(QFC61GwUl5MjDFONIypi%-zO_Hg%Pzs_w%K^M7IWp#Ipp5X$`h29ub_&o z3Kxq&;GLY%&j@@|B06C|`Ma^E7+fuVlzW{3LsF6kCN5uQzH@oK$_HZ8yfY&a^eJo^ z17#85I$>r$)bgk&EZiB&YCZ7w?a{WvtKoMc#FHMC)y4*=o(AKeaDxlJCv(P;w+A)p zHGbp#vjPC}_ftNfs0OiH?Q zMUXTY(gk2dQd)v+Ntfr_m-E9F9s$4R)5N}L%b!v@zz9iU#q2V*wR3K`6$fcAxc$P(sXtnIsxfdLh-2j(rDRE|S#E7%XB zlAU2Zy2@>diDmdcITg#nB@T(<*G2~e-r)G6rT$m)BlTAX6IE*%X7W^f16vB-PM+E2 zUnn)+5G2CPfYnb_h1QSy5H3&OHJ7%B`Y*aiZWvS*!EQKhCJ$1pOFkG4SHTrh}$54V0@qYmiMaA zV8q=y$i5xIe{7A4^uW*3XG5^cC~}j^_6KIr;!er%+(gFaOV%*(3fJ{2*STu@W(5F& z7y_enEJOh9-U60;14X}ZFa)PAR`H#_Tv>;vYhcsT=1r{fFX;Pd`^PngzkC^)w9<yF6|v)XScpxdqI>yy*&g*jwku!n*ob`{!>xv8RBTUP zrqC=qiDk3g3`cf&4!~Q+h%a3G+O}UP>*8Tlpn|gEq7)m4>cNLe3&z0ETaVNj*-met z!F`$Q*NfyBj`L&$Q3oGsSSe{Koxwn>Y#q?XGP=`%5E&%wB#G3WYK#e0s%v`osZ(X0 zT!uyi{7h(_bABWpKSsLR`K9du0|?7^EAJUKA^rUeD`M+G4e98+agu=yN?a&%dp^=; zsXy=Pa!!D24s@;O@FZDfcQ47R=889zBT z_68`5_dhlZT`w^OMv8!`kX3fM579^<7^r4V3SK;O3mMm6@!>7Fe?;h4qaLcZnob z=DB-G-46l!H7Gp^;y*I0wazv&1g>B5h4_IjQ4EYup&o2!s}W!j zSC?rB1Lr&>s-p*%8^OjmX{PaRN@1_wYQ~_bz0)d@L<|*i7lxfn%Ui@QY*L-QN z9me??*k(VhWWLU50wE=%{Ge&E*?W_QpBl_nmG`&lW30fYZ+d)p^3Af_M|Q?C9G6Q9 z(qdvvp%u-_dSRWvF(P7I$=m$m4C}|G1~rDyicLxsn$1PWT|mI-@*xb|w8IEw$W*t^ z00P4G#YB~!NNtc~Te@>$C>5;`Z)55l9G;*jEIhdNI0;n0PG6akCoH7m8SuE9FP~}w z882iU->tHG9~4xWR$R+sX8MuFa0Ts~Mq-P&C|O?WhXKpMm{Xr{4p{=Q=3^y|U$!YS zuf|XM*ZX4oxgWQkB;de9>0n?ayq*5Z@T?ZG`@2&(^UHl$fVZ%-hi-UM!98?>D3gHziJi( zE%X`GyOdbuQ$l`LF>PrroPt~QgvWdHUrFnA}1(Tkwx zaM}Qr5n|Lc;CUZm^(vW>w)gP(mGO9#9pl#ypn=CA(pR&-KiJ#@gA}mm+yuC5q>>oD zg5RA^o8RrV&&TS42HKGt0+ty`7mDS*97rXPc^3UeEaAiyByFB?57XpW-jn*9LgA4f z46}P5Y@l6jj~5P^rkgWu@0g#q97avN8hzx?=<5(#n;P5v-Rwbi?c-N@$a!}!NgeI3 zYkwhwCstSa4R!j97`33W?yT&!B4z&AZ%)7cSUl|({rIlj{A`q_m*X=9OkbLv5PP_0 zo2-&$9GbNqG~by_T|O4F6Qa|`7^WYlVjuGL#X-}4-W4(#-8tUB+ZVC$z68Ac?H)Kl zTOjNk^_^^{mG6BOI;-PEIi8tVeu&@a`m_21jsbxPHmbJzo zuhP1hulvzKeTC+tKR1;*0MG0L8@@ zF62>v5AtlC+omF1eQ9VdUcc!#Hu7aZ7SLD5dQKsRn{UtSp!~=X()13Q(J1)Zp=EQG z?rA@JY_}}T&AKf3#v$OuI|cjos}z21xoH=x?77tW@LC_n-@R z)y_Qav>n9iva37#bZ)Zgbysg>UXIc%W1xv;jdQv?Yc79bn#?(U9ACpI0k*4W?}|HI z$K4OV;4|i3lWA2VO@D1K+WCyIo`RPViU<#qd@`2G@{y7U01>>6aQC4hZU&T0r@&@K zOnT1?3ExoBS`@j*>n!SZe!(#-AgrF_gsJlZL;U+Uycl2(jMbdF03EU_NU9=gO2tRl z0Fj2NksfsUOPu^e``S!Q&rWJmuoG}3KsfgSHuDQEWYNBuvF!Z3p zf)wBj&|;tQ^))*to~5{J zP)2vC14b@UpK9CbuAAdWg~~+G_4%$2&}6vQoaUzo zsc&W6Sk4<}^Bb5rh%TksXRRe+^DZejPHDJv@hW?XEpirj18@G z5$a-Nr>^YPi^1O%YCa55xce`rx|8tZSb{{xOd8PGc)rdlm`ZsblJ_qE_m&Ffyl}@z zdit$}w_sulqe8JFED$GSAMQrQ4CTE6!e{m-ea%v_u6QkE#JrVHAE^6bn!Zaz{N`bh z9L{|c-Si|g$c6{FPxKY0?;E+1iN}hGweGT+937f58S@afl z==I(Q2oFiw@C>;#7J&CsJ^7&o5gZ7D+mv5jdGdFZ}_7I{hm}onp&K2UE0)d7Z zUrb5TG1G?hZ}}u$Hv{*byIdos%>S()NvC-mcdsd)m<*oxGuCwV+en>}@g@p#NXC77 zg9z<=5{qUCgm=!Kj9XC^hX(QwAs9nx^_>4he9==uDc$K&h>g9J)VWK|)6z_o#66_C zmVKNlB}Ox=%K^=0@aY1&m5e1^l+DDsmm( z{;eg^wk3 zE@6e5W#*U1~G>I(aAF-Be>r+|C6~MH0W=8biz_t z)gU4i5gasq@?%70>y^%7>q9lDA>lj&Mb!V*2e4estQ3P8$roE8cX>fHalE=}t{TpB zs(yO~tV*@l1+UiS$)%z4T<+|wICGx=D@s$ws7}$1 zIQIp2!}{oG^lg)$4rP`)zR?w?SKRV{xO?w^HrxLVSlheYg&g(pn z_whd7$NNtT$ycfo+IKSXdCVrKQ!n|k^5%x^v@0-Li;katADT8=*YudHm3D?_>huOWNR_XL3E;i%Tg#8PhX)D!QQs z#NKl>RkGKbc1T*YrI!{P)m^?zU1Lx_sy;jIPjfNE6n6*VyV>^O^8I`E5UV4iXR4<* zr#cXf<>s1q)Nz>rx-nJQtI{!!ax*j#O6E3?rZEbIh_BIPXEolaBAyv>UR4e%cL@>j z{qq5ouEAB*7w$rMM@P;a&)J=*ZkGVIoS`RHQcpH&7S`tS1D2d`&U!Bw#KGdqrQ zw|8BBuFx!vl^95T3onzowL2lT)b9{3k?#7zXV0*zJ&h62$F|aFrtg~O44}^FJ)ae zKhIAxLe{nHiJt$)@6Pj$FaDE(?bSaEIjcM|L(k^FMl3D^6c@08ZBPc%C&hYKoFdyD zn%zV`$MrL$$YzVnr+D_D9`dQ|nI;g@!Kl1dA&v=e2BM!+Zy(PFTX{$Bl|upFgAYDi z<#pRfV5b+9=*e$TYdmKEQ*kvf+f}u9*yX6M{rC8#=? zu~XYnaUrzW#tx`8A8Jfg9iR5WR$ORKmxxv^6*L*(aF*ZiJXV>#J#qRwnM;-x`v|CI zlbiUpwUBeA(eErHg&a~IL{XTsTkZ^4?0A0E+wVuRvH!}9J|ds|YW}@!RpcR6ju`ps z&{RHF-|zJxW&DUxReqwCmAFyIx;>yb3Ca1OpRBGjQz>lhVJ&xUB2KWvsl^XS|1G^M z!6J?@>zF~7JwqrHb|$0ogE_%#Y3L`x7)R6GhNVuQOhEi2sXYs@^X3H#&Q=g^~OaS;118mGc%Jbxv{G4T%}`$2hD9A9eSV| zlcuhud}ZAot9|$#E6=J0W{8@+({#O{1#2pMANA|u`2+6hI^o4$o3J%!m`sF}*K>C0 zz)kCmW2!%ty#8#(!RWqJrMY=cMPxafJ+OjSEqd0Fe=eC%bf^2MgJiLc4Qs8Ev;H+O zct}~<*1_mLDN?}M=Yey@pA7(v%rTfIGNxWEUCM!>M`jlf?%!-PW)u#Aa~iM$fyV*X znaIg?66m(wRh$sRd1gA@D7#wTRRNuNNi0y@WRi#dT-y%8%xCdJ=6lv`>^pg`)66P* z%5f~E`3@}7b?ph*HdGP!eb!S{85)m%jR7>6mfRK3w-~GvuG8v=L>sfb*F#fMNyFyO zxZfrTa0&@8LF`X?@uXNN$G8S9@*!gRiDJ3BMg=F&K~W|sVW-qo zu%@J-<1c8HOg#8U-UmG?_`WPlV<~CGjc76FATy_5GPY$c)$q>YZ-?*r0FZak1$t^_Sub6hH2 z%Dqx~;|6^Ha!C)*+-U2#4R^d%%zHk`$`!fyhMEJb0nXBQM|bI8gF*WH@f8f%ecl)b zyq^g>eEhr2t$3b|_(G#5+(HflOnduG6Erhg{_YIg{p6^%dBBGA0jZZ~eyDMo=qx1_ zA!qUa*JB;ugG0~MTYkG|jTy3y(KO)@=fqMKO`TBfyZ3Y^RgMT5FuaS)P*8l&+uBnE z3kTa;c~>nEDE6RJa&?oI(!JGzFa~<*g*j{it<8BUc)JM>lX&I)=(VVB7umXj_Q8zlUVl zLGeXr3DZy|-%o^`8=6N0(%`R6r;%&fRnC&}#p_)GPB9(m6o=;~m-i1^SlWWi=f+g= z<`>u1Rvt`$`f#h6;o3CK`K-V|Y^mJtq-1guW}}POu+%z=9qsQSZ{uoL%kPhAw^_P2 z?*=+{@pM=G9B{`t*li|Ww|9OzR|;r|Vzc+Vuou;sjtgeTe-DIJJ*oVoWZmE-$R3~b zc3+GtCBDoDYAW|UL%}j;=)vjb68VQFe#gEpy;*eI{NnfCQ)%77b7`aF>WP2qge0Y= z<#R!j^3^9HUI6MmG{)dupBQJLhbo6y6VU#Bul8In}wmj2a!Uv1*f%PglOU%A*5gJSFvUYD(wf++0+iZksu_FchFgx1U64{}f33I|0M3^LX01B+1ZmB9Y-P+bW#~ zzth4kVYpEE1V)7utqXceGOoc0%QvRhpC|)SK9_)#w?pbDYrfXesmWL_d!*8PLnU+0 zzHs^pS4_zyYqLO{oWS&lwd}3&;U4n-y1VB1WowQ&$Sn2$Q5i$(GiIPCzU?kY?Sa;u zl<0j`-e^*YgEs0;Jb#TCMr?kqZmU0!T;ULwQYho-NvUb5d4huC9KQRAaeXCfdcUlx z-+2C;)W3MpmLbC@=hwvntLvgT+yPbE>XpAo5Kh$Za=EK+{ECqFc~}1wju?7*jJ$h^ z3fW#K(qIM?$vfLSn{iAtMzG(a9mGy&q*Edo%Os1Bss*#xX(tQUN~ zJ9v1mc@)*v#x3hvzIX@|`HHbrFRI;&WMN}X%J)y=xunJYMY#n2IOTUw6(z6(!eem1 z((0(e{cMC2=2M@%k#Ld9zX(9x$u|1&#z8?H<>o(suGcOmAPA_+lPB@vZ^gt%7m7OY z5~+{lM-phuroWTCob>G!O~L^%$fn3CCZFzQrRobebbIkA5q~YFe8tQ9Y_O0t*V8`dl|M_U$zQXqF2e!Fd^mfGnft(WC(zIlf`79J}F3IC4*g zZr=q+Z_!78Q#CUv7jbrU3;XeGr1Pi78F1tTF|7PE?O<_dYKQV-yW+#}M2TU{B7XRR zpIv2X_K-QFfsC{qg>I=ejZrYfy8I^~v-37bY^s~+@KnaK-{$DyU4s;pB25pvVgg&J zka|)r)lc!l$0I3`AzCs$Y4NRQbMR+6JvL$|}EDh>&a=MHQ`r;laNDA63( zshTm5Qiqdh{%E~Q9w}e%S{j|-VBtmAI`zz9i~C!2TYx;cr-w)3I0nX?a%nG#S%PqC zaU(3(%jp%^msrs=y5Pvm(!0_9!hmcIeF>1$h5GWhu!5T*mu_N10YN%>Ku;0i6)H0Y zJaT^!ZcwqYbM~RLGT_@6o+P)!FfQX#QU(!#*A_;qF{^;Bly{e2Oj?KE|@p0!8Qh1M&=0nL1xKg0P`ded;aM*58 z+18eggF_e@Ebmpv2}vt708&d11?Y;Wd-~%Xk@6K%(yt360jhw<#E;-RNYL&EOXlOd zpUj2g5a(2Uc5clLdpPx6^E&w~zUU5%@N&OEI3s;zd+pRu0NFDS4ok#x#pKTQJV^6@ zEYGI+W`i42#Px-S(=;i#JTzB{z4k1%4;8&Z%K=3C=<~XsCppu@Pn$shZUifg&;MDkLb%Fruq}*&!rJG?`4x z$9KnLDFEvXeQGNrTj{3PfRXn;Qr!MIz)?VoBQ-U8O2z|Y0q4@kl)bXFyZ8Y2 zR@kN-AM2X3h7j0chYVx8;=l4PQgunC1(w~V>V3o(`Q?>cUHQ-XC>f*rB30F*fZ?Rh zOE2!bP=KWm*wcLmbAH!4TrbrlkX_J-^0)0l6iPmpx-G-FTnNC=#MP6bR&$h189>cV z@$g+^xCCqf?nBKJyc|~p0EZ@>65JZ~Y*+#@J(Al&kR2o=ax>#(Zvy~q?&=dm16Qrt z>o&tkV}ql&$V)$#aP}Rq1zZ-wBj?Sjfnr<$T1Yp6Q9PKlsfzEh;jnWe(dTTjV|y>c zmUtvQxyzfjQBi9Dz1zt{=D1T-fb6kq7&B>&bcwBO2!P?YrMQPa@{JO4H(4fackawzzNdU1}W@F5;mlBnEH1k)_`Y-6%s+=T+O z4`oAr$}JJ72q_bkJ!lQBSG9y-rj!2pXOoX?U9}YB9Vy!&<;=L#*0ULE3d0s0o;nWw z>Gbkh4U=eN!j|-Bv`p}lxa3db&Ci!}<=`$wPD$YiX@}>8qvi7Y*>z(^p>Qz$*~2OC zy?0NXbrna(ShkslIpJ1NqwNzKb6oA|vYlfnu=dSyEO9Ucnqre51O?Eu(zEYLTfgUe z$XlP>hxCm-@}Jh#iF2hS#-fNOLe0Jnu3nd&;JmZm4UcPeb^=Brcvx9k% zwkE^^QgWvA)!@kZRji;Xmjv<9-FLe2W7Ig@7s4J-FLX(TWd-5vSrzyecN4G^veh@f zquQUl)N}_gVHB&njhIV5dOy{>S2QJ|^P&0>;B(*BNt68K{5B$F-QR&+^gCDp&Acu7RFa#`y!toyq;KAzv6sKm(GZSJwp$Ue$Wr7`Ow7&Rl}@mTau1ld zGndxV_8)UnFJ>G!v^@Y2ZqZ4GduUy0v_ng^f zRn=S{nfccH`?UTm_Ygut%sS$KjbHmWWn1MQ?_j4P4%nDQm31@7dic~w%Y)LAq{NK4 zQWlwboY@3ae|#ZG^4I;mqPWg1Uq83vwR$K%h?}~B9;I(ZL<;s$>x2Omv+bwj7I~QK zF*(7HT^X`7?^spbxD`bo=*shpwxC-nSqvPrx|Htv8!(KKkO98bNh-y2hdUkrl$Yyj z4`jcMzDoXfI1o4JtrrK!TU@%{wqC~9svBCizGbVv-?Az9bh=$h{JUBG#YE~M3vlvR zrMMp+iluyv8J1y=j*edo9s@hzxUe`ceiST@1YUB4$`6MszsaX|<`O57|0B-^PQ z!Tbv>Q)gHi-==Ev$0hR3@bi?_hH7LRqZ8s@cbrmTKhIylhCDlNJ@d7CQ89ASS}@XL zZEEDhq%}kM;yxFLK$DH(@NNM@gJPW1_sE&MDj_~99czQhU}s~?7(b=Q+GSi8on@&w znX+zj0ZK=VhljaL&qxhyociHN3Pq+6(rFTQwwb4P9{dA&V|+XV)9{uSt>eik4fd>UgorUsouaCytxl`vqD zUtZw~3Al-!SeJ90DD#$zngPhfusuw>4}zDyY+o1bq}=f>;D!0&mScHCqtoM$T^<<{%kJKzqC);_goo#*kL^#INDn{K$(Bhs;00|A%;A&QfNXsK@grQZ zgSz{$nF8G2+;`Cd@F`tUi!(B*7Nqw8fnsOJrr-uft{W#XwqSSA#;1vu(>5RVa`nVp z|7?&PLZ(k@D&~Ul8DFHiOl|`CIN~PIk%MarbCSPrq@ZBdJYKf z7bwP2cKjRohc)9H!P;{M>-bM`p7{c*@fIsb56uhw8rydu8O_k;B)vHjkX=*>0DESJ zOUSSt->2CXf}@3NVXk|7dYwzjJogo)?Ps?p@Gg5Mi482Cm81`W?Z&wPCb`*{HlESj zk3Vuap|7r;IXMQ%52lRM$v3`LTMpX-8_qn=;Gc76yQjl*($ju-NC*p9BjX#auqt)B z*ZkCkc_gsu8*)}`D_w5`7T)p+Db2u;sT$)$U;e<9s*%_t`w|$(p*c{m+=IDg+p}bW zedpjK!{`t7_~GDT=xTwHKWZ?Q;cfKm0XFQOMu+cZ*O{<_8?W)S$5AHfMdH1-|R7}eLf4D2tMv#*AuwBr?pV34FeT4wsyT+ zktA2Qu(We9%49&Y(nKr7*&vC#-;X%l#y=_j^_ZUK4dC-Uvn zlUtsviE^GE@J5o@=wP~_+t9oflE0#0n*yzpQ5xto1!-J^z`@=7`8*1&A4s^#v{bp+A+te=st0J^{`;q)luT60e&>w(CK zaBvSs^qJ$E09V_`XugzI%dxkcL|jPB3-a};l}D2s;_39?kyl7N{Tb9oT7f0uRhDCU z_?X$7Rou4UeO)u6s4cW|OT-Q{_#4H-MVp&te64wLnn`?BACR0PCE3Qy}1>vg- z`C`)X^H-{DqkVfqC@iB}zy5-=ijg+hrqp2$vybM)?l!G}E~qFL*u6JB8=#9TGFf?d z81y#lShlq6E>Z~z_G+Z+z@6diht zqmCbG*$b{>7W->9j=j8~7?v@O^Hi0??P6@ly`b0cA4tn7e?()}6H6^=I#NNhrveU^ z>*lTv_iYJ2Em&J)L+Ni>9=0v|M?RTKZZxJ&`Z^&qztNxr1gTSmH1HKki zJ~vcu>6t2N)COb=n1%*J21BP!H@thjDE%_>CS3|fpUO-84U=j(aRc_uQ$)S8V#r5} zRxy`qL>o+-`%MSoV7mLMYn6d-q_VQ0K~TUL<2(owZE(PQJ2Vm;nzl8yf@}|kSsps& zPoLiLpjD`HvOfbd2kT!18SPE&9=(Up-wD8+ z#uu1jjEc^Hi0KdOuImM3Own&k0KZ6IS-(W}8269$#dCSp`ElOP!^}?ml35j5_y5FY zqvs;B>^s#L)}3(qZaM#Vx;+VW8Tqn6{Yx#?I0A(W*=POVNBED4R3MvU0ax?yjoM$8 za<5a>8B`jCzc0I*yN>uzyZO7a@CKDuPFYrLuw;8r%-tvAZT`kt$-(L=tQ?E~`xvUk zr7?8F0WsVQ)&eSPht`{#kX%9s;;kHMur*4hWe~0F>kft zIm04Gc@_Zu1|YG^ZSUv?s-A8+OYh&}t-7T38u&RPfSBhH!hFr&*mhPhS171rKXG)? zduG_U&>;L48y6sD3=OvR-&=h!skp&svH2=j^GV&vC>6djY_WCyHiyPT?~9|9(-W|uAJ@me9NPDRQZtXul+Gv6y~@}}5dL59nw_Gi49-Q4;R)GU zQ#1X=nnk+rZMkgfSy&aVQ&ig>wgI3c1Omcx+9x+#6i0KQ`z5ne*L`68~g91E)BcoN{xJ7sTN{y zCZT+#%#7tW2RM!keV2_|#kxTIY*~-HN^_UaZps8c5>E~Ch8PIQ>sW??s z#uBC?S=g}GFPQ^>7Fu&ww?-@qGd!`s-Xa`#G{JAJ-f^oXaE5#MHsjMPac zd*xXF?ai9%m#<(EqqqmVYZalfc!DhQO66q*PS^?OE0O5!;f4qQ`!2J8Z!U)}yvQ54 z%+iV^@*SqgT2hp&>OrO&krJeB!UlOB52;EY(>G;H$yR@cLV;H!miVC^bUGmSxK0=M zE?Il`KY#v#3jUe!PpSx{kjq{wnMCGU0yTsUE#VRFvC&aHA z4pISRm~09=@6Cj|sR*YN-!2Nw$&#b$)tcokE7@L@7to-MKyh-ajU~PYr$9w2dxtz@ zQkEMf%qBNSjY;lJ zhiYZ?PaDLupL?bpR?q!)!bv<0xUMt8<35+YCT6d%lI5?c7|}h@W-6fGi6DGV^f5^OEPMufM3MXp^jQK0 zeg;X`%qO+UDg3trkMjd-gU6MQt#80M$K=i*mgk;NBM);dLOX98Rjm|y!jJy4Cb8sR zK2r+PsY~`dJFE(I`0|Kje06wYT7-+q+k19JiEj)oWPE}->Ix$X4X-~9-(CR4(ZLUk z>{L^kN44aIaWFsqY?}0EwCEfkZ-kZ3 z@aGkXj4}I}qU(rz)Q*me>>~zk%Vb`hXq~Q)8JzH;pSW6JN;n{J; zqWb7j^hitzZaEDPw^f&DdTm;-2l#r<_icB(cpge~jje1|5j)Ptl`kos_|#KVnMp*; zt;egeC?6Uc*Tl94&tN?y9A)UpQYno>28$L?9zSIyX@aX+#y_3J;1SN6At`Y#lrL)JR> zi#3qaSRCEG?zL;~`<}lLa`oLV8NqP^*_5da$Vx*Be7lAfF!6m>yP5}^POmF{4Q^;u z5xuF6;$e-E_^qLlmq|Z%L?qL=sS3m?Id0w~z~DE+c^v)}zI(0^SMq8e3hq=QxK+07 zkqXTjNytyf&xR@00`uxYz;bUE382|#A*P+9ky|0Pj?@e+6&KbYW9|hWMNV&mYBin% zmSCb8F zP`BPILP z_{1?jZ3aZ$B+;Nj^R;te+L!R^?S9C?3=zFMNGPRs^GU7{LF$<+d7hwB8q545hzWr8 zy+K{Qk7uLk&0xoYes^&IolfRixV?Iu_w<&>330gc2kLo1(Bg8lx$;N}7$fpRPFiia zZ&>?pha>X4z0^Jx$KuGOg=8u??A6N*Y{BRpmk>@C+dDkAw}#}(kk!ljR;8LS0X%)n zW#$1>Q&g-z8cFLA!C7CArhq*Ap5~7rj(-VBEl%ckugpXA^;OP* z?XAguA7W1*6&p&|;5|F388Y9rcd_X9$q<#bZ4zX2{X;N~Ebh_>C zeNElk%Mn~5u=UgP4qwlXVvC0?eNR`h!#jkW1*z@);@aN_AG`Z^P*aE!3FfBNwK*k3 z)U)xMIZd%?2`giFj}n%bTa+qo-WHvbTi?ri>x z2w;{#X4kQrTAqRuu5U!1e5+LNXAyf8z=HB!%X*Al8p4B!6bv0NezL|IBjx@QNs0YY z#NYv-E_#=J+DwMU1yvS_*`zs5Uf*;gyPUIvQ!dhjmH|N`8%wh1;$d`Gq6%>)$P1{vEzkW7;e0 z5`yE_F=TT-bVE3SpO$d7t)Hm`j&r$-&V(9dDwq2iQm0LG_GP)m)Aj3AaO-Uw{xPp zyF&WTLR4C0RI${}aPi%@wj^XStuLRfOYaUeN$pkcCHZyviYMkZ4@3N}>?Eaz0!GuWY$;mEb@0EM+Fn>AwA|v@1g} z2j4_kHY@qRd^mR>?b%Na<=2y(B!~*H=lCuo9QPfDgm@Q(@@%UotdSov1+l?( zP1#+bImNl?$-UF_h2%DzVJ5xtk-kYDlMqa~BUKyahF#7Rw{HRLOY3_+=|#O!=t+>U zJ81Ti-Ns&m?m{zcx^SCorV`;*Yq3fiZ-He!GkdlU7$@K`!$zq%QTA1dXR~zrXKSw! zmpAe)9g=Lv*(OCZUp^?voc^ozy0@Q)`7movGH7@0$kM(&tznn6313Z{-W~Y(LPT39 zPF*HS(vZ)9kXAUiH7ZaX$mqXt=wZP%*)g9>K~TzV-mxU1XlNLfWedu zdV!j^DaOF`DP5aTG9tmf^kI4(rpNJWM{3S#h}ZIhrW2WZk7|0*?nw(ND64S#D@5p; zoWasd_tP`g^2l7QA#lY5TL4o3_uAn>i@t8xccBJ*d4^(Zgw0hlxhbzaFWp9Ir8+XV z(fv)Y%BG3aTQUBcWu7+qr~M6S`SaUwf@Nz8r!qqPZdvwTceMOK{cDpIWQTym?{3@G z%0cRE6>$Nr6;$orf`q(L0ckv1??LWI;zc%{+Q}$>yBJCnO%HRg+dQ3@`@Bg~@^*p= z<#)V^_v%t^?P>45SMT}Su2Y5_G%Dx~q=o~#L@q_RiZ+05EYV3NSKONvDW-?kfFUm= za}{|hpR%hAtw%r=|0W{ zJSsvJOR8_LZvgX+ zYa)1~VB3;)z~O#!_QC8*Xp^WeDop?!52mGT^u?^i>DCvwep*Z6orK|PhR_xY>YgX1 zFMX@xO3FdSLT$dX2d~5TYJ5U`-62W7r_XBo>kyT%Pb@-A(k3&9-e+`=wL)hP*f|;? z@g7LCW^*>K4U<3KJIkM?@Xu`bW``=Dq@Cp9LSI_ebsU%Xy7Kg0S^I;IU)^ph9Q2jf zN7V?VD7N9hNRCi2RvHbiC8Tt0YGqn`;t-ED*7~L1cE$CipnM#axy!WO zVEp?xeaQ%&lY<*D&9xi{Kkv`T6lr!QloO)+HoEY~t=ZA-? zb{i}rKA{78)}vFqQbv2Sw#y1|MR5rWHf^(CUNh3IWF$Z_Z^YMT zNm4NA+AuRKd6-*juXceIHuJtOaV-Bd_d=V;fRfCyM>zjjyY{4Leq~q-r2{Q47@4GG zCNGbEgG0aHhRI@nO-lGyX|@QP6o!!u=H#Fxw>46B=GY(?cEcj?3HXT0^Teqxzrt>; z)#AI8rOw+l_ZC|EI-4l9CRiN4I$2XDUfW#9$kUjhPYgwz540ZHwkLeQZ{)W14#rqn z(L6;yL#3A2UaraXu&u3I;L#6>jx{J&R5OKL;wtTg3z@j(N+wrkCu&WgQ27jmyqI#S z!=2W%n-Y!;ug=F^>PE53znn-ZO(^emJBP1)GawKm$vHBKN4R9};jKRcSO1`7m34GF zQ@-*oSUCK7)~#EU9I-sL~b$shaWhM@0l>!BsGwD@uV=HDfS; zMf?i$-apF)Yu?M{N8Q?T66+W}p2}j-RCzr!^Am33?iFiEgGIH3{;fcwhFyG$s|jhF zxS&14V}N}6-W^Hu?KOsYN=4Qo6gSgsgY(FUu{8a{U+fvh@DD}&W3{5RUL!U)SNgxZ z>L@e-fzD=zM;TEAnfF9UMsC_82DW=*X;3w4jHxq4t7KjEOtT|w2ySo=TuMk-O_{4O z+zUOdDI}cW-Nv|Qgsfj-s#GJh%e<5nH$cU_6~!!X=@EhDg{c2UeH6y8f`)bCBJtI@&mVrIFB2+2c;==KA{~ zXISNJ30`4vnB19BpyGL3(lV)GBL`*9g@cUqBU&9Gx% zw;3D`ARGUP&;K#I@Wj>R(pA=9YiK5)2_Xv=g^sO<0{1hI36CnrIBikVyIL~$fwnYt z8**AR_K4DU52r+>ZoZZ87M6khjc(=Jryue}BIIjq|EdcwV(l^@mei3WQFU`Ss}_p2 zIfui3BT;gl_lKX%bZf^Slbq9baHVmr1l;Wr!AuFiVPnyRm%Eccs0B?pbRMK@ss$;& z@c!&RvaGUeQ~aTF2ATc?jB#~Z%kA}7)bLEcCL7=w@6uNgC; zZ^WF*9@;+xlk%~!QXUX!Xh=x^R-)Bmy_WPA3RLiF?}lITJT(cw{cT1qV?n6H*4r*c zLU=;v1SpwYZ7oak-fNNS`>^k<`a%If3gINS@*^RMVST?hVhh_l_)U+YS#w~TdJ|kY zjzo(j$MXD6>PK|J_Ft;;PjVbHx9rW!V-arPBpGB~k65PtGXPSKDP=%pDBagNJoH#z z-8U?G$@Wj6wuUY;fy=KoZB$l5wN2dn*ehIq)kY^Sq3X7TD6e6O>|u_{0akwA>jgjG zM%JdWOO13|_Ggc;K|$58|LGfxIpt24tWgQxn0^H;G0T$wr4mYM3W^=op~iZ0C0BCS zGf`9#W7LIqYxk8NXz>em>cW52SHEOZOhs!=pA?N=7L9eSXZu{E2@;&2XHh!!M+w(G z3&EjdPfqAkdR(~i$0}`es8iemtvn?&m9ruDWsr1OZFs!>uXny+1-`1mKV0+yJH&Yi z+p=w(?9d;R3vm4GK@kI;)`i~BJ-tQZOe)(?Rpl93<5#9e<*huxQeJV_KDbjpI!4ln zg0-Ia{#EEI!k*^mKJ|Ld40+Oi5S}NhqPL!zwHBP0%+=H5?oPT^!=`GUZY4T2w%>!1 zkUrA|S{>r;7l$hctkItwmDal7c}QU?D@xml+TV$NDtVY;Zi1tAV`$^8>%G{dDiT=eU0P(J~E;cllOQEenb_7J3?9UZ5DSj zQ|sSf78Uu?FS~jC8wY`QZgtd=$v=FOT4XR*6SS&Hf6`saQax*GF{auh?oLCibc|NX z>Q+el^dwj?8yhpdGkk2ZV3D@`MbP33ZXfb5t-owm4fyE;M>FR=)^IdNh(7~cM*l`S zy5n=EkBKaB?AW|8UGVnMP76-MRwbFd7=D+d z8W=tF5#6`II81iOeE8eR&Onri9PP*!EX=D~nH>uXuG7a@Gm$5v`%bzXptbZQxI_h3 zX7X5$EaBJYW{(AoI>w<^eI@a{51~T+>N1VHwC`Y7Z2=`9h5vm^(7gRmltJ5jeB(KlYIBxQmON3H2gOqaxM7@&HS$`A& zwJ!3r>IVVKd&(vok2v=clI#%zq9doZ4Roz@+uS*IO}-hc+wkR_5DAoAEV}(!gi0)$ zz9QrJ!eQ;MdP~5*KuxhqP18|D>Q@!+(L&7y#>S!pg8ug2%Giy6@J5ooA$1v08Hy#Y zv9_T&uYD2Jy_Qu5{I1OGB)L4z+}xx+gZ;fPuUJQl#@;Z;#xBQoa1G6SP7jV@Rv& zU-e1JgTX}n)it*pe}4d+r<#LiyGG)BKpRxZXgz{ssR+Hgbgj}vLYzChW$WsKedj=n z;;H7ED|}VD#Q2f32#VG3BZz_tJxrY?cj2=WewEbcYm1=@H`DW0r%yGza>EES4JrJg zjtbpDE${7A4xw)so_os;y&%>vY{Nwk4}gbh;DVr`ab9$P)&|3uq%w{S$Q*;LwzK?M z&J^qvmUgh=T;+bd#{H)PVrMjERobF@kJZnA%4?C2jaKydqrYrG!!8QFvl_#QTJn#Q zIwWk`6O+ZgHwqM?!(d@uD-R5V{Go+TY@8$1_>5$)d~D`V+P_{Ges+I9-{Z8x8DGL8r#gvii#r( z`h`mraZp1p7OFHsoWCP?e15vqzn-Wda z6C}Gbo^>5l!@N)56~g5gZNx_*Houh5w0t$9LzC?y^K{6OAD4V ziJaZam-F4{*k{$#rs04W2K~d8!`#!OqobAg0xY8e3rwr@nYk!vSlXPXXu~$vkxJp& zV0nBYvq2wYmnN51sqLEqNxFSO(6~m83vBw3s(BPxjZ2ju7}TZ$gCJHWd7+G>oup*) z1Mo8XGGk_#D0C-HTj7=XZZh8Sv(srmJQ;6k8mKF5M@cPDVie%C7ERD3Y^A+4(igYi zQ9^*zj9_0!c@$O`i=0i)duB_CbjWgv;T$kW$yG^0oLUISa0V%oeRn7SfFV}CIN(_(9pL`cqsrO&_yGI`)5bZnN?=G6J>6U0X`Ld{bpFk1jgwxvJ%h~SjA8FRQ zEBdv?u4}16h1va95 zXE<6Hd6j`MSRKX3a^pa8{vSG&#X%2y(y*bT{lk`9BDu3VtMd=y=Dz?t+4%hfBHh&P zu8ZM>Z(a3pTg;WXggD#vQ*$x9=}S9XbtQItUni2ET86H!@k2Yu8j;7T-|WyRyw6ti zU-xDQ^WpT53J9Mr28vX~zG>a~&>^cRxPj|2;p*w{nMRVh>9xMe-}cPwB4D1~rt*oC z^6aiEsd%?viCu$gk6j%s{YZ4w`-iI35{PZrS^3mPsMdX2TRhnJD=@QH6j-Px<(aNd zk*II4Z>c22+}06y^|loFKt^b~u;@&=CDaq)-Aw<0Dx)1Et_0&o9_nl5ew8N(oL%YQ z`!im}{3a-FhZ>Oc`pdqk>Q72G);S53i%h%bW%P^fdgrSP`(OZgK*=RpRrLK;|_|$-^?mc1Ye7#2nK&!F> zB&s{89n)F}hdPc>x7I?ZFhYD6expZf9zue@vgPoJvIs1ilyb`E1n+TaEk zjRQc^RHhe9>^#^r_$C}t6rFg2Q8V1Xy&yXFO|#tGIy3fGcDNW$4XptEKma;Pk-5#Kc|NVn8N1~5PVTVuv?_v zYBa0g&LIDW?3>M|QhGEX$Em4)5T!4`&Xhvkg?g-10h zH?>|RP(<{`-_*OU0g?8zc%~bF2Vz(mL@)X(qI*~u9|SBB^&)zB{vv>Q2iz zrM5@jezW_SYW(%TW@pX1NrkT-{?;-V8`ZoVjU`e|h9UphbyK$}Ja6(B?gb@_Lkm)& zQ*D?}db{HCy4{E~H(J_J$aQ;PCYAW2_`GVpKtIH@#*Z&HO11CtyZ}%X>8oS7bPkxD&MRA-!arunH~o^sA!P8GCZSsZKVV7`v9}R{YH);>K9C zf620_<`N7=vb?_A9#ba&AJ*PHoUQGRAJytai?*s-V~0~Mikd==ZMC%Knqm%Bvrr{s zrW0)y)tYBT5c3oyrdAC#69f^dswpAH2qE0~Mku)ym)ytC|dV-1HM87J)PzpEHt^C3;_CdQi9SvFo)GxqI! z^;`kw>~AN-opbv{dE{XDhZt?dWYi&1ING_mGBR7i{BlF0$mE zyJz5fuj4&7A-?m`56l}soMvf(&j;&4@+m^S^P>)vCR^4sg2k7RdV){+$L}i22bH#55 ze`Ly3wg8*uJ-i7x;_6WMu3VO68ddlk*EH&<f=uoO427at{A^mbt9Z&#n2})Rjv< zGG5ws+=$72TLxUl4b$wXPHxz8sPN**VC3bc0&JHm8F;~6QYVFR<@ z-7gFwXOWV{qFwJn8jiCM^>8q7!Be?87IGGMyH5#L&?eC{O?o1wnaw`j&SC9#l1uR4 zF{H8mM{v{lV#^eL17mXMa9sf0YQLt*wJqn7xs*pbn`WYI0ROYldlxSvZ%PndxIZUd zROzwO&wd)DIo%$6f@%EfI(>7X>P@n2NbV;VT6Kv$7)a!t*bF|zG%95+d;t9+jDUSJ zCHa{8U&w}k4^;LqSRv}=lPUyn>^9BC22JV3?SwQzNuLId{DD*knxc?EtR@KNH2Z4_Xmn#xV18p7F}7L42o1hJB6FmB#2V| z!+)t^0loTOIyNJv?#F8P6YiU4*&bHDDPfz-h>!{Yn8SPg@TA`EeadlbFPeREaPSY| zHOhCpA)m-$OLL!q^aPqpaR zv9WjMvL^zWP{4{J#%PR{Ty-r(az&~yZNDoJLG7Q>0CIErUtU+Qs`b)i1wM!1MvyH!-PiL|e6e|6YS9OTd&0y% z56_p*6Gy#&J+_0*;@a^`+g~o1cOn%SYa{!l+Lp~h7L5-By=C4H>LxQ3z%fpN5z|Rf zzQHD=pn&+msb+k9cXE?u;fT@VhWpo^bsBBI(Yf#igYK9I_3AIKy@ey}xs%MywSoFu z{g)GImZi9C*G4%@@Gk5<`(#sPKvV8qM`R)`g!4$Dh=hl9cW+l(zLawPcI2SadA4xl zsY%e&$!JbWy^Dy*h~DS%O_wiqw}XS$%-)!YyY!nSP|q+`frqQ9n~Gg3G_UY&zNDBl z`t~qoEA7M0pi7s4tpR9d*Lvce_c`fM0B$+PDb;+KhWt zWyX--1&G&lxmT)CpceZTp#4CoMd-825atwLYdPG{syaJ6FRYt80aPOn08%vQyQKM7 z7?&p8dl-(Wg6k6(K-zban&|!CeE#~*P@&Zi1f6W!_gMEPj9qmz{U9)Os0wVQ$ThmY ztWNXF*{**K5gh2as|?fu_&aiOxV3osrbX{=td@Bxt? zv$A^^vp%*(E*92LbR)Lwx)_0bn%Ck6e>Ubq?`a6o;GCh!DggzoYf{4)jd%P4UJ3gx zh0DXAxLm1QkPyUP1IDdN`KaHRPwm3{m3q-VYXd z_6wJN=ZP&@7bg!GC6-vPr(|wrlAA>F@m?~>?CkW%=xHO1Abq_QB_-6Ep$U`H;^#GX zbx1PLaIsgGI%fURvNU$yCf*($9(#R87ysZSfvB2P{V+Rh!`*zJZTwpDX0{!T8lKS$ zj=?CuI__=w&5C%GqVTkCr@R4>$8?`@4%MVmSuzrkxbjVb$vmCY z5{%tMm~}b&TToQI$f8R7wE89(_bG5^7YW0c1%yz(#jzd|e zN4cox{mJBZXB8i8UwFaL=8x_N=7!d}^{LvC{KI-<6cwH5n%f1lcj8$pg6LyciY*PN zKyMxELYNzt0zUx@NtHQbOfRF?+%3UAs;ePv^RezG-98Cc>hZw|6_Cm%IHC15Q|~jP zcbK&1!p7UL#!UBiRyaRlubYl)rr@m!JzjWRNf+X9A|TpYbo*UBT-5L$(a%hR&AcF+ zo$_V4kauZYKfrDht^j9W80S?T2o8#X?b;Gp!Vfv&6u4&Q%NSu!ogW@h=03jO6d34Y z)DIW}I_v?kF`t*7pJTfyo#pohsXwv z7|_Z;^6*^k{L$O5*BDaY7`#y%pQ)*1^{TgtUW#79Xb7vyX*D%XJ0Dd$WQ-fa-x=t^ z1>>!Bu}8P(h>1lZO7wVZzjOrn-h!NvW~y+g<7e zO(zr=nmjO0^Y;-maB%22=Eb^JvC+JgF#mm-P^8LOdcfHIKZD zm88OZ@OpK4AZHYM^V4V8uNb=}rTst*v@91?qzor+~sa>inNTO9xJjMT~ zeUOw_xjNfI!7Cd7m$mmiY1C<;Nc1vau3#$c={o4P4`gpG9$;CIRsp96bc4d|SdZ?a z7we~~J<~K-mY^gUJ@BihRlxdHb97)a>v|>NtaVV}o*|^%w%Jv)>$I&kA_W#gQjy36CTh9SFM+7C>w z!|)8dqfD4r!OB`gg;gDl)HwZHr%yB4;QxooFv7poEKUOOI!S{lcw1#_s~ZBN7R~m0 z@vXgEB+CQ-dg5uuT73I&njeg~90K4yvUckcBpabX*rahtwQ;imdfI?Wu`UV=%ct!0 zkwMR|(hG_u!ttOoJ+Ly3bdHULH`bekzhM(hvKfmE@w?a5xa6t4k&toY+{VHN zXFK^_g38cq4o7m4A-hYZZfS#@n%Z$6T>0_nDq5GQ>&EYWp@m(UYo^|6>`qxya$^@&AV zmr)gZome_w0om{3&(pWl*c>h;EHrtM?$Uq2_ABmYS2lT;Vf2295iU*o5Kp3~ArfR# zgzjLCo=QI@#V^8ygd{5e?%kJUP~qI8W$^IVWX|qyp+n}eW?CCV3^3BeOTPC=toDT;uh#~pq+gOIg!Mvdcg*-hwk|N}@3D)X z+p!ouofAbC&RM}edn{tc%IU$|S20Vf^xgr=p8QY{`(YGmRGFgzVX8D~kkoYxoA}X+ z?0y}qjr(}*L7n?_Ctd4JU;SE)djsvLRUlQGW?7inRts#EMuV2=Hnb_!8lPSL%YfXZTE z$htIBh0Xo}|D6E{9{I$f5x%!$&!E-OWP_LQ(z~d{nz-+K)KLrij-7|gTg9Fq8rWML zO#F8ZTl-> zG)9|E6)6w+nZ|Er=moma15tI9pNE*Bp2-JH=|Jq|m*_j8v7ET^;-@0jKQ>GhhSu#R zViSW>AGTF5yfy66&1}Vhxnk2c^uFyysAE#;>vTT zV&;t|(sMV=8nbs6;^28>p$5q;>-DrLN1sUts}I81^CA?`M***n8v&|gmiDe| z8*k_>)x?3OJ>Qr{&&5U*375P^il27Lc+80Vd}iqP#gmH+S=U17wL%~OUaQW&WG`x! zM8*6%etrCoLBcO3#qwtbPZ$*x}`{y@Y{w)E+`m(A{CTgX_a*?!&hbr4^? z3zcTeV`EWc=R0x86*oLTucl7kmF56j`yh(PHyP6GxB*~+i0k9wx#DAKIJN|6_c*yI zJQ%z;{}=BuX}tIM7cH=1VeFQ);X<(Lg z7o!Tyqy0T>D}8(_03Ws-+UJP5K*j8gOvu^Z!alH9c<2~!|LyH{f!pfbseU<{TC7GS zO5uizCfqJ*b2LKGsrRKZeNIrKYM3CUb|}EoI$+=jo&I>*yRW5Gn9o_E1W@RZfSCyE zd5||MF0FH`$EbY=TiRRRl3HvujmD*ct*q|Tb}5G=`35WN3j3~D^eh3}$Oc8Da>;jg zc_AvKF{dOgMdUMeX>3eDb=FkMwwywdf$x#Ic->tmdyr74Dyx^)-kXx3A%_ui#d?&X zJfGpRdz$xZUHj;q2$29!NDU}Dwn;%cXNa9=%A9s$efQ|2|0@_ zZH;Ze7gp!Lt?lr~d9xyml!~-UcTJPnR2l;J&j2Aiz}JgOSbJ|?{7jW2QR0+eGA8pl zliS5S{{!h~S`^kkYj+(YT}}8t_FZFVXVbmrO3hvImdEnRE|oqGF!DEp=qr5*?&m~V zX9{m*-(g!V!+fA>NpjV>WK&5o1hd;OZZ2srFnwexN^DME5-J^DcSnN)L)Q5CJ%65v z^AUEtm@Y8);WK-G@|uWFMnlewB$hgvcqWL8bsK-v)uzdS_n6dh;wkKdpXK2nP*(dr z7PTt+_ULg7L+flic0!^3=;9mE;o>S_S|uO0l&F_9mUfgFR&LsV(e`1nFr5$q-C9WX z5cu*?W~4&St3r)5QvI;YBo@CTl=&11Y1pL56pIwPupZJxNhPvK!QctD-yFkOxc%G` zqThk=w%>G)f8fvg9L0mLmoPwOeZ7f$jn$sN$zvtx>+1Gl9O;7iG}>m+dFt5WgBl2N zGjUeTJX>RLjT6sihvxm(^C+o}4K9cf1y_UI^z|%e#1>0FOHQxbn^!z3ZuHx2>><|& zPXuYuaBuG_8!boeKcJGwT73vxYxoS2^N^+{z~;(hw6n_qAswo&3`6JIrvf(A^=qZL z!d1KD9y0rEuUuwt`))7kxt%W2a4rw(9h95qrTB<@;vAkuyT381QAyRIZAlF`8;Fr@kSW`Mp1Ai=^)^jj(XAE9s(>b?0j*ou)+mU>uN`e*r63 zoprvebK}iM%TyjY+=HR1QWGbJPq0@rCtr!v%=twBT84EqbpuP;mUNO7*yahjJu4et zBgI--#TTjrsr2TEF+G*#wuBh%%(maJwNKoD5(k*$Qa)$FRg-hGm{ZV%00v=!M=~ej z7;R}>Y7+gPhj*4`D+R%r$8Pf|B1@M*71d{61&os*@8XZdwjcBQ*7JhLuxG)MJ2jKM zzgw%@uwud{INFkiNND>oZ`Y!&GhBoUyn_&R?6Imf@Mu)UGU_yE;;LPoK8*H%E_go! z9d=1JQ1Gbp4F7`IX|?IkD-7QMIJx~SK6ooFdkCYE2kmr7_Dn|CPx!iV21-ZY?$H|} zRmZuvJdZ-dp``RmDlwIXlvEuM0T_A|1mfDvz8 zCb_0?AulPsK(hv2H0U!tUeUAqG-b709^{+o#A~jV`w65}R{h=)ur&?Qm>&~-O_cqf zJ%3t@EP8*z&wrscj;8_K*hVofDoIlV@(;!|E8l&O+5v?ZusY~wAd%V0Di{Mn9qT-< z2y!)e`HJ$+T#K>!CtE?ich4?_650pj$TE!e84B5bHUQIUjT)%d{Rp^wrunkbLfg5Yvn6-wyZ=V6}F z)`9&rKBHybUZGzq_~j2T+6%=;G8GZZ`EgWxAJgd65_;1siJ=#Sw#AS3fft3Gp7LCh zgP%%d`czlF@M$Q!$HcCJqh>tw%QrWEd54daX);;>QC6)BiC5N>G>873I&F>?knsX> z)xA`KbjdYbjmZyKd@7?ni5QG;*R5&n`WVYbJaU*^n8aVzcUswdCz{&!{bKTM$0X~F zTUPu2yJ7$nl>UJqaZLPr&1_R&Ln(fL?1{1gO$78@FQ{)V~CzTpG!& zp(EDZD|F8=>N-Ku4STQK3yMD6KurMthnJw9Mdt03CZrILkT1Vp@)3NuFY+kY*Px6= z>Zon*m4ujw-N*3i91FQ^PE|b{cWK)KaurSV4NJix`sR=0W2b-ODdnJmNmj<(7z=ti z21iZv;LADL6yTkr4{>$a=|&0FTvp-M5?XAOyIiePP8f5V77vtz2fFp?GleP~;XGSe zx_wcPtrv?icwl@#684C89vs$Q z7!-DnTtno~70A^YcEi4ayHJ=$(~NMS(TrIw{@MG4K)-wh`fG4{=KBl4uKK6$*5>J1 zbLYpws7_HAuE^_dzE3nPRb_9V;HBqWPwR^p z$FPq(md7rCEza4=nBIf$l^MeBWvFc|jQ2qw6f`KZfo*T#qSoV%eNTR6Qk*gOazcCN z>6@xA#&60-wBI1mVQ<+E2R+shP%a!#IR1o&zqwe~0zQ?*7Yj<9Ny(gvu$m0>Z@x-RRPf2EZOAE*WqxVvWDqJO0 zT76#01Kvdj3Dxe_iVCJcHDoQ%am~6|85ehs4ChyQO{upVoB34}q8Dr(>Q-mnHRPUd z7A%0I88s;-IdKah6u_u_B0##YtHOAO-YU3PNSxTt+4IA|9F1wJ4;+cbsfj`fl$^C)OYnyW5pyc-g-2qmolECj{Ef8V_dx*c@aJ$~t!_gE)P zhfLf}&53RuX;I*st&P-+GASvsY$`4eF0nMU&66~q1)|w}dh4nSv}kYrq7s$abkSkw z-Y@3G1x^5%PP^RKrSi}U)K-d-^%2}c|6Qo{_+iWO--Q7cSKc=zRL0w z+1d<9U{73_2HV0Tz-f;<^sk6qnmz3D$TP}?{O$ey>6GH(rB-MeRUFG+@J?1>TYMs!ntAGOM>3mopzSZY;GhJW3gXrLE;_xs#oH|gr ztX*jwuMHFP;>!`$0(dLW;ah*_Uv8j2h`cxnCTN|cP85Ew1|kn_(fq%yJ0zT>brW=t zCa-QA(Bwa$$rpyUBwuaxwNH**&k!3NjbWJIgG$tc#*Q@}-a{VOgpW1Dwx(E>ERVV}}b zqo5+;eEWOO89io*tXKZ14<6s1)Qw$4oi&v?@4@<1Hejz>81z^CjMsD)4<2n8Kql3~_x82HBQZPocFg7uQOgh}( z5El)e*a`*WDdW!)S^9V9#@_jFEx=*D1;_Doa$<24wGnEH6DHAG)ug@hM>o#(pE>?v zC|e>hla|*#fQ!0NIZ%Z-*$&u~)q%!_*D4w9r-lFL$XZc6xFv;rdYZUyx~aY9+SV(kja}ct0~Zq#f&WLe7{j^&+z!lA2@YmEbDK z3vzXd>VeZVg5BdMw{7Ct;sL#N@fjybnyYSaZyq28b*XC6ZY^?zq_{C_@tTNgLEXbg zj}vCD8~TLs)Q#y_$%>nm>ozq~2dG)yMuu;qM~*fu?rA$qE3+fUk0?YCsIpwxy;Y*z z260YkJT&2=fK{Sm>dau!LVaW3Ny>q^$os6ljWP+!d=GhhOL*~dUQRHeVF zfYs&C4ySR{KY3*h%w|-}m4U<(%+ljD*pG~Ao7LoayD7ku#sThm#;@poV|!SG5+%^i zof*r&znihQI&VQ5D;|xSqiuUroWmDd^k&JkO)7TLlF?Qs+D#bpMV&;Yc1FPMs%y`1 zKxwDovxZ`NE5}+skI#D?l&@<>P!|RsT^DRf01Sen&5OecBwMDkNC0hlAb8CJ$O0{S z@f;tc*(=)|O+m5Z7)pi$)c^wz##;!MF!yR5rU?-(Igtj!j@NXX%*5&uepV7q)OYCE zbG0qKD6Twhgv0WgAXpwo3}E2RQAq{qF#=%2Yr|x>ni%4C+grkgf?u67YqQUE#jlq+GGJrc&%)+chx7AN!I)~Yf_F3q3 ztjS}my!Xj;9f$8Oyx+_XO@{eKU06@Ckt~ozYd>`G#`;cNMnW!x@!M$qRDqjSHPUrt z8Eh!rv#7`EGVas}v&BQ7D#f{7^!aM0F)l6?0-wYB&nMAh6FwJgN0^XFf@wJyy}f5JoI>0y0y!%IzjjX()12z{2r z9ounP8TxYxi1EoZ6XbA=4U3+grUEJaZK8sap~KCp%O%|(LMct#8a_OR{Jn0`EY{E) z6aht{OHSz5kCv}v0k^w*!^4E#wPnisiUu1AH_R_l(8pcU|MrD4tV^P)&?d9s)5OB{ zsG7?a7Rw=bqZ?KlmmIvl{u=h#hwN8kWk;070Z{r|6^2{6Lmwo|*j0466z-^4uNQrm zF+*P-6PMPLrwJ(r!u;Y!jE6|^kU)(*zDm8WO@4XzGGJG@NN-W@BlXZ6ks7!w7FpeV zw5GaW-ux7SJZFRp5h)vo>Pi_o`yY@N?&H`q$Bw=Sq&}BFA1r#A0 z_XTh&i?#3&Y~v`X*mE;uOrHxZ9Z;7U<%3370@?}jvNOXzytIqM#IOu=4=uE4k6#3M zVOnptu=a*Vc(kDHoa0Svl!-^kRuZ6R0_II=si3lMFs&Re)Zno!xJzh0p1kqX?cSS>aa#^#o_lXL4Rdr~_+M_4#`)POf6X?l;1uYTEvN*>O&=ex={m*N8Rz)Z*2VZB87O3=K$dwQmtw*(Eykv)B$kv{E5FaGxBxbIx)%+r*#$nLJ2RdXe;PfPgU{m zxhJ~sbu%{R1@%%&wL++R1O|S+inh`?o(dt*6VpTNDuSt>JxJfLwxM2EgC^rqUHBoo z`ZxB=N%YNweD1DVgU(W;k1pS=ol4y&dh0DuQV z&6F(YYEGF&VW=x3U6U)@6GK~w0No7S*kUbjRMQk1k@ViKIHL>Iu8W_p=&`e~q|l*b zzq}@Qa{@q;P2OcaQP-grVNbkJMLoN6m{N-#JHrspS>MrT0TuwxALn5hQhG2DWevdd z-U6L^JMhPybLV(!$TlrXp-3r@FH~2qD5fn{~Mi!!JN{*twkpklu zDWBL|cIkO}$oxwdO_=qIKzxA1`}d0$=urS}bBgzbxrJFFO*Q>$sUiiYx?_@(ITS|5 z{D6*ydCGAs;rqk7CZAP|OqGqS6P`g*0@B46yV$~^?sX}6%1DcaM1a@V!jV@&J@}t) zEvs2NMXp_epcC|JWgpZtOQ>gEdQVBq?1)FUT`je7Y+@tD_gUo*np!WnE>}La=`UB= z@TcH%b1y*8ckrY@jidWPcBTo$l~5H(*U>A>p$t4Kpn>;j9{zPSDa9}H-J)LqJE>S zwS>t6tWCmK-sev9`mhvV+9>(u* zqxU58oXcUG3J2XlL}z z(O4DLrIM;td&oZYYQ?09U02@rHh{d%D>aF#$aV2a^kMF`Nd_*y7Ois~X@$Ur9 zgWHDkw_ax&eQ@tZD9^Wn*<-M1rE~N{E7x+(_dFqXFI8^Wmo46kns!>~d`@`Y=Oy*H zyoDx+@ZhUio^6NI7~gX352BKy=Of6!?b-qA3-$S~H8gp(-*6d71VSv#zfxaNB+5Hs zyWmRnz&%!iKXIKjUUvr!*@D^IhgC@@26+Ji31Hplo(JNoBa>I@D5ZGzktEMQLjrWr z3ZUi-wQ8%1b96XzF0%EUUe0|Q*tH~P+f%bK!gc&Z`gSZ=o z49N4eDsB9!PU>zQ+tQL8qRA%~2q@~LHR`jmojFiDKp1w32I{-Xzmx%^Ei z<)yJdkuZYQ;~`2FRUoY8h|~|e!KEd2^S{#?52~#H)0z=LxVlX1wt(TIZV$|rY%Osn zNWAcc*=rY2Sl@;y*7XXbpOYgdLnMZ<7cir*rl2<_GEFUvVQE4-X@%Cgtw40ds6%k{ zw~~dPD-v2=Qe`iq?GJih^_!piE^@z(`|lj$-RqBx^`ue?C` zd#Y`IGFn~C+#HdbdWyO*S)eo8`5MtZ9g$FKY;q&B5g>|x&aYkOn=!yF1Km6e_`4}W z%ul3eZ^Gsb5qE>MWZi6{XJH-{K30+OMW%(%WDo$Vhi!h?IO`*esNB(np3Nfh!a>rS z2uwoEz%#g)z68g?w~;U;vIMLLXLwaggGN<#fRq;*_>?ABEjBrGwEj(DEZN;F@0+E) z7Xc=W^WgP$8IM(DWSb;cg|#0%S%GV=Af!|r$V?WC z28=Q984tmsqhLaIu4t!R^5*h>kXqi}iiz^h_-`E(4lTuJ6E^nt4)fT)_)-0_FqK3l zFk9r~NZw({#ViELo&~PFw@H+vGDMe0I7;gD60B$*LHlb>q(KOSaI8~X{-#ye+Em{4 z`Ph^02M!FpFTY@bmLp?}T_?TKCcAymwvI?xjqPptRSKY>Kt~v_0N|Mnl=h-L4^o0QvhyjL0!tt`NX@ihcHS6( zb^$v7>9{zs>)MAmKBmaqamK_8!y98-Lj^a$*ZF0ABFK_Y@4zE@2DMJc7Y$UIMc|bj zkO2TP@Qz{`QhD|l zt>JsYO)M{}t}tbfoq?Iqcw8UPSfOiDiHHw1cEjayOS znJ6{N=(YY5S14)`rbmBT!oUBK5$5i>y-;$!J(@qix79CM$}Vffco=oF)jQ}Sy9R82 zujMz4kGDhWZgoW8Z4&@mb<-lNs2Z4KJC#|IU-%0e8_TclIvjR!j%pE>|8L|Q6{-&C zcWp3@zag!;N2Gk(eNo|DfgFe>~MxMsv9W z21T+AtmM47m7SVhsa_&tQ2dR;uh1!WZ*})J1RN7H}#f>s5Po3azffT zWQ8WC9)J%TL#^ym{PG6(-JVdFc2SZm4{rYN)nbzMfMIOr{^_fvK>zsIf1BX!AJS?0 zV45We1h3OocwVHL++vm@o#_DvJd;|64&qf-ioIKb1xsP8E6f^B0U!7W^cpbm=Xnq} zOry;DyVfVbbXx&U18c$^_}+R536H5Psr`xJk;=r>TbBk}YxLFA%hLn5TR_v-KRvo; zXwY1V*~OADz|EFt8EMd8FQ+)&2+y z?T+|jE|OG1@IWNk>l^=X03p|a_d6bWPR@OzHs}X$qrZ?TtfNs43Xk+UUo@0&+aTj` zKLeWVvzvIAWE5NIXubUqK(>znl4k7g6NbVC2e9e)FJn)7Xjj_;l_Giack7El)91LJ zb}dIq2W=V*kI2EUCu!zx{7NyTehMHPjiM~NUNm5CNMFsAo6IP1glwA{0|{@KXSpT& zSWaN0F}yKPB>CHiz4S>EIzj^=4$xc~PM+-eze%6}9@$1sZ%G!Lgox{(-nZ+3t-mWW z1>P3&onF7awp0Htid*ZiDj%zAHmzz0v{&F(60PcqF^~%~Cx5$_zI%<9!hJnjZhWc8`8-rfWEXMaO4UUFFWERH|yZ!@ndOh(GN8p8z#S z0afCg#kr!syWPibpC)?X|ZQ$JQ$#w)IJ&DfLi*9D8x3GaW)lWEZC!Fy>T zZD&BwV53{MP{c*a{Jh@n*~>gbd1Uo<>AMxLXuL?APuWmplBKwT zQgKBcwd2(Op@oyskBgS@i+O2fO_PBkh_kCX0F>3m3@8|U%J{B33SGnFkQy>OB_iHA z@I88Nv@ErK>JNqP!Sn@pRX!OW2VD!;Y($zfZMuO|4Sr6tR#yUfNpI{t{rLScN6YMz z-oLhP&+gX0Gb*Xw(K&W$;;G&hC{-ic+;MeqI+=3L0?b)S5cN+5O{?sEJ5FCp(<5J> zdwR-Q@uKIwAy~wMEJzV0-DJS@nS9>;V zCgm(r+7X-oG8{80eiPFJ*Vbqr8m-h}s9C4%0O?|7T8V9|PW8}d0O{#D&hwgo831Tc z?95+{^*TqB7eDH}Mhs57Z}E6gQc`1CyoZOwtKIDD1pM4zE(r`8{iJ^*qu#+rZ*>p* zGlHz)4fQE1;)CP~|G>WUA_GuTkeloJCBFY`jnLAw2bRcvrKGB1qxn6y=H4gP*4?g= zdPlyGbFSBGCpA7)-KMHS<7(O-YA2)y6#*i8^FIkFFQ_Z~^N<|Dwt5eI;=%xm!d};~zfBQlx zf3!*hko)))*jNT}Ng9B#Q}go`2UGr!7QKJRG|Db^3AhT*urxK*#k@tNMpL8~<05cEM#n z!%jxuRV0xt~4BT`0T! zb!6!=AU*HTf9?Nv(2p{%a^HLJj5MFNFjYBPx7$HVwOiHxoWa!p4@nEE^R?K#$=PPg zrMLrG`P0lVA6Utu#hTZanhu8&4wahx`%4kvA_6{Jw3}TGC7htL|9eN%zU%*-;{EW4 zktSf9J$SM@`#uxUP4wpE4Q^b}cTm4`aKh%Pe_yXcrjty`8uHk+U!FCW6Q2X8X1-ha z)X;btfu_HGf3i~kU}Tv#nf^OJ@&1*9YA%>N^D|AA3^4E;R9@i07s-G|eS=t04n`G_ z&dNXt$YtO!z=?MJleQ37 zRO^gyEl_}g3Dy1jM@vlY!7tu!mcqb=K%2i~Fq@GxF9N(Xx47F@4}}a5Z2kW7f4vX$ zcRJo2VhUaQT6fTc{?&hv17Ak%&YU?osPIqbAn@r=`yV6vp97)noqq=K;DF+NfU(Bz zuUw~_!ZwatK$iM+3~}zUx;`sCp98o#=nwwm?`yYSK6w`#AKNMe^)x*RZZ$GX(AQBj zm@?HYFwzvDsoFStMoaOYK6zBBv&-8kaoUjBs}f1#HRyZNRBs)n3Jd|TO_Y3cp_`l( z)M8rHK1k#{Xo?zYe-+^JWSxE$9RpaajZ(?RHBU0=-t76e@P*x(8d1}arCN6VKgN@s$UsI1YtazrO&(UE&`%eMnu?0Z3PUn-Vp3JMa+Ux6Xv$xAKxrqW( z!-vhQv2BbeG)?4MwAyXhKQ~>xu%I=*dS6pQuNYToNd8^Ryh#xpDicT&`LQm1?&H}< z!b!ioBDP$T?697@Yv_dgymo`hov$5kM$M0LxKJm;VG7PfH-1?KybAkbuSuNo0)UeS zybZ&78t>HEDR;fDGPyAy+?=I!x})Xb?j3=W*twN^z)NaO%)x(2*b@RYvVPKVVYSo1 zmAHfiQ3(mXhMi60Xm`Tzby+L(a;xL3myZJ?q^5bir5oz(ryUYhU_f>`tKjb)*0#YR zeEs)h`E-Mu;>L2PPqviI!Ac{1#5Rz6kAR|pn*;S_>3f+bg5B=@Zy=&Z34zIP%L9-F zC&|?;8Nvd3wHfXFI$ORahgC|C(ImK-Y`T1D$ASHV;aUzId2>R?mALrhRn~`|BKJ$(gh=T>-1wB0!4+TXw7zjdw?w*$eahn*w-BX zTGH0!6=fR|r@qNLpW0Gs96N0ch#4|0Cv@|#7;WJ*ao+}F0hYZef}&vn?IfvyMy9TZ zlny^=pL1OdJ#`3^a;>Y2E9!n+#%E_ph9RYEdH0$<&rqpoM*ATfQNUIjwZC^L1HbYs z_@O5+MCn|}(Xtj9+3v8HK$2)^kM!Ks)U%0(MEVR*W%p6DcJht+bmES}L&&v-XQN^@ zlgPpnv`co-&}hGnhpS2PV8C*Hb^xUT%$*vd!krDsr9$S$#mR`zzSq0||ZGIII zwETNF>>fAHC1xf`ii)}00lfi?I7))AQg1^Q6`&{z#4*tz8%#xQ@r2# z8mZLzTw-W;#{0vuFfb&@7;NqNhCv|HM9)NwA#KOZ%*?A@;<|r?-JW}^o{gSOcJ*GA zY|qz-7I5L%dJ3#NsN*>M8p^O9iJDR4>fL*0j0dOsR5UC}LX{cwneL<+T&e*f%2~Vt zK=ssIR$8M>@QKTki$KhFIp;+X#%1$m9H==ZBcY)Ds{cu#t~IbQE+`$z z5ePAMvS6tR86R%%nsMkdP_yAa=oIGadV+vnVkex1U+QLykE-8}R~*Umr(u)$A-sA^ z*P-!BqxA$V`=nzgeRDC#NhwRfiWT}`eS~m`nF$mLxCVV*aU7(#HurKxv>F@YoJ8Fz z^Azsx-9VL69-fxY?v>>cf^tH!RiS-X0-mT7SDiXxN!U(k%Gbh`_>ATzSR>mr0u4S- zh7jFrSlZ(-*edSXmwwH2hH5vj_-$tb!w>57Ruo`eYMg^ATENr{;7z6lE$VM%f}iQf z!5)3H9bHPY8$H4wEs$0q8|O*b)PA$K#W-t!jPD-E3D<4^0r4?0J3aRSRFIR&#?@y6 z3=B8W?qEhvzZ0H89rN`WtdUmg*$2tARVk89%#pO~xvuv|;_bo*1JN~*yp z_gnpU6QfyRQ#rbm68JFpy&c_1e%+2^OIGyfVcTEgS#Ia8uAKr97)Srvz&B67^ED7~ zoU>l5$@;6Ne894Zx)^5v(y#{{cbr4O%|jguEl&6S#Uci2oG{_GXz;IAp|7;)=;Evs z#qUR)tgJ4nposcS9%5>yv>r(V9H$Cg4%o^Thi)^0;oo((LHiw+N9!rKZ4);RDa1ci z%B|PdxW1fFuwQSO2T-dQx?a|4ciJ~8DHsGWI*(-pz(j#GndcAnCc6NSYBFfVlzlooVDSD2d(Ot%k~Zc`xbQ`e~S;TZeRg zizZ*p!vNNtP2chQsB9{`E zhsz4*M{so|)F_=l^=7D2gM+$Egm}&NV80zb`MYSDcyB3YTE4S~9e8_8cy~WP%XM(8 z_v}}*O+K`-gu|WiL2tK!s{PE5z>P1hf5tPie80@(}W(NhK8ndilY zu^)r2Sd^KYP2~THA%xnC-@pwIFOaWixV6}-Kn$llIv!_ltnKgRIb#tEZ@8`dYs>BM zVc%*`1GG;4w%X~${E1W1{;;(INs&yolkJ~K__5ONOc~hS`*(oyQy0r-Zhig{%eE4h zEondZ`u~=z+u!Ncr<`Y&5hMh_x$jFA*xbqwSgz9u7x2%Ql9F1KkBf_gA|&I`z$2FJ zNOQ#@B%++2aWnz6ivcPRX`kjtf3XNFwv=Bk?~A|x{c`kXkseL=!2Jazl_HCr_3v88 zx3Mo@1wcfPuL4x{K+BBl14^dQTc;n3#ONht!p85^$_k&pnJn8Y{ikJ?2$Q&MlEToR|LXtwZ2 zI;rvldonHB)2xI8a^b~v6R^;^$h;xpzC6==%>tJJO7iD~J`lw*siL#?x%A<~Ty=8xb+619rsjlS!K~g+AAm&%Vi29KyrfqeU_G_EZDjU1 zOOP?If!-W0nNA02p`bry2%1yQR?%atA02sUymc+!Q zr4894Bj2}Ai$Q#n`jG%!6tGhDWFsHVyyo*$o_&5IL%huKmTiZAb4>eZ0~qs;j+Qy5H%QbZrE z{L6gCFH3zX`T_wX9S>18W zXr5s?bs5`E%brxsy#q#?mp~v&i0_?flJ)>UUnoCfVdlGCAtxwoy0i1dN>3L5_j6CR z9Q^+CNm_25U|gnZ(#M7z@&gpQts7g z1x=zV&610on;fsY_n3`JCC#umO@;Axe>f+xV^q*4yY>_wS89{LBh;A-L^whT?AsR> z-z|DBPx*_CS6>sw3G+mVs>)$e%)bH5tnZjP8?U~LY8S9UMi<5cI;-K>b$7J&)Kj)* zT^nn2Z6?|C(V{=+yd5=GFgudxwZ=IJ_1?@*rJpDfM$H;>Eu+Cdl5^kp2Hbyf*Rkuz z5{B8l<}$q@+6(1!<&1LV-(jsS5%Uio~Vs(uj0} z#0BZu*Z6&Z?>qB8&&)hC^UVGOZk&70KKtyw_FA9yS!-coV57%4CNe0;X4;^Yu)O2N zqBATHx(RQNe6X}X5MXJ2!}3)m;wi8-PL%oYsXIP<`vNS#YkG=THt&Ve7=O81?m7`m z%*G=WdF+*OcMn*(wO4TP;9#|IZClzOH%33XBD!pLD&XzEXyK{eO19E9w(;}^MxgdH zt1$l_1BUHsF$Vhl=`C3+ptBnCjI|M{5kM9>I-#7IPVhZLm2X`SR8=qWoO~z!0lt_Xy zqF#?jTakX{;<_5%F(01ppKf+})rMY}vUaZBt~m6{RDVK;TTIr&6$E0f_$Q{Aw%cek z!D?WoUmhlck@7LU&X1+RPUh579m? z54McG&+;_6MeVuui@q~<53ULd!>Ez<9eynD)W)8q#SDt3b`L_WKVECcRhc!S4R5>0 zwDn$@?Gd24dBLxj+r{Eu`$_Pisd^#RXzJ>ftg~fwHtMZcp=D0Hw5it^ECWyKsh}nC z7x|qXd<=qH&!qBaS?O!DgeMFa7g2Ha#iu87k9g#@`cx}@QsdkeHifU|Jl3-5;R$E; z*S#2`;_<)T?EM{dbX>%Z^(Q0s>fn0UCm;M%{AlO%YZO_x-jnmH{>XW!;O7V zMq-Of@6!KjcCbsDkXcGCCp5tD8d$C*7$D#5+kg3H-;QVxR>G(FvUxPmT(D%A+*+s- zpX=oPnp93KF@E@_J28B;>pny5lMH)J4yVBv0w3n1piV=gq+0h7xxP7?z`@GB`{~lV zk#b6jh}S{!GWG)0Iw>N(*M-9$-Y$^*q+`j|7;h9w2RAc((y^UE4Yps7zvjM)vMK2m zPnW5rwU^`OWFR;JqoZbH3$e)*2rHZP*s^Zub-f$bPN1A3Z^ixmDkO+b$=M_3OjPCz zr1xohrS@!WrcbjLNLlOta{%Y6+3EnW!oFvcR3P_5P`Chja8rDL42(Fi4Ts+R)mWsE z+IQD8Q-i56s9!<(+m^UF&2#3^)RxT9zS=7gaW4IzJhW2gx5x;w@7|kwd#h_xj9>D; zHII#AbL^Q>jM=RxW~*LTMPrKA<4#OGbULDYwFWv?fQm23B>3((dT4_8F7d8P%TARc z?lul^;6!(*Y7x!~&LS0qord=%xWU@9?|FSp(UFtl+jnPHZY89lA?K*i<0y)IcfZ`1 zvX_KF5ZV(f0}8`15x)RgcUi#fh>rO8oNlVi6|svAhlI@ULLq~=$83pekKR=iFktOf zE$+M2nQBMS-<$MrI0gWWF~NQOh>lxt9psImIZm5dIkowGHfxigDkUlPy>GAH?5TL4 zJ~#MElcQ0Dx`Yd_Ptz*4-x-XT#pmHaCHR`YpyW`#TqnG_OS3xhcpffNcK#NI1TfzdBPSqv5>$${5<0U~BNgo=D0M6R2kw+INyarLKV;7HA25Z(rMog9=mg&vhTlGz(V6CneIm?R4|- z5?j?T=fn|v$kdTUo5`(}5fy9B#E!uF*K>nlYhKHVKyXG!G5?0S(EW$BV7yIw4u(_!(p88KrMJCcx*%Ur7t&~{e8p>fDQNB~-i*q8v4u-GWlRSpjPA~|tCRa+f~fADVSTPvJ?-=X&*)Ah zM^4q&FFJDRn!6x!3iCf`C$RJFWlIE9_EG+>@XKsPk>h~U%(Rj0X_Xv|qo2hf<8%F@ z5Y{j98|Rc{16P``=ps^NlaAX-Q?V*piXd8UA=F~^SA{N9kK=Pv4;fkj zYPV2^{5M8YIYH?)9@=A3LodIVpnMKkdO4!H_@mWQ?t6l20W~?B**QV8!y{68K^Yy3 zckVMX-cu-S;>cSM$-T`*xa8yhtJ8jqk3V4C2pp1~vrs~XJ{A<+**&Hp!&7*NMo`mD z`|b-<#sFIs{gs=&=b<=!;810&eoRHrk!^t|Ei3MUE*|I~Y~K1e5^gF+wYy_vZfOl^xy^Fm zaIdF>=qjTo{r{+R*}-mp^>&vUB1avfLkj{f2>YRx*G#DnK3ZK|mM6)Et?S+2o;biB?(RrKx7IEmS z{k#S7co)>K%Cx4=l4*lWH$Xrh#8%9RbLXcI0oIGODiPsrg%^gBI_lPswI2K+f^3$3 zohtyb@I_^9$Nx1lU`RI2z62?Q{HD84)#SRyAiv@idqy_$T*iqkjY3)ny&U!(`pLUb-Xr1`B9{QiwS8(KIAZ7uRsB1d@E z!-(jBK0o?$A;jjO{Chfa|KR2^^T9lsIuF2mDExmicoxWHHM9asa5#t}he;k$)gvrZ z#K(L3lh@MHskEFWK;_P*2_~1ow1L!Ar^%3`|IF?Z83lA5>!w&h6(7pD4A8ywJ97{@ z81W_3#8ybJGI`^8*%9I6I&aO|Dr&rndwQziv$HUvZy@b06-CI(I-IumT~cPlaNja0 z0LjL9Tjc?=z$auL(arC5`N3zgk7@(fEHq0Y)@=DJvaZ{0-#BaVb&S|pbll|A%S6V( z(*G=852xQUpnN6?rk9W4B7FYhQjMa#-z2fLc<>v-6;4g7&geaz)rxtj(LXf5T&|_A z*S+Hk$X&`LAA%aGV(3SelMS*~v1H6VW)wYBY)gYdjWJp7G8>l1F&*YRu;LO;O{GTv zX_i?xcUui4_jtI~n!Ii4O1wSFx3r<8a&Mitd)mMqC=!30iS<5u0*pVqFPd4pRLpd+ zrH(uF4ijhW^85Ruu=thLgZqccAXpjO zYEGwnc5J}bcpe4<;mSa6B`lNZMx*LKVNk_s5^?*xRP#;p>&=Y$&>zf_0cfR0&;=X?`M@aGC zy+ZtuzlAxVM?KfwQBKVVbSl7<>(<&Moo-II@@vv?B`O;kcqvky^X;B9t8wGA581PH z6`w_6Xdola9a&URqz)h~5Pdnh&6K|GFNfs0jSe%n*{i-GXfyjz(5=~&_cLlJU7g$V zNw9-X!}3Am}3_1-cJLx~b}?h=+y3B!Zj<)HtlXE>2JJ}xGt6xap=5lJ@8}JA&`52Ka1K zjRBVT#A7c(Pe)KoEy4UEX z-YoIE*&R(9LOZUmb-3pCN$*ILFKVpKnraxT&=DU^o&%$(Nd`0R={^z6k>opZbP-6m z=f88%>cHPa%=I}&zJZV_QE|;TpTe=#)ao<>+Z7N6qvlddH2^!en%mIMr_rHd>f=ExgNY)uLl>W7yEV*BuBXjsyL<<~ zBH|B*^s`n|wMCO=GbVln7=k2ehe#03+1lm_X?YB+WzCf)vG#>qsy+805S{G~^?l2N z=}i>{S_kwbp+K|jff-s=OYdI>GERVSJ;q@oP`;H{h%%=1Q(_{ChI2sl?RT15W5o~H z6pVSYX7h{EFt!@TAm^Ky^aaqe3fc!&7rW_YkK#cQD(mMma@yts+vSaJzkbc>RhK9N zYBVx`JHTrvXQF=Yke)ypdVtDl$ASdG)C?s@n%L4jnL{~~Adm;W=MVI$u!{#pv>6`v zXgv^DzYF>`Fz7rAjH4h2HBEIG=N33pI&I9_+(?vHh;I*(AjsqHIb;AS9u++r=T-7ZPeZ~ zmmn&wi+Oz{k0!al7<5z|vx7to{I6k{p+JHFv1+e2z1yq9nrJ32AnM77?{AfUV$hbl zF)X=I(B15wt4>VKy7WZ;@YiqCi zCeM~uJwC4*Eh!y}j+3Qv-`b%XWF$IM1@y4lWsYHfD$k?PTSd+l6ri_7rPxzhluW?3 z4eI?VNJ$$fH${Qoxqz{Xh1TZBKe{#S+3hH4XH1O?2W)2d^76(|ap}A{hS|+025Vg1?D z3ByUxo(G0!GdT`7H}>WTYRr+xBW&b9O(i&$x%T%^x;_o%7`a21p~v_zxH8`0-eSpT^l9bXvIB4?(+~YrsWxNQmhXETgO8WLz z-nI7I-#5Kx8VJ{a?zW;p!W^Eh{XE_WNq4b;5WBjxVhxWZ!nv_o{&~8D^)cU%qe2C_ zrf_VR1jhwli3(8>g@Y;P{(*BqiU-Wh1hF65EJ-Rr<`bQWXd!H&xykx*?kdiZKwEKP>G<=>Xoi zLmL}x%&t`v8ZI}U@qK51q9xh1C_L5lif76!g|%x1m5#alH=>mA1o<=sLjXS^d zePI~BVP4~_Am(^sD`KypSir zhMs#$?0q{&lj&(Ywcm-X(i}6e`5Kx49xi?&HG7e%mRlkn)yG-4xO zES^0S{88Vy*uTe^sECj(egGtC=uo}(o{ex`A@6ld1Dyy|ai+M>mQ@E`9fw-PNSC9*5_}?a@4vAJt+uB5;@4Z-`>KLc63bOrjyYNP+GW}N)1Sq^hD@!;f>Cwk$0f#r;#mdJGp^8udI71o= zEryRU?D;q2--j*!~sOBeN`@ zS*$+uM}o3(y!_6Hk11W1EHD*B}+Fub5@isuU9u zq~@PXVca#h)l^+n^VUOFWWCH(aNs;}h=5!KMJ) zWN6{Ln$gknC!(ww({0PdT;B*OwW1W=GI;zb!}e{a0l3a_UH~$Y{@|B0Y_;Kj6Lenn z*o_aEWaa7dJeaokwvVR)$UNGuzV5!-i9^xg83H&>$T#Xmwu%l`W><_0pSqpz2#`cS zBTgEFLB<`X?(Y{wj7E zWm0O`6=C%^t#~CO#N05hL87p}rv81TR!rC^HRH)^^jh@bq9eBoA9|g`cz#&n!pau2 z)>i{h`nSxji8wCLkj+i_7y~H7L3IZ!{L#J(kA56-8q!NsJL}JNZ6ny~v1g9? zjbyGgkBJg(X?4v!i^&;}4^(s6#TO6>VI~Qavr9(!9v-vKmH(QqD{9!%dSl(E{rTaC zPxJ*|OshsgxZ{8nj0kGjISZ$sNk3l0jgV=?!d-=a;LCu%8l2%i3%^EC8fDYYO2up^ zUWg?xsdqU4sMq^*Ek@R81EX&HL*Vrt*iJ2xm~O<_JEb3EBv)APpkHLh|5;G^n9=3t z_)JR7Ljw3McZ8?g1mgH)7}U#NDs-+F8I6}pvT1%ZvJKC==B z<_BDXQl@a7llH>1L3qYj_L)KWi2SuvMlS^0Cc`e?clFj|Yetg&E^PxQWseXGpFBk< zzA|n4WB!H)=2Y2Exw?*YUop-5&KXj*tdfXl~Qp>pu3*RAEJoE?tt zlJdu?lmO9h0)>j@W1bZZl3*^~!)k+hRCW6OG2e@+nO#(UY zOcfeZPc}mDG`80+6F0?AO^Zx;UYmE}98Y=@6nkWsJ^Wa-?sEj6t`K?bv*=eyC$aBn z(x9(Mqp6DgVU(@JB*v${CGR>b9_D%RFh3wsLs%RkJLK7+S}yDRDg}WdmF> z!c@m4Kcl`ZzLnuOri~&w5-U_0#UBl1cy2TH!OEw&I$k9DvUoBdtWAYlY#2KsZQQbA zF+B0A-HNfQW!7M|Aj(SrnolCby@+B4v5o-U*vE8NOg8v80NJ)hi*|;&ebDBkCdf`T znGB5xVtkY1Y`U-5R=%Frp5!PX(raqmpZp1~xYO6a;%N_I1;MYFu6|F+5>5_o<%MI- zXbt2Fi+WO2NxHOCILgRL=@`hmjrhd{`%-t{A-9S zA@Cc2;Rij0_kIFWq$8v?GA|ZHfa(p-c)f!~Kid03 zweq18(~&CTx2F6VK)0c(U8V>PwNxkzA@99t=)sREq~Hr1S;_iS%_;Fb9yfC6FAq)s zCpo9rd7w_Rc%OAG8`PKTsV2F6m-3JE`1$D7LG^?5R?|m78chZL(IA%{wCI~ji*rE2Maw0jqLlh4?h4Nnr}(sm z)Sqc}`Zwi-ODza(9RpF?z;+*(%b2ngfy0QuhfU_|uh+OvrY>mVB|Aq<;rwxd<3rk8 zUwf(sPpD&Hlip6?_k!S1OEoE+ExnWR$fxFq{w&N~HleB#AUWAzAsT*_2I4`s{bn=4 zyY~@Qjok8lVc`LsD;2wv<0vX;#_+u%>YbW|=jpi>#eMl-_mmPxhGK-A5j&K^SB{cy z%I+_B`L?Ha{o2atomKN7Q`Ighza8APJ+s8js0C}jcyduDrMDL$);(O8?>AG=7;9Um zD6_?))EeF|$M7SJd2W|6WlsD2D^sGfVdF5s*2a?YmkT}?8AZ+6?6X(FxgD-T%B{H) zwc-wiS^0!Obw>;*`bQPH-!mq%&oOTRElbn=8jN9s@Wd|}uj$(<2q^>yw5*^pC` zkl5RkwF3Lv&-bh&9Nf1l^WFfG+k<)qj35WV)dzd6euw)*NWC5KQoSPWiCn*AVUV&wKgd ze!SX)xyI{UI&HO=MlUZKHBXAE3QgPnkV?jnl-WOgdbD?Zk9GF>q0juDAjeSvDE%<_ zCGvHVb#h${&PbnC{Uks;Y7Al0O0`*X@<72Zm*L8n$b{Z$E<7Z)u_iE{itkU7)|?_9 zOSzZ`|91TPW%SnNL<1f~piHoTp&84N0!sg!yX#9FOSb@-IhhQ5`}nv;`2;OX$JX4N zV4=f9ljMb@+bxe-I;1_44P7wIYCChZK@c~t!wr(=$J~^NN=wl z2Rv2s)YkUw=U&>@-Z@{tED9)zkLg-}nP03|_hrm^zv?O}vP%E-ARSYSZq_ulTJJDC zFV z*K?(fJ}+Lpe@D`Fzg4HIp1gO{&FylS;HsI8jead&=ZIU6C6{g9ju}TW>(pr~P%zqG zg3@{DmRkm-Wli~e(kdZmkRIf=&XN*?Yy(VFoNZ%`}M^Y0kxdIiP&KPR*7 zzNizuqyd*AeKrwO?GXXXbNyWXh`E=SyMci-F@}{wggHmEUyI=N=7EiY;g(ctRmGWJ zHhVaa#vV^&RU*&@BCq{*mkmtMGpvc0j{Eoh)#lelF;JJnFb##g{JrW*@iOXXg*tX*7EvU|6YoKW$~5cw77K2FuJiH-=r zRhc#COVGc8lp2%_X_<7IP_JrKHV*VHgFsp8aK@lG<7o6374e)jz6J&B_b)W`DjKw~ zt8FgaGCaOMxdLf#Kl@`UXV>W_vGl8MzOFR5PpEJbcP?8)v6bmD97fK!7vxGT~@|qH(s%ZC%YjX1X2V`O}{j~^E2Lwj$C#n$A@Yz z>b)e_99Gdesg%QXCnc^bww3MCxL>8I<-E)N?hfdsjUug>nyBsl`Xt0DR~ro!gR9MA z6du2j$baqxvC+s09m)%bz5LKs=C0ba!UKtXbCUgg9J7y6@0m7I$!Iz>j$H6Dse>sG zWp^s?k6trR6+b*638y?Re3j*=^__q!!Sk?qF#uH?9^jeWv-xfmvC?I`qe!)Ed`WSG z&rkpRt2`_-<1)j<`!R|SZ5eMz)nN_U(iwKdZ6qT-HgSN!%;vZo^+m(7C{!1*tJ)cO z-R4D%2wb(BN#jCZMHuZATTZZxCBy`-g7RIoVk8WvAX3_GBHU?8Qc)7!uR#jzYRn3Hz>pUBA8>6dd z@sObNe;I+&3}^&cht6u{V=kiR6&aX477LVjm#6aj+A5X38@@J((le9BZo%oVQ6qRW zS$*do6*^VTlU=C2b7CwKP^0zk@=GpRh*m12&j8^XZpu?GAsCASFR@_FO#Yx?k&K0d zk94Y3ciffswD&_nP($w;me4DJ6dEMW(>OCIE})i2SWMgD=9@y=v+380s8C{ki_~@bHzsWDzF`f89?i9@_dtBwGrpvK99g zmNLTmgbU9uPt3i$qm_(dXNhdxcKx)$tO$?100Kfq^^QDiwCgy^PCB2N&Aia_fkw!C z9OzA0Gw{@q=h0B6o|Ret&1 z!JT;2c)q2zYq(xSrSFbr_56ONvR~FFm*@j#F~d1lszkU$u*R);N^AWh-Ee<>XBXSN zWQ3rrE{_5WbC0~?{-XcS{v|_qr4yILjppLj8e_()Rh>5ynumQxN~bMUof)GK(Zf@= zttGSNEwfR3LyvdtJx2?EyiI^rI0Bh&x9;eQOrj?VYxh`p#&$dSO0st@PO-(hcF~O= z4`KhuUdle!rPlnGbdmT~X*Q0lxXe2EZDq*-!H%t-47CnLVR=gR5&SZ z=qY;TFy-wzt&&PCIJmT!8Y2mAyxTNraGOr8#HLm^oc6AIb$Qy+)t6e>f{X7sQ8M4% zr#c1K{ynh0?kp5se8u#9T5fZ+kt3)QOFP;uM(3V0Y+NQhJAa$76W6#pp@b6NEtvR#sY_kYM!*$3J&qZ`c*NoNOcHos& zt(6h1;nQg)1X=GgPb$B9Ycp;O@=OTRGs}O6i=&E6E8{o77|##N-z)p*BDGsY5bkvO z=A#msWv@}JeVX=X|Ay%gR|uMSsRdlEm|3MfUbmVq)7nYIo|QTKP^c4F59dsfZ(b{X z*Zel9-6ZYlvmrXfCq-({2NBlZX=Cw&Gs+s(FQo1EplyED*^H?<_vw$FN6r6vD8}vFTy-Vsp%S}XE)0V6Lp;l=m-aACXV1`* zuJVJ$Kn5lxc63|7UaUo!9m2BmX_Kb0b`=3KP6BC>A@5x2V5H?7viSykeWTVP(XgoV zG?}eS1a&aTCoQ9fFU6$&Y7rI{f5~_ArjkRXjX9HPAZB|)wexf5Y`r^7S^GA$!lB3b z9KAK&%53XYft8XgPIx?3-CBiTSFqERA6na@M#W=Fk5C=3EIOd$eEz$?vP*f9$Vlet z+}CA(6unSQ`UW{b9krYsVbOmM3$Jv=7yYRvfV{{}&XszXoXEpU-++AGYR_1}Mh844 z%*=}48yD*h6-JRPZ4yIpHLk?AxVG!9OfRd=GfHYE_Ab^i1iGJ*-U?lN%Xul5(6bX) zPIh_;43eLZ*k1;yn^qg_YIb^mbOe-qQ{uevn8Cj>5! zeG~^vA}o$u?KKwmBlE;rUG>S)`q@ECgcz8(5%am9h;`Ti`ku7uX*~>!(!D>f^>?v)H zINryS&uu7|n=flV=NXG~m#_*+Hl8nS0X?nvj~fyKQM_B@P*~L{)hHAi_oaoQ(O<_! z0RhpokRFV?OkMGuL9lz(2+=^N{U+Gzba&gG7y{N|d8ILVnqMvIDmUMo@i8->pUy8T zN5)~x)L1iO#Ya82Y?EACw5_a+5mNqMEXr^?l00Vo^>yne(MluxumbNUH*lzAo2IaP z6Ne1t+B8!LX$<+zX3W zNAk4^z>3!Tt3{+dsnpdLnUO-_X=q@7bz}NU#Er$y86MmF={ei-l6QP*@fVyB>9stb z6;j=*SsI51x!n^ddHwCAldMFH8fM&hG0t*}R9&+3#r`syBRRV=&@*FwzcQT0aY{QNMJ+H=#T)(7BLM?2m+yj z%QaUnGpdP7Ob>C}dQ&QBXWJA2?pK`aFX{`VwzHwTCjr6J!7_REerxszl*hL8m*iua z->zZhNR;uHl0q}JyEqkek}$I3PP7@Zicwo&fhm2j(jh6_Al5Zhi2F{kBhYWX)|NRn z_}NeKE}#yu<0i?zrI0nqk}w=I8ZVP)9c?}we}mn3unZ%&g_s2qSn`A$hIJL80w^-gDN`qiyn0 zhnOT(O~fQ>ohxGP9G?mUV)Fc7QB(BgHajvOR`6wvLEE^5&o}x#pSPV9mGM^7`wNdl9RICR(eo7;hT8~^Hk$}%JCAz8%jn z0y9of7aw$vO84DY7Bov|30Ifsi4K>$uKEu&KyiK`1Y|B%AJup;yZ~kjDD*75iwa z%nC{daUtkS+WHkNL(A&x|8*NdlH8s?(mM?w6@)^bfJ9ZuoDJrBPfN| zlZm&QEW4u5`d`gP?Y~_M{b{z`Z|7vo_@n*SKYAVd(;&o~-&3sJBCoVv2Qu)pWU!wF z{l!A-t|<67u(`^MMn_W&IZ<`EJzE#E)D@&!YajR ziGhFZ7FW&6HWoqjbOa^ABjNZ$!ilw?} z@GA&6-|rzIu6@74=ocG)Q^W zt5#e|(EYUu_&r81WH)oKP2`9J+v0XV&I#dO=5x4;Dt;8<6tMBsQ*H6awa&*g*Bihu zS`z+E>ukAH;gn>KVQCKcKT)R;c1>rW|75E9-8shp_Po#^4CMaZMA2{1O+5aeW$@=c z7{mWLzbNET=AYyt3-g#Cn2_Q%mI%%WNVfA+)e^Nl=##$J-|YYF$6)#i z`t*X=>~!}(|2%&|Lrw>=;YamD`Ac2?Ya~7Xa1S|hu5pOIH?#6j&u9nkd;|GM0A^G}!cKLeDXTUfO8-xuS6*bZK|OYgEn;%`#v z{`>rI-~YV@n%nRqY@R2_b9ZXfo zVoN?=Q))1pm~8qM{XkNfY|g=&0FCk1r} zrikA$S?{kRai`0JMK=K#{J23UJU&oTl0Zr|*Oo%+_u-B;zt0I7(7?d#E<#=vh*knl z7|3R8HJ-uP;0uIy-8}FW0#WhLLp$*FqioU1OZ#+3DhTAoiyDVNybCDA2Mc{da9!CF zhTCDf_~;9ov%{r>+0ks)pyhu_ZL-5+vH#r2|0q<|6Qn+)rIC%#|FQH$pZibyf9Me} zp>YM!PBpRfzJHOGa1k*_j@@t``VeQZ^LWyQ0)0EaVP?b8ij%Q&)SQIu6+NO>Os_s6 z3ScYamEa*kHiQ4nW5QIbgU44=$ccl#VDlJ6p^7`Mox4aW;faxLAgm>iO*zNo4nBTE zjG5l~)+Lw}ow>G}J5gig9GMdlQ-%$l%MBXx$d|S2?kf?Ptqpy${w#;f%C-2N|4)q z?-?E92T5xANlG}ov8`!$P8ZLj*-2Ih4WTgnH7dtlRG#m1y#`jrou0*9v+qeOBbXPF zitX~nZ$-*Q2Mn3b{V#ZJzqsi;1%~B|)wig7+MSc+ygdiZ$L!Q&xtyO(iIy{{r@H<2 z#d-AjZt(iFK~pFekRoxqFM z@QW?Jri;zW_uqZrC5N>3i1)sud~13JZi75LWwiAZGDcuM*;7n)6mOek#6)1E!d1BO zaP!r&qAM~Dro5ZnRQwsvA6IgMU|arWl}&Cd>69Hh`s-v5c6tzhYGCN^!ErmBs;oXS1KYGI`O_%TZllo)4VJWXg4fh2V=jIZS5KMKE89qOknD;#)ktEDW`%d^ddM?D2!VR2t%MJ1XW9--m_ z4qjwCokY^qw(xYXiqH`ZJe<^FW?zQ)p zQtq`=wx^YZvWaeba#z?$tFj0yYod*Rti85aqj>9f8eVy^mM`Qs|MmTMR5&i2h`$2R zU)*Qc(3Fmsp{ z$R(NCx-GJ_Xk%h$Zk)S+=^<-ZBb~-E^X!_r`HfcT4&~mFgd75zAx&oyx6#bnk+iCV zZW;fK-LVhbXYqu+@)$TV769pWb`{Z2bYbY2I|VT!DNp};=9M4WzjXtXX_w66PB_8!3NyM7kbwvG-ANmI z8luFZE)&{3B(yJP!bq-jYtkvjdVSK+1OT`+e$;h%4enDP1bs-gH@kdsNGb}GY(yd0 z1W^fRQ&XuJF}o0Eenw?oISgNP(Mv9UuOFB>fp8VA;u=W}cojIQ;x7{TvhUZ|1L3#%J7b@Huox3cU_0xH&72{z z9X;@IhXr#S7d;2X2uauC;^QR}BSbQ|I~1uc7~h&D`rAqx=i(XeY@}YOup0U6AxJW6 zGH33tS!PiVQByf2CRYr-{6_umE?1&B)V;>$w9ix*K{ee;FasptT`Q(6Y1HA%!p~-t zPUFpkFUI|unNOziJXfPu2vOnc`=ksVYY5jC3r?M|L|4|vt*^ZBrO<<2C)9dBETe2; z+45GH>5xusg*INPPQDojJ^k^;WVL1fw!aO$Pt2|m0>S(vAR$4Fk0`#T{dg9*f1s)t zUc5`kOhR$sHAbZ1DwAzcv%c~G}_bBc`7MU(< z19^r=q;!YgjvPqKBnd`wli+z%l(x{S_ZKb@2$VPNYUp-@Ck_KnUa_%Sf^kGV?}IsZZcMR>R{(@&G^sL#01u`4IF2&*eMuus;nZ7E%ickMhzP4$6Zs4%-=cWH~;p- z2DKl>V}@{74G)Ixi`>>MAP-qKkB-1#e)mkYYnXz$tDa+GWp;ND_exX`d3U=1*#7j% z2e10aR%jPwD3Ph!AB8Q5y2I`M4soS0yR=BOKt4xKGn1Bu6#-j1s2dxs=`l|(3&Nmj+(4?pF;XTKX z%ko@fdO<Fi~D-P42B_;0Ub!<8JtZ)Ml-d{Y%#wW%BtGbX+ZSGEl zXX@kS^C(P0VTEYxtELM(uX$mOwlcX5+Hb%d7GJ1$veYHf7WApf4`<6ROgz`==8Z*$H70=<6vPps! zu8s?2?&&IALADMozGBznu(rE!(~c=A#=TqOnVnjWZ@vdfdIEB_4%pRd9Fqp`IH5d|liA4!wGo9DxiEpTms!GcOmr zY*Vo{dx4fUFDv`OE_u5eF7#9!C^!kBMjc`bx;jg6%&{xpU3N=us}eq69t@XHlzl_a zBR!CU5h9Pkwd;0&&pnvl@4HUMl9$+40IFL2lbjIl#<}qIeJRSr0VSY34HS6b$UN_zTC1M>X2GtR z!fx@{TmgP#;CAT3YB;sc?T z@rlo-Fv%4?2hYem0^%ay;2){q=y|WY6aBiU=YTodAq3kqzi)K&EvP*fyCZWS3p*Zn zvuzmd!Npjjzb!Vr%|%cA^lk=K3e&trGaqf-f?K#|wrXnQFu9L4XPI=4sq>I&?|&R# z#I+CBXi&jHjD!$w^80tXeH=*%uN|(8)RoN-jD2}xMt9wCvj*klc~XxmRLQe2u{c%9 z6$^M%oJ^HG1MV>i@H$$;xAVk9_UrqKC}sm!szD=6a@Hr+gtWD%q;ePhT>2 zlOBHo{%(_;hLz>-0mlIa6CxBYhUu|rR)|O(c!tD8VmIT1x5by)-n@0)t+w6hVdpYui(YV}gv*cFh4jL2b*B^yzV&i@}?CC!qnBF`;|`Jczdk;+KWL!t{J&AC6G$n|MW z%ITo&a<3IYwbKkN_Q<@v zC9M^#!zCEKd5g8>12IdY5(!6Lbi7eom+AR6T4>7kzk7WUhm>2$)f5`VReIhm-ALE1 zWPt`=-o#wGpXXuHM)~gqtz<5zkKQLE#b!0sdo_ajzcyzUDkjMVyz_kTzsNMLStPMn zZz^brbG``B_F5mECJj`gLP(hK|Hj>0hD8;%@1rOp3IYZ#p&%h4-2y5i3PVYEcS*xg zCMYQ&-O@E9T~Z=3Gz>FC2&i-`Fdzd1XN~dx-v4#Z`EWj*>-?{?J}7MVtXX^Q70>hB z_j9kLocFQ=t4|X8^e4a2Ir=2=%4a{PPtA&FDs|qVO4whA3SPF?@U-(m?4XOa_mFRQ zG#X04%vJ}Hjjm_62*KLOs5vJPdg_oM)$OsdPI)_iruosL!yLEr&{r2xPCDN&&PsY2 zi}u~o50xr=!0}+UR@X*()q9r96qRg=(7?SrUE~5*B@jFi-XPZUt}~>dan~I;4fi*y zDzvGFdh!Ql*(>Kvq$sd^^3{1*sgm2(Biz|Lx&c{=S<%FAufmacarEhU3KvU$d~^mE zSaKx!!Tv~Ol`BErcH@&0`k8-bsbxE|O@m|OVGZS6lT88q!gFp0$dA$dk#Y8r)!CMo zY*u-}qDr$YACLFMuWU6cwBv;l-qO#<$wJ$+F3#II& zCHbwHjWg3G^zvb?5^gqW$?FRfSF`NMw-4ORt=1Q>fL9QfP&@abP9%wXR2(&THE`i} zL?TUUs#L38Cse*%F7VT*^rwhxbJAe`eZ5GahJy0+X4lKlWCvv>C_~0gXpVEA?ya=~ znqJ0~6x~q>{NS11yw{S}vld^oY$?z6dD-EYg77+b&pZ`%4{QZ*%={g4g=8?2j$X@e zlvdfVjVVe^@7Wu03H0|;>z4{$;YxkGrtjdr(4?p9HCRDHlVWkL>RN4UMj`rR`T5tUkL}UUG6>dCew=*SAufA$g+ql2zZ+vXn!p)h zCo%(!5k^YnT)fE&&;$!|VS{u$y+GH*9ao z|7=V*4Jj%04bIO~(hKXYopf}o8LeE~@LySq!R+Re&P{ud$Ctvm2G&35kjF1Qa@|h? zuf&pBW)?+v9H6$uSM1wx9}FcTBF_Br>H}vL*7_2aknK)>+#YjBAP~0k?MhreC775^hxW1=Oz`K6G?9#?TXM z#hk{>2u9|=h$_Uv_S$D{TxHy;R;Qu8QJaOthw=es1xgZ%xD8xtEyaxtZWx2m8=5n= z*~6p7V106K%+<^Ix;kzE`yzYV?gAkO4GG@siTn;I6}WMu!nOF>HJy-~qeeqcIsxsY z8lBbbDIS-jhSYo(E!9@9hJ#o{Xmx|Y7}utz!`{BsjpzQ^{O zso0Gp;#4s@j#3Bhn|H5>oL%OSq*%7d)#6zBW|}Pkm}7xgk>y8_g2*Jzxqu z!MZWFHspY+@HA0G(9&dO(=*1$Go>eociVCuZ}6DO*B9nFl-J4~?6G*$G8ZJAAu=cX zm-i(2X>w@!_4(%u!i-ni2@_yZl=1S~$`fp{Z~w4_>jqK}9P>Qdka4-#Om_a`=pCa4M&^LDXENn@! z3O64nmgAr4x=`-+NSh70QXvKBx@V+S$pb!m9gg%Ozyh`U&s7FJL=o}l-oHD7Y8Sk}oj+*y*dfsZUf_nnN6I^!uIIlb?M;WEh z?{CySG_-1xL=8wXwr~#%!W>A3{udSo(G;SAnO?j?EuvD)=rDjzMY|O{AfxY0omDuf zxi%fjN69(5tR34>Ru0qE>lWy7K=7%P7udZU_yE*gp8iMqe!$l29dI_xFVIGXcD#RE z<9+nexskp=idN~&k0;j2C*dw>Cy4;CeD=@Xwr8tN5#Y?QS=q)#C&JY__K!p>S=TR^G&W&LpsY30L}OQXFdRc+U=j{dXB4=}%*2|MMH+hyU$f!2h)FFB#x}F8p7>S?_qkpTA!GTtWRn z(}SbI;yjV57l3kW5AG0#DxsK%{0&(@zxEhKHR?=%UWh0v=h$F=Jh3uUMdP=%1a5n} z^m{3J>i!e?L|--WAZbzHoCT5n8SD_7*2D+x;^Mb`7cc+W_d%Uh1TgTa(0xsSE$#uS z;yYFqqf~|Z3G-F|^-llb93><>s>`-$ktJGjQtVdCE~ zjuaWZM$l6eM5NfxE0pr$5O@VBG#;K}5*?kf0E|tcg&#v>QY^?N(L`WYL0TyoC*gr-C3pFoUgbI9?ap_0-SK8@I&nG1G7Trjg>~;NztZ0 zom*Bn&spibus;02!bTi@rm9p5QK6`pmYafP%BU19?Nj5o$JgHRy=^)I!ukr`1N*x$ zJ+C}1jcB>hT!5SwQ2yNovx`$MqF+$S6@?rxh0N5rhJ5=l(jH43H8(LS`}#7obsB!! z?w%z$me$_>A?wjsQY;o8k^wS9xr&qw^&xzyMoKrIrL z|JS6IARIDFO@K~mrJ6*K0NJ0+E|&sZ>QKn!0pX<)@g~>MpaP^>h5t1 zvGeFVUSIFF3tm9>0ogaxH4F@O^opDiVe7I@eaeYpyNIe8PpH|yurOn!sS**W=0<R+hjMrFyuP!@$9<!lf18ch1 zvsBg3V+y;^yYoNKabABHlap%P>LrRD|d|zM$~K~HFyQ)lPw zmoy$qUWgBWGmy;VClkgFnDM#$9EaQ6Vl?|-d};fQx4NDe*ro%Q$fiB^=Nb>| z)YrmV4PnpPg@JcVPx@Pt3pBAaIdbgDgkJtCFPYh|w#0Ju z1M##UNvl(&;bO@jE$Czz1|pbbucVwyk?NRHeqm|*#%sBXEb1&8G<0~pC1fd^#mH#H z8@@RV?0901=N8FdaAEBH>+asu3mdqwtP9IUnUMgz(`>W;;G^JXC|g#O93Q_cze|mPB%-1lF%4kbRk}u_!!j%S(Ds! ziSJbJIzlVvkR>LBwuZ*%YbD;QYM#DoDBx@k4tKDBFK7)m5=uqLngwg_R!?tK3#>Ij z!|gUa`K;r)upEP3iZE&0=t6*Vy@yYWXRFGrV&6`5j4UfBl=Nq=N-i!>Kko9@f#CqM z@Naq7dbl}sHGo!d*67$Va7?#T&J=SC;*pU9QgN*Yc+SOn(&Px(g-86Rt;@aBI zl40v%O;cqqP$R&$ReioKLDJ0ICnyjPN`VK=4UTb z$=VEE>Z>d;$4cQBYz1oiAxut2mGckK*v{C&8e{&-bkX~^GwqTMvy2CrhmcOuD@5u2 zcYzHET#eOA(=M7l@1^ic@qJP#mVpH2=)P@w-k!%cYwv}AQr67-nlz`zeS}A2X6`xB z%34b`c*~C)11TdpgSF3Z$?fa&n>+-Ie%Bvz4+x0dM^SO!?zjH*XwjQfMC96c&Mjn# z}}VboAo8ol=shwEE8SrX3$o2faK!?_y{eaUxyuKehp z$sGUcG<7FiA{`m0tA9E*VWR1UHLJX*gRWV#&BM~Vv1)7D98J+%p4_sND09zja-y)8 zf7nmfDJ_=3QQ{=-^SVez`s9AWJyG@x@uKwNy@3lLLw;d?vOse5(|pj?hiDh|<9Z~& z(vFLsQQVj*|~su9O)F^P%UCLhQrD|^n=!^!e5x(> zk2I{62eVPlHERgN9R(5QS8AgS4@u52Q7+01sq7UeXy(~Om*!ZLmU66ZBV^$SE?3XYD|~Wl`!o>~(_iA#na{#7 z_Ey^7oEPWr5SP$dg)jIsd>4@9`((WWfo4f`#roWkNfxPWiTe^+J z`vnr(`22T#SL55`toV$A5R=S^?aO^AyiXZcGmoP(OYcr(2{Wi9YGoqh*(YvY-?VZQ zlWTY4MGm1#Bbr@rY8nKtT?-A>DLJzO$SEQHkBnfsm8|DL3RcTBFdPM4Id9N5)_%J7 z_y(ILk!c`jvK0?|ig|Jh52>Va|9*OL9V(75hNCXJCN78%w&m$MdOpawvUS>}Q`&GS zrt=s$Cc|%`q^D~ZUHoDBuTNVhv-P)__8YELm&lN^pNeuE`RFQp^chNVA#vW;IH6aE zU&gT*#M%FNDGJ}mYnft7xPaws*F`L2?GVJ2pYR!CUY5v*N62#Zpo0)+6?B_N$FIx# z_S>@qYml9hN2xaeWP^+jlG|5VgbQjxpeeXFdL1@<0irNV6*WEMz;M7kNeGl5ZYbm* ziF+mcUyE*Aa5BTI0uJIERNb+`SHhsGqfwQQVtEiagT!+G+T0Dzvb`luipbwm5K(U? zA0?MKPcapKF42^xqUfLZk(nfe0tQb(`-2h$aoaCV+#k!^T)zKnHWzP(?RMyglAe+* z2fI)eh}fIX#0=dl0dg6E6uewNUg~na)$VYgK_*n9UGdtFKQER4wwJ7@%jb}7Zu=LP)?A^Kn0dpC%LOue%e4WSK;f<&9TQw*97N3V)189I>c74$18zJT6qk_irqmK= z@o6{Grb>=MWbARDT8CxWBpm@61tZtNHjEcjQ5_^`St1Kz0KnruMc9gHyoj_20ehop zHU!k7^3L?h2oegtK-nKXj39sfU+nd()B|EO&aQ+m+mJAcg z5CbKY6ew=+9gJGOO}UBb_Y85f zM7`dijGAUe?dmfv+fS^jpcCW0tKvv+$2)I19l@`>yx$8xz=*2!H}I}&Xxv%}H}&<2 z;HlAncV41PabYyp2!@8CmuhJ_S-zRY$4dJOfSiUzN#n$$twgee1zkt4ecY{n-V&_RYTC$G2zy^1u5Ls&Q25ng za`@_U4oataA7uh54iBxf`(I}kNGW4D7x{g9hePctw?Y3?D|YX(+S*#4q&~zxUZ_s2 z)!T0SkS?yS7;ukktQ2+mX1@ptbN-4dDyg&e-~nh4N1aI@Iu7g#$BU{~vaK$>(QN!O zKhH3q2LXwT55n2NO0XU$`e?oZX6$?VYH0kIzG$ZrRjP#Dz6B2av10k7CLXo4(K}Uw z-C;1Nl|m{?r}hi#h5UoStP~aaJN+S-4$=#AzBx8OdCB=u*fFtB99?~*%64FMr7#O- zYaXXI8K{Kr|-$dA!rNY6}eia4|SmG&L4X4|cnWc$=x-as>jWJSX%qNRiub;cO;E`!a7Io#Kh=t%4 zvVft0nfSAxDZ4(h+V;5l|4LW3`nh?i)Ozro8?E6XC0c2#s!tU*!9G+WZz~@aH!>bE zC~u~c$d9~F_PmINvWoAlA29IbR=>Hcw3x*^+1abhciYzoBb?RY{*Ovqe3!DJ(#8W@ zK4bDsms+~>Fe8?02vsrs*Uh-#lQ{8#RS&``AH(y1!+4Ge^D5Re;mBCGPZmUsuM*Uv zVgUz>I$W}7F#6u)n*43!^8I60*hsmg?)9bOzS|+h3>mB2s&3GDIsA(HT01H07n(6B ztfFP2YiB@07xXz|`2^n~KbGZoYcqmf<RH%|R!BWMTNYUDBXDBL_iXY+%uabKGyh&mLbrdogssNF~zwx22PB=*k zdM~{o+?Aaus{O*3hh&tsM+PTA!(ZH_LGOaejI)F*EZ5-j|!q?H|x(+gefknhoAQ# zK-ei(N0OG4pYe}MySuPEH)jI;3+1r*4ct0K5H6Q@(t9itA`K$;Ba5HrEuyESBWiVT z&#h&-tz%MDcA!ff%qu~QT2I*>KrHCbaue zU{#PN+NW*)$=rgzq_9wlmnBFW-n7ay?RB^zeffcceU)wha9zt!ij0t-omW6K#vj$Z z)Nuw3h8@dPXU-do(us0Mp|1G8A)i$@z(A}^p9N^|KryXy2Mwc%eIUPq3YdaPR0&>q zlMX0~fggI<$-E-RlFx+)0E(#8@7YMN#V@GE1=%$hC;BM3D8d7Q)^>2mDbNVr4x5v7 zcvx1RtI*oAAwKQi&5S!*l7}~KHmVR46O%o^6Bcp)X$ppynoQbu<#F=@7LJ*k!QNWf zIjD}KpDhHNcg3){RIyX{rjr8*2g!f*)i13(10AwqZk(S_^h&Gtpl1b(4&?k9;OBq@Ae{4+UD{Ljfs5P>J;W-RZJ zBdR#U0~ce4UE6Y9Mmj`PNgpPsFRFRYFF2DtlX6g0HcINBdBfgW62>slvYO0spjqDO zaBUsv|KO%qOK69-cT?n(b25Fnf?iJ)N=cvk2ctc$$e;Ky0@DCdG5FHiYRKz8zAFx1 z%HT*)(C&DQea!0>=9H<7k?xtv@}?~AH9abdwFn6cM`&8PqKGm7!$goiC^hsLU99K+ z9!AgEyBy3ATH*Li`=^x)Yjm}bifX9@>7*RJllHOyfmrMI)-&Hkv3e?|tMllWodR&jXd*R-{ zb2MH6>Ja^v!S725wIKjoMWve-i@#PH4iX|^1#$YoA{9g#AWq^sE6#_47fGab{y%L_qC71^`4@13 zJo2LWUpnu9r!2$&SMwdSi$DM8!E^s?YR<6-0RP}5{y!)F5kN=BZ6f=?L@`_n7zuyM zg~mS#iJ02|V2(Bhl`j8h%yGW4*0@`f{x{|bl3dk}ZA5MXQvBa)K9Ca7nl{OKPgq$A z^~-NPjBNBojEaWRkOl}RMfJbi!1?yzOgkcw^kJZP*P8bNo2JTzJGmeDc_~6uRQ^^) z^x+Q%C3yqp(uXY?2K(RN0y!JGP8bLODgXa}(Y=Id11PDFnN)P--0VVy@^Qrw`pD4Dl z_INuRXcM@2L;v8g66JhO!ma;>WeLSUN_}QJRWJpX5~g$)BOaYVLM8UDyVQRMglH^x zc%0(@9URt0{a|j3FZ#WH7#noZQ_1Qh`gk*DP9Z=QbO%ENpql(H0l)KjBik!nTqYZ} zP7cbAw4D(*FR*u_XNZUMAL~Rc?k}g74k2>{k!JY45fpAPW^3e?fUH`QiC-_+HkPlB z%7+}#rJ_+KzRd@gHnOQTzw5otO8b+rQf99R#etRWsr$+rD|+AcqtbMSq7FX0#Un;; zenW=TdxoKpQtFy*nPHudS@W@YP0eE0h1U!n`6Vs!N4LnAeAYJL@@D_N3arx+Od>$X z#kh->R~4nmX5f0f;W{~A5%5<{n^ey=%K7~b&^cND=|cgjvQ;WBouBG!eVtXRppz&h z>Xz&hw%bOftixY39a$vmonxf7drPzp?>+tBInu0Ven1B>VQFQI6 z9#7`mFKtZwYS)CyW-^E;CnUZ!yvx-AZ z6cQe;)uMODIL&}Q!ODgszb=2I6ym~_-rcu0adOpxXkDXr^sbMdimi>8w)%rt>CG6Q zbuc<1CV#9USL|(VKKXN$^cO{Pibsf06s`602AjxLtdTZ3+d#@ham1jk=2q}dXRnP2 zDaD(VC;AQF*W_D*s$5%kG+q3BnwYvrWM;i`W&%5<9u+1f9dR}(Smr*1VXp>YU%5@^ z$*}s}d!djZs_oPDCIXx3WCrUbw_Tq+uHcebCT2Z}gEio6pc4jtyCbdm0v5vxwGdnj zW|i6PBXBURb3rW4IO2T`F2t_yTaWA@_0*zQ*%m*zi$`t$5(G@FrgIec=g;K8@X?-@C~56uhy-C>6waXPzoID_h3Q9tFm; z7|Xm1WPKU#sEL#H)Yq?NQtt(htz>Hv)yKesnJ0#NxJi)5sZ2LL=B`(hgo%5!+;*|sGR9Y~#O-*Q5 z*{?|zsp2&W*>Ex|*gsyO8D_4vDj8@lw?ZEsAFfGpNN zXcsw)mJd3h!|i6_gAFG-efYovg`HnjMQmXofo^_Ef46@ zgNz$D!&TIfkq#S(*^^{(*BOEe1m~rlpK^?phXQ6MxdMT18ShjdY?7%3S%1!}rL+=w zMBPzR|c7T@IaIsKPd%G>Qf>oV6l!bCwrWuJ5IrWzfQ6@HFn>qe7;Il~z-9 zffO=bG|lR{)ZKS{kBl^g4g{GD&^=VhR@Ahu(_VG@plKpJD`1(u!c)R>wCdfatu zDpFWXACM)E0c-QYZ#r6AJ+MGO365n2eqSTYVR}R z>M05NM<=bRpX^OdR+V~pYF+hSB355TC8gJbdi**MxYib%SX2rlztr1=Ee>shG)Ir( zk}EM8h4OwoolPaNDdani1)q|W5cW#5!=^)6Gg!m@kROQ=$eV%$c~Q=ok@gsQ9NIAG z+nSU00RO{iG4~ffgFXA<=W}X08;`rMV!P(|_K%<3{6jA0qzL2aTWq-#g58+ObGv~K z39?Cx&Pa>RGFES-^U*OOHRb!+Lscc9%U;YlrVcTFDyo3*4b z=@z;;xQSkL!LLpW)jKJ7%ceX*C%Ob7Jgb-M^LKp8SvTN@wSKPB0}Gi!@T-~AnQ8_G zrHYNmT>)aoFf?oY?H+Bht2{c|SSQ4mF4gP&5Orz}_(-ZQ^;G=%Ad}#cVu!EmC-|i( z#KINnwRaqb(6M!|#y)gnU?X#eJ&~Y)^0U|`J*w5LmL^kl`n^$h%awY!q12bYc>1TU+zn1 zwBngz7R%K#Yjn^!*&$EEE@AfUUF%d%z`~$)=C`X6Ic0xm9asXLfK-`!3$)}4YLF2q1I8@AT&1BHg$0=dpz6HCDY26iQ}BsnXu%lFMGD0RjR z``1=#(rS(>Pytxy6cMheDY8`bNUGW^sb0p0t%ig)NcT9$QU-T-&29$kZ2aw6Lx8@A zS36D$DGiLJ!Efuq15hfXJeGiUH)rNL{kSzwO8DCc&6TGr+ZmQni{s4#JMV)x%-V!= zJwfhAmCFLB8S9BIe9#~fX=JLa-5G;R8jeKSAtV31r56`~E8InmYR35rT+ZxL> z2;_P+l(av#$vRYVI9Fbhg7gg=GmOK0X$-nneuUkuYq_2AU0D3&J8mmOh-N5m_GVUJ z-qcnv$>+mEnYARk%%JCkvOy`eyL(Rf0#n&)%Zy;}!w%eM`RVLHxl%n7zi_a)D@??A zb(dT({<;n%ub<5p(LPdmb~EDUg2}`y>$f+;sivsktvD?j%T~JT6DMde`zu{}g@^*~ zNX}~q*>#M)c>y6^7B&k@;u~1yU!nnW5jy0SpG1?r`sJMVX4CCfnCa-SSrU&zuoO80Vet8ekl z&n|MB`#GB?$Ka%pR8fb#cIBdHkBgQPH-#l~9l2#*?@p3KcaJecnB80IBTqwq@hVC` zuAwc4Q`@r%6@byv)hD{(eEIsJ%?zzp{Sj;MmqG)R=ov3lO^#L;N@7mlDzh{)Kg72f z5ERxd_g6Xj5zidXb}uHGsRVULz7=~(HhGspzGBq?KfgHZfJB2sWB`Pid#cUaX7GZ9-Ir8Bx2@YMCuP-KP4w5+EX?LVM{q5womD}LuuwF2T!I=#S)UVlaH%C#1i9; zO#>{Pq+hMVS#RaZk?wBUScd@d%;~;Z^w`MQJw0S{nX}LEktHtTikYD4Z z?a8Rr!j_@$A|e92$U8Km*AkL;ykFP?6Hzma)7=B=vI&j2%B1D!LsoU($S7TykdEnb zcK~d7GHPn~F4FmlAOdX<|a#-A-CZo9d4zqdLy`BG>s?ra!Xz1Oww$!G2C zrs}1`T7MmSGicd?ph{}uiS-8@)1H^1knBDPo%879u(=!V zJDSEkX)7VTtDAhi?u=W3)q=2-jl-k%{5y7;!^OT?nD`mdwB2gk-JhjPYZ+Poe4qhO zglDDNFP(FxF{6?~&{5#cK z;bo3;8PM=E^)|dK@kOi%O!(5~J1&<*Yj^FXWe02am+5R)rZ)98dlUng zz{p4IL)qr74ESjO3NvnkzYnbMVu0L!yc7U~@V;{i^a(J`@Ap}4jqrzMI&9#M%vzNQ zF5-0X7FqdJy((alGH~5O8^Pry4)Amv&`nE~TL!HUBVtmUXD{R6tw+wPS?B`Rqa(TL zsw2d1nWLxfd|nZ?0(UeVJZU_37*9Te0a6y7E)vbJ#)9Y-`z4NmZ4ij1MJOh)>|J!afjA zkJoelw1eGi--WReuaW+!A`Kk_7)o{Lgy=(J(cEv!c~wca-uqW*ru|Od&OixyYqi#Q zV>0z?9vKCY&U4~F6@={N+3cJlqED;&IpGpe6SQ2DffxmWiw99>b0FvX z=u3BgHK3Gnq{;}-`;IO*^X#|^hZs;YxpT3O*12#C*Hmoo{#BaRg2%-riTA`AehlWV zLCpIueAtru+Pq}dKjx6{q0-v^8d8@QWziA+zI4ot>?D^_|J(#^angz|vNc zD>W7u**-)QpCJ-VNd^=L+EJg3^AX%YT?-I>X~?C-;=E-7Gk$~w0}w8Ce&dptcz);6 zo!P*X3TPh#a#ZIur&T;baOxqI!v8-@F z1vbC$p`W>9TtnmevV+nb`K>V!$kG-9y^@HSlgDOdQhUr16JKkwaFjs-i zrF|}@v*wpev|sFcu*rYwU-A_5&%g(IiQkwK%du9%9O2^|f4*lh6qYRvY4)!Yci};7 z^zsOvs+29%tzE^oJ}9giw%_bUAM?Iz?h$5CLQ01_T$?%?3=9H>WK9$$OoRm6K7FDs zwIcF1abTdx_{#7Y$cec^>jxe=JP{6C{y4{pXlE*5+CkYwyX-v5+G5~h<^X&2-$WuH zcZ=#1b!1D)=@JazFXo&mg4p-&F)6;F3BvL^t#YvKM2i-_r6fCBGw;)CcG3Z%FI(<) zb*G`^WPgQvs4G5!Rz99_Ox~>=uZ%n7*E(O-9uCn@R3r#%iCSiYgyWC3B{rVIAbXsV zPwNfMKsoK_RcH6=>~7WlARzy5eJ1_X*Mq;o@;2&uICN`)ob0`=t8S#7fjP^rAC5b(D3|W>oh=Yp}G<3tE1jY{ljlO$vl`< z)ZNOLck0|wFQ_)OzpSDa?4lA0jw!Tu3)UMu}3YpPGnz&;_fW$?bNDPVa1Q@#sy-(ME@c}QY~-OQl8Cmpr^#8w0} z#kwkAK%<7%NjE9pm@N5nT*rIsEZ_LKjBp_HpDGVz6&STR4$O}aT&SF;qVGx}g!Zqf zyN!bFUa`s!WHJCY)oHT+2TT#f!fm8a6&jY-}?8Z;FXK)Y8)+2F@5~OB-jJV11^u&p`zarx{Ab z#^r`LmAsB6Bw8h>8&5sHIFi{hX325l`0V^w{a(A9Sz%q)CD3m7^!40KH5rQ>BBHQR zH-Nx{^3MaYt_bvD>Ze^wJ(OYDH^;JQT9NMhrv*#n z$}eJpxM(3DhB6uz8>4d7T{R})E6XGzVgV2Sdp-~vb;H)XzSoA&Wrm@sP!=TNKDD(w zZP`p5D-6qD*&wt)r$lVpuWze5Xz2q}_vn?rVoDSaSOP?% zF}1Wt(;G0fGeyzmh?^bjtGp5isUrLxK=0hX6yt{6JGStyrW@1c>+xig-Tr)*aKgo3 zP4+f&@#B#WeQnTV;9vw!YM<>->AJ3E4n>T8Ha93#2fg_y-mwFH*Stfy;T}HE~XUuwo-+E&tZN=OH zUDIFOgYA>8US!%xI9Bc^D>p%YSjYYfI&B;4+!wpV)bLGzmP(tPy_1?B45|RZiC>T| zozN2AL)7%*^2=A5VDEc>WzpY3MwpE~lym+D>sbr#>4fz)pqW-$-ez~2F-Z!BHP!>V z5cg$|cR6+Nsfm2)Ih%vJ|M4@xi)l``L-dUwhSuITD!BI9CM&?Wvn=;oc!KI#W!5R} z#6`N@H7=a@=IrePLE#r%%7v!qMFP%zj9Wt}fP=;jilaZ~PWH^PPV2`_3J0|6YG<`@ zY%!^jnIA1JihqJ;PS`;M-x2v5dp)xHbh8 zb}-Fg;`P{6zQtEx#V%mwJSmFN#$e?3HbSnsih8AcpK2|kkQSwx{%el<>#Oe<>4-Ar z4UY)J?za|q6y%Mhdp|NwBMxQWT+7fsYSUe(((%odx%cP8VHN%qXeS(P?VwJJ0a5xj zPpxQ}L=wN6p}Uory&#K!bP%iC1`VZ%TXQIJhNoBB37yRy7D4-d%(+O@?8IS+Rw+^%Ztm6n#8^->3c_bu}m3Wr=W7((8+ z3;iysJJ?rr3BRvmi@4bsu96cyKc0C$PccC-V{p48X$XB^h1f}}-jE5PEb>>lW7sDt z4y|TrhiZ18l}cmBp=R>I*?H*G*M%U#wyd`n`GfW)It;7ChQmxT31_>0af85O!^ESh zR<|LVa(~9!-+dyQ8#t&+E06(w!uHbQhQM4={#%{gH*5|+cgy5X!jx2GO2ad?9yz~H z#=P!B95zp5x?u)I$fY`npO+JlItQT4QZl=Q;YFkbcLw{jGDp0Vd{+TA9g8IGb@BznOMop~p}yDmILoP1*KG9H7QKl? z6A~T2@MeX8yMrPCgGJvnv@9|HIglP7mY=QPY_7vM740L(Ur2*wJhr%O^qH#;6st^h!3(W_V4~{vp$#g*~ zas|0Zt^-teqKP=;q=q`zL2k@D$%!4uyXk5njkYS;hgl`7KPg%ky&7;?dfqE}xgOwk z4s3mz6A74BnT;)*XXdbP_aX>CbDK2JUPSL$xdk_`Ski01941!N7Qk}_*BjZ<4@ zITEdlOw8wX}XlCGw=p=dM_`DR1o?WrpRaW~txFl<-~C8(`Yb~!%h zlZG@jx^UlYB0rwaHcO8c@R5=OO+7=CY+HRIFoS6*&r+jS*bs-jyf_piCeU}8AWYJB zs~{c$v{hVYa!!sb1RTwmPwITR+B8wVOl_?JtPP0k2A7rZ5ykezUL#pJXMRWM>np<- z-_O6!pbUrT81lyIjun%!SpDRF-97eRQYRFueXi)-xwArD^j4p5+<0cJQi(8is|q>^ zsa#vUb5HuyaG-p@t?$WU$QFL~s;$p{^Vae4En}gz(|qajs-by-j$!ZPl#e!QsD2pS zJ-->U>wOzLTt19-XW=jHe3@J%NKb*wnk?hI@H+fydv=GwBZvTJD8Cvm_{jef6QxAr zt#syX=v8pmQN+*OFgD>&Y~ACF`+db+0nJgj@I&+OQttmLV-ip-b?3`(wEm?|+uzna zBzhfh7ZvGgXczZ{Tiq{w@vF4}Rijt0QkC-LQNdlcH`3ScQL(YHt(wu=r8QRD?{u0S z6t~Td!o^Y(%b^k|Z2s#PxzWh@*z>-s?zv|+J2KbGPr)5-q@97>GOk6;!o~$`{7sp#420+ zHWZs1DIyGH4d|FD3z09jC^0>FhTGhfn#@Suvt3N%dYzci-c_jRIPquYG~@PUbz2ec=V?&7rJ%t`tc6p`WQ;0Zz1lom5@Nd$Flnc7>5@Y zMKkAK=B%so`|U04R|NUy;OcdB`Bff8cZ*5I3x*8dZ^&p9@KHQb=v?qyVV!dSI=ug+ zy3wMBUI_Q4UH@HxknoUt0zNYUtMIXL!736`g>bapFPphRY!Fla@s!9d7WD!TeWi14di{@Y2>q{iWr~Pjx4+8E0p$#VKyLG~kjSUB5W^BbO@qtH0}AdoAYaA7Z7wLq=aRVL0|azsD;Qks=*VH|Y(xFwVi8ZLw&mTP_pP)qR0;eaU07*2 z8(0@^%b-Y!eNV@d8nIW^*lQ~lu~uV`B^sg?)s7*U(b~6G8B(9ER7D7a5hbM&`_fix zjfPgK+FQ#|k(ikIW`4~3=iVRpKF>MtdEa}Udmd1=feZ94wyE?f?7X8uPEJXBhVH8* zJCh8^)1li~vEh-Ic~s)}1E;C$%ve)tK7rqtiN)}}y&Jk#QGI|WZEaJ2&?cm}8J^R|5PPR&YtBJ;>O@!iqpTC7*hyWo zUVDh}OVve%Th#XY1mF7H_4At0zUM-n=@-fCIj(N7N7^D)^S^1dJ2~wr<-fyaRSGmB zrHFByIp{P2pxbc4hmpKqB=uuu1s(ym2!!Mh_l!q^c_x+Au#COWt3Suku<)b)0O+tS zEiuB9R8_{Ua9{G`NNI9EX4SF18i-^9s4U?Q=3~#~-(7gly(ckmntKkKPscCsL>QdY zk9?9)W5u<|zTGmxxWWVajKdJMf zf{~=EN$lF{`4L`(jli@kW`ZY(&EL%b|HS^auu}L^(!#DyR;#egeam8Sub0P_y$LChTq}S zed~(pSv%cGBobMdh06=4w$Ty;XW@ton&hgu(lMUP8O-Z8Z#rJo#r2@VLa9Zy5mXGk ztZORknq$xVOC$pfUhpzn_MUx9b1UX1{j9g4iKVHe_+%!Je6XV|7qN|TV8cr{{M0uV za^ZDNy*$yVZlfL2nw=(1`})pLqIzb8Z$s#B+d6N4yYv?+*GtV8wR6__D}B8@3-WLc zI?cqHwWWcU)^-8`-G}$%ZM+M~>kr6=!E3knx%HdcY`f0@130(dt!E$XMj+Q^RgaQCPFSyDdUV zbL`YiXG@&`JfOlx*?;OmcXq_DoI2gQgm6f-?qVgaV(&q~pxw9g3~n{&lXM6?|7dX*DU zRF`S|ZfUAsT3G=fg8`9_c{`3IHn zcHIyiGmKkE$Du!@gzfFpNHtL()d(vSp%>W5$MtD|1s`-zdUX7n>dw6Rqq`5)Km-{U z>Zit_lgG3k)#T6~2KA$l3XrG6xRm3#3xo$q0Fiz`1aaNe~BSaXJ?>-qEfB4 zv>*xtDokH+oWW6Ly+)8G*vVIzue9@$8%RJ3x4i*~YKd@{zdl%np&h z*l*=>2E__v`g)J|nQDjI48}ex>aiS6X;13}5SVlJ>23FSv#^svoa&Z4C8&+r#O;A1 zkGTPI`rax37OAWv3tgqF_m`KGH^ga%sUa#@f}8~AO;h4;<9#C33o6s|lWcEt?gRjK1v-UK2yX+i3;y=yu_ph;^*p@HrwBtAn8X=TSV&32%F4?3 zrk~wI5<+TKZT>y=&*pX^3K6~?SUi8L*1^t>v=>h@CE)8u-kkD~0rc+AXmfk+e!8sp z-;g+V*wkx4AIcc%vG1)k6`Pu!0iWw#st{re04BvEvy|;QpP&;b?5tr{bry)be*(z5 B{Bi&Q literal 0 HcmV?d00001 diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index c45d7e5d630..b7ac6da15bc 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -12920,6 +12920,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index b7fad43ebbf..7b1013077d7 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -69,6 +69,7 @@ export enum FeatureFlag { BrowserPremiumSpotlight = "pm-23384-browser-premium-spotlight", MigrateMyVaultToMyItems = "pm-20558-migrate-myvault-to-myitems", PM27632_SdkCipherCrudOperations = "pm-27632-cipher-crud-operations-to-sdk", + PM29437_WelcomeDialog = "pm-29437-welcome-dialog-no-ext-prompt", PM31039ItemActionInExtension = "pm-31039-item-action-in-extension", /* Platform */ @@ -135,6 +136,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.BrowserPremiumSpotlight]: FALSE, [FeatureFlag.PM27632_SdkCipherCrudOperations]: FALSE, [FeatureFlag.MigrateMyVaultToMyItems]: FALSE, + [FeatureFlag.PM29437_WelcomeDialog]: FALSE, /* Auth */ [FeatureFlag.PM23801_PrefetchPasswordPrelogin]: FALSE, diff --git a/libs/state/src/core/state-definitions.ts b/libs/state/src/core/state-definitions.ts index ae6938b2069..30ee2be0592 100644 --- a/libs/state/src/core/state-definitions.ts +++ b/libs/state/src/core/state-definitions.ts @@ -212,6 +212,9 @@ export const SETUP_EXTENSION_DISMISSED_DISK = new StateDefinition( web: "disk-local", }, ); +export const VAULT_WELCOME_DIALOG_DISK = new StateDefinition("vaultWelcomeDialog", "disk", { + web: "disk-local", +}); export const VAULT_BROWSER_INTRO_CAROUSEL = new StateDefinition( "vaultBrowserIntroCarousel", "disk", From 04aad4432206ce4f19edf2cd28aec71a0f2b4837 Mon Sep 17 00:00:00 2001 From: Mike Amirault Date: Thu, 19 Feb 2026 12:54:15 -0500 Subject: [PATCH 094/134] [PM-31774] Remove toggle visibility callout on hidden text sends (#18924) --- .../send-access-text.component.html | 24 ++++++------------- .../send-access/send-access-text.component.ts | 4 ++-- apps/web/src/locales/en/messages.json | 4 ---- 3 files changed, 9 insertions(+), 23 deletions(-) diff --git a/apps/web/src/app/tools/send/send-access/send-access-text.component.html b/apps/web/src/app/tools/send/send-access/send-access-text.component.html index ca772251146..c7fa148169d 100644 --- a/apps/web/src/app/tools/send/send-access/send-access-text.component.html +++ b/apps/web/src/app/tools/send/send-access/send-access-text.component.html @@ -1,26 +1,16 @@ -{{ "sendHiddenByDefault" | i18n }}
    - + @if (send.text.hidden) { + + }
    diff --git a/apps/web/src/app/tools/send/send-access/send-access-text.component.ts b/apps/web/src/app/tools/send/send-access/send-access-text.component.ts index 794cfbc9678..8a947eafb69 100644 --- a/apps/web/src/app/tools/send/send-access/send-access-text.component.ts +++ b/apps/web/src/app/tools/send/send-access/send-access-text.component.ts @@ -6,7 +6,7 @@ import { FormBuilder } from "@angular/forms"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SendAccessView } from "@bitwarden/common/tools/send/models/view/send-access.view"; -import { ToastService } from "@bitwarden/components"; +import { IconModule, ToastService } from "@bitwarden/components"; import { SharedModule } from "../../../shared"; @@ -15,7 +15,7 @@ import { SharedModule } from "../../../shared"; @Component({ selector: "app-send-access-text", templateUrl: "send-access-text.component.html", - imports: [SharedModule], + imports: [SharedModule, IconModule], }) export class SendAccessTextComponent { private _send: SendAccessView = null; diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index b7ac6da15bc..2d9cba6d409 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -5855,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, From 8399815ea7bfe00a251a0091ee8a504c5a1bf784 Mon Sep 17 00:00:00 2001 From: John Harrington <84741727+harr1424@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:59:59 -0700 Subject: [PATCH 095/134] [PM-32237] Add back functionality to email OTP auth flow (#19024) * add back functionality to OTP auth flow * respond to review comments * hoist email value to parent component --------- Co-authored-by: Alex Dragovich <46065570+itsadrago@users.noreply.github.com> --- .../send-access-email.component.html | 26 ++++++------ .../send-access-email.component.ts | 41 +++++++++++++++++-- .../send/send-access/send-auth.component.html | 1 + .../send/send-access/send-auth.component.ts | 7 ++++ 4 files changed, 60 insertions(+), 15 deletions(-) diff --git a/apps/web/src/app/tools/send/send-access/send-access-email.component.html b/apps/web/src/app/tools/send/send-access/send-access-email.component.html index ee5a03670bb..82ef9a397c5 100644 --- a/apps/web/src/app/tools/send/send-access/send-access-email.component.html +++ b/apps/web/src/app/tools/send/send-access/send-access-email.component.html @@ -20,16 +20,18 @@ {{ "verificationCode" | i18n }} -
    - -
    + + } diff --git a/apps/web/src/app/tools/send/send-access/send-access-email.component.ts b/apps/web/src/app/tools/send/send-access/send-access-email.component.ts index b1374cd6c66..0915a47e4ad 100644 --- a/apps/web/src/app/tools/send/send-access/send-access-email.component.ts +++ b/apps/web/src/app/tools/send/send-access/send-access-email.component.ts @@ -1,6 +1,14 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { ChangeDetectionStrategy, Component, input, OnDestroy, OnInit } from "@angular/core"; +import { + ChangeDetectionStrategy, + Component, + effect, + input, + OnDestroy, + OnInit, + output, +} from "@angular/core"; import { FormControl, FormGroup, Validators } from "@angular/forms"; import { SharedModule } from "../../../shared"; @@ -18,18 +26,45 @@ export class SendAccessEmailComponent implements OnInit, OnDestroy { protected otp: FormControl; readonly loading = input.required(); + readonly backToEmail = output(); constructor() {} ngOnInit() { this.email = new FormControl("", Validators.required); - this.otp = new FormControl("", Validators.required); + this.otp = new FormControl(""); this.formGroup().addControl("email", this.email); this.formGroup().addControl("otp", this.otp); - } + // Update validators when enterOtp changes + effect(() => { + const isOtpMode = this.enterOtp(); + if (isOtpMode) { + // In OTP mode: email is not required (already entered), otp is required + this.email.clearValidators(); + this.otp.setValidators([Validators.required]); + } else { + // In email mode: email is required, otp is not required + this.email.setValidators([Validators.required]); + this.otp.clearValidators(); + } + this.email.updateValueAndValidity(); + this.otp.updateValueAndValidity(); + }); + } ngOnDestroy() { this.formGroup().removeControl("email"); this.formGroup().removeControl("otp"); } + + onBackClick() { + this.backToEmail.emit(); + if (this.otp) { + this.otp.clearValidators(); + this.otp.setValue(""); + this.otp.setErrors(null); + this.otp.markAsUntouched(); + this.otp.markAsPristine(); + } + } } diff --git a/apps/web/src/app/tools/send/send-access/send-auth.component.html b/apps/web/src/app/tools/send/send-access/send-auth.component.html index c3e90cea4ea..fa5bef77274 100644 --- a/apps/web/src/app/tools/send/send-access/send-auth.component.html +++ b/apps/web/src/app/tools/send/send-access/send-auth.component.html @@ -31,6 +31,7 @@ [formGroup]="sendAccessForm" [enterOtp]="enterOtp()" [loading]="loading()" + (backToEmail)="onBackToEmail()" > } } diff --git a/apps/web/src/app/tools/send/send-access/send-auth.component.ts b/apps/web/src/app/tools/send/send-access/send-auth.component.ts index 994bd7f3ee3..97b71778539 100644 --- a/apps/web/src/app/tools/send/send-access/send-auth.component.ts +++ b/apps/web/src/app/tools/send/send-access/send-auth.component.ts @@ -90,6 +90,11 @@ export class SendAuthComponent implements OnInit { this.loading.set(false); } + onBackToEmail() { + this.enterOtp.set(false); + this.updatePageTitle(); + } + private async attemptV1Access() { try { const accessRequest = new SendAccessRequest(); @@ -247,10 +252,12 @@ export class SendAuthComponent implements OnInit { if (this.enterOtp()) { this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({ pageTitle: { key: "enterTheCodeSentToYourEmail" }, + pageSubtitle: this.sendAccessForm.value.email ?? null, }); } else { this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({ pageTitle: { key: "verifyYourEmailToViewThisSend" }, + pageSubtitle: null, }); } } else if (authType === AuthType.Password) { From caa28ac5b3a4d339a967a6dc1d360445e80128ec Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Thu, 19 Feb 2026 21:18:17 +0100 Subject: [PATCH 096/134] [PM-32481] Apply same custom scrollbar to nav (#19083) * Apply same custom scrollbar to nav * Split colors --- apps/desktop/src/scss/base.scss | 25 ++++++++++++++++++++++--- apps/desktop/src/scss/variables.scss | 4 ++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/scss/base.scss b/apps/desktop/src/scss/base.scss index a95d82dacd4..2371192e0ea 100644 --- a/apps/desktop/src/scss/base.scss +++ b/apps/desktop/src/scss/base.scss @@ -102,23 +102,30 @@ textarea { div:not(.modal)::-webkit-scrollbar, .cdk-virtual-scroll-viewport::-webkit-scrollbar, -.vault-filters::-webkit-scrollbar { +.vault-filters::-webkit-scrollbar, +#bit-side-nav::-webkit-scrollbar { width: 10px; height: 10px; } div:not(.modal)::-webkit-scrollbar-track, .cdk-virtual-scroll-viewport::-webkit-scrollbar-track, -.vault-filters::-webkit-scrollbar-track { +.vault-filters::-webkit-scrollbar-track, +#bit-side-nav::-webkit-scrollbar-track { background-color: transparent; } div:not(.modal)::-webkit-scrollbar-thumb, .cdk-virtual-scroll-viewport::-webkit-scrollbar-thumb, -.vault-filters::-webkit-scrollbar-thumb { +.vault-filters::-webkit-scrollbar-thumb, +#bit-side-nav::-webkit-scrollbar-thumb { border-radius: 10px; margin-right: 1px; +} +div:not(.modal)::-webkit-scrollbar-thumb, +.cdk-virtual-scroll-viewport::-webkit-scrollbar-thumb, +.vault-filters::-webkit-scrollbar-thumb { @include themify($themes) { background-color: themed("scrollbarColor"); } @@ -130,6 +137,18 @@ div:not(.modal)::-webkit-scrollbar-thumb, } } +#bit-side-nav::-webkit-scrollbar-thumb { + @include themify($themes) { + background-color: themed("scrollbarColorNav"); + } + + &:hover { + @include themify($themes) { + background-color: themed("scrollbarHoverColorNav"); + } + } +} + // cdk-virtual-scroll .cdk-virtual-scroll-viewport { width: 100%; diff --git a/apps/desktop/src/scss/variables.scss b/apps/desktop/src/scss/variables.scss index a00257ed608..51a6d2ac840 100644 --- a/apps/desktop/src/scss/variables.scss +++ b/apps/desktop/src/scss/variables.scss @@ -56,6 +56,8 @@ $themes: ( backgroundColorAlt2: $background-color-alt2, scrollbarColor: rgba(100, 100, 100, 0.2), scrollbarHoverColor: rgba(100, 100, 100, 0.4), + scrollbarColorNav: rgba(226, 226, 226), + scrollbarHoverColorNav: rgba(197, 197, 197), boxBackgroundColor: $box-background-color, boxBackgroundHoverColor: $box-background-hover-color, boxBorderColor: $box-border-color, @@ -115,6 +117,8 @@ $themes: ( backgroundColorAlt2: #15181e, scrollbarColor: #6e788a, scrollbarHoverColor: #8d94a5, + scrollbarColorNav: #6e788a, + scrollbarHoverColorNav: #8d94a5, boxBackgroundColor: #2f343d, boxBackgroundHoverColor: #3c424e, boxBorderColor: #4c525f, From 8ec9c55b1812e6843d61d1e7e339ea058de1a1d0 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Thu, 19 Feb 2026 21:18:48 +0100 Subject: [PATCH 097/134] Adjust desktop header color (#19082) --- apps/desktop/src/scss/variables.scss | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/scss/variables.scss b/apps/desktop/src/scss/variables.scss index 51a6d2ac840..62d4f23ad46 100644 --- a/apps/desktop/src/scss/variables.scss +++ b/apps/desktop/src/scss/variables.scss @@ -61,10 +61,10 @@ $themes: ( boxBackgroundColor: $box-background-color, boxBackgroundHoverColor: $box-background-hover-color, boxBorderColor: $box-border-color, - headerBackgroundColor: rgb(var(--color-background-alt3)), - headerBorderColor: rgb(var(--color-background-alt4)), - headerInputBackgroundColor: darken($brand-primary, 8%), - headerInputBackgroundFocusColor: darken($brand-primary, 10%), + headerBackgroundColor: var(--color-sidenav-background), + headerBorderColor: var(--color-sidenav-active-item), + headerInputBackgroundColor: darken($brand-primary, 20%), + headerInputBackgroundFocusColor: darken($brand-primary, 25%), headerInputColor: #ffffff, headerInputPlaceholderColor: lighten($brand-primary, 35%), listItemBackgroundColor: $background-color, @@ -122,8 +122,8 @@ $themes: ( boxBackgroundColor: #2f343d, boxBackgroundHoverColor: #3c424e, boxBorderColor: #4c525f, - headerBackgroundColor: rgb(var(--color-background-alt3)), - headerBorderColor: rgb(var(--color-background-alt4)), + headerBackgroundColor: var(--color-sidenav-background), + headerBorderColor: var(--color-sidenav-active-item), headerInputBackgroundColor: #3c424e, headerInputBackgroundFocusColor: #4c525f, headerInputColor: #ffffff, From 702e6086b914509dcc3b77f10e7ccf9361ac2a2b Mon Sep 17 00:00:00 2001 From: bmbitwarden Date: Thu, 19 Feb 2026 19:26:18 -0500 Subject: [PATCH 098/134] PM-30876 resolved screenreader for icons on send table rows (#18940) * PM-30876 resolved screenreader for icons on send table rows * PM-30876 resolved grey icon issue * PM-30876 resolved blank underline issue * PM-30876 resolved screen reader * PM-30876 resolved screen reader --- .../src/send-table/send-table.component.html | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/libs/tools/send/send-ui/src/send-table/send-table.component.html b/libs/tools/send/send-ui/src/send-table/send-table.component.html index 1c235415cae..14dc93bb706 100644 --- a/libs/tools/send/send-ui/src/send-table/send-table.component.html +++ b/libs/tools/send/send-ui/src/send-table/send-table.component.html @@ -29,9 +29,9 @@ class="bwi bwi-exclamation-triangle" appStopProp title="{{ 'disabled' | i18n }}" - aria-hidden="true" + aria-label="{{ 'disabled' | i18n }}" + tabindex="0" >
    - {{ "disabled" | i18n }} } @if (s.authType !== authType.None) { @let titleKey = @@ -40,36 +40,36 @@ class="bwi bwi-lock" appStopProp title="{{ titleKey | i18n }}" - aria-hidden="true" + aria-label="{{ titleKey | i18n }}" + tabindex="0" >
    - {{ titleKey | i18n }} } @if (s.maxAccessCountReached) { - {{ "maxAccessCountReached" | i18n }} } @if (s.expired) { - {{ "expired" | i18n }} } @if (s.pendingDelete) { - {{ "pendingDeletion" | i18n }} }
    From 36635741138bb5235083724112d653ed8850154a Mon Sep 17 00:00:00 2001 From: Alex <55413326+AlexRubik@users.noreply.github.com> Date: Thu, 19 Feb 2026 17:43:51 -0800 Subject: [PATCH 099/134] [PM-31496] Reports back button placement (#18706) * place back button fixed at bottom right * fix type errors * add the new button logic to org reports also * fix: restore keyboard focus for reports back button in CDK overlay The CDK Overlay renders outside the cdkTrapFocus boundary, making the floating "Back to reports" button unreachable via Tab. Add a focus bridge element that intercepts Tab and programmatically redirects focus to the overlay button, with a return handler to cycle focus back into the page. --- .../organization-reporting.module.ts | 9 ++- .../reporting/reports-home.component.html | 29 ++++++-- .../reporting/reports-home.component.ts | 72 ++++++++++++++++++- .../reports/reports-layout.component.html | 30 +++++--- .../dirt/reports/reports-layout.component.ts | 71 +++++++++++++++--- .../src/app/dirt/reports/reports.module.ts | 2 + 6 files changed, 187 insertions(+), 26 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/reporting/organization-reporting.module.ts b/apps/web/src/app/admin-console/organizations/reporting/organization-reporting.module.ts index 46599d7da46..d96e2cbb6c0 100644 --- a/apps/web/src/app/admin-console/organizations/reporting/organization-reporting.module.ts +++ b/apps/web/src/app/admin-console/organizations/reporting/organization-reporting.module.ts @@ -1,3 +1,4 @@ +import { OverlayModule } from "@angular/cdk/overlay"; import { NgModule } from "@angular/core"; import { ReportsSharedModule } from "../../../dirt/reports"; @@ -8,7 +9,13 @@ import { OrganizationReportingRoutingModule } from "./organization-reporting-rou import { ReportsHomeComponent } from "./reports-home.component"; @NgModule({ - imports: [SharedModule, ReportsSharedModule, OrganizationReportingRoutingModule, HeaderModule], + imports: [ + SharedModule, + OverlayModule, + ReportsSharedModule, + OrganizationReportingRoutingModule, + HeaderModule, + ], declarations: [ReportsHomeComponent], }) export class OrganizationReportingModule {} diff --git a/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.html b/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.html index 59eac5b6300..9a931f66af9 100644 --- a/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.html +++ b/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.html @@ -8,9 +8,26 @@ - +@if (!(homepage$ | async)) { + + +} + + + + diff --git a/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.ts b/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.ts index 6043bfd3193..503a4f88050 100644 --- a/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.ts +++ b/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.ts @@ -1,6 +1,18 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Component, OnInit } from "@angular/core"; +import { Overlay, OverlayRef } from "@angular/cdk/overlay"; +import { TemplatePortal } from "@angular/cdk/portal"; +import { + AfterViewInit, + Component, + inject, + OnDestroy, + OnInit, + TemplateRef, + viewChild, + ViewContainerRef, +} from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute, NavigationEnd, Router } from "@angular/router"; import { filter, map, Observable, startWith, concatMap, firstValueFrom } from "rxjs"; @@ -21,16 +33,30 @@ import { ReportVariant, reports, ReportType, ReportEntry } from "../../../dirt/r templateUrl: "reports-home.component.html", standalone: false, }) -export class ReportsHomeComponent implements OnInit { +export class ReportsHomeComponent implements OnInit, AfterViewInit, OnDestroy { reports$: Observable; homepage$: Observable; + private readonly backButtonTemplate = + viewChild.required>("backButtonTemplate"); + + private overlayRef: OverlayRef | null = null; + private overlay = inject(Overlay); + private viewContainerRef = inject(ViewContainerRef); + constructor( private route: ActivatedRoute, private organizationService: OrganizationService, private accountService: AccountService, private router: Router, - ) {} + ) { + this.router.events + .pipe( + takeUntilDestroyed(), + filter((event) => event instanceof NavigationEnd), + ) + .subscribe(() => this.updateOverlay()); + } async ngOnInit() { this.homepage$ = this.router.events.pipe( @@ -51,6 +77,46 @@ export class ReportsHomeComponent implements OnInit { ); } + ngAfterViewInit(): void { + this.updateOverlay(); + } + + ngOnDestroy(): void { + this.overlayRef?.dispose(); + } + + returnFocusToPage(event: Event): void { + if ((event as KeyboardEvent).shiftKey) { + return; // Allow natural Shift+Tab behavior + } + event.preventDefault(); + const firstFocusable = document.querySelector( + "[cdktrapfocus] a:not([tabindex='-1'])", + ) as HTMLElement; + firstFocusable?.focus(); + } + + focusOverlayButton(event: Event): void { + if ((event as KeyboardEvent).shiftKey) { + return; // Allow natural Shift+Tab behavior + } + event.preventDefault(); + const button = this.overlayRef?.overlayElement?.querySelector("a") as HTMLElement; + button?.focus(); + } + + private updateOverlay(): void { + if (this.isReportsHomepageRouteUrl(this.router.url)) { + this.overlayRef?.dispose(); + this.overlayRef = null; + } else if (!this.overlayRef) { + this.overlayRef = this.overlay.create({ + positionStrategy: this.overlay.position().global().bottom("20px").right("32px"), + }); + this.overlayRef.attach(new TemplatePortal(this.backButtonTemplate(), this.viewContainerRef)); + } + } + private buildReports(productType: ProductTierType): ReportEntry[] { const reportRequiresUpgrade = productType == ProductTierType.Free ? ReportVariant.RequiresUpgrade : ReportVariant.Enabled; diff --git a/apps/web/src/app/dirt/reports/reports-layout.component.html b/apps/web/src/app/dirt/reports/reports-layout.component.html index 0cb5d304a34..c290fc88335 100644 --- a/apps/web/src/app/dirt/reports/reports-layout.component.html +++ b/apps/web/src/app/dirt/reports/reports-layout.component.html @@ -1,11 +1,25 @@ -
    -
    - @if (!homepage) { - - {{ "backToReports" | i18n }} - - } +@if (!homepage) { + + +} + + + -
    + diff --git a/apps/web/src/app/dirt/reports/reports-layout.component.ts b/apps/web/src/app/dirt/reports/reports-layout.component.ts index a6d84ccb037..136b70c81e4 100644 --- a/apps/web/src/app/dirt/reports/reports-layout.component.ts +++ b/apps/web/src/app/dirt/reports/reports-layout.component.ts @@ -1,4 +1,14 @@ -import { Component } from "@angular/core"; +import { Overlay, OverlayRef } from "@angular/cdk/overlay"; +import { TemplatePortal } from "@angular/cdk/portal"; +import { + AfterViewInit, + Component, + inject, + OnDestroy, + TemplateRef, + viewChild, + ViewContainerRef, +} from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { NavigationEnd, Router } from "@angular/router"; import { filter } from "rxjs/operators"; @@ -10,20 +20,65 @@ import { filter } from "rxjs/operators"; templateUrl: "reports-layout.component.html", standalone: false, }) -export class ReportsLayoutComponent { +export class ReportsLayoutComponent implements AfterViewInit, OnDestroy { homepage = true; - constructor(router: Router) { - const reportsHomeRoute = "/reports"; + private readonly backButtonTemplate = + viewChild.required>("backButtonTemplate"); - this.homepage = router.url === reportsHomeRoute; - router.events + private overlayRef: OverlayRef | null = null; + private overlay = inject(Overlay); + private viewContainerRef = inject(ViewContainerRef); + private router = inject(Router); + + constructor() { + this.router.events .pipe( takeUntilDestroyed(), filter((event) => event instanceof NavigationEnd), ) - .subscribe((event) => { - this.homepage = (event as NavigationEnd).url == reportsHomeRoute; + .subscribe(() => this.updateOverlay()); + } + + ngAfterViewInit(): void { + this.updateOverlay(); + } + + ngOnDestroy(): void { + this.overlayRef?.dispose(); + } + + returnFocusToPage(event: Event): void { + if ((event as KeyboardEvent).shiftKey) { + return; // Allow natural Shift+Tab behavior + } + event.preventDefault(); + const firstFocusable = document.querySelector( + "[cdktrapfocus] a:not([tabindex='-1'])", + ) as HTMLElement; + firstFocusable?.focus(); + } + + focusOverlayButton(event: Event): void { + if ((event as KeyboardEvent).shiftKey) { + return; // Allow natural Shift+Tab behavior + } + event.preventDefault(); + const button = this.overlayRef?.overlayElement?.querySelector("a") as HTMLElement; + button?.focus(); + } + + private updateOverlay(): void { + if (this.router.url === "/reports") { + this.homepage = true; + this.overlayRef?.dispose(); + this.overlayRef = null; + } else if (!this.overlayRef) { + this.homepage = false; + this.overlayRef = this.overlay.create({ + positionStrategy: this.overlay.position().global().bottom("20px").right("32px"), }); + this.overlayRef.attach(new TemplatePortal(this.backButtonTemplate(), this.viewContainerRef)); + } } } diff --git a/apps/web/src/app/dirt/reports/reports.module.ts b/apps/web/src/app/dirt/reports/reports.module.ts index 4fc152917f4..c4bd9fef809 100644 --- a/apps/web/src/app/dirt/reports/reports.module.ts +++ b/apps/web/src/app/dirt/reports/reports.module.ts @@ -1,3 +1,4 @@ +import { OverlayModule } from "@angular/cdk/overlay"; import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; @@ -29,6 +30,7 @@ import { ReportsSharedModule } from "./shared"; @NgModule({ imports: [ CommonModule, + OverlayModule, SharedModule, ReportsSharedModule, ReportsRoutingModule, From b0549dbfb6be006858452db619bbc5e0020347ff Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 12:04:36 +0100 Subject: [PATCH 100/134] Autosync the updated translations (#19093) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/af/messages.json | 9 +++++++ apps/desktop/src/locales/ar/messages.json | 9 +++++++ apps/desktop/src/locales/az/messages.json | 9 +++++++ apps/desktop/src/locales/be/messages.json | 9 +++++++ apps/desktop/src/locales/bg/messages.json | 9 +++++++ apps/desktop/src/locales/bn/messages.json | 9 +++++++ apps/desktop/src/locales/bs/messages.json | 9 +++++++ apps/desktop/src/locales/ca/messages.json | 9 +++++++ apps/desktop/src/locales/cs/messages.json | 9 +++++++ apps/desktop/src/locales/cy/messages.json | 9 +++++++ apps/desktop/src/locales/da/messages.json | 9 +++++++ apps/desktop/src/locales/de/messages.json | 9 +++++++ apps/desktop/src/locales/el/messages.json | 9 +++++++ apps/desktop/src/locales/en_GB/messages.json | 9 +++++++ apps/desktop/src/locales/en_IN/messages.json | 9 +++++++ apps/desktop/src/locales/eo/messages.json | 9 +++++++ apps/desktop/src/locales/es/messages.json | 9 +++++++ apps/desktop/src/locales/et/messages.json | 9 +++++++ apps/desktop/src/locales/eu/messages.json | 9 +++++++ apps/desktop/src/locales/fa/messages.json | 9 +++++++ apps/desktop/src/locales/fi/messages.json | 9 +++++++ apps/desktop/src/locales/fil/messages.json | 9 +++++++ apps/desktop/src/locales/fr/messages.json | 9 +++++++ apps/desktop/src/locales/gl/messages.json | 9 +++++++ apps/desktop/src/locales/he/messages.json | 9 +++++++ apps/desktop/src/locales/hi/messages.json | 9 +++++++ apps/desktop/src/locales/hr/messages.json | 9 +++++++ apps/desktop/src/locales/hu/messages.json | 9 +++++++ apps/desktop/src/locales/id/messages.json | 9 +++++++ apps/desktop/src/locales/it/messages.json | 9 +++++++ apps/desktop/src/locales/ja/messages.json | 9 +++++++ apps/desktop/src/locales/ka/messages.json | 9 +++++++ apps/desktop/src/locales/km/messages.json | 9 +++++++ apps/desktop/src/locales/kn/messages.json | 9 +++++++ apps/desktop/src/locales/ko/messages.json | 9 +++++++ apps/desktop/src/locales/lt/messages.json | 9 +++++++ apps/desktop/src/locales/lv/messages.json | 13 ++++++++-- apps/desktop/src/locales/me/messages.json | 9 +++++++ apps/desktop/src/locales/ml/messages.json | 9 +++++++ apps/desktop/src/locales/mr/messages.json | 9 +++++++ apps/desktop/src/locales/my/messages.json | 9 +++++++ apps/desktop/src/locales/nb/messages.json | 9 +++++++ apps/desktop/src/locales/ne/messages.json | 9 +++++++ apps/desktop/src/locales/nl/messages.json | 9 +++++++ apps/desktop/src/locales/nn/messages.json | 9 +++++++ apps/desktop/src/locales/or/messages.json | 9 +++++++ apps/desktop/src/locales/pl/messages.json | 9 +++++++ apps/desktop/src/locales/pt_BR/messages.json | 25 +++++++++++++------- apps/desktop/src/locales/pt_PT/messages.json | 9 +++++++ apps/desktop/src/locales/ro/messages.json | 9 +++++++ apps/desktop/src/locales/ru/messages.json | 9 +++++++ apps/desktop/src/locales/si/messages.json | 9 +++++++ apps/desktop/src/locales/sk/messages.json | 9 +++++++ apps/desktop/src/locales/sl/messages.json | 9 +++++++ apps/desktop/src/locales/sr/messages.json | 9 +++++++ apps/desktop/src/locales/sv/messages.json | 9 +++++++ apps/desktop/src/locales/ta/messages.json | 9 +++++++ apps/desktop/src/locales/te/messages.json | 9 +++++++ apps/desktop/src/locales/th/messages.json | 9 +++++++ apps/desktop/src/locales/tr/messages.json | 9 +++++++ apps/desktop/src/locales/uk/messages.json | 9 +++++++ apps/desktop/src/locales/vi/messages.json | 9 +++++++ apps/desktop/src/locales/zh_CN/messages.json | 13 ++++++++-- apps/desktop/src/locales/zh_TW/messages.json | 9 +++++++ 64 files changed, 588 insertions(+), 12 deletions(-) diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json index c0824c61d03..dfcdd36da20 100644 --- a/apps/desktop/src/locales/af/messages.json +++ b/apps/desktop/src/locales/af/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Ongeldige bevestigingskode" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Gaan Voort" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json index 3e668c327b0..1273058bfc9 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "رمز التحقق غير صالح" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "متابعة" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index 4e5d414eb1c..ba43fecfc60 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Yararsız doğrulama kodu" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Davam" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Birdən çox e-poçtu daxil edərkən vergül istifadə edin." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/be/messages.json b/apps/desktop/src/locales/be/messages.json index f2f9d0a736d..c554352c438 100644 --- a/apps/desktop/src/locales/be/messages.json +++ b/apps/desktop/src/locales/be/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Памылковы праверачны код" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Працягнуць" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index ea0355ad7f6..6913b3b563e 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Грешен код за потвърждаване" }, + "invalidEmailOrVerificationCode": { + "message": "Грешна е-поща или код за потвърждаване" + }, "continue": { "message": "Продължаване" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Можете да въведете повече е-пощи, като ги разделите със запетая." }, + "emailsRequiredChangeAccessType": { + "message": "Потвърждаването на е-пощата изисква да е наличен поне един адрес на е-поща. Ако искате да премахнете всички е-пощи, променете начина за достъп по-горе." + }, "emailPlaceholder": { "message": "потребител@bitwarden.com , потребител@acme.com" + }, + "userVerificationFailed": { + "message": "Проверката на потребителя беше неуспешна." } } diff --git a/apps/desktop/src/locales/bn/messages.json b/apps/desktop/src/locales/bn/messages.json index 6a211c93052..2919e52b0fb 100644 --- a/apps/desktop/src/locales/bn/messages.json +++ b/apps/desktop/src/locales/bn/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "অবিরত" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/bs/messages.json b/apps/desktop/src/locales/bs/messages.json index 4ca3aa8ffc2..61bb17d5171 100644 --- a/apps/desktop/src/locales/bs/messages.json +++ b/apps/desktop/src/locales/bs/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Neispravan verifikacijski kod" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Nastavi" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index 3b8562814fd..ac59b1bd040 100644 --- a/apps/desktop/src/locales/ca/messages.json +++ b/apps/desktop/src/locales/ca/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Codi de verificació no vàlid" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continua" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index 75136c41831..0772645a8d4 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Neplatný ověřovací kód" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Pokračovat" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Zadejte více e-mailů oddělených čárkou." }, + "emailsRequiredChangeAccessType": { + "message": "Ověření e-mailu vyžaduje alespoň jednu e-mailovou adresu. Chcete-li odebrat všechny emaily, změňte výše uvedený typ přístupu." + }, "emailPlaceholder": { "message": "uživatel@bitwarden.com, uživatel@společnost.cz" + }, + "userVerificationFailed": { + "message": "Ověření uživatele se nezdařilo." } } diff --git a/apps/desktop/src/locales/cy/messages.json b/apps/desktop/src/locales/cy/messages.json index 46df0aca8c5..6f39024dd17 100644 --- a/apps/desktop/src/locales/cy/messages.json +++ b/apps/desktop/src/locales/cy/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continue" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index f6abcd51740..f7f6dd31da5 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Ugyldig bekræftelseskode" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Fortsæt" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index a2c346896ac..fb045e30489 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Ungültiger Verifizierungscode" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Weiter" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Gib mehrere E-Mail-Adressen ein, indem du sie mit einem Komma trennst." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "benutzer@bitwarden.com, benutzer@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index 624560f5888..fe21423ceba 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Μη έγκυρος κωδικός επαλήθευσης" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Συνέχεια" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index aaf1e12955c..04684ffe9bd 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continue" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json index 1dca7070bfc..bda8ffa8fd5 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continue" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/eo/messages.json b/apps/desktop/src/locales/eo/messages.json index cefd462e99f..79e1ece499d 100644 --- a/apps/desktop/src/locales/eo/messages.json +++ b/apps/desktop/src/locales/eo/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Nevalida kontrola kodo" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Daŭrigi" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index d9fb17907aa..91ec21c717f 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Código de verificación incorrecto" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continuar" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Introduce varios correos electrónicos separándolos con una coma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/et/messages.json b/apps/desktop/src/locales/et/messages.json index ba930db8961..84f432ac410 100644 --- a/apps/desktop/src/locales/et/messages.json +++ b/apps/desktop/src/locales/et/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Vale kinnituskood" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Jätka" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json index e03da9ef685..2adb34fa9f2 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Egiaztatze-kodea ez da baliozkoa" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Jarraitu" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/fa/messages.json b/apps/desktop/src/locales/fa/messages.json index a443cc8c2e7..94e5a54ab4b 100644 --- a/apps/desktop/src/locales/fa/messages.json +++ b/apps/desktop/src/locales/fa/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "کد تأیید نامعتبر است" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "ادامه" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index c7b51def9b2..78eedc7e1ce 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Virheellinen todennuskoodi" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Jatka" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/fil/messages.json b/apps/desktop/src/locales/fil/messages.json index 5835821f526..25dd12dd51e 100644 --- a/apps/desktop/src/locales/fil/messages.json +++ b/apps/desktop/src/locales/fil/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Maling verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Magpatuloy" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index e8d07e28d2d..04e0725c9b4 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Code de vérification invalide" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continuer" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/gl/messages.json b/apps/desktop/src/locales/gl/messages.json index 6d0922bd680..3b80024392e 100644 --- a/apps/desktop/src/locales/gl/messages.json +++ b/apps/desktop/src/locales/gl/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continue" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json index 763401ac6fe..66654aafbc6 100644 --- a/apps/desktop/src/locales/he/messages.json +++ b/apps/desktop/src/locales/he/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "קוד אימות שגוי" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "המשך" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/hi/messages.json b/apps/desktop/src/locales/hi/messages.json index 33b69ac1519..d797b6319c0 100644 --- a/apps/desktop/src/locales/hi/messages.json +++ b/apps/desktop/src/locales/hi/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continue" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json index 2dc081fa3c7..969a0df9cee 100644 --- a/apps/desktop/src/locales/hr/messages.json +++ b/apps/desktop/src/locales/hr/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Nevažeći kôd za provjeru" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Nastavi" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index 3ec097c2a7a..d73d746fded 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Érvénytelen ellenőrző kód" }, + "invalidEmailOrVerificationCode": { + "message": "Az email cím vagy az ellenőrző kód érvénytelen." + }, "continue": { "message": "Folytatás" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Írjunk be több email címet vesszővel elválasztva." }, + "emailsRequiredChangeAccessType": { + "message": "Az email cím ellenőrzéshez legalább egy email cím szükséges. Az összes email cím eltávolításához módosítsuk a fenti hozzáférési típust." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "A felhasználó ellenőrzése sikertelen volt." } } diff --git a/apps/desktop/src/locales/id/messages.json b/apps/desktop/src/locales/id/messages.json index 7648f4bb99b..f5c74c471de 100644 --- a/apps/desktop/src/locales/id/messages.json +++ b/apps/desktop/src/locales/id/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Kode verifikasi tidak valid" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Lanjutkan" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index eb2ade245a0..c394dd84a6f 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Codice di verifica non valido" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continua" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/ja/messages.json b/apps/desktop/src/locales/ja/messages.json index a9b05f728d8..908fa271a16 100644 --- a/apps/desktop/src/locales/ja/messages.json +++ b/apps/desktop/src/locales/ja/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "認証コードが間違っています" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "続行" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/ka/messages.json b/apps/desktop/src/locales/ka/messages.json index 68bba7fcb27..b9fb3f528d7 100644 --- a/apps/desktop/src/locales/ka/messages.json +++ b/apps/desktop/src/locales/ka/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "არასწორი გადამოწმების კოდი" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "გაგრძელება" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/km/messages.json b/apps/desktop/src/locales/km/messages.json index 6d0922bd680..3b80024392e 100644 --- a/apps/desktop/src/locales/km/messages.json +++ b/apps/desktop/src/locales/km/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continue" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/kn/messages.json b/apps/desktop/src/locales/kn/messages.json index 3c6aced3a73..13463f63da1 100644 --- a/apps/desktop/src/locales/kn/messages.json +++ b/apps/desktop/src/locales/kn/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "ಮುಂದುವರಿಸಿ" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json index 8932f0efb48..524d1bc8b01 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "유효하지 않은 확인 코드" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "계속" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json index a19856a776e..3a003cb565e 100644 --- a/apps/desktop/src/locales/lt/messages.json +++ b/apps/desktop/src/locales/lt/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Neteisingas patvirtinimo kodas" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Tęsti" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index 8a863256ed1..b8a0cdd8434 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Nederīgs apliecinājuma kods" }, + "invalidEmailOrVerificationCode": { + "message": "Nederīga e-pasta adrese vai apliecinājuma kods" + }, "continue": { "message": "Turpināt" }, @@ -4388,10 +4391,10 @@ "message": "Šeit parādīsies arhivētie vienumi, un tie netiks iekļauti vispārējās meklēšanas iznākumos un automātiskās aizpildes ieteikumos." }, "itemArchiveToast": { - "message": "Item archived" + "message": "Vienums ievietots arhīvā" }, "itemUnarchivedToast": { - "message": "Item unarchived" + "message": "Vienums izņemts no arhīva" }, "archiveItem": { "message": "Arhivēt vienumu" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "E-pasta apliecināšanai ir nepieciešama vismaz viena e-pasta adrese. Lai noņemtu visas e-pasta adreses, augstāk jānomaina piekļūšanas veids." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "Lietotāja apliecināšana neizdevās." } } diff --git a/apps/desktop/src/locales/me/messages.json b/apps/desktop/src/locales/me/messages.json index 773a596a10d..de1c690bb8e 100644 --- a/apps/desktop/src/locales/me/messages.json +++ b/apps/desktop/src/locales/me/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Nastavi" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/ml/messages.json b/apps/desktop/src/locales/ml/messages.json index 3bddc3baa5b..0b15dafd31b 100644 --- a/apps/desktop/src/locales/ml/messages.json +++ b/apps/desktop/src/locales/ml/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "തുടരുക" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/mr/messages.json b/apps/desktop/src/locales/mr/messages.json index 6d0922bd680..3b80024392e 100644 --- a/apps/desktop/src/locales/mr/messages.json +++ b/apps/desktop/src/locales/mr/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continue" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/my/messages.json b/apps/desktop/src/locales/my/messages.json index 22f4a30329a..6efe4072ecb 100644 --- a/apps/desktop/src/locales/my/messages.json +++ b/apps/desktop/src/locales/my/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continue" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/nb/messages.json b/apps/desktop/src/locales/nb/messages.json index b2b1631fe04..a4842046c16 100644 --- a/apps/desktop/src/locales/nb/messages.json +++ b/apps/desktop/src/locales/nb/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Ugyldig verifiseringskode" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Fortsett" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/ne/messages.json b/apps/desktop/src/locales/ne/messages.json index b9eba55d8bd..43a96999ed3 100644 --- a/apps/desktop/src/locales/ne/messages.json +++ b/apps/desktop/src/locales/ne/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continue" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index 306bf31efe2..f6908fd6498 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Ongeldige verificatiecode" }, + "invalidEmailOrVerificationCode": { + "message": "Ongeldig e-mailadres verificatiecode" + }, "continue": { "message": "Doorgaan" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Voer meerdere e-mailadressen in door te scheiden met een komma." }, + "emailsRequiredChangeAccessType": { + "message": "E-mailverificatie vereist ten minste één e-mailadres. Om alle e-mailadressen te verwijderen, moet je het toegangstype hierboven wijzigen." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "Gebruikersverificatie is mislukt." } } diff --git a/apps/desktop/src/locales/nn/messages.json b/apps/desktop/src/locales/nn/messages.json index dc62d73a236..1c0ab7c51a8 100644 --- a/apps/desktop/src/locales/nn/messages.json +++ b/apps/desktop/src/locales/nn/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Ugyldig stadfestingskode" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Fortsett" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/or/messages.json b/apps/desktop/src/locales/or/messages.json index e8ea506a873..f28ba8ad5a0 100644 --- a/apps/desktop/src/locales/or/messages.json +++ b/apps/desktop/src/locales/or/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continue" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index 0cee8d6683d..e429217c278 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Kod weryfikacyjny jest nieprawidłowy" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Kontynuuj" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/pt_BR/messages.json b/apps/desktop/src/locales/pt_BR/messages.json index 817e9de0c50..1ec4807e2a4 100644 --- a/apps/desktop/src/locales/pt_BR/messages.json +++ b/apps/desktop/src/locales/pt_BR/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Código de verificação inválido" }, + "invalidEmailOrVerificationCode": { + "message": "E-mail ou código de verificação inválido" + }, "continue": { "message": "Continuar" }, @@ -4388,10 +4391,10 @@ "message": "Os itens arquivados aparecerão aqui e serão excluídos dos resultados gerais de busca e das sugestões de preenchimento automático." }, "itemArchiveToast": { - "message": "Item archived" + "message": "Item arquivado" }, "itemUnarchivedToast": { - "message": "Item unarchived" + "message": "Item desarquivado" }, "archiveItem": { "message": "Arquivar item" @@ -4487,7 +4490,7 @@ "message": "Ação do limite de tempo" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "Erro: Não é possível descriptografar" }, "sessionTimeoutHeader": { "message": "Limite de tempo da sessão" @@ -4588,11 +4591,11 @@ "message": "Por que estou vendo isso?" }, "sendPasswordHelperText": { - "message": "Indivíduos precisarão utilizar a senha para ver este Send", + "message": "Os indivíduos precisarão digitar a senha para ver este Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "emailProtected": { - "message": "E-mail protegido" + "message": "Protegido por e-mail" }, "emails": { "message": "E-mails" @@ -4601,7 +4604,7 @@ "message": "Qualquer um com o link" }, "anyOneWithPassword": { - "message": "Qualquer um com uma senha definida por você" + "message": "Qualquer pessoa com uma senha configurada por você" }, "whoCanView": { "message": "Quem pode visualizar" @@ -4613,9 +4616,15 @@ "message": "Após compartilhar este link de Send, indivíduos precisarão verificar seus e-mails com um código para visualizar este Send." }, "enterMultipleEmailsSeparatedByComma": { - "message": "Insira múltiplos e-mails separando-os com vírgula." + "message": "Digite vários e-mails, separados com uma vírgula." + }, + "emailsRequiredChangeAccessType": { + "message": "A verificação de e-mail requer pelo menos um endereço de e-mail. Para remover todos, altere o tipo de acesso acima." }, "emailPlaceholder": { - "message": "user@bitwarden.com , user@acme.com" + "message": "usuário@bitwarden.com , usuário@acme.com" + }, + "userVerificationFailed": { + "message": "Falha na verificação do usuário." } } diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index 3076291a4a3..ca5091ccbe6 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Código de verificação inválido" }, + "invalidEmailOrVerificationCode": { + "message": "E-mail ou código de verificação inválido" + }, "continue": { "message": "Continuar" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Introduza vários e-mails, separados por vírgula." }, + "emailsRequiredChangeAccessType": { + "message": "A verificação por e-mail requer pelo menos um endereço de e-mail. Para remover todos os e-mails, altere o tipo de acesso acima." + }, "emailPlaceholder": { "message": "utilizador@bitwarden.com , utilizador@acme.com" + }, + "userVerificationFailed": { + "message": "Falha na verificação do utilizador." } } diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json index b8fc25b4105..6c67e9d9e06 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Cod de verificare nevalid" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continuare" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index 089e815fefe..e48319dac55 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Неверный код подтверждения" }, + "invalidEmailOrVerificationCode": { + "message": "Неверный email или код подтверждения" + }, "continue": { "message": "Продолжить" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Введите несколько email, разделяя их запятой." }, + "emailsRequiredChangeAccessType": { + "message": "Для проверки электронной почты требуется как минимум один адрес email. Чтобы удалить все адреса электронной почты, измените тип доступа выше." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "Проверка пользователя не удалась." } } diff --git a/apps/desktop/src/locales/si/messages.json b/apps/desktop/src/locales/si/messages.json index a1a84b8ba15..4d75c329656 100644 --- a/apps/desktop/src/locales/si/messages.json +++ b/apps/desktop/src/locales/si/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "ඉදිරියට" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index 2876361bbf0..50d88a49405 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Neplatný verifikačný kód" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Pokračovať" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Zadajte viacero e-mailových adries oddelených čiarkou." }, + "emailsRequiredChangeAccessType": { + "message": "Overenie e-mailu vyžaduje aspoň jednu e-mailovú adresu. Ak chcete odstrániť všetky e-maily, zmeňte typ prístupu vyššie." + }, "emailPlaceholder": { "message": "pouzivate@bitwarden.com, pouzivatel@acme.com" + }, + "userVerificationFailed": { + "message": "Overenie používateľa zlyhalo." } } diff --git a/apps/desktop/src/locales/sl/messages.json b/apps/desktop/src/locales/sl/messages.json index 82e0a20b29e..63ae05a12b5 100644 --- a/apps/desktop/src/locales/sl/messages.json +++ b/apps/desktop/src/locales/sl/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Neveljavna verifikacijska koda" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Nadaljuj" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index 633d3123242..32715ed60d1 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Неисправан верификациони код" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Настави" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index ed9e62e8319..b44d54a14a3 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Ogiltig verifieringskod" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Fortsätt" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Ange flera e-postadresser genom att separera dem med kommatecken." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "användare@bitwarden.com , användare@acme.com" + }, + "userVerificationFailed": { + "message": "Verifiering av användare misslyckades." } } diff --git a/apps/desktop/src/locales/ta/messages.json b/apps/desktop/src/locales/ta/messages.json index 53e155874f6..feb4b92c77e 100644 --- a/apps/desktop/src/locales/ta/messages.json +++ b/apps/desktop/src/locales/ta/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "தவறான சரிபார்ப்புக் குறியீடு" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "தொடரவும்" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json index 6d0922bd680..3b80024392e 100644 --- a/apps/desktop/src/locales/te/messages.json +++ b/apps/desktop/src/locales/te/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continue" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/th/messages.json b/apps/desktop/src/locales/th/messages.json index b7117a7ccad..eeb0029b928 100644 --- a/apps/desktop/src/locales/th/messages.json +++ b/apps/desktop/src/locales/th/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "รหัสการตรวจสอบสิทธิ์ไม่ถูกต้อง" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "ดำเนินการต่อไป" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index 96bb11c7a35..29171ca8fef 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Geçersiz doğrulama kodu" }, + "invalidEmailOrVerificationCode": { + "message": "E-posta veya doğrulama kodu geçersiz" + }, "continue": { "message": "Devam Et" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "E-posta adreslerini virgülle ayırarak yazın." }, + "emailsRequiredChangeAccessType": { + "message": "E-posta doğrulaması için en az bir e-posta adresi gerekir. Tüm e-postaları silmek için yukarıdan erişim türünü değiştirin." + }, "emailPlaceholder": { "message": "kullanici@bitwarden.com , kullanici@acme.com" + }, + "userVerificationFailed": { + "message": "Kullanıcı doğrulaması başarısız oldu." } } diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index abcdbea0b1f..e982d46b454 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Недійсний код підтвердження" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Продовжити" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json index f06556568a1..8fe0adee948 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Mã xác minh không đúng" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Tiếp tục" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index 9941e296da3..ad09c8f032e 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "无效的验证码" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "继续" }, @@ -4388,10 +4391,10 @@ "message": "已归档的项目将显示在此处,并将被排除在一般搜索结果和自动填充建议之外。" }, "itemArchiveToast": { - "message": "Item archived" + "message": "项目已归档" }, "itemUnarchivedToast": { - "message": "Item unarchived" + "message": "项目已取消归档" }, "archiveItem": { "message": "归档项目" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "输入多个电子邮箱(使用逗号分隔)。" }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com, user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index 461eb031068..364f67c7b58 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "無效的驗證碼" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "繼續" }, @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "請以逗號分隔輸入多個電子郵件地址。" }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } From 2f6a5133f8c53d345c8cd133a722578813e09963 Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 13:24:40 +0100 Subject: [PATCH 101/134] Autosync the updated translations (#19094) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/src/_locales/ar/messages.json | 51 +++++ apps/browser/src/_locales/az/messages.json | 51 +++++ apps/browser/src/_locales/be/messages.json | 51 +++++ apps/browser/src/_locales/bg/messages.json | 51 +++++ apps/browser/src/_locales/bn/messages.json | 51 +++++ apps/browser/src/_locales/bs/messages.json | 51 +++++ apps/browser/src/_locales/ca/messages.json | 51 +++++ apps/browser/src/_locales/cs/messages.json | 51 +++++ apps/browser/src/_locales/cy/messages.json | 51 +++++ apps/browser/src/_locales/da/messages.json | 51 +++++ apps/browser/src/_locales/de/messages.json | 51 +++++ apps/browser/src/_locales/el/messages.json | 51 +++++ apps/browser/src/_locales/en_GB/messages.json | 51 +++++ apps/browser/src/_locales/en_IN/messages.json | 51 +++++ apps/browser/src/_locales/es/messages.json | 51 +++++ apps/browser/src/_locales/et/messages.json | 51 +++++ apps/browser/src/_locales/eu/messages.json | 51 +++++ apps/browser/src/_locales/fa/messages.json | 51 +++++ apps/browser/src/_locales/fi/messages.json | 91 ++++++-- apps/browser/src/_locales/fil/messages.json | 51 +++++ apps/browser/src/_locales/fr/messages.json | 165 +++++++++----- apps/browser/src/_locales/gl/messages.json | 51 +++++ apps/browser/src/_locales/he/messages.json | 51 +++++ apps/browser/src/_locales/hi/messages.json | 51 +++++ apps/browser/src/_locales/hr/messages.json | 57 ++++- apps/browser/src/_locales/hu/messages.json | 51 +++++ apps/browser/src/_locales/id/messages.json | 51 +++++ apps/browser/src/_locales/it/messages.json | 51 +++++ apps/browser/src/_locales/ja/messages.json | 51 +++++ apps/browser/src/_locales/ka/messages.json | 51 +++++ apps/browser/src/_locales/km/messages.json | 51 +++++ apps/browser/src/_locales/kn/messages.json | 51 +++++ apps/browser/src/_locales/ko/messages.json | 51 +++++ apps/browser/src/_locales/lt/messages.json | 51 +++++ apps/browser/src/_locales/lv/messages.json | 55 ++++- apps/browser/src/_locales/ml/messages.json | 51 +++++ apps/browser/src/_locales/mr/messages.json | 51 +++++ apps/browser/src/_locales/my/messages.json | 51 +++++ apps/browser/src/_locales/nb/messages.json | 51 +++++ apps/browser/src/_locales/ne/messages.json | 51 +++++ apps/browser/src/_locales/nl/messages.json | 51 +++++ apps/browser/src/_locales/nn/messages.json | 51 +++++ apps/browser/src/_locales/or/messages.json | 51 +++++ apps/browser/src/_locales/pl/messages.json | 51 +++++ apps/browser/src/_locales/pt_BR/messages.json | 83 +++++-- apps/browser/src/_locales/pt_PT/messages.json | 51 +++++ apps/browser/src/_locales/ro/messages.json | 51 +++++ apps/browser/src/_locales/ru/messages.json | 51 +++++ apps/browser/src/_locales/si/messages.json | 51 +++++ apps/browser/src/_locales/sk/messages.json | 51 +++++ apps/browser/src/_locales/sl/messages.json | 209 +++++++++++------- apps/browser/src/_locales/sr/messages.json | 51 +++++ apps/browser/src/_locales/sv/messages.json | 51 +++++ apps/browser/src/_locales/ta/messages.json | 51 +++++ apps/browser/src/_locales/te/messages.json | 51 +++++ apps/browser/src/_locales/th/messages.json | 51 +++++ apps/browser/src/_locales/tr/messages.json | 51 +++++ apps/browser/src/_locales/uk/messages.json | 51 +++++ apps/browser/src/_locales/vi/messages.json | 51 +++++ apps/browser/src/_locales/zh_CN/messages.json | 55 ++++- apps/browser/src/_locales/zh_TW/messages.json | 51 +++++ apps/browser/store/locales/sl/copy.resx | 6 +- 62 files changed, 3293 insertions(+), 182 deletions(-) diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 7334362d446..5768966511d 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "رمز التحقق غير صالح" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "تم نسخ $VALUE$", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "إرسال رابط منسوخ", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 6168f7cf2dd..6572c2d09d9 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Yararsız doğrulama kodu" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ kopyalandı", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Risk altındakı girişlərin olduğu siyahının təsviri." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Risk altında olan saytda Bitwarden avto-doldurma menyusu ilə güclü, unikal parolları cəld yaradın.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send keçidi kopyalandı", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Birdən çox e-poçtu daxil edərkən vergül istifadə edin." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Şəxslər, Send-ə baxması üçün parolu daxil etməli olacaqlar", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index ea569cabdf4..cd48ff09ee4 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Памылковы праверачны код" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ скапіяваны", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index e5d68bce366..4f2f26bade8 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Грешен код за потвърждаване" }, + "invalidEmailOrVerificationCode": { + "message": "Грешна е-поща или код за потвърждаване" + }, "valueCopied": { "message": "Копирано е $VALUE$", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Илюстрация на списък с елементи за вписване, които са в риск." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Генерирайте бързо сложна и уникална парола от менюто за автоматично попълване на Битуорден, на уеб сайта, който е в риск.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ часа", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Копирайте и споделете връзката към Изпращането. То ще бъде достъпно за всеки с връзката в рамките на следващите $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Копирайте и споделете връзката към Изпращането. То ще бъде достъпно за всеки с връзката и паролата, която зададете, в рамките на следващите $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Копирайте и споделете връзка към това Изпращане. То ще може да бъде видяно само от хората, които сте посочили, в рамките на следващите $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Връзката към Изпращането е копирана", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Можете да въведете повече е-пощи, като ги разделите със запетая." }, + "emailsRequiredChangeAccessType": { + "message": "Потвърждаването на е-пощата изисква да е наличен поне един адрес на е-поща. Ако искате да премахнете всички е-пощи, променете начина за достъп по-горе." + }, "emailPlaceholder": { "message": "потребител@bitwarden.com , потребител@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Хората ще трябва да въведат паролата, за да видят това Изпращане", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "Проверката на потребителя беше неуспешна." } } diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index 533b12ab0a5..8aa07e2ec82 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ অনুলিপিত", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index 35c4177e5eb..8ef61e0e63e 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copied", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index 8e82fc34be4..d524731fb46 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Codi de verificació no vàlid" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "S'ha copiat $VALUE$", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index ed1b37134e1..9106d0518db 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Neplatný ověřovací kód" }, + "invalidEmailOrVerificationCode": { + "message": "Neplatný e-mail nebo ověřovací kód" + }, "valueCopied": { "message": "Zkopírováno: $VALUE$", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Ilustrace seznamu přihlášení, která jsou ohrožená." }, + "welcomeDialogGraphicAlt": { + "message": "Ilustrace rozložení stránky trezoru Bitwarden." + }, "generatePasswordSlideDesc": { "message": "Rychle vygeneruje silné, unikátní heslo s nabídkou automatického vyplňování Bitwarden na ohrožených stránkách.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hodin", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Zkopírujte a sdílejte tento odkaz Send. Send bude k dispozici komukoli s odkazem na dalších $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Zkopírujte a sdílejte tento odkaz Send. Send bude k dispozici komukoli s odkazem a heslem na dalších $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Zkopírujte a sdílejte tento Send pro odesílání. Můžou jej zobrazit osoby, které jste zadali, a to po dobu $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Odkaz Send byl zkopírován", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Zadejte více e-mailů oddělených čárkou." }, + "emailsRequiredChangeAccessType": { + "message": "Ověření e-mailu vyžaduje alespoň jednu e-mailovou adresu. Chcete-li odebrat všechny emaily, změňte výše uvedený typ přístupu." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Pro zobrazení tohoto Send budou muset jednotlivci zadat heslo", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "Ověření uživatele se nezdařilo." } } diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 165cd05de8e..12dc8d7b44f 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Cod dilysu annilys" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ wedi'i gopïo", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index 615cc6a2a0b..d19d07ee9f5 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Ugyldig bekræftelseskode" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ kopieret", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send-link kopieret", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 8f2b023bc00..6d301950e03 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Ungültiger Verifizierungscode" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ kopiert", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration einer Liste gefährdeter Zugangsdaten." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Generiere schnell ein starkes, einzigartiges Passwort mit dem Bitwarden Auto-Ausfüllen-Menü auf der gefährdeten Website.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ Stunden", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Kopiere und teile diesen Send-Link. Das Send wird für jeden mit dem Link für die nächsten $TIME$ verfügbar sein.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Kopiere und teile diesen Send-Link. Das Send wird für jeden mit dem Link und dem von dir festgelegten Passwort für die nächsten $TIME$ verfügbar sein.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Kopiere und teile diesen Send-Link. Er kann von den von dir angegebenen Personen für die nächsten $TIME$ angesehen werden.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send-Link kopiert", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Gib mehrere E-Mail-Adressen ein, indem du sie mit einem Komma trennst." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "benutzer@bitwarden.com, benutzer@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Personen müssen das Passwort eingeben, um dieses Send anzusehen", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "Benutzerverifizierung fehlgeschlagen." } } diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index 68f7267825d..0e5fc2eaeb1 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Μη έγκυρος κωδικός επαλήθευσης" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ αντιγράφηκε", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Ο σύνδεσμος Send αντιγράφηκε", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index d61774df145..be564f8a950 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copied", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index 3622ffce241..18e02ec48ca 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copied", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index 131263ea4d9..45d2139fd6b 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Código de verificación no válido" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "Valor de $VALUE$ copiado", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Genera rápidamente una contraseña segura y única con el menú de autocompletado de Bitwarden en el sitio en riesgo.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Enlace del Send copiado", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Introduce varios correos electrónicos separándolos con una coma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Los individuos tendrán que introducir la contraseña para ver este Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index cd78c444c89..789454218e1 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Vale kinnituskood" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ on kopeeritud", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index 3e4382a3d3b..f08604910af 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Egiaztatze-kodea ez da baliozkoa" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ kopiatuta", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index 5bb22dc6292..4c2474d614c 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "کد تأیید نامعتبر است" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ کپی شد", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "تصویری از فهرست ورودهایی که در معرض خطر هستند." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "با استفاده از منوی پر کردن خودکار Bitwarden در سایت در معرض خطر، به‌سرعت یک کلمه عبور قوی و منحصر به فرد تولید کنید.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "لینک ارسال کپی شد", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 2f5b1ec4932..8f5d3c05a63 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -29,7 +29,7 @@ "message": "Kirjaudu pääsyavaimella" }, "unlockWithPasskey": { - "message": "Unlock with passkey" + "message": "Poista lukitus todentamisavaimella" }, "useSingleSignOn": { "message": "Käytä kertakirjautumista" @@ -440,7 +440,7 @@ "message": "Synkronointi" }, "syncNow": { - "message": "Sync now" + "message": "Synkronoi nyt" }, "lastSync": { "message": "Viimeisin synkronointi:" @@ -607,10 +607,10 @@ "message": "Näytä kaikki" }, "showAll": { - "message": "Show all" + "message": "Näytä kaikki" }, "viewLess": { - "message": "View less" + "message": "Näytä vähemmän" }, "viewLogin": { "message": "View login" @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Virheellinen todennuskoodi" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ kopioitu", "description": "Value has been copied to the clipboard.", @@ -1492,7 +1495,7 @@ "message": "This file is using an outdated encryption method." }, "attachmentUpdated": { - "message": "Attachment updated" + "message": "Liite päivitetty" }, "file": { "message": "Tiedosto" @@ -1661,7 +1664,7 @@ "message": "Passkey authentication failed" }, "useADifferentLogInMethod": { - "message": "Use a different log in method" + "message": "Käytä vaihtoehtoista kirjautumistapaa" }, "awaitingSecurityKeyInteraction": { "message": "Odotetaan suojausavaimen aktivointia..." @@ -1953,7 +1956,7 @@ "message": "Erääntymisvuosi" }, "monthly": { - "message": "month" + "message": "kuukausi" }, "expiration": { "message": "Voimassaolo päättyy" @@ -2052,7 +2055,7 @@ "message": "Sähköposti" }, "emails": { - "message": "Emails" + "message": "Sähköpostit" }, "phone": { "message": "Puhelinnumero" @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Kuvitus vaarantuneiden kirjautumistietojen luettelosta." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Luo vahva ja ainutlaatuinen salasana nopeasti Bitwardenin automaattitäytön valikosta vaarantuneella sivustolla.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send-linkki kopioitiin", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4824,7 +4869,7 @@ "message": "Hallintapaneelista" }, "admin": { - "message": "Admin" + "message": "Ylläpitäjä" }, "automaticUserConfirmation": { "message": "Automatic user confirmation" @@ -4854,16 +4899,16 @@ "message": "Turned on automatic confirmation" }, "availableNow": { - "message": "Available now" + "message": "Saatavilla nyt" }, "accountSecurity": { "message": "Tilin suojaus" }, "phishingBlocker": { - "message": "Phishing Blocker" + "message": "Tietojenkalasteluhyökkäysten estäminen" }, "enablePhishingDetection": { - "message": "Phishing detection" + "message": "Tietojenkalasteluhyökkäysten tunnistaminen" }, "enablePhishingDetectionDesc": { "message": "Display warning before accessing suspected phishing sites" @@ -4981,7 +5026,7 @@ } }, "downloadAttachmentLabel": { - "message": "Download Attachment" + "message": "Lataa liite" }, "downloadBitwarden": { "message": "Lataa Bitwarden" @@ -5716,10 +5761,10 @@ "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, "vulnerablePassword": { - "message": "Vulnerable password." + "message": "Heikko salasana" }, "changeNow": { - "message": "Change now" + "message": "Vaihda nyt" }, "missingWebsite": { "message": "Missing website" @@ -5773,13 +5818,13 @@ "message": "Tervetuloa holviisi!" }, "phishingPageTitleV2": { - "message": "Phishing attempt detected" + "message": "Havaittu tietojenkalasteluhyökkäyksen yritys" }, "phishingPageSummary": { - "message": "The site you are attempting to visit is a known malicious site and a security risk." + "message": "Sivusto jota olet avaamassa on tunnetusti haitallinen ja sen avaaminen on turvallisuusriski" }, "phishingPageCloseTabV2": { - "message": "Close this tab" + "message": "Sulje tämä välilehti" }, "phishingPageContinueV2": { "message": "Jatka tälle sivustolle (ei suositeltavaa)" @@ -5893,10 +5938,10 @@ "description": "'WebAssembly' is a technical term and should not be translated." }, "showMore": { - "message": "Show more" + "message": "Näytä enemmän" }, "showLess": { - "message": "Show less" + "message": "Näytä vähemmän" }, "next": { "message": "Seuraava" @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index abb06f0f19f..069be19dc86 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Maling verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "Kinopya ang $VALUE$", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index 596315c4d3f..afd5415e249 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -29,7 +29,7 @@ "message": "Se connecter avec une clé d'accès" }, "unlockWithPasskey": { - "message": "Unlock with passkey" + "message": "Déverrouiller avec une clé d'accès" }, "useSingleSignOn": { "message": "Utiliser l'authentification unique" @@ -574,28 +574,28 @@ "message": "Les éléments archivés apparaîtront ici et seront exclus des résultats de recherche généraux et des suggestions de remplissage automatique." }, "itemArchiveToast": { - "message": "Item archived" + "message": "Élément archivé" }, "itemUnarchivedToast": { - "message": "Item unarchived" + "message": "Élément désarchivé" }, "archiveItem": { "message": "Archiver l'élément" }, "archiveItemDialogContent": { - "message": "Once archived, this item will be excluded from search results and autofill suggestions." + "message": "Une fois archivé, cet élément sera exclu des résultats de recherche et des suggestions de remplissage automatique." }, "archived": { - "message": "Archived" + "message": "Archivé" }, "unarchiveAndSave": { - "message": "Unarchive and save" + "message": "Désarchiver et enregistrer" }, "upgradeToUseArchive": { "message": "Une adhésion premium est requise pour utiliser Archive." }, "itemRestored": { - "message": "Item has been restored" + "message": "L'élément a été restauré" }, "edit": { "message": "Modifier" @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Code de vérification invalide" }, + "invalidEmailOrVerificationCode": { + "message": "Courriel ou code de vérification invalide" + }, "valueCopied": { "message": "$VALUE$ copié", "description": "Value has been copied to the clipboard.", @@ -988,10 +991,10 @@ "message": "Non" }, "noAuth": { - "message": "Anyone with the link" + "message": "Toute personne disposant du lien" }, "anyOneWithPassword": { - "message": "Anyone with a password set by you" + "message": "N'importe qui avec un mot de passe défini par vous" }, "location": { "message": "Emplacement" @@ -1318,7 +1321,7 @@ "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { - "message": "Choisit la manière dont la détection des correspondances URI est gérée par défaut pour les connexions lors d'actions telles que la saisie automatique." + "message": "Choisissez le mode de traitement par défaut de la détection de correspondance URI, pour les connexions lors de l'exécution d'actions telles que le remplissage automatique." }, "theme": { "message": "Thème" @@ -1341,7 +1344,7 @@ "message": "Exporter à partir de" }, "exportVerb": { - "message": "Export", + "message": "Exporter", "description": "The verb form of the word Export" }, "exportNoun": { @@ -1353,7 +1356,7 @@ "description": "The noun form of the word Import" }, "importVerb": { - "message": "Import", + "message": "Importer", "description": "The verb form of the word Import" }, "fileFormat": { @@ -1552,13 +1555,13 @@ "message": "Options de connexion propriétaires à deux facteurs telles que YubiKey et Duo." }, "premiumSubscriptionEnded": { - "message": "Your Premium subscription ended" + "message": "Votre abonnement Premium est terminé" }, "archivePremiumRestart": { - "message": "To regain access to your archive, restart your Premium subscription. If you edit details for an archived item before restarting, it'll be moved back into your vault." + "message": "Pour récupérer l'accès à vos archives, redémarrez votre abonnement Premium. Si vous modifiez les détails d'un élément archivé avant de le redémarrer, il sera déplacé dans votre coffre." }, "restartPremium": { - "message": "Restart Premium" + "message": "Redémarrer Premium" }, "ppremiumSignUpReports": { "message": "Hygiène du mot de passe, santé du compte et rapports sur les brèches de données pour assurer la sécurité de votre coffre." @@ -2052,7 +2055,7 @@ "message": "Courriel" }, "emails": { - "message": "Emails" + "message": "Courriels" }, "phone": { "message": "Téléphone" @@ -2483,7 +2486,7 @@ "message": "Élément définitivement supprimé" }, "archivedItemRestored": { - "message": "Archived item restored" + "message": "Élément archivé restauré" }, "restoreItem": { "message": "Restaurer l'élément" @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration d'une liste de connexions à risque." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Générez rapidement un mot de passe fort et unique grâce au menu de saisie automatique de Bitwarden sur le site à risque.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ heures", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copiez et partagez ce lien Send. Le Send sera accessible à toute personne disposant du lien pour les prochains $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copiez et partagez ce lien Send. Le Send sera accessible à toute personne possédant le lien et le mot de passe que vous avez défini pendant $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copiez et partagez ce lien Send. Il peut être vu par les personnes définies pendant $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Lien Send copié", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -3350,10 +3395,10 @@ "message": "Erreur" }, "prfUnlockFailed": { - "message": "Failed to unlock with passkey. Please try again or use another unlock method." + "message": "Impossible de déverrouiller avec la clé d'accès. Veuillez réessayer ou utiliser une autre méthode de déverrouillage." }, "noPrfCredentialsAvailable": { - "message": "No PRF-enabled passkeys are available for unlock. Please log in with a passkey first." + "message": "Aucune clé d'accès PRF disponible pour le déverrouillage. Veuillez vous connecter avec une clé d'accès en premier lieu." }, "decryptionError": { "message": "Erreur de déchiffrement" @@ -4142,7 +4187,7 @@ "message": "Ok" }, "toggleSideNavigation": { - "message": "Basculer la navigation latérale" + "message": "Activer la navigation latérale" }, "skipToContent": { "message": "Accéder directement au contenu" @@ -4731,7 +4776,7 @@ } }, "moreOptionsLabelNoPlaceholder": { - "message": "More options" + "message": "Plus de paramètres" }, "moreOptionsTitle": { "message": "Plus d'options - $ITEMNAME$", @@ -4827,46 +4872,46 @@ "message": "Admin" }, "automaticUserConfirmation": { - "message": "Automatic user confirmation" + "message": "Confirmation d'utilisateur automatique" }, "automaticUserConfirmationHint": { - "message": "Automatically confirm pending users while this device is unlocked" + "message": "Confirmer automatiquement les utilisateurs en attente pendant que cet appareil est déverrouillé" }, "autoConfirmOnboardingCallout": { - "message": "Save time with automatic user confirmation" + "message": "Gagnez du temps grâce à la confirmation d'utilisateur automatique" }, "autoConfirmWarning": { - "message": "This could impact your organization’s data security. " + "message": "Cela peut avoir un impact sur la sécurité des données de votre organisation. " }, "autoConfirmWarningLink": { - "message": "Learn about the risks" + "message": "En apprendre plus sur les risques" }, "autoConfirmSetup": { - "message": "Automatically confirm new users" + "message": "Confirmer les nouveaux utilisateurs automatiquement" }, "autoConfirmSetupDesc": { - "message": "New users will be automatically confirmed while this device is unlocked." + "message": "Les nouveaux utilisateurs seront confirmés automatiquement pendant que cet appareil est déverrouillé." }, "autoConfirmSetupHint": { - "message": "What are the potential security risks?" + "message": "Quels-sont les potentiels risques de sécurité ?" }, "autoConfirmEnabled": { - "message": "Turned on automatic confirmation" + "message": "Confirmation automatique activée" }, "availableNow": { - "message": "Available now" + "message": "Disponible maintenant" }, "accountSecurity": { "message": "Sécurité du compte" }, "phishingBlocker": { - "message": "Phishing Blocker" + "message": "Bloqueur d'hameçonnage" }, "enablePhishingDetection": { - "message": "Phishing detection" + "message": "Détection de l'hameçonnage" }, "enablePhishingDetectionDesc": { - "message": "Display warning before accessing suspected phishing sites" + "message": "Afficher un avertissement avant d'accéder à des sites soupçonnés d'hameçonnage" }, "notifications": { "message": "Notifications" @@ -4981,7 +5026,7 @@ } }, "downloadAttachmentLabel": { - "message": "Download Attachment" + "message": "Télécharger la pièce jointe" }, "downloadBitwarden": { "message": "Télécharger Bitwarden" @@ -5123,10 +5168,10 @@ } }, "showMatchDetectionNoPlaceholder": { - "message": "Show match detection" + "message": "Afficher la détection de correspondance" }, "hideMatchDetectionNoPlaceholder": { - "message": "Hide match detection" + "message": "Masquer la détection de correspondance" }, "autoFillOnPageLoad": { "message": "Saisir automatiquement lors du chargement de la page ?" @@ -5362,10 +5407,10 @@ "message": "Emplacement de l'élément" }, "fileSends": { - "message": "Envoi de fichiers" + "message": "Sends de fichier" }, "textSends": { - "message": "Envoi de textes" + "message": "Sends de texte" }, "accountActions": { "message": "Actions du compte" @@ -5665,7 +5710,7 @@ "message": "Très large" }, "narrow": { - "message": "Narrow" + "message": "Réduire" }, "sshKeyWrongPassword": { "message": "Le mot de passe saisi est incorrect." @@ -5716,10 +5761,10 @@ "message": "Cet identifiant est à risques et manque un site web. Ajoutez un site web et changez le mot de passe pour une meilleure sécurité." }, "vulnerablePassword": { - "message": "Vulnerable password." + "message": "Mot de passe vulnérable." }, "changeNow": { - "message": "Change now" + "message": "Changer maintenant" }, "missingWebsite": { "message": "Site Web manquant" @@ -5961,7 +6006,7 @@ "message": "Numéro de carte" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "Erreur : décryptage impossible" }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Votre organisation n'utilise plus les mots de passe principaux pour se connecter à Bitwarden. Pour continuer, vérifiez l'organisation et le domaine." @@ -6092,46 +6137,52 @@ } }, "acceptTransfer": { - "message": "Accept transfer" + "message": "Accepter le transfert" }, "declineAndLeave": { - "message": "Decline and leave" + "message": "Refuser et quitter" }, "whyAmISeeingThis": { - "message": "Why am I seeing this?" + "message": "Pourquoi vois-je ceci ?" }, "items": { - "message": "Items" + "message": "Éléments" }, "searchResults": { - "message": "Search results" + "message": "Résultats de la recherche" }, "resizeSideNavigation": { - "message": "Resize side navigation" + "message": "Ajuster la taille de la navigation latérale" }, "whoCanView": { - "message": "Who can view" + "message": "Qui peut visionner" }, "specificPeople": { - "message": "Specific people" + "message": "Personnes spécifiques" }, "emailVerificationDesc": { - "message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send." + "message": "Après avoir partagé ce lien Send, les individus devront vérifier leur courriel à l'aide d'un code afin de voir ce Send." }, "enterMultipleEmailsSeparatedByComma": { - "message": "Enter multiple emails by separating with a comma." + "message": "Entrez plusieurs courriels en les séparant d'une virgule." + }, + "emailsRequiredChangeAccessType": { + "message": "La vérification de courriel requiert au moins une adresse courriel. Pour retirer toutes les adresses, changez le type d'accès ci-dessus." }, "emailPlaceholder": { - "message": "user@bitwarden.com , user@acme.com" + "message": "utilisateur@bitwarden.com , utilisateur@acme.com" }, "downloadBitwardenApps": { - "message": "Download Bitwarden apps" + "message": "Télécharger les applications Bitwarden" }, "emailProtected": { - "message": "Email protected" + "message": "Courriel protégé" }, "sendPasswordHelperText": { - "message": "Individuals will need to enter the password to view this Send", + "message": "Les individus devront entrer le mot de passe pour visionner ce Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "Échec de la vérification d'utilisateur." } } diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index e710a489f9a..c84e1f56a95 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Código de verificación non válido" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copiado", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Ligazón do Send copiado", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index a76cbb711a9..5462940fa63 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "קוד אימות שגוי" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "השדה $VALUE$ הועתק לזיכרון", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "איור של רשימת כניסות בסיכון." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "צור במהירות סיסמה חזקה וייחודית עם תפריט המילוי האוטומטי של Bitwarden באתר שבסיכון.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "קישור סֵנְד הועתק", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index d30cbd2cc6e..1fe7310a1f5 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "सत्यापन कोड अवैध है" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ कॉपी हो गया है।", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "जोखिमग्रस्त लॉगिन की सूची का चित्रण।" }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index 98fdee3b657..b73f52ffaf5 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -29,7 +29,7 @@ "message": "Prijava pristupnim ključem" }, "unlockWithPasskey": { - "message": "Unlock with passkey" + "message": "Otključaj prisutupnim ključem" }, "useSingleSignOn": { "message": "Jedinstvena prijava (SSO)" @@ -574,10 +574,10 @@ "message": "Arhivirane stavke biti će prikazane ovdje i biti će izuzete iz rezultata općih pretraga i preporuka auto-ispune." }, "itemArchiveToast": { - "message": "Item archived" + "message": "Stavka arhivirana" }, "itemUnarchivedToast": { - "message": "Item unarchived" + "message": "Stavka vraćena iz arhive" }, "archiveItem": { "message": "Arhiviraj stavku" @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Nevažeći kôd za provjeru" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": " kopirano", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Ilustracija liste rizičnih prijava." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Brzo generiraj jake, jedinstvene lozinke koristeći Bitwarden dijalog auto-ispune direktno na stranici.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Kopirana poveznica Senda", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index e6765219f15..5736378a638 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Érvénytelen ellenőrző kód" }, + "invalidEmailOrVerificationCode": { + "message": "Az email cím vagy az ellenőrző kód érvénytelen." + }, "valueCopied": { "message": "$VALUE$ másolásra került.", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "A kockázatos bejelentkezések listájának illusztrációja." }, + "welcomeDialogGraphicAlt": { + "message": "A Bitwarden széf oldal elrendezésének illusztrációja." + }, "generatePasswordSlideDesc": { "message": "Gyorsan generálhatunk erős, egyedi jelszót a Bitwarden automatikus kitöltési menüjével a kockázatos webhelyen.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ óra", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Másoljuk és osszuk meg ezt a Send elem hivatkozást. A Send elem bárki számára elérhető lesz, aki rendelkezik a hivatkozással a következő $TIME$ alatt.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Másoljuk és osszuk meg ezt a Send elem hivatkozást. A Send elem bárki számára elérhető lesz, aki rendelkezik a hivatkozással és a jelszóval a következő $TIME$ alatt.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Másoljuk és osszuk meg ezt a Send hivatkozást. Megtekinthetik a megadott személyek a következő $TIME$ intervallumban.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "A Send hivatkozás másolásra került.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Írjunk be több email címet vesszővel elválasztva." }, + "emailsRequiredChangeAccessType": { + "message": "Az email cím ellenőrzéshez legalább egy email cím szükséges. Az összes email cím eltávolításához módosítsuk a fenti hozzáférési típust." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "A személyeknek meg kell adniuk a jelszót a Send elem megtekintéséhez.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "A felhasználó ellenőrzése sikertelen volt." } } diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index ccf35569f36..63e975466ed 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Kode verifikasi tidak valid" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ disalin", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Gambaran daftar info masuk yang berpotensi bahaya." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Hasilkan kata sandi yang kuat dan unik dengan cepat dengan menu isi otomatis Bitwarden pada situs yang berpotensi bahaya.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Tautan Send disalin", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 42efa025207..d66dfe7bfba 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Codice di verifica non valido" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copiata", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustrazione di una lista di login a rischio." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Genera rapidamente una parola d'accesso forte e unica con il menu' di riempimento automatico Bitwarden nel sito a rischio.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Link del Send copiato", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Inserisci più indirizzi email separandoli con virgole." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 915308cec13..ecb03f3321d 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "認証コードが間違っています" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ をコピーしました", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "危険な状態にあるログイン情報の一覧表示の例" }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Bitwarden の自動入力メニューで、強力で一意なパスワードをすぐに生成しましょう。", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send リンクをコピーしました", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index 791664e6eec..17c86de13fb 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "არასწორი გადამოწმების კოდი" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copied", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index c28007c3838..51ca51960d7 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copied", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index faef7703a66..0dd4699a9a2 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ ನಕಲಿಸಲಾಗಿದೆ", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index b4a04e75e43..66467d99888 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "유효하지 않은 확인 코드" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$를 클립보드에 복사함", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send 링크 복사됨", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index 68eb11aa234..a4cfb34942c 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Neteisingas patvirtinimo kodas" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "Nukopijuota $VALUE$", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index 6eaf545e390..d6e9a171978 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -574,10 +574,10 @@ "message": "Šeit parādīsies arhivētie vienumi, un tie netiks iekļauti vispārējās meklēšanas iznākumos un automātiskās aizpildes ieteikumos." }, "itemArchiveToast": { - "message": "Item archived" + "message": "Vienums ievietots arhīvā" }, "itemUnarchivedToast": { - "message": "Item unarchived" + "message": "Vienums izņemts no arhīva" }, "archiveItem": { "message": "Arhivēt vienumu" @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Nederīgs apliecinājuma kods" }, + "invalidEmailOrVerificationCode": { + "message": "Nederīga e-pasta adrese vai apliecinājuma kods" + }, "valueCopied": { "message": "$VALUE$ ir starpliktuvē", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Riskam pakļauto pieteikšanās vienumu saraksta attēlojums." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Riskam pakļauto vienumu vietnē ar automātiskās aizpildes izvēlni var ātri izveidot stipru, neatkārtojamu paroli.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send saite ievietota starpliktuvē", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "E-pasta apliecināšanai ir nepieciešama vismaz viena e-pasta adrese. Lai noņemtu visas e-pasta adreses, augstāk jānomaina piekļūšanas veids." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Cilvēkiem būs jāievada parole, lai apskatītu šo Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "Lietotāja apliecināšana neizdevās." } } diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index db48220ffbb..61b9f4d45ab 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ പകർത്തി", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index abf2f7db968..2a793784aa2 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copied", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index c28007c3838..51ca51960d7 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copied", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 4689cb23b7a..d19dc3571e2 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Ugyldig bekreftelseskode" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ er kopiert", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send-lenken ble kopiert", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index c28007c3838..51ca51960d7 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copied", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index 044b3cfaa64..0d3ed844a15 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Ongeldige verificatiecode" }, + "invalidEmailOrVerificationCode": { + "message": "Ongeldig e-mailadres verificatiecode" + }, "valueCopied": { "message": "$VALUE$ gekopieerd", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Voorbeeld van een lijst van risicovolle inloggegevens." }, + "welcomeDialogGraphicAlt": { + "message": "Illustratie van de lay-out van de Bitwarden-kluispagina." + }, "generatePasswordSlideDesc": { "message": "Genereer snel een sterk, uniek wachtwoord met het automatisch invulmenu van Bitwarden op de risicovolle website.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ uur", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Kopieer en deel deze Send-link. De Send is beschikbaar voor iedereen met de link voor de volgende $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Kopieer en deel deze Send-link. De Send is beschikbaar voor iedereen met de link en het ingestelde wachtwoord voor de volgende $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Kopieer en deel deze Send-link. Het kan worden bekeken door de mensen die je hebt opgegeven voor de volgende $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send-link gekopieerd", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Voer meerdere e-mailadressen in door te scheiden met een komma." }, + "emailsRequiredChangeAccessType": { + "message": "E-mailverificatie vereist ten minste één e-mailadres. Om alle e-mailadressen te verwijderen, moet je het toegangstype hierboven wijzigen." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuen moeten het wachtwoord invoeren om deze Send te bekijken", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "Gebruikersverificatie is mislukt." } } diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index c28007c3838..51ca51960d7 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copied", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index c28007c3838..51ca51960d7 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copied", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index 44c7b5fb6dd..c470af8c1dc 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Kod weryfikacyjny jest nieprawidłowy" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "Skopiowano $VALUE$", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Ilustracja listy danych logowania, które są zagrożone." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Wygeneruj silne i unikalne hasło dla zagrożonej strony internetowej za pomocą autouzupełniania Bitwarden.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Link wysyłki został skopiowany", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 679173205b1..9b0c2483b2a 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -574,10 +574,10 @@ "message": "Os itens arquivados aparecerão aqui e serão excluídos dos resultados gerais de busca e das sugestões de preenchimento automático." }, "itemArchiveToast": { - "message": "Item archived" + "message": "Item arquivado" }, "itemUnarchivedToast": { - "message": "Item unarchived" + "message": "Item desarquivado" }, "archiveItem": { "message": "Arquivar item" @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Código de verificação inválido" }, + "invalidEmailOrVerificationCode": { + "message": "E-mail ou código de verificação inválido" + }, "valueCopied": { "message": "$VALUE$ copiado(a)", "description": "Value has been copied to the clipboard.", @@ -988,10 +991,10 @@ "message": "Não" }, "noAuth": { - "message": "Anyone with the link" + "message": "Qualquer um com o link" }, "anyOneWithPassword": { - "message": "Anyone with a password set by you" + "message": "Qualquer pessoa com uma senha configurada por você" }, "location": { "message": "Localização" @@ -2052,7 +2055,7 @@ "message": "E-mail" }, "emails": { - "message": "Emails" + "message": "E-mails" }, "phone": { "message": "Telefone" @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Ilustração de uma lista de credenciais em risco." }, + "welcomeDialogGraphicAlt": { + "message": "Ilustração do layout da página do Cofre do Bitwarden." + }, "generatePasswordSlideDesc": { "message": "Gere uma senha forte e única com rapidez com o menu de preenchimento automático no site em risco.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ horas", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copie e compartilhe este link do Send. O Send ficará disponível para qualquer um com o link por $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copie e compartilhe este link do Send. O Send ficará disponível para qualquer um com o link e a senha por $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copie e compartilhe este link do Send. Ele pode ser visto pelas pessoas que você especificou pelos próximos $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Link do Send copiado", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5961,7 +6006,7 @@ "message": "Número do cartão" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "Erro: Não é possível descriptografar" }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "A sua organização não está mais usando senhas principais para se conectar ao Bitwarden. Para continuar, verifique a organização e o domínio." @@ -6101,37 +6146,43 @@ "message": "Por que estou vendo isso?" }, "items": { - "message": "Items" + "message": "Itens" }, "searchResults": { - "message": "Search results" + "message": "Resultados da busca" }, "resizeSideNavigation": { "message": "Redimensionar navegação lateral" }, "whoCanView": { - "message": "Who can view" + "message": "Quem pode visualizar" }, "specificPeople": { - "message": "Specific people" + "message": "Pessoas específicas" }, "emailVerificationDesc": { - "message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send." + "message": "Após compartilhar este link de Send, indivíduos precisarão verificar seus e-mails com um código para visualizar este Send." }, "enterMultipleEmailsSeparatedByComma": { - "message": "Enter multiple emails by separating with a comma." + "message": "Digite vários e-mails, separados com uma vírgula." + }, + "emailsRequiredChangeAccessType": { + "message": "A verificação de e-mail requer pelo menos um endereço de e-mail. Para remover todos, altere o tipo de acesso acima." }, "emailPlaceholder": { - "message": "user@bitwarden.com , user@acme.com" + "message": "usuário@bitwarden.com , usuário@acme.com" }, "downloadBitwardenApps": { - "message": "Download Bitwarden apps" + "message": "Baixar aplicativos do Bitwarden" }, "emailProtected": { - "message": "Email protected" + "message": "Protegido por e-mail" }, "sendPasswordHelperText": { - "message": "Individuals will need to enter the password to view this Send", + "message": "Os indivíduos precisarão digitar a senha para ver este Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "Falha na verificação do usuário." } } diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index 9094e04094d..5d498908fa5 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Código de verificação inválido" }, + "invalidEmailOrVerificationCode": { + "message": "E-mail ou código de verificação inválido" + }, "valueCopied": { "message": "$VALUE$ copiado(a)", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Ilustração de uma lista de credenciais que estão em risco." }, + "welcomeDialogGraphicAlt": { + "message": "Ilustração do layout da página do cofre Bitwarden." + }, "generatePasswordSlideDesc": { "message": "Gira rapidamente uma palavra-passe forte e única com o menu de preenchimento automático do Bitwarden no site em risco.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ horas", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copie e partilhe este link do Send. O Send estará disponível para qualquer pessoa com o link durante os próximos $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copie e partilhe este link do Send. O Send estará disponível para qualquer pessoa com o link e palavras-passe durante os próximos $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copie e partilhe este link do Send. Pode ser visualizado pelas pessoas que especificou durante os próximos $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Link do Send copiado", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Introduza vários e-mails, separados por vírgula." }, + "emailsRequiredChangeAccessType": { + "message": "A verificação por e-mail requer pelo menos um endereço de e-mail. Para remover todos os e-mails, altere o tipo de acesso acima." + }, "emailPlaceholder": { "message": "utilizador@bitwarden.com , utilizador@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Os indivíduos terão de introduzir a palavra-passe para ver este Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "Falha na verificação do utilizador." } } diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 47f7ae9cae3..eb6c7eb2a66 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Cod de verificare nevalid" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": " $VALUE$ s-a copiat", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index d1fb3de89a6..75a2afb4e1a 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Неверный код подтверждения" }, + "invalidEmailOrVerificationCode": { + "message": "Неверный email или код подтверждения" + }, "valueCopied": { "message": "$VALUE$ скопировано", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Иллюстрация списка логинов, которые подвержены риску." }, + "welcomeDialogGraphicAlt": { + "message": "Иллюстрация макета страницы хранилища Bitwarden." + }, "generatePasswordSlideDesc": { "message": "Быстро сгенерируйте надежный уникальный пароль с помощью меню автозаполнения Bitwarden на сайте, подверженном риску.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ час.", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Скопируйте и поделитесь этой ссылкой Send. Send будет доступна всем, у кого есть ссылка, в течение следующих $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Скопируйте и поделитесь этой ссылкой Send. Send будет доступна всем, у кого есть ссылка и пароль в течение следующих $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Скопируйте и распространите эту ссылку для Send. Она может быть просмотрена указанными вами пользователями в течение $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Ссылка на Send скопирована", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Введите несколько email, разделяя их запятой." }, + "emailsRequiredChangeAccessType": { + "message": "Для проверки электронной почты требуется как минимум один адрес email. Чтобы удалить все адреса электронной почты, измените тип доступа выше." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Пользователям необходимо будет ввести пароль для просмотра этой Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "Проверка пользователя не удалась." } } diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index e70c620eaf8..d7e63d70f87 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "වලංගු නොවන සත්යාපන කේතය" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ පිටපත් කරන ලදි", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index e1886098a31..806e83e0b1f 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Neplatný verifikačný kód" }, + "invalidEmailOrVerificationCode": { + "message": "Neplatný e-mailový alebo overovací kód" + }, "valueCopied": { "message": " skopírované", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Príklady zoznamu prihlásení, ktoré sú ohrozené." }, + "welcomeDialogGraphicAlt": { + "message": "Ilustrácia rozloženia stránky trezoru Bitwarden." + }, "generatePasswordSlideDesc": { "message": "Rýchlo generujte silné, jedinečné heslo pomocu ponuky automatického vypĺňania Bitwardenu na ohrozených stránkach.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hod.", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Skopírujte a zdieľajte tento odkaz na Send. Send bude dostupný každému, kto má odkaz, počas nasledujúcich $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Skopírujte a zdieľajte tento odkaz na Send. Send bude dostupný každému, kto má odkaz a heslo od vás, počas nasledujúcich $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Skopírujte a zdieľajte tento odkaz na Send. Zobraziť ho môžu ľudia, ktorých ste vybrali, počas nasledujúcich $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Skopírovaný odkaz na Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Zadajte viacero e-mailových adries oddelených čiarkou." }, + "emailsRequiredChangeAccessType": { + "message": "Overenie e-mailu vyžaduje aspoň jednu e-mailovú adresu. Ak chcete odstrániť všetky e-maily, zmeňte typ prístupu vyššie." + }, "emailPlaceholder": { "message": "pouzivate@bitwarden.com, pouzivatel@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Jednotlivci budú musieť zadať heslo, aby mohli zobraziť tento Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "Overenie používateľa zlyhalo." } } diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index 100a04a3012..88f54663ccd 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -6,30 +6,30 @@ "message": "Bitwarden logo" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Bitwarden – Upravitelj gesel", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "message": "Doma, na delu ali na poti – Bitwarden enostavno zaščiti vsa vaša gesla, ključe za dostop in občutljive podatke", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Prijavite se ali ustvarite nov račun za dostop do svojega varnega trezorja." }, "inviteAccepted": { - "message": "Invitation accepted" + "message": "Povabilo sprejeto" }, "createAccount": { "message": "Ustvari račun" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "Novi na Bitwardenu?" }, "logInWithPasskey": { - "message": "Log in with passkey" + "message": "Prijava s ključem za dostop" }, "unlockWithPasskey": { - "message": "Unlock with passkey" + "message": "Odkleni s ključem za dostop" }, "useSingleSignOn": { "message": "Use single sign-on" @@ -38,10 +38,10 @@ "message": "Your organization requires single sign-on." }, "welcomeBack": { - "message": "Welcome back" + "message": "Dobrodošli nazaj" }, "setAStrongPassword": { - "message": "Set a strong password" + "message": "Nastavite močno geslo" }, "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" @@ -90,7 +90,7 @@ "message": "Namig za glavno geslo (neobvezno)" }, "passwordStrengthScore": { - "message": "Password strength score $SCORE$", + "message": "Ocena moči gesla: $SCORE$", "placeholders": { "score": { "content": "$1", @@ -99,7 +99,7 @@ } }, "joinOrganization": { - "message": "Join organization" + "message": "Pridružite se organizaciji" }, "joinOrganizationName": { "message": "Join $ORGANIZATIONNAME$", @@ -156,31 +156,31 @@ "message": "Kopiraj varnostno kodo" }, "copyName": { - "message": "Copy name" + "message": "Kopiraj ime" }, "copyCompany": { - "message": "Copy company" + "message": "Kopiraj podjetje" }, "copySSN": { "message": "Copy Social Security number" }, "copyPassportNumber": { - "message": "Copy passport number" + "message": "Kopiraj številko potnega lista" }, "copyLicenseNumber": { "message": "Copy license number" }, "copyPrivateKey": { - "message": "Copy private key" + "message": "Kopiraj zasebni ključ" }, "copyPublicKey": { - "message": "Copy public key" + "message": "Kopiraj javni ključ" }, "copyFingerprint": { "message": "Copy fingerprint" }, "copyCustomField": { - "message": "Copy $FIELD$", + "message": "Kopiraj $FIELD$", "placeholders": { "field": { "content": "$1", @@ -189,17 +189,17 @@ } }, "copyWebsite": { - "message": "Copy website" + "message": "Kopiraj spletno stran" }, "copyNotes": { - "message": "Copy notes" + "message": "Kopiraj zapiske" }, "copy": { - "message": "Copy", + "message": "Kopiraj", "description": "Copy to clipboard" }, "fill": { - "message": "Fill", + "message": "Izpolni", "description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible." }, "autoFill": { @@ -215,10 +215,10 @@ "message": "Samodejno izpolni identiteto" }, "fillVerificationCode": { - "message": "Fill verification code" + "message": "Izpolni kodo za preverjanje" }, "fillVerificationCodeAria": { - "message": "Fill Verification Code", + "message": "Izpolni kodo za preverjanje", "description": "Aria label for the heading displayed the inline menu for totp code autofill" }, "generatePasswordCopied": { @@ -297,13 +297,13 @@ "message": "Spremeni glavno geslo" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "Nadaljuj v spletno aplikacijo?" }, "continueToWebAppDesc": { - "message": "Explore more features of your Bitwarden account on the web app." + "message": "Raziščite več funkcij vašega Bitwarden računa na spletni aplikaciji." }, "continueToHelpCenter": { - "message": "Continue to Help Center?" + "message": "Nadaljuj na center za pomoč?" }, "continueToHelpCenterDesc": { "message": "Learn more about how to use Bitwarden on the Help Center." @@ -315,7 +315,7 @@ "message": "Help others find out if Bitwarden is right for them. Visit your browser's extension store and leave a rating now." }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "Vaše glavno geslo lahko zamenjate v Bitwarden spletni aplikaciji." }, "fingerprintPhrase": { "message": "Identifikacijsko geslo", @@ -332,19 +332,19 @@ "message": "Odjava" }, "aboutBitwarden": { - "message": "About Bitwarden" + "message": "O Bitwardenu" }, "about": { "message": "O programu" }, "moreFromBitwarden": { - "message": "More from Bitwarden" + "message": "Več od Bitwardena" }, "continueToBitwardenDotCom": { - "message": "Continue to bitwarden.com?" + "message": "Nadaljuj na bitwarden.com?" }, "bitwardenForBusiness": { - "message": "Bitwarden for Business" + "message": "Bitwarden za podjetja" }, "bitwardenAuthenticator": { "message": "Bitwarden Authenticator" @@ -398,10 +398,10 @@ } }, "newFolder": { - "message": "New folder" + "message": "Nova mapa" }, "folderName": { - "message": "Folder name" + "message": "Ime mape" }, "folderHintText": { "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" @@ -440,7 +440,7 @@ "message": "Sinhronizacija" }, "syncNow": { - "message": "Sync now" + "message": "Sinhroniziraj zdaj" }, "lastSync": { "message": "Zadnja sinhronizacija:" @@ -456,7 +456,7 @@ "message": "Avtomatično generiraj močna, edinstvena gesla za vaše prijave." }, "bitWebVaultApp": { - "message": "Bitwarden web app" + "message": "Bitwarden spletna aplikacija" }, "select": { "message": "Izberi" @@ -468,7 +468,7 @@ "message": "Generate passphrase" }, "passwordGenerated": { - "message": "Password generated" + "message": "Geslo generirano" }, "passphraseGenerated": { "message": "Passphrase generated" @@ -493,7 +493,7 @@ "description": "Card header for password generator include block" }, "uppercaseDescription": { - "message": "Include uppercase characters", + "message": "Vključi velike črke", "description": "Tooltip for the password generator uppercase character checkbox" }, "uppercaseLabel": { @@ -501,7 +501,7 @@ "description": "Label for the password generator uppercase character checkbox" }, "lowercaseDescription": { - "message": "Include lowercase characters", + "message": "Vključi male črke", "description": "Full description for the password generator lowercase character checkbox" }, "lowercaseLabel": { @@ -509,7 +509,7 @@ "description": "Label for the password generator lowercase character checkbox" }, "numbersDescription": { - "message": "Include numbers", + "message": "Vključi števila", "description": "Full description for the password generator numbers checkbox" }, "numbersLabel": { @@ -517,7 +517,7 @@ "description": "Label for the password generator numbers checkbox" }, "specialCharactersDescription": { - "message": "Include special characters", + "message": "Vključi posebne znake", "description": "Full description for the password generator special characters checkbox" }, "numWords": { @@ -540,7 +540,7 @@ "message": "Minimalno posebnih znakov" }, "avoidAmbiguous": { - "message": "Avoid ambiguous characters", + "message": "Izogibaj se dvoumnim znakom", "description": "Label for the avoid ambiguous characters checkbox." }, "generatorPolicyInEffect": { @@ -554,21 +554,21 @@ "message": "Reset search" }, "archiveNoun": { - "message": "Archive", + "message": "Arhiv", "description": "Noun" }, "archiveVerb": { - "message": "Archive", + "message": "Arhiviraj", "description": "Verb" }, "unArchive": { - "message": "Unarchive" + "message": "Odstrani iz arhiva" }, "itemsInArchive": { - "message": "Items in archive" + "message": "Elementi v arhivu" }, "noItemsInArchive": { - "message": "No items in archive" + "message": "Ni elementov v arhivu" }, "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." @@ -592,10 +592,10 @@ "message": "Unarchive and save" }, "upgradeToUseArchive": { - "message": "A premium membership is required to use Archive." + "message": "Za uporabo Arhiva je potrebno premium članstvo." }, "itemRestored": { - "message": "Item has been restored" + "message": "Vnos je bil obnovljen" }, "edit": { "message": "Uredi" @@ -604,13 +604,13 @@ "message": "Pogled" }, "viewAll": { - "message": "View all" + "message": "Poglej vse" }, "showAll": { - "message": "Show all" + "message": "Prikaži vse" }, "viewLess": { - "message": "View less" + "message": "Poglej manj" }, "viewLogin": { "message": "View login" @@ -640,10 +640,10 @@ "message": "Unfavorite" }, "itemAddedToFavorites": { - "message": "Item added to favorites" + "message": "Element dodan med priljubljene" }, "itemRemovedFromFavorites": { - "message": "Item removed from favorites" + "message": "Element odstranen iz priljubljenih" }, "notes": { "message": "Opombe" @@ -709,7 +709,7 @@ "message": "Vault timeout" }, "otherOptions": { - "message": "Other options" + "message": "Ostale možnosti" }, "rateExtension": { "message": "Ocenite to razširitev" @@ -718,25 +718,25 @@ "message": "Vaš brskalnik ne podpira enostavnega kopiranja na odložišče. Prosimo, kopirajte ročno." }, "verifyYourIdentity": { - "message": "Verify your identity" + "message": "Potrdite vašo identiteto" }, "weDontRecognizeThisDevice": { - "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + "message": "Te naprave ne prepoznamo. Vnesite kodo, ki je bila poslana na vaš e-poštni naslov, da potrdite vašo identiteto." }, "continueLoggingIn": { - "message": "Continue logging in" + "message": "Nadaljujte s prijavo" }, "yourVaultIsLocked": { "message": "Vaš trezor je zaklenjen. Za nadaljevanje potrdite svojo identiteto." }, "yourVaultIsLockedV2": { - "message": "Your vault is locked" + "message": "Vaš trezor je zaklenjen" }, "yourAccountIsLocked": { - "message": "Your account is locked" + "message": "Vaš račun je zaklenjen" }, "or": { - "message": "or" + "message": "ali" }, "unlock": { "message": "Odkleni" @@ -758,7 +758,7 @@ "message": "Napačno glavno geslo" }, "invalidMasterPasswordConfirmEmailAndHost": { - "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "message": "Neveljavno glavno geslo. Preverite, da je vaš e-poštni naslov pravilen, ter da je bil vaš račun ustvarjen na $HOST$.", "placeholders": { "host": { "content": "$1", @@ -776,7 +776,7 @@ "message": "Zakleni zdaj" }, "lockAll": { - "message": "Lock all" + "message": "Zakleni vse" }, "immediately": { "message": "Takoj" @@ -833,13 +833,13 @@ "message": "Confirm master password" }, "masterPassword": { - "message": "Master password" + "message": "Glavno geslo" }, "masterPassImportant": { - "message": "Your master password cannot be recovered if you forget it!" + "message": "Če pozabite glavno geslo, ga ne bo mogoče obnoviti!" }, "masterPassHintLabel": { - "message": "Master password hint" + "message": "Namig za glavno geslo" }, "errorOccurred": { "message": "Prišlo je do napake" @@ -876,7 +876,7 @@ "message": "Your new account has been created!" }, "youHaveBeenLoggedIn": { - "message": "You have been logged in!" + "message": "Prijavljeni ste!" }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Neveljavna koda za preverjanje" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ kopirana", "description": "Value has been copied to the clipboard.", @@ -970,7 +973,7 @@ "message": "Restart registration" }, "expiredLink": { - "message": "Expired link" + "message": "Pretečena povezava" }, "pleaseRestartRegistrationOrTryLoggingIn": { "message": "Please restart registration or try logging in." @@ -994,7 +997,7 @@ "message": "Anyone with a password set by you" }, "location": { - "message": "Location" + "message": "Lokacija" }, "unexpectedError": { "message": "Prišlo je do nepričakovane napake." @@ -1009,10 +1012,10 @@ "message": "Avtentikacija v dveh korakih dodatno varuje vaš račun, saj zahteva, da vsakokratno prijavo potrdite z drugo napravo, kot je varnostni ključ, aplikacija za preverjanje pristnosti, SMS, telefonski klic ali e-pošta. Avtentikacijo v dveh korakih lahko omogočite v spletnem trezorju bitwarden.com. Ali želite spletno stran obiskati sedaj?" }, "twoStepLoginConfirmationContent": { - "message": "Make your account more secure by setting up two-step login in the Bitwarden web app." + "message": "Zavarujte vaš račun tako, da nastavite dvostopenjsko prijavo v Bitwarden spletni aplikaciji." }, "twoStepLoginConfirmationTitle": { - "message": "Continue to web app?" + "message": "Nadaljuj v spletno aplikacijo?" }, "editedFolder": { "message": "Mapa shranjena" @@ -1055,7 +1058,7 @@ "message": "Nov URI" }, "addDomain": { - "message": "Add domain", + "message": "Dodaj domeno", "description": "'Domain' here refers to an internet domain name (e.g. 'bitwarden.com') and the message in whole described the act of putting a domain value into the context." }, "addedItem": { @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6062,7 +6107,7 @@ "message": "Contact your admin to regain access." }, "leaveConfirmationDialogConfirmButton": { - "message": "Leave $ORGANIZATION$", + "message": "Zapusti $ORGANIZATION$", "placeholders": { "organization": { "content": "$1", @@ -6071,10 +6116,10 @@ } }, "howToManageMyVault": { - "message": "How do I manage my vault?" + "message": "Kako uporabljam svoj trezor?" }, "transferItemsToOrganizationTitle": { - "message": "Transfer items to $ORGANIZATION$", + "message": "Prenesi elemente v $ORGANIZATION$", "placeholders": { "organization": { "content": "$1", @@ -6092,19 +6137,19 @@ } }, "acceptTransfer": { - "message": "Accept transfer" + "message": "Sprejmi prenos" }, "declineAndLeave": { "message": "Decline and leave" }, "whyAmISeeingThis": { - "message": "Why am I seeing this?" + "message": "Zakaj se mi to prikazuje?" }, "items": { - "message": "Items" + "message": "Elementi" }, "searchResults": { - "message": "Search results" + "message": "Rezultati iskanja" }, "resizeSideNavigation": { "message": "Resize side navigation" @@ -6121,11 +6166,14 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { - "message": "user@bitwarden.com , user@acme.com" + "message": "uporabnik@bitwarden.com , uporabnik@acme.com" }, "downloadBitwardenApps": { - "message": "Download Bitwarden apps" + "message": "Prenesi Bitwarden aplikacije" }, "emailProtected": { "message": "Email protected" @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index e91e003c8e0..b375ca5d536 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Неисправан верификациони код" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ копиран(а/о)", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Илустрација листе пријаве које су ризичне." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Брзо генеришите снажну, јединствену лозинку са Bitwarden менијем аутопуњења за коришћење на ризичном сајту.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send линк је копиран", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 484817b0210..eb5a7fef645 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Ogiltig verifieringskod" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ har kopierats", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration av en lista över inloggningar som är i riskzonen." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Skapa snabbt ett starkt, unikt lösenord med Bitwardens autofyllmeny på riskwebbplatsen.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ timmar", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Kopiera och dela denna Send-länk. Denna Send kommer att vara tillgänglig för alla med länken för nästa $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Kopiera och dela denna Send-länk. Denna Send kommer att vara tillgänglig för alla med den länk och lösenord du angav för nästa $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Kopiera och dela denna Send-länk. Den kan visas av personer som du har angivet nästa $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Skicka länk kopierad", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Ange flera e-postadresser genom att separera dem med kommatecken." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "användare@bitwarden.com , användare@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individer måste ange lösenordet för att visa denna Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/ta/messages.json b/apps/browser/src/_locales/ta/messages.json index 3e76c0ab0d1..72ad809e723 100644 --- a/apps/browser/src/_locales/ta/messages.json +++ b/apps/browser/src/_locales/ta/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "தவறான சரிபார்ப்புக் குறியீடு" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ நகலெடுக்கப்பட்டது", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "ஆபத்தில் உள்ள உள்நுழைவுகளின் பட்டியலின் விளக்கம்." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "ஆபத்தில் உள்ள தளத்தில் உள்ள Bitwarden தானாக நிரப்பு மெனுவுடன் ஒரு வலுவான, தனிப்பட்ட கடவுச்சொல்லை விரைவாக உருவாக்குங்கள்.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "அனுப்பு இணைப்பு நகலெடுக்கப்பட்டது", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index c28007c3838..51ca51960d7 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copied", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index 5ec728189a8..82878eb3b52 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "รหัสยืนยันไม่ถูกต้อง" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "คัดลอก $VALUE$ แล้ว", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "ภาพประกอบรายการข้อมูลเข้าสู่ระบบที่มีความเสี่ยง" }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "สร้างรหัสผ่านที่รัดกุมและไม่ซ้ำกันอย่างรวดเร็วด้วยเมนูป้อนอัตโนมัติของ Bitwarden บนเว็บไซต์ที่มีความเสี่ยง", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "คัดลอกลิงก์ Send แล้ว", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 7d5b31a9aba..b3d1d46c9a5 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Geçersiz doğrulama kodu" }, + "invalidEmailOrVerificationCode": { + "message": "E-posta veya doğrulama kodu geçersiz" + }, "valueCopied": { "message": "$VALUE$ kopyalandı", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Risk altındaki hesap listesinin illüstrasyonu." }, + "welcomeDialogGraphicAlt": { + "message": "Bitwarden kasa sayfası düzeninin illüstrasyonu." + }, "generatePasswordSlideDesc": { "message": "Riskli sitede Bitwarden otomatik doldurma menüsünü kullanarak hızlıca güçlü ve benzersiz bir parola oluştur.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ saat", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Bu Send bağlantısını kopyalayıp paylaşın. Bu Send'e önümüzdeki $TIME$ boyunca bağlantıya sahip herkes ulaşabilecektir.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Bu Send bağlantısını kopyalayıp paylaşın. Bu Send'e önümüzdeki $TIME$ boyunca bağlantıya ve parolaya sahip herkes ulaşabilecektir.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Bu Send bağlantısını kopyalayıp paylaşın. Belirlediğiniz kişiler bağlantıyı önümüzdeki $TIME$ boyunca kullanabilir.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send bağlantısı kopyalandı", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "E-posta adreslerini virgülle ayırarak yazın." }, + "emailsRequiredChangeAccessType": { + "message": "E-posta doğrulaması için en az bir e-posta adresi gerekir. Tüm e-postaları silmek için yukarıdan erişim türünü değiştirin." + }, "emailPlaceholder": { "message": "kullanici@bitwarden.com , kullanici@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Bu Send'i görmek isteyen kişilerin parola girmesi gerekecektir", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "Kullanıcı doğrulaması başarısız oldu." } } diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index 49a0c9de25b..541a60b8ff8 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Недійсний код підтвердження" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ скопійовано", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Ілюстрація списку ризикованих записів." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Швидко згенеруйте надійний, унікальний пароль через меню автозаповнення Bitwarden на сайті з ризикованим паролем.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Посилання на відправлення скопійовано", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Введіть декілька адрес е-пошти, розділяючи їх комою." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Особам необхідно ввести пароль для перегляду цього відправлення", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index 4f1165835cc..ebd3a2500aa 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Mã xác minh không đúng" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "Đã sao chép $VALUE$", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Minh họa danh sách các tài khoản đăng nhập có rủi ro." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Tạo nhanh một mật khẩu mạnh, duy nhất bằng menu tự động điền của Bitwarden trên trang web có nguy cơ.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Đã sao chép liên kết Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index c9dd30ab08e..5fc0b632676 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -574,10 +574,10 @@ "message": "已归档的项目将显示在此处,并将被排除在一般搜索结果和自动填充建议之外。" }, "itemArchiveToast": { - "message": "Item archived" + "message": "项目已归档" }, "itemUnarchivedToast": { - "message": "Item unarchived" + "message": "项目已取消归档" }, "archiveItem": { "message": "归档项目" @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "无效的验证码" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ 已复制", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "存在风险的登录列表示意图。" }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "在存在风险的网站上,使用 Bitwarden 自动填充菜单快速生成强大且唯一的密码。", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ 小时", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "复制并分享此 Send 链接。在接下来的 $TIME$ 内,拥有此链接的任何人都可以访问此 Send。", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "复制并分享此 Send 链接。在接下来的 $TIME$ 内,拥有此链接以及您设置的密码的任何人都可以访问此 Send。", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "复制并分享此 Send 链接。在接下来的 $TIME$ 内,您指定的人员可查看此 Send。", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send 链接已复制", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "输入多个电子邮箱(使用逗号分隔)。" }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com, user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "个人需要输入密码才能查看此 Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index 8da1b2ad08f..d23106948ad 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "驗證碼無效" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ 已複製", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "有風險登入清單的示意圖。" }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "在有風險的網站上,透過 Bitwarden 自動填入選單快速產生強且唯一的密碼。", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ 小時", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "複製並分享此 Send 連結。任何擁有此連結的人,都可在接下來的 $TIME$ 內存取該 Send。", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "複製並分享此 Send 連結。任何擁有此連結與您所設定密碼的人,都可在接下來的 $TIME$ 內存取該 Send。", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "複製並分享此 Send 連結。在接下來的 $TIME$ 內,只有您指定的人可以檢視。", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "已複製 Send 連結", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6121,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "請以逗號分隔輸入多個電子郵件地址。" }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6133,5 +6181,8 @@ "sendPasswordHelperText": { "message": "對方必須輸入密碼才能檢視此 Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/store/locales/sl/copy.resx b/apps/browser/store/locales/sl/copy.resx index b2a95ed5689..07864dcc1f1 100644 --- a/apps/browser/store/locales/sl/copy.resx +++ b/apps/browser/store/locales/sl/copy.resx @@ -118,10 +118,10 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden Password Manager + Bitwarden – Upravitelj gesel - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + Doma, na delu ali na poti – Bitwarden na enostaven način zaščiti vsa vaša gesla, ključe za dostop in občutljive podatke. Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! @@ -169,7 +169,7 @@ End-to-end encrypted credential management solutions from Bitwarden empower orga - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + Doma, na delu ali na poti – Bitwarden na enostaven način zaščiti vsa vaša gesla, ključe za dostop in občutljive podatke. Sinhronizirajte svoj trezor gesel in dostopajte do njega z več naprav From 40c8139e1c07740f5e23c78d55a14a80021cb167 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 20 Feb 2026 13:45:30 +0100 Subject: [PATCH 102/134] Update sdk to 550 (#19084) --- package-lock.json | 16 ++++++++-------- package.json | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index fb1111a82b9..9f6e82d98ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,8 +23,8 @@ "@angular/platform-browser": "20.3.16", "@angular/platform-browser-dynamic": "20.3.16", "@angular/router": "20.3.16", - "@bitwarden/commercial-sdk-internal": "0.2.0-main.546", - "@bitwarden/sdk-internal": "0.2.0-main.546", + "@bitwarden/commercial-sdk-internal": "0.2.0-main.550", + "@bitwarden/sdk-internal": "0.2.0-main.550", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", @@ -4936,9 +4936,9 @@ "link": true }, "node_modules/@bitwarden/commercial-sdk-internal": { - "version": "0.2.0-main.546", - "resolved": "https://registry.npmjs.org/@bitwarden/commercial-sdk-internal/-/commercial-sdk-internal-0.2.0-main.546.tgz", - "integrity": "sha512-3lIQSb1yYSpDqhgT2uqHjPC88yVL7rWR08i0XD0BQJMFfN0FcB378r2Fq6d5TMXLPEYZ8PR62BCDB+tYKM7FPw==", + "version": "0.2.0-main.550", + "resolved": "https://registry.npmjs.org/@bitwarden/commercial-sdk-internal/-/commercial-sdk-internal-0.2.0-main.550.tgz", + "integrity": "sha512-hYdGV3qs+kKrAMTIvMfolWz23XXZ8bJGzMGi+gh5EBpjTE4OsAsLKp0JDgpjlpE+cdheSFXyhTU9D1Ujdqzzrg==", "license": "BITWARDEN SOFTWARE DEVELOPMENT KIT LICENSE AGREEMENT", "dependencies": { "type-fest": "^4.41.0" @@ -5041,9 +5041,9 @@ "link": true }, "node_modules/@bitwarden/sdk-internal": { - "version": "0.2.0-main.546", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.546.tgz", - "integrity": "sha512-KGPyP1pr7aIBaJ9Knibpfjydo/27Rlve77X4ENmDIwrSJ9FB3o2B6D3UXpNNVyXKt2Ii1C+rNT7ezMRO25Qs4A==", + "version": "0.2.0-main.550", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.550.tgz", + "integrity": "sha512-uAGgP+Y2FkxOZ74+9C4JHaM+YbJTI3806akeDg7w2yvfNNryIsLncwvb8FoFgiN6dEY1o9YSzuuv0YYUnbAMww==", "license": "GPL-3.0", "dependencies": { "type-fest": "^4.41.0" diff --git a/package.json b/package.json index c18112989fe..1795e93cf83 100644 --- a/package.json +++ b/package.json @@ -161,8 +161,8 @@ "@angular/platform-browser": "20.3.16", "@angular/platform-browser-dynamic": "20.3.16", "@angular/router": "20.3.16", - "@bitwarden/commercial-sdk-internal": "0.2.0-main.546", - "@bitwarden/sdk-internal": "0.2.0-main.546", + "@bitwarden/commercial-sdk-internal": "0.2.0-main.550", + "@bitwarden/sdk-internal": "0.2.0-main.550", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", From bb110122a566ff35fb63ba505ccec6bc14a00f9b Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 20 Feb 2026 15:28:24 +0100 Subject: [PATCH 103/134] [PM-30144] Implement client-side user-key-rotation-service (#18285) * Implement client-side user-key-rotation-service * Feature flag * Add tests * Fix flag name * Fix build * Prettier * Small clean-up * Codeowners order cleanup * Fix eslint issue * Update sdk to 550 * Cleanup & fix incompatibilities * Prettier --- .github/CODEOWNERS | 1 + .../user-key-rotation.service.spec.ts | 12 +- .../key-rotation/user-key-rotation.service.ts | 24 ++ jest.config.js | 1 + libs/common/src/enums/feature-flag.enum.ts | 2 + libs/user-crypto-management/README.md | 5 + libs/user-crypto-management/eslint.config.mjs | 3 + libs/user-crypto-management/jest.config.js | 11 + libs/user-crypto-management/package.json | 11 + libs/user-crypto-management/project.json | 34 ++ libs/user-crypto-management/src/index.ts | 3 + .../src/user-crypto-management.module.ts | 25 ++ .../user-key-rotation.service.abstraction.ts | 41 +++ .../src/user-key-rotation.service.spec.ts | 295 ++++++++++++++++++ .../src/user-key-rotation.service.ts | 164 ++++++++++ libs/user-crypto-management/test.setup.ts | 1 + .../tsconfig.eslint.json | 6 + libs/user-crypto-management/tsconfig.json | 5 + libs/user-crypto-management/tsconfig.lib.json | 10 + .../user-crypto-management/tsconfig.spec.json | 10 + package-lock.json | 9 + tsconfig.base.json | 2 + 22 files changed, 674 insertions(+), 1 deletion(-) create mode 100644 libs/user-crypto-management/README.md create mode 100644 libs/user-crypto-management/eslint.config.mjs create mode 100644 libs/user-crypto-management/jest.config.js create mode 100644 libs/user-crypto-management/package.json create mode 100644 libs/user-crypto-management/project.json create mode 100644 libs/user-crypto-management/src/index.ts create mode 100644 libs/user-crypto-management/src/user-crypto-management.module.ts create mode 100644 libs/user-crypto-management/src/user-key-rotation.service.abstraction.ts create mode 100644 libs/user-crypto-management/src/user-key-rotation.service.spec.ts create mode 100644 libs/user-crypto-management/src/user-key-rotation.service.ts create mode 100644 libs/user-crypto-management/test.setup.ts create mode 100644 libs/user-crypto-management/tsconfig.eslint.json create mode 100644 libs/user-crypto-management/tsconfig.json create mode 100644 libs/user-crypto-management/tsconfig.lib.json create mode 100644 libs/user-crypto-management/tsconfig.spec.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c6c1e42ae52..c2e04d94f95 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -192,6 +192,7 @@ apps/cli/src/key-management @bitwarden/team-key-management-dev bitwarden_license/bit-web/src/app/key-management @bitwarden/team-key-management-dev libs/key-management @bitwarden/team-key-management-dev libs/key-management-ui @bitwarden/team-key-management-dev +libs/user-crypto-management @bitwarden/team-key-management-dev libs/common/src/key-management @bitwarden/team-key-management-dev # Node-cryptofunction service libs/node @bitwarden/team-key-management-dev diff --git a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts index a2330025c92..fec972c82f2 100644 --- a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts +++ b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts @@ -57,6 +57,7 @@ import { KeyRotationTrustInfoComponent, } from "@bitwarden/key-management-ui"; import { BitwardenClient, PureCrypto } from "@bitwarden/sdk-internal"; +import { UserKeyRotationServiceAbstraction } from "@bitwarden/user-crypto-management"; import { OrganizationUserResetPasswordService } from "../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service"; import { WebauthnLoginAdminService } from "../../auth"; @@ -287,6 +288,7 @@ describe("KeyRotationService", () => { let mockSdkClientFactory: MockProxy; let mockSecurityStateService: MockProxy; let mockMasterPasswordService: MockProxy; + let mockSdkUserKeyRotationService: MockProxy; const mockUser = { id: "mockUserId" as UserId, @@ -348,6 +350,7 @@ describe("KeyRotationService", () => { mockDialogService = mock(); mockCryptoFunctionService = mock(); mockKdfConfigService = mock(); + mockSdkUserKeyRotationService = mock(); mockSdkClientFactory = mock(); mockSdkClientFactory.createSdkClient.mockResolvedValue({ crypto: () => { @@ -358,6 +361,7 @@ describe("KeyRotationService", () => { } as any; }, } as BitwardenClient); + mockSecurityStateService = mock(); mockMasterPasswordService = mock(); @@ -384,6 +388,7 @@ describe("KeyRotationService", () => { mockSdkClientFactory, mockSecurityStateService, mockMasterPasswordService, + mockSdkUserKeyRotationService, ); }); @@ -509,7 +514,12 @@ describe("KeyRotationService", () => { ); mockKeyService.userSigningKey$.mockReturnValue(new BehaviorSubject(null)); mockSecurityStateService.accountSecurityState$.mockReturnValue(new BehaviorSubject(null)); - mockConfigService.getFeatureFlag.mockResolvedValue(true); + mockConfigService.getFeatureFlag.mockImplementation(async (flag: FeatureFlag) => { + if (flag === FeatureFlag.EnrollAeadOnKeyRotation) { + return true; + } + return false; + }); const spy = jest.spyOn(keyRotationService, "getRotatedAccountKeysFlagged").mockResolvedValue({ userKey: TEST_VECTOR_USER_KEY_V2, diff --git a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts index 68253a4a35d..26dcacd8f11 100644 --- a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts +++ b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts @@ -39,6 +39,7 @@ import { KeyRotationTrustInfoComponent, } from "@bitwarden/key-management-ui"; import { PureCrypto, TokenProvider } from "@bitwarden/sdk-internal"; +import { UserKeyRotationServiceAbstraction } from "@bitwarden/user-crypto-management"; import { OrganizationUserResetPasswordService } from "../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service"; import { WebauthnLoginAdminService } from "../../auth/core"; @@ -101,6 +102,7 @@ export class UserKeyRotationService { private sdkClientFactory: SdkClientFactory, private securityStateService: SecurityStateService, private masterPasswordService: MasterPasswordServiceAbstraction, + private sdkUserKeyRotationService: UserKeyRotationServiceAbstraction, ) {} /** @@ -116,6 +118,28 @@ export class UserKeyRotationService { user: Account, newMasterPasswordHint?: string, ): Promise { + const useSdkKeyRotation = await this.configService.getFeatureFlag(FeatureFlag.SdkKeyRotation); + if (useSdkKeyRotation) { + this.logService.info( + "[UserKey Rotation] Using SDK-based key rotation service from user-crypto-management", + ); + await this.sdkUserKeyRotationService.changePasswordAndRotateUserKey( + currentMasterPassword, + newMasterPassword, + newMasterPasswordHint, + asUuid(user.id), + ); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("rotationCompletedTitle"), + message: this.i18nService.t("rotationCompletedDesc"), + timeout: 15000, + }); + + await this.logoutService.logout(user.id); + return; + } + // Key-rotation uses the SDK, so we need to ensure that the SDK is loaded / the WASM initialized. await SdkLoadService.Ready; diff --git a/jest.config.js b/jest.config.js index bfe447f7a53..5ea699febff 100644 --- a/jest.config.js +++ b/jest.config.js @@ -61,6 +61,7 @@ module.exports = { "/libs/vault/jest.config.js", "/libs/auto-confirm/jest.config.js", "/libs/subscription/jest.config.js", + "/libs/user-crypto-management/jest.config.js", ], // Workaround for a memory leak that crashes tests in CI: diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 7b1013077d7..6fdb146beb8 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -41,6 +41,7 @@ export enum FeatureFlag { PrivateKeyRegeneration = "pm-12241-private-key-regeneration", EnrollAeadOnKeyRotation = "enroll-aead-on-key-rotation", ForceUpdateKDFSettings = "pm-18021-force-update-kdf-settings", + SdkKeyRotation = "pm-30144-sdk-key-rotation", LinuxBiometricsV2 = "pm-26340-linux-biometrics-v2", NoLogoutOnKdfChange = "pm-23995-no-logout-on-kdf-change", PasskeyUnlock = "pm-2035-passkey-unlock", @@ -157,6 +158,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.PrivateKeyRegeneration]: FALSE, [FeatureFlag.EnrollAeadOnKeyRotation]: FALSE, [FeatureFlag.ForceUpdateKDFSettings]: FALSE, + [FeatureFlag.SdkKeyRotation]: FALSE, [FeatureFlag.LinuxBiometricsV2]: FALSE, [FeatureFlag.NoLogoutOnKdfChange]: FALSE, [FeatureFlag.PasskeyUnlock]: FALSE, diff --git a/libs/user-crypto-management/README.md b/libs/user-crypto-management/README.md new file mode 100644 index 00000000000..5d348f8f4bb --- /dev/null +++ b/libs/user-crypto-management/README.md @@ -0,0 +1,5 @@ +# user-crypto-management + +Owned by: key-management + +Manage a user's cryptography and cryptographic settings diff --git a/libs/user-crypto-management/eslint.config.mjs b/libs/user-crypto-management/eslint.config.mjs new file mode 100644 index 00000000000..9c37d10e3ff --- /dev/null +++ b/libs/user-crypto-management/eslint.config.mjs @@ -0,0 +1,3 @@ +import baseConfig from "../../eslint.config.mjs"; + +export default [...baseConfig]; diff --git a/libs/user-crypto-management/jest.config.js b/libs/user-crypto-management/jest.config.js new file mode 100644 index 00000000000..886da6c0940 --- /dev/null +++ b/libs/user-crypto-management/jest.config.js @@ -0,0 +1,11 @@ +module.exports = { + displayName: "user-crypto-management", + preset: "../../jest.preset.js", + testEnvironment: "node", + setupFilesAfterEnv: ["/test.setup.ts"], + transform: { + "^.+\\.[tj]s$": ["ts-jest", { tsconfig: "/tsconfig.spec.json" }], + }, + moduleFileExtensions: ["ts", "js", "html"], + coverageDirectory: "../../coverage/libs/user-crypto-management", +}; diff --git a/libs/user-crypto-management/package.json b/libs/user-crypto-management/package.json new file mode 100644 index 00000000000..d71b90f7cb2 --- /dev/null +++ b/libs/user-crypto-management/package.json @@ -0,0 +1,11 @@ +{ + "name": "@bitwarden/user-crypto-management", + "version": "0.0.1", + "description": "Manage a user's cryptography and cryptographic settings", + "private": true, + "type": "commonjs", + "main": "index.js", + "types": "index.d.ts", + "license": "GPL-3.0", + "author": "key-management" +} diff --git a/libs/user-crypto-management/project.json b/libs/user-crypto-management/project.json new file mode 100644 index 00000000000..548fbe55ec3 --- /dev/null +++ b/libs/user-crypto-management/project.json @@ -0,0 +1,34 @@ +{ + "name": "user-crypto-management", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/user-crypto-management/src", + "projectType": "library", + "tags": [], + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/user-crypto-management", + "main": "libs/user-crypto-management/src/index.ts", + "tsConfig": "libs/user-crypto-management/tsconfig.lib.json", + "assets": ["libs/user-crypto-management/*.md"], + "rootDir": "libs/user-crypto-management/src" + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/user-crypto-management/**/*.ts"] + } + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/user-crypto-management/jest.config.js" + } + } + } +} diff --git a/libs/user-crypto-management/src/index.ts b/libs/user-crypto-management/src/index.ts new file mode 100644 index 00000000000..cc3cd58300b --- /dev/null +++ b/libs/user-crypto-management/src/index.ts @@ -0,0 +1,3 @@ +export { DefaultUserKeyRotationService as UserKeyRotationService } from "./user-key-rotation.service"; +export { UserKeyRotationService as UserKeyRotationServiceAbstraction } from "./user-key-rotation.service.abstraction"; +export { UserCryptoManagementModule } from "./user-crypto-management.module"; diff --git a/libs/user-crypto-management/src/user-crypto-management.module.ts b/libs/user-crypto-management/src/user-crypto-management.module.ts new file mode 100644 index 00000000000..8eb59ebd313 --- /dev/null +++ b/libs/user-crypto-management/src/user-crypto-management.module.ts @@ -0,0 +1,25 @@ +import { NgModule } from "@angular/core"; + +import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; +import { DialogService } from "@bitwarden/components"; +import { LogService } from "@bitwarden/logging"; +import { safeProvider } from "@bitwarden/ui-common"; + +import { DefaultUserKeyRotationService } from "./user-key-rotation.service"; +import { UserKeyRotationService } from "./user-key-rotation.service.abstraction"; + +/** + * Angular module that provides user crypto management services. + * This module handles key rotation and trust verification for organizations + * and emergency access users. + */ +@NgModule({ + providers: [ + safeProvider({ + provide: UserKeyRotationService, + useClass: DefaultUserKeyRotationService, + deps: [SdkService, LogService, DialogService], + }), + ], +}) +export class UserCryptoManagementModule {} diff --git a/libs/user-crypto-management/src/user-key-rotation.service.abstraction.ts b/libs/user-crypto-management/src/user-key-rotation.service.abstraction.ts new file mode 100644 index 00000000000..796af456526 --- /dev/null +++ b/libs/user-crypto-management/src/user-key-rotation.service.abstraction.ts @@ -0,0 +1,41 @@ +import { PublicKey } from "@bitwarden/sdk-internal"; +import { UserId } from "@bitwarden/user-core"; + +/** + * Result of the trust verification process. + */ +export type TrustVerificationResult = { + wasTrustDenied: boolean; + trustedOrganizationPublicKeys: PublicKey[]; + trustedEmergencyAccessUserPublicKeys: PublicKey[]; +}; + +/** + * Abstraction for the user key rotation service. + * Provides functionality to rotate user keys and verify trust for organizations + * and emergency access users. + */ +export abstract class UserKeyRotationService { + /** + * Rotates the user key using the SDK, re-encrypting all required data with the new key. + * @param currentMasterPassword The current master password + * @param newMasterPassword The new master password + * @param hint Optional hint for the new master password + * @param userId The user account ID + */ + abstract changePasswordAndRotateUserKey( + currentMasterPassword: string, + newMasterPassword: string, + hint: string | undefined, + userId: UserId, + ): Promise; + + /** + * Verifies the trust of organizations and emergency access users by prompting the user. + * Since organizations and emergency access grantees are not signed, manual trust prompts + * are required to verify that the server does not inject public keys. + * @param user The user account + * @returns TrustVerificationResult containing whether trust was denied and the trusted public keys + */ + abstract verifyTrust(userId: UserId): Promise; +} diff --git a/libs/user-crypto-management/src/user-key-rotation.service.spec.ts b/libs/user-crypto-management/src/user-key-rotation.service.spec.ts new file mode 100644 index 00000000000..25b99fc979a --- /dev/null +++ b/libs/user-crypto-management/src/user-key-rotation.service.spec.ts @@ -0,0 +1,295 @@ +import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject, of } from "rxjs"; + +import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; +import { DialogService } from "@bitwarden/components"; +import { + AccountRecoveryTrustComponent, + EmergencyAccessTrustComponent, + KeyRotationTrustInfoComponent, +} from "@bitwarden/key-management-ui"; +import { LogService } from "@bitwarden/logging"; +import { UserId } from "@bitwarden/user-core"; + +import { DefaultUserKeyRotationService } from "./user-key-rotation.service"; + +// Mock dialog open functions +const initialPromptedOpenTrue = jest.fn(); +initialPromptedOpenTrue.mockReturnValue({ closed: new BehaviorSubject(true) }); + +const initialPromptedOpenFalse = jest.fn(); +initialPromptedOpenFalse.mockReturnValue({ closed: new BehaviorSubject(false) }); + +const emergencyAccessTrustOpenTrusted = jest.fn(); +emergencyAccessTrustOpenTrusted.mockReturnValue({ + closed: new BehaviorSubject(true), +}); + +const emergencyAccessTrustOpenUntrusted = jest.fn(); +emergencyAccessTrustOpenUntrusted.mockReturnValue({ + closed: new BehaviorSubject(false), +}); + +const accountRecoveryTrustOpenTrusted = jest.fn(); +accountRecoveryTrustOpenTrusted.mockReturnValue({ + closed: new BehaviorSubject(true), +}); + +const accountRecoveryTrustOpenUntrusted = jest.fn(); +accountRecoveryTrustOpenUntrusted.mockReturnValue({ + closed: new BehaviorSubject(false), +}); + +// Mock the key-management-ui module before importing components +jest.mock("@bitwarden/key-management-ui", () => ({ + KeyRotationTrustInfoComponent: { + open: jest.fn(), + }, + EmergencyAccessTrustComponent: { + open: jest.fn(), + }, + AccountRecoveryTrustComponent: { + open: jest.fn(), + }, +})); + +describe("DefaultUserKeyRotationService", () => { + let service: DefaultUserKeyRotationService; + + let mockSdkService: MockProxy; + let mockLogService: MockProxy; + let mockDialogService: MockProxy; + + const mockUserId = "mockUserId" as UserId; + + let mockUserCryptoManagement: { + get_untrusted_emergency_access_public_keys: jest.Mock; + get_untrusted_organization_public_keys: jest.Mock; + }; + + beforeEach(() => { + jest.clearAllMocks(); + + mockSdkService = mock(); + mockLogService = mock(); + mockDialogService = mock(); + + mockUserCryptoManagement = { + get_untrusted_emergency_access_public_keys: jest.fn(), + get_untrusted_organization_public_keys: jest.fn(), + }; + + mockUserCryptoManagement.get_untrusted_emergency_access_public_keys.mockResolvedValue([]); + mockUserCryptoManagement.get_untrusted_organization_public_keys.mockResolvedValue([]); + + const mockSdkClient = { + take: jest.fn().mockReturnValue({ + value: { + user_crypto_management: () => mockUserCryptoManagement, + }, + [Symbol.dispose]: jest.fn(), + }), + }; + + mockSdkService.userClient$.mockReturnValue(of(mockSdkClient as any)); + + service = new DefaultUserKeyRotationService(mockSdkService, mockLogService, mockDialogService); + + KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue; + EmergencyAccessTrustComponent.open = emergencyAccessTrustOpenTrusted; + AccountRecoveryTrustComponent.open = accountRecoveryTrustOpenTrusted; + }); + + describe("verifyTrust", () => { + const mockEmergencyAccessMembership = { + id: "mockId", + name: "mockName", + public_key: new Uint8Array([1, 2, 3]), + }; + + const mockOrganizationMembership = { + organization_id: "mockOrgId", + name: "mockOrgName", + public_key: new Uint8Array([4, 5, 6]), + }; + + it("returns empty arrays if initial dialog is closed", async () => { + KeyRotationTrustInfoComponent.open = initialPromptedOpenFalse; + mockUserCryptoManagement.get_untrusted_emergency_access_public_keys.mockResolvedValue([ + mockEmergencyAccessMembership, + ]); + mockUserCryptoManagement.get_untrusted_organization_public_keys.mockResolvedValue([ + mockOrganizationMembership, + ]); + + const { + wasTrustDenied, + trustedOrganizationPublicKeys: trustedOrgs, + trustedEmergencyAccessUserPublicKeys: trustedEmergencyAccessUsers, + } = await service.verifyTrust(mockUserId); + + expect(trustedEmergencyAccessUsers).toEqual([]); + expect(trustedOrgs).toEqual([]); + expect(wasTrustDenied).toBe(true); + }); + + it("returns empty arrays if account recovery dialog is closed", async () => { + KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue; + AccountRecoveryTrustComponent.open = accountRecoveryTrustOpenUntrusted; + mockUserCryptoManagement.get_untrusted_emergency_access_public_keys.mockResolvedValue([ + mockEmergencyAccessMembership, + ]); + mockUserCryptoManagement.get_untrusted_organization_public_keys.mockResolvedValue([ + mockOrganizationMembership, + ]); + + const { + wasTrustDenied, + trustedOrganizationPublicKeys: trustedOrgs, + trustedEmergencyAccessUserPublicKeys: trustedEmergencyAccessUsers, + } = await service.verifyTrust(mockUserId); + + expect(trustedEmergencyAccessUsers).toEqual([]); + expect(trustedOrgs).toEqual([]); + expect(wasTrustDenied).toBe(true); + }); + + it("returns empty arrays if emergency access dialog is closed", async () => { + KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue; + AccountRecoveryTrustComponent.open = accountRecoveryTrustOpenTrusted; + EmergencyAccessTrustComponent.open = emergencyAccessTrustOpenUntrusted; + mockUserCryptoManagement.get_untrusted_emergency_access_public_keys.mockResolvedValue([ + mockEmergencyAccessMembership, + ]); + mockUserCryptoManagement.get_untrusted_organization_public_keys.mockResolvedValue([]); + + const { + wasTrustDenied, + trustedOrganizationPublicKeys: trustedOrgs, + trustedEmergencyAccessUserPublicKeys: trustedEmergencyAccessUsers, + } = await service.verifyTrust(mockUserId); + + expect(trustedEmergencyAccessUsers).toEqual([]); + expect(trustedOrgs).toEqual([]); + expect(wasTrustDenied).toBe(true); + }); + + it("returns trusted keys when all dialogs are confirmed with only emergency access users", async () => { + KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue; + EmergencyAccessTrustComponent.open = emergencyAccessTrustOpenTrusted; + AccountRecoveryTrustComponent.open = accountRecoveryTrustOpenTrusted; + mockUserCryptoManagement.get_untrusted_emergency_access_public_keys.mockResolvedValue([ + mockEmergencyAccessMembership, + ]); + mockUserCryptoManagement.get_untrusted_organization_public_keys.mockResolvedValue([]); + + const { + wasTrustDenied, + trustedOrganizationPublicKeys: trustedOrgs, + trustedEmergencyAccessUserPublicKeys: trustedEmergencyAccessUsers, + } = await service.verifyTrust(mockUserId); + + expect(wasTrustDenied).toBe(false); + expect(trustedEmergencyAccessUsers).toEqual([mockEmergencyAccessMembership.public_key]); + expect(trustedOrgs).toEqual([]); + }); + + it("returns trusted keys when all dialogs are confirmed with only organizations", async () => { + KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue; + EmergencyAccessTrustComponent.open = emergencyAccessTrustOpenTrusted; + AccountRecoveryTrustComponent.open = accountRecoveryTrustOpenTrusted; + mockUserCryptoManagement.get_untrusted_emergency_access_public_keys.mockResolvedValue([]); + mockUserCryptoManagement.get_untrusted_organization_public_keys.mockResolvedValue([ + mockOrganizationMembership, + ]); + + const { + wasTrustDenied, + trustedOrganizationPublicKeys: trustedOrgs, + trustedEmergencyAccessUserPublicKeys: trustedEmergencyAccessUsers, + } = await service.verifyTrust(mockUserId); + + expect(wasTrustDenied).toBe(false); + expect(trustedEmergencyAccessUsers).toEqual([]); + expect(trustedOrgs).toEqual([mockOrganizationMembership.public_key]); + }); + + it("returns empty arrays when no organizations or emergency access users exist", async () => { + mockUserCryptoManagement.get_untrusted_emergency_access_public_keys.mockResolvedValue([]); + mockUserCryptoManagement.get_untrusted_organization_public_keys.mockResolvedValue([]); + + const { + wasTrustDenied, + trustedOrganizationPublicKeys: trustedOrgs, + trustedEmergencyAccessUserPublicKeys: trustedEmergencyAccessUsers, + } = await service.verifyTrust(mockUserId); + + expect(wasTrustDenied).toBe(false); + expect(trustedEmergencyAccessUsers).toEqual([]); + expect(trustedOrgs).toEqual([]); + }); + + it("returns trusted keys when all dialogs are confirmed with both organizations and emergency access users", async () => { + KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue; + EmergencyAccessTrustComponent.open = emergencyAccessTrustOpenTrusted; + AccountRecoveryTrustComponent.open = accountRecoveryTrustOpenTrusted; + mockUserCryptoManagement.get_untrusted_emergency_access_public_keys.mockResolvedValue([ + mockEmergencyAccessMembership, + ]); + mockUserCryptoManagement.get_untrusted_organization_public_keys.mockResolvedValue([ + mockOrganizationMembership, + ]); + + const { + wasTrustDenied, + trustedOrganizationPublicKeys: trustedOrgs, + trustedEmergencyAccessUserPublicKeys: trustedEmergencyAccessUsers, + } = await service.verifyTrust(mockUserId); + + expect(wasTrustDenied).toBe(false); + expect(trustedEmergencyAccessUsers).toEqual([mockEmergencyAccessMembership.public_key]); + expect(trustedOrgs).toEqual([mockOrganizationMembership.public_key]); + }); + + it("does not show initial dialog when no organizations or emergency access users exist", async () => { + mockUserCryptoManagement.get_untrusted_emergency_access_public_keys.mockResolvedValue([]); + mockUserCryptoManagement.get_untrusted_organization_public_keys.mockResolvedValue([]); + + await service.verifyTrust(mockUserId); + + expect(KeyRotationTrustInfoComponent.open).not.toHaveBeenCalled(); + }); + + it("shows initial dialog when organizations exist", async () => { + KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue; + AccountRecoveryTrustComponent.open = accountRecoveryTrustOpenTrusted; + mockUserCryptoManagement.get_untrusted_emergency_access_public_keys.mockResolvedValue([]); + mockUserCryptoManagement.get_untrusted_organization_public_keys.mockResolvedValue([ + mockOrganizationMembership, + ]); + + await service.verifyTrust(mockUserId); + + expect(KeyRotationTrustInfoComponent.open).toHaveBeenCalledWith(mockDialogService, { + numberOfEmergencyAccessUsers: 0, + orgName: mockOrganizationMembership.name, + }); + }); + + it("shows initial dialog when emergency access users exist", async () => { + KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue; + EmergencyAccessTrustComponent.open = emergencyAccessTrustOpenTrusted; + mockUserCryptoManagement.get_untrusted_emergency_access_public_keys.mockResolvedValue([ + mockEmergencyAccessMembership, + ]); + mockUserCryptoManagement.get_untrusted_organization_public_keys.mockResolvedValue([]); + + await service.verifyTrust(mockUserId); + + expect(KeyRotationTrustInfoComponent.open).toHaveBeenCalledWith(mockDialogService, { + numberOfEmergencyAccessUsers: 1, + orgName: undefined, + }); + }); + }); +}); diff --git a/libs/user-crypto-management/src/user-key-rotation.service.ts b/libs/user-crypto-management/src/user-key-rotation.service.ts new file mode 100644 index 00000000000..a1af0f7f80e --- /dev/null +++ b/libs/user-crypto-management/src/user-key-rotation.service.ts @@ -0,0 +1,164 @@ +import { catchError, EMPTY, firstValueFrom, map } from "rxjs"; + +import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { DialogService } from "@bitwarden/components"; +import { + AccountRecoveryTrustComponent, + EmergencyAccessTrustComponent, + KeyRotationTrustInfoComponent, +} from "@bitwarden/key-management-ui"; +import { LogService } from "@bitwarden/logging"; +import { RotateUserKeysRequest } from "@bitwarden/sdk-internal"; +import { UserId } from "@bitwarden/user-core"; + +import { + TrustVerificationResult, + UserKeyRotationService, +} from "./user-key-rotation.service.abstraction"; + +/** + * Service for rotating user keys using the SDK. + * Handles key rotation and trust verification for organizations and emergency access users. + */ +export class DefaultUserKeyRotationService implements UserKeyRotationService { + constructor( + private sdkService: SdkService, + private logService: LogService, + private dialogService: DialogService, + ) {} + + async changePasswordAndRotateUserKey( + currentMasterPassword: string, + newMasterPassword: string, + hint: string | undefined, + userId: UserId, + ): Promise { + // First, the provided organizations and emergency access users need to be verified; + // this is currently done by providing the user a manual confirmation dialog. + const { wasTrustDenied, trustedOrganizationPublicKeys, trustedEmergencyAccessUserPublicKeys } = + await this.verifyTrust(userId); + if (wasTrustDenied) { + this.logService.info("[Userkey rotation] Trust was denied by user. Aborting!"); + return; + } + + return firstValueFrom( + this.sdkService.userClient$(userId).pipe( + map(async (sdk) => { + if (!sdk) { + throw new Error("SDK not available"); + } + + using ref = sdk.take(); + this.logService.info("[UserKey Rotation] Re-encrypting user data with new user key..."); + await ref.value.user_crypto_management().rotate_user_keys({ + master_key_unlock_method: { + Password: { + old_password: currentMasterPassword, + password: newMasterPassword, + hint: hint, + }, + }, + trusted_emergency_access_public_keys: trustedEmergencyAccessUserPublicKeys, + trusted_organization_public_keys: trustedOrganizationPublicKeys, + } as RotateUserKeysRequest); + }), + catchError((error: unknown) => { + this.logService.error(`Failed to rotate user keys: ${error}`); + return EMPTY; + }), + ), + ); + } + + async verifyTrust(userId: UserId): Promise { + // Since currently the joined organizations and emergency access grantees are + // not signed, manual trust prompts are required, to verify that the server + // does not inject public keys here. + // + // Once signing is implemented, this is the place to also sign the keys and + // upload the signed trust claims. + // + // The flow works in 3 steps: + // 1. Prepare the user by showing them a dialog telling them they'll be asked + // to verify the trust of their organizations and emergency access users. + // 2. Show the user a dialog for each organization and ask them to verify the trust. + // 3. Show the user a dialog for each emergency access user and ask them to verify the trust. + this.logService.info("[Userkey rotation] Verifying trust..."); + const [emergencyAccessV1Memberships, organizationV1Memberships] = await firstValueFrom( + this.sdkService.userClient$(userId).pipe( + map(async (sdk) => { + if (!sdk) { + throw new Error("SDK not available"); + } + + using ref = sdk.take(); + const emergencyAccessV1Memberships = await ref.value + .user_crypto_management() + .get_untrusted_emergency_access_public_keys(); + const organizationV1Memberships = await ref.value + .user_crypto_management() + .get_untrusted_organization_public_keys(); + return [emergencyAccessV1Memberships, organizationV1Memberships] as const; + }), + ), + ); + this.logService.info("result", { emergencyAccessV1Memberships, organizationV1Memberships }); + + if (organizationV1Memberships.length > 0 || emergencyAccessV1Memberships.length > 0) { + this.logService.info("[Userkey rotation] Showing trust info dialog..."); + const trustInfoDialog = KeyRotationTrustInfoComponent.open(this.dialogService, { + numberOfEmergencyAccessUsers: emergencyAccessV1Memberships.length, + orgName: + organizationV1Memberships.length > 0 ? organizationV1Memberships[0].name : undefined, + }); + if (!(await firstValueFrom(trustInfoDialog.closed))) { + return { + wasTrustDenied: true, + trustedOrganizationPublicKeys: [], + trustedEmergencyAccessUserPublicKeys: [], + }; + } + } + + for (const organization of organizationV1Memberships) { + const dialogRef = AccountRecoveryTrustComponent.open(this.dialogService, { + name: organization.name, + orgId: organization.organization_id as string, + publicKey: Utils.fromB64ToArray(organization.public_key), + }); + if (!(await firstValueFrom(dialogRef.closed))) { + return { + wasTrustDenied: true, + trustedOrganizationPublicKeys: [], + trustedEmergencyAccessUserPublicKeys: [], + }; + } + } + + for (const details of emergencyAccessV1Memberships) { + const dialogRef = EmergencyAccessTrustComponent.open(this.dialogService, { + name: details.name, + userId: details.id as string, + publicKey: Utils.fromB64ToArray(details.public_key), + }); + if (!(await firstValueFrom(dialogRef.closed))) { + return { + wasTrustDenied: true, + trustedOrganizationPublicKeys: [], + trustedEmergencyAccessUserPublicKeys: [], + }; + } + } + + this.logService.info( + "[Userkey rotation] Trust verified for all organizations and emergency access users", + ); + return { + wasTrustDenied: false, + trustedOrganizationPublicKeys: organizationV1Memberships.map((d) => d.public_key), + trustedEmergencyAccessUserPublicKeys: emergencyAccessV1Memberships.map((d) => d.public_key), + }; + } +} diff --git a/libs/user-crypto-management/test.setup.ts b/libs/user-crypto-management/test.setup.ts new file mode 100644 index 00000000000..f0cf585fdd8 --- /dev/null +++ b/libs/user-crypto-management/test.setup.ts @@ -0,0 +1 @@ +import "core-js/proposals/explicit-resource-management"; diff --git a/libs/user-crypto-management/tsconfig.eslint.json b/libs/user-crypto-management/tsconfig.eslint.json new file mode 100644 index 00000000000..3daf120441a --- /dev/null +++ b/libs/user-crypto-management/tsconfig.eslint.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": ["src/**/*.ts", "src/**/*.js"], + "exclude": ["**/build", "**/dist"] +} diff --git a/libs/user-crypto-management/tsconfig.json b/libs/user-crypto-management/tsconfig.json new file mode 100644 index 00000000000..9c607a26b09 --- /dev/null +++ b/libs/user-crypto-management/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.base", + "include": ["src", "spec"], + "exclude": ["node_modules", "dist"] +} diff --git a/libs/user-crypto-management/tsconfig.lib.json b/libs/user-crypto-management/tsconfig.lib.json new file mode 100644 index 00000000000..9cbf6736007 --- /dev/null +++ b/libs/user-crypto-management/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.js", "src/**/*.spec.ts"] +} diff --git a/libs/user-crypto-management/tsconfig.spec.json b/libs/user-crypto-management/tsconfig.spec.json new file mode 100644 index 00000000000..1275f148a18 --- /dev/null +++ b/libs/user-crypto-management/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "moduleResolution": "node10", + "types": ["jest", "node"] + }, + "include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"] +} diff --git a/package-lock.json b/package-lock.json index 9f6e82d98ef..f5ac6ccbc0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -642,6 +642,11 @@ "version": "0.0.0", "license": "GPL-3.0" }, + "libs/user-crypto-management": { + "name": "@bitwarden/user-crypto-management", + "version": "0.0.1", + "license": "GPL-3.0" + }, "libs/vault": { "name": "@bitwarden/vault", "version": "0.0.0", @@ -5101,6 +5106,10 @@ "resolved": "libs/user-core", "link": true }, + "node_modules/@bitwarden/user-crypto-management": { + "resolved": "libs/user-crypto-management", + "link": true + }, "node_modules/@bitwarden/vault": { "resolved": "libs/vault", "link": true diff --git a/tsconfig.base.json b/tsconfig.base.json index 995eac031fd..fb76ea752a7 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -33,6 +33,7 @@ "@bitwarden/client-type": ["./libs/client-type/src/index.ts"], "@bitwarden/common/spec": ["./libs/common/spec"], "@bitwarden/common/*": ["./libs/common/src/*"], + "@bitwarden/common/spec": ["./libs/common/spec"], "@bitwarden/components": ["./libs/components/src"], "@bitwarden/core-test-utils": ["./libs/core-test-utils/src/index.ts"], "@bitwarden/dirt-card": ["./libs/dirt/card/src"], @@ -64,6 +65,7 @@ "@bitwarden/ui-common": ["./libs/ui/common/src"], "@bitwarden/ui-common/setup-jest": ["./libs/ui/common/src/setup-jest"], "@bitwarden/user-core": ["./libs/user-core/src/index.ts"], + "@bitwarden/user-crypto-management": ["./libs/user-crypto-management/src/index.ts"], "@bitwarden/vault": ["./libs/vault/src"], "@bitwarden/vault-export-core": ["./libs/tools/export/vault-export/vault-export-core/src"], "@bitwarden/vault-export-ui": ["./libs/tools/export/vault-export/vault-export-ui/src"], From 767caa431275a2c3fe6c1ca7b1c6ced8fa7932aa Mon Sep 17 00:00:00 2001 From: John Harrington <84741727+harr1424@users.noreply.github.com> Date: Fri, 20 Feb 2026 07:51:05 -0700 Subject: [PATCH 104/134] [PM-32472] [Defect] Generator page will not display on desktop (#19085) * remove redundant link and import * apply lost styles --- .../generator/credential-generator.component.html | 11 +++++++---- .../tools/generator/credential-generator.component.ts | 10 ++-------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/apps/desktop/src/app/tools/generator/credential-generator.component.html b/apps/desktop/src/app/tools/generator/credential-generator.component.html index 12088a147c5..241d21b1bb7 100644 --- a/apps/desktop/src/app/tools/generator/credential-generator.component.html +++ b/apps/desktop/src/app/tools/generator/credential-generator.component.html @@ -5,14 +5,17 @@ diff --git a/apps/desktop/src/app/tools/generator/credential-generator.component.ts b/apps/desktop/src/app/tools/generator/credential-generator.component.ts index 42313c48f7f..036a5e104aa 100644 --- a/apps/desktop/src/app/tools/generator/credential-generator.component.ts +++ b/apps/desktop/src/app/tools/generator/credential-generator.component.ts @@ -1,13 +1,7 @@ import { Component } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { - ButtonModule, - DialogModule, - DialogService, - ItemModule, - LinkModule, -} from "@bitwarden/components"; +import { ButtonModule, DialogModule, DialogService, ItemModule } from "@bitwarden/components"; import { CredentialGeneratorHistoryDialogComponent, GeneratorModule, @@ -18,7 +12,7 @@ import { @Component({ selector: "credential-generator", templateUrl: "credential-generator.component.html", - imports: [DialogModule, ButtonModule, JslibModule, GeneratorModule, ItemModule, LinkModule], + imports: [DialogModule, ButtonModule, JslibModule, GeneratorModule, ItemModule], }) export class CredentialGeneratorComponent { constructor(private dialogService: DialogService) {} From aa4eac7d409cd86fd74e680932d2f7e49f85057e Mon Sep 17 00:00:00 2001 From: Jonathan Prusik Date: Fri, 20 Feb 2026 10:01:04 -0500 Subject: [PATCH 105/134] do not show passkey dialog and notifications at the same time (#18878) --- .../notification.background.spec.ts | 21 ++++- .../background/notification.background.ts | 7 ++ .../abstractions/fido2.background.ts | 2 + .../fido2/background/fido2.background.spec.ts | 78 +++++++++++++++++++ .../fido2/background/fido2.background.ts | 34 +++++--- .../browser-fido2-user-interface.service.ts | 3 + .../browser/src/background/main.background.ts | 1 + 7 files changed, 136 insertions(+), 10 deletions(-) diff --git a/apps/browser/src/autofill/background/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts index 95d4111987b..1bd1ae5513b 100644 --- a/apps/browser/src/autofill/background/notification.background.spec.ts +++ b/apps/browser/src/autofill/background/notification.background.spec.ts @@ -28,6 +28,7 @@ import { TaskService, SecurityTask } from "@bitwarden/common/vault/tasks"; import { BrowserApi } from "../../platform/browser/browser-api"; import { NotificationType } from "../enums/notification-type.enum"; +import { Fido2Background } from "../fido2/background/abstractions/fido2.background"; import { FormData } from "../services/abstractions/autofill.service"; import AutofillService from "../services/autofill.service"; import { createAutofillPageDetailsMock, createChromeTabMock } from "../spec/autofill-mocks"; @@ -81,6 +82,8 @@ describe("NotificationBackground", () => { const configService = mock(); const accountService = mock(); const organizationService = mock(); + const fido2Background = mock(); + fido2Background.isCredentialRequestInProgress.mockReturnValue(false); const userId = "testId" as UserId; const activeAccountSubject = new BehaviorSubject({ @@ -115,6 +118,7 @@ describe("NotificationBackground", () => { userNotificationSettingsService, taskService, messagingService, + fido2Background, ); }); @@ -759,7 +763,6 @@ describe("NotificationBackground", () => { notificationBackground as any, "getEnableChangedPasswordPrompt", ); - pushChangePasswordToQueueSpy = jest.spyOn( notificationBackground as any, "pushChangePasswordToQueue", @@ -822,6 +825,22 @@ describe("NotificationBackground", () => { expectSkippedCheckingNotification(); }); + it("skips checking if a notification should trigger if a fido2 credential request is in progress for the tab", async () => { + const formEntryData: ModifyLoginCipherFormData = { + newPassword: "", + password: "", + uri: mockFormURI, + username: "ADent", + }; + + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); + fido2Background.isCredentialRequestInProgress.mockReturnValueOnce(true); + + await notificationBackground.triggerCipherNotification(formEntryData, tab); + + expectSkippedCheckingNotification(); + }); + it("skips checking if a notification should trigger if the user has disabled both the new login and update password notification", async () => { const formEntryData: ModifyLoginCipherFormData = { newPassword: "Bab3lPhs5h", diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 3713cd7c4c2..64c52701e21 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -61,6 +61,7 @@ import { } from "../content/components/cipher/types"; import { CollectionView } from "../content/components/common-types"; import { NotificationType } from "../enums/notification-type.enum"; +import { Fido2Background } from "../fido2/background/abstractions/fido2.background"; import { AutofillService } from "../services/abstractions/autofill.service"; import { TemporaryNotificationChangeLoginService } from "../services/notification-change-login-password.service"; @@ -165,6 +166,7 @@ export default class NotificationBackground { private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction, private taskService: TaskService, protected messagingService: MessagingService, + private fido2Background: Fido2Background, ) {} init() { @@ -665,6 +667,11 @@ export default class NotificationBackground { return false; } + // If there is an active passkey prompt, exit early + if (this.fido2Background.isCredentialRequestInProgress(tab.id)) { + return false; + } + // If no cipher add/update notifications are enabled, we can exit early const changePasswordNotificationIsEnabled = await this.getEnableChangedPasswordPrompt(); const newLoginNotificationIsEnabled = await this.getEnableAddedLoginPrompt(); diff --git a/apps/browser/src/autofill/fido2/background/abstractions/fido2.background.ts b/apps/browser/src/autofill/fido2/background/abstractions/fido2.background.ts index 6ad069ad56e..c5346d61566 100644 --- a/apps/browser/src/autofill/fido2/background/abstractions/fido2.background.ts +++ b/apps/browser/src/autofill/fido2/background/abstractions/fido2.background.ts @@ -45,6 +45,8 @@ type Fido2BackgroundExtensionMessageHandlers = { interface Fido2Background { init(): void; + isCredentialRequestInProgress(tabId: number): boolean; + isPasskeySettingEnabled(): Promise; } export { diff --git a/apps/browser/src/autofill/fido2/background/fido2.background.spec.ts b/apps/browser/src/autofill/fido2/background/fido2.background.spec.ts index 752851b3d37..6ead7416b96 100644 --- a/apps/browser/src/autofill/fido2/background/fido2.background.spec.ts +++ b/apps/browser/src/autofill/fido2/background/fido2.background.spec.ts @@ -256,6 +256,84 @@ describe("Fido2Background", () => { }); }); + describe("isCredentialRequestInProgress", () => { + beforeEach(() => { + fido2Background.init(); + }); + + it("returns false when no credential request is active", () => { + expect(fido2Background.isCredentialRequestInProgress(tabMock.id)).toBe(false); + }); + + it("returns true while a register credential request is in progress", async () => { + let duringRequestResult: boolean; + fido2ClientService.createCredential.mockImplementation(async () => { + duringRequestResult = fido2Background.isCredentialRequestInProgress(tabMock.id); + return mock(); + }); + + const message = mock({ + command: "fido2RegisterCredentialRequest", + requestId: "123", + data: mock(), + }); + + sendMockExtensionMessage(message, senderMock); + await flushPromises(); + + expect(duringRequestResult).toBe(true); + }); + + it("returns true while a get credential request is in progress", async () => { + let duringRequestResult: boolean; + fido2ClientService.assertCredential.mockImplementation(async () => { + duringRequestResult = fido2Background.isCredentialRequestInProgress(tabMock.id); + return mock(); + }); + + const message = mock({ + command: "fido2GetCredentialRequest", + requestId: "123", + data: mock(), + }); + + sendMockExtensionMessage(message, senderMock); + await flushPromises(); + + expect(duringRequestResult).toBe(true); + }); + + it("returns false after a credential request completes", async () => { + fido2ClientService.createCredential.mockResolvedValue(mock()); + + const message = mock({ + command: "fido2RegisterCredentialRequest", + requestId: "123", + data: mock(), + }); + + sendMockExtensionMessage(message, senderMock); + await flushPromises(); + + expect(fido2Background.isCredentialRequestInProgress(tabMock.id)).toBe(false); + }); + + it("returns false after a credential request errors", async () => { + fido2ClientService.createCredential.mockRejectedValue(new Error("error")); + + const message = mock({ + command: "fido2RegisterCredentialRequest", + requestId: "123", + data: mock(), + }); + + sendMockExtensionMessage(message, senderMock); + await flushPromises(); + + expect(fido2Background.isCredentialRequestInProgress(tabMock.id)).toBe(false); + }); + }); + describe("extension message handlers", () => { beforeEach(() => { fido2Background.init(); diff --git a/apps/browser/src/autofill/fido2/background/fido2.background.ts b/apps/browser/src/autofill/fido2/background/fido2.background.ts index 22ee4a1822d..495b0d85f0b 100644 --- a/apps/browser/src/autofill/fido2/background/fido2.background.ts +++ b/apps/browser/src/autofill/fido2/background/fido2.background.ts @@ -35,6 +35,7 @@ export class Fido2Background implements Fido2BackgroundInterface { private currentAuthStatus$: Subscription; private abortManager = new AbortManager(); private fido2ContentScriptPortsSet = new Set(); + private activeCredentialRequests = new Set(); private registeredContentScripts: browser.contentScripts.RegisteredContentScript; private readonly sharedInjectionDetails: SharedFido2ScriptInjectionDetails = { runAt: "document_start", @@ -61,6 +62,16 @@ export class Fido2Background implements Fido2BackgroundInterface { private authService: AuthService, ) {} + /** + * Checks if a FIDO2 credential request (registration or assertion) + * is currently in progress for the given tab. + * + * @param tabId - The tab id to check. + */ + isCredentialRequestInProgress(tabId: number): boolean { + return this.activeCredentialRequests.has(tabId); + } + /** * Initializes the FIDO2 background service. Sets up the extension message * and port listeners. Subscribes to the enablePasskeys$ observable to @@ -307,20 +318,25 @@ export class Fido2Background implements Fido2BackgroundInterface { abortController: AbortController, ) => Promise, ) => { - return await this.abortManager.runWithAbortController(requestId, async (abortController) => { - try { - return await callback(data, tab, abortController); - } finally { - await BrowserApi.focusTab(tab.id); - await BrowserApi.focusWindow(tab.windowId); - } - }); + this.activeCredentialRequests.add(tab.id); + try { + return await this.abortManager.runWithAbortController(requestId, async (abortController) => { + try { + return await callback(data, tab, abortController); + } finally { + await BrowserApi.focusTab(tab.id); + await BrowserApi.focusWindow(tab.windowId); + } + }); + } finally { + this.activeCredentialRequests.delete(tab.id); + } }; /** * Checks if the enablePasskeys setting is enabled. */ - private async isPasskeySettingEnabled() { + async isPasskeySettingEnabled() { return await firstValueFrom(this.vaultSettingsService.enablePasskeys$); } diff --git a/apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts b/apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts index 19c1dbc8790..11dc170db16 100644 --- a/apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts +++ b/apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts @@ -363,6 +363,9 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi ), ); + // Defensive measure in case an existing notification appeared before the passkey popout + await BrowserApi.tabSendMessageData(this.tab, "closeNotificationBar"); + const popoutId = await openFido2Popout(this.tab, { sessionId: this.sessionId, fallbackSupported: this.fallbackSupported, diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 25c7b344982..95ec6e5ad20 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1409,6 +1409,7 @@ export default class MainBackground { this.userNotificationSettingsService, this.taskService, this.messagingService, + this.fido2Background, ); this.overlayNotificationsBackground = new OverlayNotificationsBackground( From e16503f0930075679d8f6002fd5989d364766099 Mon Sep 17 00:00:00 2001 From: Vicki League Date: Fri, 20 Feb 2026 10:01:38 -0500 Subject: [PATCH 106/134] [PM-24178] Handle focus when routed dialog closes in vault table (#18409) --- .../collections/vault.component.ts | 39 ++- .../navigation-switcher.component.html | 1 + .../product-switcher-content.component.html | 3 + .../vault-cipher-row.component.html | 5 + .../vault/individual-vault/vault.component.ts | 45 ++- .../src/a11y/router-focus-manager.mdx | 69 ++++ .../a11y/router-focus-manager.service.spec.ts | 323 ++++++++++++++++++ .../src/a11y/router-focus-manager.service.ts | 73 ++-- .../src/dialog/dialog/dialog.component.ts | 6 +- .../src/input/autofocus.directive.ts | 16 +- .../src/navigation/nav-item.component.html | 3 + .../src/navigation/nav-item.component.ts | 12 + .../tabs/tab-nav-bar/tab-link.component.html | 2 +- .../services/routed-vault-filter.service.ts | 2 +- 14 files changed, 546 insertions(+), 53 deletions(-) create mode 100644 libs/components/src/a11y/router-focus-manager.mdx create mode 100644 libs/components/src/a11y/router-focus-manager.service.spec.ts diff --git a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts index a641116f4de..1d9178e6fed 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { ActivatedRoute, Params, Router } from "@angular/router"; +import { ActivatedRoute, NavigationExtras, Params, Router } from "@angular/router"; import { BehaviorSubject, combineLatest, @@ -588,7 +588,7 @@ export class VaultComponent implements OnInit, OnDestroy { queryParamsHandling: "merge", replaceUrl: true, state: { - focusMainAfterNav: false, + focusAfterNav: false, }, }), ); @@ -812,7 +812,7 @@ export class VaultComponent implements OnInit, OnDestroy { async editCipherAttachments(cipher: CipherView) { if (cipher.reprompt !== 0 && !(await this.passwordRepromptService.showPasswordPrompt())) { - this.go({ cipherId: null, itemId: null }); + this.go({ cipherId: null, itemId: null }, this.configureRouterFocusToCipher(cipher.id)); return; } @@ -869,7 +869,7 @@ export class VaultComponent implements OnInit, OnDestroy { !(await this.passwordRepromptService.showPasswordPrompt()) ) { // didn't pass password prompt, so don't open add / edit modal - this.go({ cipherId: null, itemId: null }); + this.go({ cipherId: null, itemId: null }, this.configureRouterFocusToCipher(cipher.id)); return; } @@ -893,7 +893,10 @@ export class VaultComponent implements OnInit, OnDestroy { !(await this.passwordRepromptService.showPasswordPrompt()) ) { // Didn't pass password prompt, so don't open add / edit modal. - await this.go({ cipherId: null, itemId: null, action: null }); + await this.go( + { cipherId: null, itemId: null, action: null }, + this.configureRouterFocusToCipher(cipher.id), + ); return; } @@ -943,7 +946,10 @@ export class VaultComponent implements OnInit, OnDestroy { } // Clear the query params when the dialog closes - await this.go({ cipherId: null, itemId: null, action: null }); + await this.go( + { cipherId: null, itemId: null, action: null }, + this.configureRouterFocusToCipher(formConfig.originalCipher?.id), + ); } async cloneCipher(cipher: CipherView) { @@ -1422,7 +1428,25 @@ export class VaultComponent implements OnInit, OnDestroy { } } - private go(queryParams: any = null) { + /** + * Helper function to set up the `state.focusAfterNav` property for dialog router navigation if + * the cipherId exists. If it doesn't exist, returns undefined. + * + * This ensures that when the routed dialog is closed, the focus returns to the cipher button in + * the vault table, which allows keyboard users to continue navigating uninterrupted. + * + * @param cipherId id of cipher + * @returns Partial, specifically the state.focusAfterNav property, or undefined + */ + private configureRouterFocusToCipher(cipherId?: string): Partial | undefined { + if (cipherId) { + return { + state: { focusAfterNav: `#cipher-btn-${cipherId}` }, + }; + } + } + + private go(queryParams: any = null, navigateOptions?: NavigationExtras) { if (queryParams == null) { queryParams = { type: this.activeFilter.cipherType, @@ -1436,6 +1460,7 @@ export class VaultComponent implements OnInit, OnDestroy { queryParams: queryParams, queryParamsHandling: "merge", replaceUrl: true, + ...navigateOptions, }); } diff --git a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.html b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.html index 9c8f2125614..b6e06e7d06f 100644 --- a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.html +++ b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.html @@ -9,6 +9,7 @@ [route]="product.appRoute" [attr.icon]="product.icon" [forceActiveStyles]="product.isActive" + focusAfterNavTarget="body" > } diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.html b/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.html index f2154ec74a3..290e07c932a 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.html +++ b/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.html @@ -19,6 +19,9 @@ " class="tw-group/product-link tw-flex tw-h-24 tw-w-28 tw-flex-col tw-items-center tw-justify-center tw-rounded tw-p-1 tw-text-primary-600 tw-outline-none hover:tw-bg-background-alt hover:tw-text-primary-700 hover:tw-no-underline focus-visible:!tw-ring-2 focus-visible:!tw-ring-primary-700" ariaCurrentWhenActive="page" + [state]="{ + focusAfterNav: 'body', + }" >
    + diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 5ff72b0d147..1f80748ab29 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -1,5 +1,5 @@ import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit, viewChild } from "@angular/core"; -import { ActivatedRoute, Params, Router } from "@angular/router"; +import { ActivatedRoute, NavigationExtras, Params, Router } from "@angular/router"; import { combineLatest, firstValueFrom, lastValueFrom, Observable, of, Subject } from "rxjs"; import { concatMap, @@ -398,7 +398,7 @@ export class VaultComponent implements OnInit, OnDestr queryParamsHandling: "merge", replaceUrl: true, state: { - focusMainAfterNav: false, + focusAfterNav: false, }, }), ); @@ -877,7 +877,10 @@ export class VaultComponent implements OnInit, OnDestr */ async editCipherAttachments(cipher: C) { if (cipher?.reprompt !== 0 && !(await this.passwordRepromptService.showPasswordPrompt())) { - await this.go({ cipherId: null, itemId: null }); + await this.go( + { cipherId: null, itemId: null }, + this.configureRouterFocusToCipher(typeof cipher?.id === "string" ? cipher.id : undefined), + ); return; } @@ -950,7 +953,10 @@ export class VaultComponent implements OnInit, OnDestr } // Clear the query params when the dialog closes - await this.go({ cipherId: null, itemId: null, action: null }); + await this.go( + { cipherId: null, itemId: null, action: null }, + this.configureRouterFocusToCipher(formConfig.originalCipher?.id), + ); } /** @@ -1010,7 +1016,10 @@ export class VaultComponent implements OnInit, OnDestr !(await this.passwordRepromptService.showPasswordPrompt()) ) { // didn't pass password prompt, so don't open add / edit modal - await this.go({ cipherId: null, itemId: null, action: null }); + await this.go( + { cipherId: null, itemId: null, action: null }, + this.configureRouterFocusToCipher(cipher.id), + ); return; } @@ -1052,7 +1061,10 @@ export class VaultComponent implements OnInit, OnDestr !(await this.passwordRepromptService.showPasswordPrompt()) ) { // Didn't pass password prompt, so don't open add / edit modal. - await this.go({ cipherId: null, itemId: null, action: null }); + await this.go( + { cipherId: null, itemId: null, action: null }, + this.configureRouterFocusToCipher(cipher.id), + ); return; } @@ -1553,7 +1565,25 @@ export class VaultComponent implements OnInit, OnDestr this.vaultItemsComponent()?.clearSelection(); } - private async go(queryParams: any = null) { + /** + * Helper function to set up the `state.focusAfterNav` property for dialog router navigation if + * the cipherId exists. If it doesn't exist, returns undefined. + * + * This ensures that when the routed dialog is closed, the focus returns to the cipher button in + * the vault table, which allows keyboard users to continue navigating uninterrupted. + * + * @param cipherId id of cipher + * @returns Partial, specifically the state.focusAfterNav property, or undefined + */ + private configureRouterFocusToCipher(cipherId?: string): Partial | undefined { + if (cipherId) { + return { + state: { focusAfterNav: `#cipher-btn-${cipherId}` }, + }; + } + } + + private async go(queryParams: any = null, navigateOptions?: NavigationExtras) { if (queryParams == null) { queryParams = { favorites: this.activeFilter.isFavorites || null, @@ -1569,6 +1599,7 @@ export class VaultComponent implements OnInit, OnDestr queryParams: queryParams, queryParamsHandling: "merge", replaceUrl: true, + ...navigateOptions, }); } diff --git a/libs/components/src/a11y/router-focus-manager.mdx b/libs/components/src/a11y/router-focus-manager.mdx new file mode 100644 index 00000000000..aa882f9deac --- /dev/null +++ b/libs/components/src/a11y/router-focus-manager.mdx @@ -0,0 +1,69 @@ +import { Meta } from "@storybook/addon-docs/blocks"; + + + +# Router Focus Management + +On a normal non-SPA (Single Page Application) webpage, a page navigation / route change will cause +the full page to reload, and a user's focus is placed at the top of the page when the new page +loads. + +Bitwarden's Angular apps are SPAs using the Angular router to manage internal routing and page +navigation. When the Angular router performs a page navigation / route change to another internal +SPA route, the full page does not reload, and the user's focus does not move from the trigger +element unless the trigger element no longer exists. There is no other built-in notification to a +screenreader user that a navigation has occured, if the focus is not moved. + +## Web + +We handle router focus management in the web app by moving the user's focus at the end of a SPA +Angular router navigation. + +See `router-focus-manager.service.ts` for the implementation. + +### Default behavior + +By default, we focus the `main` element. + +Consumers can change or opt out of the focus management using the `state` input to the +[Angular route](https://angular.dev/api/router/RouterLink). Using `state` allows us to access the +value between browser back/forward arrows. + +### Change focus target + +In template: `` + +In typescript: `this.router.navigate([], { state: { focusAfterNav: '#selector' }})` + +Any valid `querySelector` selector can be passed. If the element is not found, no focus management +occurs as we cannot make the assumption that the default `main` element is the next best option. + +Examples of where you might want to change the target: + +- A full page navigation occurs where you do want the user to be placed at the top of the page (aka + the body element) like a non-SPA app, such as going from Password Manager to Secrets Manager +- A routed dialog needs to manually specify where the focus should return to once it is closed + +### Opt out of focus management + +In template: `` + +In typescript: `this.router.navigate([], { state: { focusAfterNav: false }})` + +Example of where you might want to manually opt out: + +- Tab component causes a route navigation, and the focus will be handled by the tab component itself + +### Autofocus directive + +Consumers can use the autofocus directive on an applicable interactive element. The autofocus +directive will take precedence over the router focus management system. See the +[Autofocus Directive docs](?path=/docs/component-library-form-autofocus-directive--docs). + +## Browser + +Not implemented yet. + +## Desktop + +Not implemented yet. diff --git a/libs/components/src/a11y/router-focus-manager.service.spec.ts b/libs/components/src/a11y/router-focus-manager.service.spec.ts new file mode 100644 index 00000000000..236a05ac038 --- /dev/null +++ b/libs/components/src/a11y/router-focus-manager.service.spec.ts @@ -0,0 +1,323 @@ +import { + computed, + DestroyRef, + EventEmitter, + Injectable, + NgZone, + Signal, + signal, +} from "@angular/core"; +import { TestBed } from "@angular/core/testing"; +import { Event, Navigation, NavigationEnd, Router } from "@angular/router"; +import { mock } from "jest-mock-extended"; +import { BehaviorSubject, Subject } from "rxjs"; + +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; + +import { RouterFocusManagerService } from "./router-focus-manager.service"; + +describe("RouterFocusManagerService", () => { + @Injectable() + class MockNgZone extends NgZone { + onStable: EventEmitter = new EventEmitter(false); + constructor() { + super({ enableLongStackTrace: false }); + } + run(fn: any): any { + return fn(); + } + runOutsideAngular(fn: any): any { + return fn(); + } + simulateZoneExit(): void { + this.onStable.emit(null); + } + + isStable: boolean = true; + } + + @Injectable() + class MockRouter extends Router { + readonly currentNavigationExtras = signal({}); + + readonly currentNavigation: Signal = computed(() => ({ + ...mock(), + extras: this.currentNavigationExtras(), + })); + + // eslint-disable-next-line rxjs/no-exposed-subjects + readonly routerEventsSubject = new Subject(); + + override get events() { + return this.routerEventsSubject.asObservable(); + } + } + + let service: RouterFocusManagerService; + let featureFlagSubject: BehaviorSubject; + let mockRouter: MockRouter; + let mockConfigService: Partial; + let mockNgZoneRef: MockNgZone; + + let querySelectorSpy: jest.SpyInstance; + let consoleWarnSpy: jest.SpyInstance; + + beforeEach(() => { + // Mock ConfigService + featureFlagSubject = new BehaviorSubject(true); + mockConfigService = { + getFeatureFlag$: jest.fn((flag: FeatureFlag) => { + if (flag === FeatureFlag.RouterFocusManagement) { + return featureFlagSubject.asObservable(); + } + return new BehaviorSubject(false).asObservable(); + }), + }; + + // Spy on document.querySelector and console.warn + querySelectorSpy = jest.spyOn(document, "querySelector"); + consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(); + + TestBed.configureTestingModule({ + providers: [ + RouterFocusManagerService, + { provide: Router, useClass: MockRouter }, + { provide: ConfigService, useValue: mockConfigService }, + { provide: NgZone, useClass: MockNgZone }, + { provide: DestroyRef, useValue: { onDestroy: jest.fn() } }, + ], + }); + + service = TestBed.inject(RouterFocusManagerService); + mockNgZoneRef = TestBed.inject(NgZone) as MockNgZone; + mockRouter = TestBed.inject(Router) as MockRouter; + }); + + afterEach(() => { + querySelectorSpy.mockRestore(); + consoleWarnSpy.mockRestore(); + + mockNgZoneRef.isStable = true; + TestBed.resetTestingModule(); + }); + + describe("default behavior", () => { + it("should focus main element after navigation", () => { + const mainElement = document.createElement("main"); + mainElement.focus = jest.fn(); + querySelectorSpy.mockReturnValue(mainElement); + + // Subscribe to start the service + service.start$.subscribe(); + + // Emit first navigation (should be skipped) + mockRouter.routerEventsSubject.next(new NavigationEnd(1, "/first", "/first")); + + // Emit second navigation (should trigger focus) + mockRouter.routerEventsSubject.next(new NavigationEnd(2, "/test", "/test")); + + expect(querySelectorSpy).toHaveBeenCalledWith("main"); + expect(mainElement.focus).toHaveBeenCalled(); + }); + }); + + describe("custom selector", () => { + it("should focus custom element when focusAfterNav selector is provided", () => { + const customElement = document.createElement("button"); + customElement.id = "custom-btn"; + customElement.focus = jest.fn(); + querySelectorSpy.mockReturnValue(customElement); + + // Subscribe to start the service + service.start$.subscribe(); + + // Emit first navigation (should be skipped) + mockRouter.routerEventsSubject.next(new NavigationEnd(1, "/first", "/first")); + + // Emit second navigation with custom selector + mockRouter.currentNavigationExtras.set({ state: { focusAfterNav: "#custom-btn" } }); + mockRouter.routerEventsSubject.next(new NavigationEnd(2, "/test", "/test")); + + expect(querySelectorSpy).toHaveBeenCalledWith("#custom-btn"); + expect(customElement.focus).toHaveBeenCalled(); + }); + }); + + describe("opt-out", () => { + it("should not focus when focusAfterNav is false", () => { + const mainElement = document.createElement("main"); + mainElement.focus = jest.fn(); + querySelectorSpy.mockReturnValue(mainElement); + + // Subscribe to start the service + service.start$.subscribe(); + + // Emit first navigation (should be skipped) + mockRouter.routerEventsSubject.next(new NavigationEnd(1, "/first", "/first")); + + // Emit second navigation with opt-out + mockRouter.currentNavigationExtras.set({ state: { focusAfterNav: false } }); + mockRouter.routerEventsSubject.next(new NavigationEnd(2, "/test", "/test")); + + expect(querySelectorSpy).not.toHaveBeenCalled(); + expect(mainElement.focus).not.toHaveBeenCalled(); + }); + }); + + describe("element not found", () => { + it("should log warning when custom selector does not match any element", () => { + querySelectorSpy.mockReturnValue(null); + + // Subscribe to start the service + service.start$.subscribe(); + + // Emit first navigation (should be skipped) + mockRouter.routerEventsSubject.next(new NavigationEnd(1, "/first", "/first")); + + // Emit second navigation with non-existent selector + mockRouter.currentNavigationExtras.set({ state: { focusAfterNav: "#non-existent" } }); + mockRouter.routerEventsSubject.next(new NavigationEnd(2, "/test", "/test")); + + expect(querySelectorSpy).toHaveBeenCalledWith("#non-existent"); + expect(consoleWarnSpy).toHaveBeenCalledWith( + 'RouterFocusManager: Could not find element with selector "#non-existent"', + ); + }); + }); + + // Remove describe block when FeatureFlag.RouterFocusManagement is removed + describe("feature flag", () => { + it("should not activate when RouterFocusManagement flag is disabled", () => { + const mainElement = document.createElement("main"); + mainElement.focus = jest.fn(); + querySelectorSpy.mockReturnValue(mainElement); + + // Disable feature flag + featureFlagSubject.next(false); + + // Subscribe to start the service + service.start$.subscribe(); + + // Emit first navigation (should be skipped) + mockRouter.routerEventsSubject.next(new NavigationEnd(1, "/first", "/first")); + + // Emit second navigation with flag disabled + mockRouter.routerEventsSubject.next(new NavigationEnd(2, "/test", "/test")); + + expect(querySelectorSpy).not.toHaveBeenCalled(); + expect(mainElement.focus).not.toHaveBeenCalled(); + }); + + it("should activate when RouterFocusManagement flag is enabled", () => { + const mainElement = document.createElement("main"); + mainElement.focus = jest.fn(); + querySelectorSpy.mockReturnValue(mainElement); + + // Ensure feature flag is enabled + featureFlagSubject.next(true); + + // Subscribe to start the service + service.start$.subscribe(); + + // Emit first navigation (should be skipped) + mockRouter.routerEventsSubject.next(new NavigationEnd(1, "/first", "/first")); + + // Emit second navigation with flag enabled + mockRouter.routerEventsSubject.next(new NavigationEnd(2, "/test", "/test")); + + expect(querySelectorSpy).toHaveBeenCalledWith("main"); + expect(mainElement.focus).toHaveBeenCalled(); + }); + }); + + describe("first navigation skip", () => { + it("should not trigger focus management on first navigation after page load", () => { + const mainElement = document.createElement("main"); + mainElement.focus = jest.fn(); + querySelectorSpy.mockReturnValue(mainElement); + + // Subscribe to start the service + service.start$.subscribe(); + + // Emit first navigation (should be skipped) + mockRouter.routerEventsSubject.next(new NavigationEnd(1, "/first", "/first")); + + expect(querySelectorSpy).not.toHaveBeenCalled(); + expect(mainElement.focus).not.toHaveBeenCalled(); + }); + + it("should trigger focus management on second and subsequent navigations", () => { + const mainElement = document.createElement("main"); + mainElement.focus = jest.fn(); + querySelectorSpy.mockReturnValue(mainElement); + + // Subscribe to start the service + service.start$.subscribe(); + + // Emit first navigation (should be skipped) + mockRouter.routerEventsSubject.next(new NavigationEnd(1, "/first", "/first")); + + // Emit second navigation (should trigger focus) + mockRouter.routerEventsSubject.next(new NavigationEnd(2, "/second", "/second")); + + expect(querySelectorSpy).toHaveBeenCalledWith("main"); + expect(mainElement.focus).toHaveBeenCalledTimes(1); + + // Emit third navigation (should also trigger focus) + mainElement.focus = jest.fn(); // Reset mock + mockRouter.routerEventsSubject.next(new NavigationEnd(3, "/third", "/third")); + + expect(mainElement.focus).toHaveBeenCalledTimes(1); + }); + }); + + describe("NgZone stability", () => { + it("should focus immediately when zone is stable", () => { + const mainElement = document.createElement("main"); + mainElement.focus = jest.fn(); + querySelectorSpy.mockReturnValue(mainElement); + + // Subscribe to start the service + service.start$.subscribe(); + + // Emit first navigation (should be skipped) + mockRouter.routerEventsSubject.next(new NavigationEnd(1, "/first", "/first")); + + // Emit second navigation + mockRouter.routerEventsSubject.next(new NavigationEnd(2, "/test", "/test")); + + expect(mainElement.focus).toHaveBeenCalled(); + }); + + it("should wait for zone stability before focusing when zone is not stable", async () => { + const mainElement = document.createElement("main"); + mainElement.focus = jest.fn(); + querySelectorSpy.mockReturnValue(mainElement); + + // Set zone as not stable + mockNgZoneRef.isStable = false; + + // Subscribe to start the service + service.start$.subscribe(); + + // Emit first navigation (should be skipped) + mockRouter.routerEventsSubject.next(new NavigationEnd(1, "/first", "/first")); + + // Emit second navigation + mockRouter.routerEventsSubject.next(new NavigationEnd(2, "/test", "/test")); + + // Focus should not happen yet + expect(mainElement.focus).not.toHaveBeenCalled(); + + // Emit zone stability + mockNgZoneRef.onStable.emit(true); + + // flush promises + await Promise.resolve(); + + // Now focus should have happened + expect(mainElement.focus).toHaveBeenCalled(); + }); + }); +}); diff --git a/libs/components/src/a11y/router-focus-manager.service.ts b/libs/components/src/a11y/router-focus-manager.service.ts index f7371e02a17..56e91e94e3a 100644 --- a/libs/components/src/a11y/router-focus-manager.service.ts +++ b/libs/components/src/a11y/router-focus-manager.service.ts @@ -1,40 +1,23 @@ -import { inject, Injectable } from "@angular/core"; +import { inject, Injectable, NgZone } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { NavigationEnd, Router } from "@angular/router"; -import { skip, filter, combineLatestWith, tap } from "rxjs"; +import { skip, filter, combineLatestWith, tap, map, firstValueFrom } from "rxjs"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { queryForAutofocusDescendents } from "../input"; + @Injectable({ providedIn: "root" }) export class RouterFocusManagerService { private router = inject(Router); + private ngZone = inject(NgZone); private configService = inject(ConfigService); /** - * Handles SPA route focus management. SPA apps don't automatically notify screenreader - * users that navigation has occured or move the user's focus to the content they are - * navigating to, so we need to do it. - * - * By default, we focus the `main` after an internal route navigation. - * - * Consumers can opt out of the passing the following to the `state` input. Using `state` - * allows us to access the value between browser back/forward arrows. - * In template: `` - * In typescript: `this.router.navigate([], { state: { focusMainAfterNav: false }})` - * - * Or, consumers can use the autofocus directive on an applicable interactive element. - * The autofocus directive will take precedence over this route focus pipeline. - * - * Example of where you might want to manually opt out: - * - Tab component causes a route navigation, but the tab content should be focused, - * not the whole `main` - * - * Note that router events that cause a fully new page to load (like switching between - * products) will not follow this pipeline. Instead, those will automatically bring - * focus to the top of the html document as if it were a full page load. So those links - * do not need to manually opt out of this pipeline. + * See associated router-focus-manager.mdx page for documentation on what this pipeline does and + * how to customize focus behavior. */ start$ = this.router.events.pipe( takeUntilDestroyed(), @@ -46,19 +29,47 @@ export class RouterFocusManagerService { skip(1), combineLatestWith(this.configService.getFeatureFlag$(FeatureFlag.RouterFocusManagement)), filter(([_navEvent, flagEnabled]) => flagEnabled), - filter(() => { + map(() => { const currentNavExtras = this.router.currentNavigation()?.extras; - const focusMainAfterNav: boolean | undefined = currentNavExtras?.state?.focusMainAfterNav; + const focusAfterNav: boolean | string | undefined = currentNavExtras?.state?.focusAfterNav; - return focusMainAfterNav !== false; + return focusAfterNav; }), - tap(() => { - const mainEl = document.querySelector("main"); + filter((focusAfterNav) => { + return focusAfterNav !== false; + }), + tap(async (focusAfterNav) => { + let elSelector: string = "main"; - if (mainEl) { - mainEl.focus(); + if (typeof focusAfterNav === "string" && focusAfterNav.length > 0) { + elSelector = focusAfterNav; + } + + if (this.ngZone.isStable) { + this.focusTargetEl(elSelector); + } else { + await firstValueFrom(this.ngZone.onStable); + + this.focusTargetEl(elSelector); } }), ); + + private focusTargetEl(elSelector: string) { + const targetEl = document.querySelector(elSelector); + const mainEl = document.querySelector("main"); + + const pageHasAutofocusEl = mainEl && queryForAutofocusDescendents(mainEl).length > 0; + + if (pageHasAutofocusEl) { + // do nothing because autofocus will handle the focus + return; + } else if (targetEl) { + targetEl.focus(); + } else { + // eslint-disable-next-line no-console + console.warn(`RouterFocusManager: Could not find element with selector "${elSelector}"`); + } + } } diff --git a/libs/components/src/dialog/dialog/dialog.component.ts b/libs/components/src/dialog/dialog/dialog.component.ts index c32ce176d27..39a4db88695 100644 --- a/libs/components/src/dialog/dialog/dialog.component.ts +++ b/libs/components/src/dialog/dialog/dialog.component.ts @@ -20,6 +20,7 @@ import { combineLatest, firstValueFrom, switchMap } from "rxjs"; import { I18nPipe } from "@bitwarden/ui-common"; import { BitIconButtonComponent } from "../../icon-button/icon-button.component"; +import { queryForAutofocusDescendents } from "../../input"; import { SpinnerComponent } from "../../spinner"; import { TypographyDirective } from "../../typography/typography.directive"; import { hasScrollableContent$ } from "../../utils/"; @@ -67,7 +68,7 @@ const drawerSizeToWidth = { export class DialogComponent implements AfterViewInit { private readonly destroyRef = inject(DestroyRef); private readonly ngZone = inject(NgZone); - private readonly el = inject(ElementRef); + private readonly el = inject>(ElementRef); private readonly dialogHeader = viewChild.required>("dialogHeader"); @@ -187,8 +188,7 @@ export class DialogComponent implements AfterViewInit { * AutofocusDirective. */ const dialogRef = this.el.nativeElement; - // Must match selectors of AutofocusDirective - const autofocusDescendants = dialogRef.querySelectorAll("[appAutofocus], [bitAutofocus]"); + const autofocusDescendants = queryForAutofocusDescendents(dialogRef); const hasAutofocusDescendants = autofocusDescendants.length > 0; if (!hasAutofocusDescendants) { diff --git a/libs/components/src/input/autofocus.directive.ts b/libs/components/src/input/autofocus.directive.ts index bffac8eb757..a4390405e55 100644 --- a/libs/components/src/input/autofocus.directive.ts +++ b/libs/components/src/input/autofocus.directive.ts @@ -13,6 +13,18 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { FocusableElement } from "../shared/focusable-element"; +/** + * Helper function to query for descendents of a given el that have the AutofocusDirective + * applied to them + * + * @param el element that supports querySelectorAll + * @returns querySelectorAll results + */ +export function queryForAutofocusDescendents(el: Document | Element) { + // ensure selectors match the directive selectors + return el.querySelectorAll("[appAutofocus], [bitAutofocus]"); +} + /** * Directive to focus an element. * @@ -21,9 +33,7 @@ import { FocusableElement } from "../shared/focusable-element"; * Will focus the element once, when it becomes visible. * * If the component provides the `FocusableElement` interface, the `focus` - * method will be called. Otherwise, the native element will be focused. - * - * If selector changes, `dialog.component.ts` must also be updated + * method will be called. Otherwise, the native element will be focused. * */ @Directive({ selector: "[appAutofocus], [bitAutofocus]", diff --git a/libs/components/src/navigation/nav-item.component.html b/libs/components/src/navigation/nav-item.component.html index 9dda3b3b6a7..a0f2d8ae38b 100644 --- a/libs/components/src/navigation/nav-item.component.html +++ b/libs/components/src/navigation/nav-item.component.html @@ -32,6 +32,9 @@ [ariaCurrentWhenActive]="ariaCurrentWhenActive()" (isActiveChange)="setIsActive($event)" (click)="mainContentClicked.emit()" + [state]="{ + focusAfterNav: focusAfterNavTarget(), + }" > diff --git a/libs/components/src/navigation/nav-item.component.ts b/libs/components/src/navigation/nav-item.component.ts index 53b181ec083..d4ef4855dd8 100644 --- a/libs/components/src/navigation/nav-item.component.ts +++ b/libs/components/src/navigation/nav-item.component.ts @@ -91,6 +91,18 @@ export class NavItemComponent extends NavBaseComponent { */ readonly ariaCurrentWhenActive = input("page"); + /** + * By default, a navigation will put the user's focus on the `main` element. + * + * If the user's focus should be moved to another element upon navigation end, pass a selector + * here (i.e. `#elementId`). + * + * Pass `false` to opt out of moving the focus entirely. Focus will stay on the nav item. + * + * See router-focus-manager.service for implementation of focus management + */ + readonly focusAfterNavTarget = input(); + /** * The design spec calls for the an outline to wrap the entire element when the template's * anchor/button has :focus-visible. Usually, we would use :focus-within for this. However, that diff --git a/libs/components/src/tabs/tab-nav-bar/tab-link.component.html b/libs/components/src/tabs/tab-nav-bar/tab-link.component.html index aa36eb37f99..932e2ce3b69 100644 --- a/libs/components/src/tabs/tab-nav-bar/tab-link.component.html +++ b/libs/components/src/tabs/tab-nav-bar/tab-link.component.html @@ -5,7 +5,7 @@ [routerLinkActiveOptions]="routerLinkMatchOptions" #rla="routerLinkActive" [active]="rla.isActive" - [state]="{ focusMainAfterNav: false }" + [state]="{ focusAfterNav: false }" [disabled]="disabled" [attr.aria-disabled]="disabled" ariaCurrentWhenActive="page" diff --git a/libs/vault/src/services/routed-vault-filter.service.ts b/libs/vault/src/services/routed-vault-filter.service.ts index 9005d507da7..e0d9f765361 100644 --- a/libs/vault/src/services/routed-vault-filter.service.ts +++ b/libs/vault/src/services/routed-vault-filter.service.ts @@ -82,7 +82,7 @@ export class RoutedVaultFilterService implements OnDestroy { }, queryParamsHandling: "merge", state: { - focusMainAfterNav: false, + focusAfterNav: false, }, }; return [commands, extras]; From bc23640176b9a79107f00dbab67475800c1a3775 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Fri, 20 Feb 2026 16:09:05 +0100 Subject: [PATCH 107/134] [CL] Document the start and end icon attributes (#19100) --- .../components/src/button/button.component.ts | 20 +++++++++++++++++-- libs/components/src/button/button.mdx | 15 +++++++------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/libs/components/src/button/button.component.ts b/libs/components/src/button/button.component.ts index 1055d134e53..4fcaaf6118e 100644 --- a/libs/components/src/button/button.component.ts +++ b/libs/components/src/button/button.component.ts @@ -1,4 +1,3 @@ -import { NgClass, NgTemplateOutlet } from "@angular/common"; import { input, HostBinding, @@ -72,7 +71,7 @@ const buttonStyles: Record = { selector: "button[bitButton], a[bitButton]", templateUrl: "button.component.html", providers: [{ provide: ButtonLikeAbstraction, useExisting: ButtonComponent }], - imports: [NgClass, NgTemplateOutlet, SpinnerComponent], + imports: [SpinnerComponent], hostDirectives: [AriaDisableDirective], }) export class ButtonComponent implements ButtonLikeAbstraction { @@ -124,14 +123,31 @@ export class ButtonComponent implements ButtonLikeAbstraction { return this.showLoadingStyle() || (this.disabledAttr() && this.loading() === false); }); + /** + * Style variant of the button. + */ readonly buttonType = input("secondary"); + /** + * Bitwarden icon displayed **before** the button label. + * Spacing between the icon and label is handled automatically. + */ readonly startIcon = input(undefined); + /** + * Bitwarden icon (`bwi-*`) displayed **after** the button label. + * Spacing between the label and icon is handled automatically. + */ readonly endIcon = input(undefined); + /** + * Size variant of the button. + */ readonly size = input("default"); + /** + * When `true`, the button expands to fill the full width of its container. + */ readonly block = input(false, { transform: booleanAttribute }); readonly loading = model(false); diff --git a/libs/components/src/button/button.mdx b/libs/components/src/button/button.mdx index 3080f6ffe4a..c74efcc58a2 100644 --- a/libs/components/src/button/button.mdx +++ b/libs/components/src/button/button.mdx @@ -80,21 +80,20 @@ where the width is fixed and the text wraps to 2 lines if exceeding the button ## With Icon -To ensure consistent icon spacing, the icon should have .5rem spacing on left or right(depending on -placement). +Use the `startIcon` and `endIcon` inputs to add a Bitwarden icon (`bwi-*`) before or after the +button label. Do not use a `` component inside the button as this may not have the correct +styling and spacing. -> NOTE: Use logical css properties to ensure LTR/RTL support. - -**If icon is placed before button label** +### Icon before the label ```html - + ``` -**If icon is placed after button label** +### Icon after the label ```html - + ``` From 1f69b96ed61d5ea770fe28f928ba80b43b066e39 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Fri, 20 Feb 2026 16:54:36 +0100 Subject: [PATCH 108/134] Add linting rule to detect when icons are used in buttons (#19104) * Add linting rule to detect when icons are used in buttons * Update docs for links * Add lint for link --- eslint.config.mjs | 1 + libs/components/src/link/link.mdx | 32 ++++-- libs/eslint/components/index.mjs | 2 + .../no-icon-children-in-bit-button.mjs | 74 ++++++++++++++ .../no-icon-children-in-bit-button.spec.mjs | 97 +++++++++++++++++++ 5 files changed, 199 insertions(+), 7 deletions(-) create mode 100644 libs/eslint/components/no-icon-children-in-bit-button.mjs create mode 100644 libs/eslint/components/no-icon-children-in-bit-button.spec.mjs diff --git a/eslint.config.mjs b/eslint.config.mjs index 974aaafeef6..2e35b011c73 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -208,6 +208,7 @@ export default tseslint.config( { ignoreIfHas: ["bitPasswordInputToggle"] }, ], "@bitwarden/components/no-bwi-class-usage": "warn", + "@bitwarden/components/no-icon-children-in-bit-button": "warn", }, }, diff --git a/libs/components/src/link/link.mdx b/libs/components/src/link/link.mdx index 4954effb6c0..51ec62f8787 100644 --- a/libs/components/src/link/link.mdx +++ b/libs/components/src/link/link.mdx @@ -1,4 +1,12 @@ -import { Meta, Story, Primary, Controls, Title, Description } from "@storybook/addon-docs/blocks"; +import { + Meta, + Story, + Canvas, + Primary, + Controls, + Title, + Description, +} from "@storybook/addon-docs/blocks"; import * as stories from "./link.stories"; @@ -33,15 +41,25 @@ You can use one of the following variants by providing it as the `linkType` inpu If you want to display a link with a smaller text size, apply the `tw-text-sm` class. This will match the `body2` variant of the Typography directive. -## With icons +## With Icon -Text Links/buttons can have icons on left or the right. +Use the `startIcon` and `endIcon` inputs to add a Bitwarden icon (`bwi-*`) before or after the link +label. Do not use a `` component inside the link as this may not have the correct styling +and spacing. -To indicate a new or add action, the icon on is used on the -left. +### Icon before the label -An angle icon, , is used on the left to indicate an expand to -show/hide additional content. +```html +Add item +``` + +### Icon after the label + +```html +Next +``` + + ## Accessibility diff --git a/libs/eslint/components/index.mjs b/libs/eslint/components/index.mjs index 101fdde414c..23116dc6958 100644 --- a/libs/eslint/components/index.mjs +++ b/libs/eslint/components/index.mjs @@ -1,11 +1,13 @@ import requireLabelOnBiticonbutton from "./require-label-on-biticonbutton.mjs"; import requireThemeColorsInSvg from "./require-theme-colors-in-svg.mjs"; import noBwiClassUsage from "./no-bwi-class-usage.mjs"; +import noIconChildrenInBitButton from "./no-icon-children-in-bit-button.mjs"; export default { rules: { "require-label-on-biticonbutton": requireLabelOnBiticonbutton, "require-theme-colors-in-svg": requireThemeColorsInSvg, "no-bwi-class-usage": noBwiClassUsage, + "no-icon-children-in-bit-button": noIconChildrenInBitButton, }, }; diff --git a/libs/eslint/components/no-icon-children-in-bit-button.mjs b/libs/eslint/components/no-icon-children-in-bit-button.mjs new file mode 100644 index 00000000000..926093f2e44 --- /dev/null +++ b/libs/eslint/components/no-icon-children-in-bit-button.mjs @@ -0,0 +1,74 @@ +export const errorMessage = + 'Avoid placing icon elements ( or ) inside a bitButton or bitLink. ' + + "Use the [startIcon] or [endIcon] inputs instead. " + + 'Example: '; + +export default { + meta: { + type: "suggestion", + docs: { + description: + "Discourage using icon child elements inside bitButton; use startIcon/endIcon inputs instead", + category: "Best Practices", + recommended: true, + }, + schema: [], + }, + create(context) { + return { + Element(node) { + if (node.name !== "button" && node.name !== "a") { + return; + } + + const allAttrNames = [ + ...(node.attributes?.map((attr) => attr.name) ?? []), + ...(node.inputs?.map((input) => input.name) ?? []), + ]; + + if (!allAttrNames.includes("bitButton") && !allAttrNames.includes("bitLink")) { + return; + } + + for (const child of node.children ?? []) { + if (!child.name) { + continue; + } + + // child + if (child.name === "bit-icon") { + context.report({ + node: child, + message: errorMessage, + }); + continue; + } + + // child with bwi class + if (child.name === "i") { + const classAttrs = [ + ...(child.attributes?.filter((attr) => attr.name === "class") ?? []), + ...(child.inputs?.filter((input) => input.name === "class") ?? []), + ]; + + for (const classAttr of classAttrs) { + const classValue = classAttr.value || ""; + + if (typeof classValue !== "string") { + continue; + } + + if (/\bbwi\b/.test(classValue)) { + context.report({ + node: child, + message: errorMessage, + }); + break; + } + } + } + } + }, + }; + }, +}; diff --git a/libs/eslint/components/no-icon-children-in-bit-button.spec.mjs b/libs/eslint/components/no-icon-children-in-bit-button.spec.mjs new file mode 100644 index 00000000000..656a320678d --- /dev/null +++ b/libs/eslint/components/no-icon-children-in-bit-button.spec.mjs @@ -0,0 +1,97 @@ +import { RuleTester } from "@typescript-eslint/rule-tester"; + +import rule, { errorMessage } from "./no-icon-children-in-bit-button.mjs"; + +const ruleTester = new RuleTester({ + languageOptions: { + parser: require("@angular-eslint/template-parser"), + }, +}); + +ruleTester.run("no-icon-children-in-bit-button", rule.default, { + valid: [ + { + name: "should allow bitButton with startIcon input", + code: ``, + }, + { + name: "should allow bitButton with endIcon input", + code: ``, + }, + { + name: "should allow a[bitButton] with startIcon input", + code: `Link`, + }, + { + name: "should allow with bwi inside a regular button (no bitButton)", + code: ``, + }, + { + name: "should allow inside a regular div", + code: `
    `, + }, + { + name: "should allow bitButton with only text content", + code: ``, + }, + { + name: "should allow without bwi class inside bitButton", + code: ``, + }, + { + name: "should allow bitLink with startIcon input", + code: `Link`, + }, + { + name: "should allow bitLink with only text content", + code: `Link`, + }, + ], + invalid: [ + { + name: "should warn on with bwi class inside button[bitButton]", + code: ``, + errors: [{ message: errorMessage }], + }, + { + name: "should warn on with bwi class and extra classes inside button[bitButton]", + code: ``, + errors: [{ message: errorMessage }], + }, + { + name: "should warn on with bwi class inside a[bitButton]", + code: ` Link`, + errors: [{ message: errorMessage }], + }, + { + name: "should warn on inside button[bitButton]", + code: ``, + errors: [{ message: errorMessage }], + }, + { + name: "should warn on inside a[bitButton]", + code: ` Copy`, + errors: [{ message: errorMessage }], + }, + { + name: "should warn on multiple icon children inside bitButton", + code: ``, + errors: [{ message: errorMessage }, { message: errorMessage }], + }, + { + name: "should warn on both and children", + code: ``, + errors: [{ message: errorMessage }, { message: errorMessage }], + }, + { + name: "should warn on with bwi class inside a[bitLink]", + code: ` Link`, + errors: [{ message: errorMessage }], + }, + { + name: "should warn on inside button[bitLink]", + code: ``, + errors: [{ message: errorMessage }], + }, + ], +}); From e82669b99969bbdbc0c815e530043d8ca79ab8d6 Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:08:39 +0100 Subject: [PATCH 109/134] Autosync the updated translations (#19095) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/web/src/locales/af/messages.json | 101 +++++++--- apps/web/src/locales/ar/messages.json | 101 +++++++--- apps/web/src/locales/az/messages.json | 101 +++++++--- apps/web/src/locales/be/messages.json | 101 +++++++--- apps/web/src/locales/bg/messages.json | 101 +++++++--- apps/web/src/locales/bn/messages.json | 101 +++++++--- apps/web/src/locales/bs/messages.json | 101 +++++++--- apps/web/src/locales/ca/messages.json | 101 +++++++--- apps/web/src/locales/cs/messages.json | 103 +++++++--- apps/web/src/locales/cy/messages.json | 101 +++++++--- apps/web/src/locales/da/messages.json | 101 +++++++--- apps/web/src/locales/de/messages.json | 109 ++++++++--- apps/web/src/locales/el/messages.json | 101 +++++++--- apps/web/src/locales/en_GB/messages.json | 101 +++++++--- apps/web/src/locales/en_IN/messages.json | 101 +++++++--- apps/web/src/locales/eo/messages.json | 101 +++++++--- apps/web/src/locales/es/messages.json | 101 +++++++--- apps/web/src/locales/et/messages.json | 101 +++++++--- apps/web/src/locales/eu/messages.json | 101 +++++++--- apps/web/src/locales/fa/messages.json | 101 +++++++--- apps/web/src/locales/fi/messages.json | 101 +++++++--- apps/web/src/locales/fil/messages.json | 101 +++++++--- apps/web/src/locales/fr/messages.json | 101 +++++++--- apps/web/src/locales/gl/messages.json | 101 +++++++--- apps/web/src/locales/he/messages.json | 101 +++++++--- apps/web/src/locales/hi/messages.json | 101 +++++++--- apps/web/src/locales/hr/messages.json | 101 +++++++--- apps/web/src/locales/hu/messages.json | 101 +++++++--- apps/web/src/locales/id/messages.json | 101 +++++++--- apps/web/src/locales/it/messages.json | 101 +++++++--- apps/web/src/locales/ja/messages.json | 101 +++++++--- apps/web/src/locales/ka/messages.json | 101 +++++++--- apps/web/src/locales/km/messages.json | 101 +++++++--- apps/web/src/locales/kn/messages.json | 101 +++++++--- apps/web/src/locales/ko/messages.json | 101 +++++++--- apps/web/src/locales/lv/messages.json | 113 ++++++++--- apps/web/src/locales/ml/messages.json | 101 +++++++--- apps/web/src/locales/mr/messages.json | 101 +++++++--- apps/web/src/locales/my/messages.json | 101 +++++++--- apps/web/src/locales/nb/messages.json | 101 +++++++--- apps/web/src/locales/ne/messages.json | 101 +++++++--- apps/web/src/locales/nl/messages.json | 101 +++++++--- apps/web/src/locales/nn/messages.json | 101 +++++++--- apps/web/src/locales/or/messages.json | 101 +++++++--- apps/web/src/locales/pl/messages.json | 101 +++++++--- apps/web/src/locales/pt_BR/messages.json | 229 ++++++++++++++--------- apps/web/src/locales/pt_PT/messages.json | 101 +++++++--- apps/web/src/locales/ro/messages.json | 101 +++++++--- apps/web/src/locales/ru/messages.json | 101 +++++++--- apps/web/src/locales/si/messages.json | 101 +++++++--- apps/web/src/locales/sk/messages.json | 101 +++++++--- apps/web/src/locales/sl/messages.json | 101 +++++++--- apps/web/src/locales/sr_CS/messages.json | 101 +++++++--- apps/web/src/locales/sr_CY/messages.json | 101 +++++++--- apps/web/src/locales/sv/messages.json | 101 +++++++--- apps/web/src/locales/ta/messages.json | 101 +++++++--- apps/web/src/locales/te/messages.json | 101 +++++++--- apps/web/src/locales/th/messages.json | 101 +++++++--- apps/web/src/locales/tr/messages.json | 101 +++++++--- apps/web/src/locales/uk/messages.json | 101 +++++++--- apps/web/src/locales/vi/messages.json | 101 +++++++--- apps/web/src/locales/zh_CN/messages.json | 113 ++++++++--- apps/web/src/locales/zh_TW/messages.json | 111 ++++++++--- 63 files changed, 4937 insertions(+), 1598 deletions(-) diff --git a/apps/web/src/locales/af/messages.json b/apps/web/src/locales/af/messages.json index eb983fc3512..72666452d86 100644 --- a/apps/web/src/locales/af/messages.json +++ b/apps/web/src/locales/af/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Gebruiker $ID$ gewysig.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Hierdie send is by verstek versteek. U kan sy sigbaarheid wissel deur die knop hier onder.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Laai aanhegsels af" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Persoonlike eienaarskap" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Ongeldige bevestigingskode" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/ar/messages.json b/apps/web/src/locales/ar/messages.json index 647c4602c17..67fb72af9f9 100644 --- a/apps/web/src/locales/ar/messages.json +++ b/apps/web/src/locales/ar/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "لا تعرف كلمة المرور؟ اطلب من المرسل كلمة المرور المطلوبة للوصول إلى هذا الإرسال.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "هذا الإرسال مخفي بشكل افتراضي. يمكنك تبديل الرؤية باستخدام الزر أدناه.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "تحميل المرفقات" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "رمز التحقق غير صالح" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index fec06600593..a97c11ea852 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "$ID$ istifadəçisinə düzəliş edildi.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Access Intelligence-niz yaradılır..." }, - "fetchingMemberData": { - "message": "Üzv veriləri alınır..." - }, - "analyzingPasswordHealth": { - "message": "Parol sağlamlığı analiz edirilir..." - }, - "calculatingRiskScores": { - "message": "Risk xalı hesablanır..." - }, - "generatingReportData": { - "message": "Hesabat veriləri yaradılır..." - }, - "savingReport": { - "message": "Hesabat saxlanılır..." - }, - "compilingInsights": { - "message": "Təhlillər şərh edilir..." - }, "loadingProgress": { "message": "İrəliləyiş yüklənir" }, - "thisMightTakeFewMinutes": { - "message": "Bu, bir neçə dəqiqə çəkə bilər." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Hesabatı işə sal" @@ -5849,10 +5855,6 @@ "message": "Parolu bilmirsiniz? Bu \"Send\"ə erişmək üçün parolu göndərən şəxsdən istəyin.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Bu \"send\" ilkin olaraq gizlidir. Aşağıdakı düyməni istifadə edərək görünməni dəyişdirə bilərsiniz.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Qoşmaları endir" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Bu riskləri və siyasət güncəlləmələrini qəbul edirəm" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Fərdi seyfi xaric et" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Yararsız doğrulama kodu" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domeni" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Birdən çox e-poçtu daxil edərkən vergül istifadə edin." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Yararsız Send parolu" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Şəxslər, Send-ə baxması üçün parolu daxil etməli olacaqlar", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "Ödəniş üsulunuzu güncəlləyərkən xəta baş verdi." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index 4d6fe96c6d2..91fce4817d0 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Карыстальнік $ID$ адрэдагаваны.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Не ведаеце пароль? Спытайце ў адпраўніка пароль, які неабходны для доступу да гэтага Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Прадвызначана гэты Send схаваны. Вы можаце змяніць яго бачнасць выкарыстоўваючы кнопку ніжэй.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Выдаліць асабістае сховішча" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Памылковы праверачны код" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index 7b5ee07efe1..484e6fd5a1b 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Потребител № $ID$ е редактиран.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Създаване на Вашия анализ на достъпа…" }, - "fetchingMemberData": { - "message": "Извличане на данните за членовете…" - }, - "analyzingPasswordHealth": { - "message": "Анализиране на състоянието на паролите…" - }, - "calculatingRiskScores": { - "message": "Изчисляване на оценките на риска…" - }, - "generatingReportData": { - "message": "Създаване на данните за доклада…" - }, - "savingReport": { - "message": "Запазване на доклада…" - }, - "compilingInsights": { - "message": "Събиране на подробности…" - }, "loadingProgress": { "message": "Зареждане на напредъка" }, - "thisMightTakeFewMinutes": { - "message": "Това може да отнеме няколко минути." + "reviewingMemberData": { + "message": "Преглеждане на данните за членовете…" + }, + "analyzingPasswords": { + "message": "Анализиране на паролите…" + }, + "calculatingRisks": { + "message": "Изчисляване на рисковете…" + }, + "generatingReports": { + "message": "Създаване на доклади…" + }, + "compilingInsightsProgress": { + "message": "Събиране на подробности…" + }, + "reportGenerationDone": { + "message": "Готово!" }, "riskInsightsRunReport": { "message": "Изпълнение на доклада" @@ -5849,10 +5855,6 @@ "message": "Ако не знаете паролата, поискайте от изпращача да ви я даде.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Стандартно изпращането е скрито. Може да промените това като натиснете бутона по-долу.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Сваляне на прикачените файлове" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Приемам тези рискове и промени в политиката" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Индивидуално притежание" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Грешен код за потвърждаване" }, + "invalidEmailOrVerificationCode": { + "message": "Грешна е-поща или код за потвърждаване" + }, "keyConnectorDomain": { "message": "Домейн на конектора за ключове" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Можете да въведете повече е-пощи, като ги разделите със запетая." }, + "emailsRequiredChangeAccessType": { + "message": "Потвърждаването на е-пощата изисква да е наличен поне един адрес на е-поща. Ако искате да премахнете всички е-пощи, променете начина за достъп по-горе." + }, "emailPlaceholder": { "message": "потребител@bitwarden.com , потребител@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Неправилна парола за Изпращане" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Хората ще трябва да въведат паролата, за да видят това Изпращане", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "Възникна грешка при обновяването на разплащателния метод." + }, + "sendPasswordInvalidAskOwner": { + "message": "Неправилна парола. Попитайте изпращача за паролата за достъп до това Изпращане.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "Това Изпращане изтича в $TIME$ на $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/bn/messages.json b/apps/web/src/locales/bn/messages.json index 9935cc538a1..3cc5f01c689 100644 --- a/apps/web/src/locales/bn/messages.json +++ b/apps/web/src/locales/bn/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/bs/messages.json b/apps/web/src/locales/bs/messages.json index dda9249b383..effcfd3062b 100644 --- a/apps/web/src/locales/bs/messages.json +++ b/apps/web/src/locales/bs/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index 642ae65228e..52a0f9cdd44 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "S'ha editat l'usuari $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "No sabeu la contrasenya? Demaneu al remitent la contrasenya necessària per accedir a aquest Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Aquest Send està ocult per defecte. Podeu canviar la seua visibilitat mitjançant el botó següent.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Baixa els fitxers adjunts" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Suprimeix la caixa forta individual" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Codi de verificació no vàlid" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index ae41eeebfef..300b1b583b7 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automaticky potvrzený uživatel $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Byl upraven uživatel $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generování Vaší přístupové inteligence..." }, - "fetchingMemberData": { - "message": "Načítání dat člena..." - }, - "analyzingPasswordHealth": { - "message": "Analyzování zdraví hesla..." - }, - "calculatingRiskScores": { - "message": "Výpočet skóre rizik..." - }, - "generatingReportData": { - "message": "Generování dat hlášení..." - }, - "savingReport": { - "message": "Ukládání hlášení..." - }, - "compilingInsights": { - "message": "Sestavování přehledů..." - }, "loadingProgress": { "message": "Průběh načítání" }, - "thisMightTakeFewMinutes": { - "message": "Může to trvat několik minut." + "reviewingMemberData": { + "message": "Přezkoumávání dat členů..." + }, + "analyzingPasswords": { + "message": "Analyzování hesel..." + }, + "calculatingRisks": { + "message": "Výpočet rizik..." + }, + "generatingReports": { + "message": "Generování zpráv..." + }, + "compilingInsightsProgress": { + "message": "Sestavování přehledů..." + }, + "reportGenerationDone": { + "message": "Hotovo!" }, "riskInsightsRunReport": { "message": "Spustit hlášení" @@ -5849,10 +5855,6 @@ "message": "Neznáte heslo? Požádejte odesílatele o heslo potřebné pro přístup k tomuto Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Tento Send je ve výchozím nastavení skrytý. Viditelnost můžete přepnout pomocí tlačítka níže.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Stáhnout přílohy" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Přijímám tato rizika a aktualizace zásad" }, + "autoConfirmEnabledByAdmin": { + "message": "Zapnuto Automatické potvrzování uživatele" + }, + "autoConfirmDisabledByAdmin": { + "message": "Vypnuto Automatické potvrzování uživatele" + }, + "autoConfirmEnabledByPortal": { + "message": "Přidána zásada automatického potvrzování uživatele" + }, + "autoConfirmDisabledByPortal": { + "message": "Odebrána zásada automatického potvrzování uživatele" + }, + "system": { + "message": "Systém" + }, "personalOwnership": { "message": "Odebrat osobní trezor" }, @@ -6757,7 +6774,7 @@ } }, "bulkResendInvitations": { - "message": "Try sending again" + "message": "Zkusit odeslat znovu" }, "bulkRemovedMessage": { "message": "Úspěšně odebráno" @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Neplatný ověřovací kód" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Doména Key Connectoru" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Zadejte více e-mailů oddělených čárkou." }, + "emailsRequiredChangeAccessType": { + "message": "Ověření e-mailu vyžaduje alespoň jednu e-mailovou adresu. Chcete-li odebrat všechny emaily, změňte výše uvedený typ přístupu." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Neplatné heslo k Send" }, + "vaultWelcomeDialogTitle": { + "message": "Jste u nás! Vítejte v Bitwardenu" + }, + "vaultWelcomeDialogDescription": { + "message": "Uložte všechna Vaše hesla a osobní informace v trezoru Bitwarden. Provedeme Vás tady." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Zahájit prohlídku" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Přeskočit" + }, "sendPasswordHelperText": { "message": "Pro zobrazení tohoto Send budou muset jednotlivci zadat heslo", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "Při aktualizaci Vaší platební metody došlo k chybě." + }, + "sendPasswordInvalidAskOwner": { + "message": "Neplatné heslo. Požádejte odesílatele o heslo potřebné pro přístup k tomuto Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "Tento Send vyprší v $TIME$ dne $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/cy/messages.json b/apps/web/src/locales/cy/messages.json index 10639a60017..9fd6bb263ed 100644 --- a/apps/web/src/locales/cy/messages.json +++ b/apps/web/src/locales/cy/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index 2943127dbb8..1f89b9f62e5 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Redigerede bruger $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Kender ikke adgangskoden? Bed afsenderen om adgangskoden, der kræves for at tilgå denne Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Denne Send er som standard skjult. Dens synlighed kan ændres vha. knappen nedenfor.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download vedhæftninger" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Fjern individuel boks" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Ugyldig bekræftelseskode" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index a89e10c8d45..e76dc238b5e 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -269,7 +269,7 @@ } }, "numCriticalApplicationsMarkedSuccess": { - "message": "$COUNT$ applications marked critical", + "message": "$COUNT$ Anwendungen als kritisch markiert", "placeholders": { "count": { "content": "$1", @@ -278,7 +278,7 @@ } }, "numApplicationsUnmarkedCriticalSuccess": { - "message": "$COUNT$ applications marked not critical", + "message": "$COUNT$ Anwendungen als nicht-kritisch markiert", "placeholders": { "count": { "content": "$1", @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Benutzer $ID$ bearbeitet.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Deine Access Intelligence wird generiert..." }, - "fetchingMemberData": { - "message": "Mitgliedsdaten werden abgerufen..." - }, - "analyzingPasswordHealth": { - "message": "Passwortsicherheit wird analysiert..." - }, - "calculatingRiskScores": { - "message": "Risikobewertung wird berechnet..." - }, - "generatingReportData": { - "message": "Berichtsdaten werden generiert..." - }, - "savingReport": { - "message": "Bericht wird gespeichert..." - }, - "compilingInsights": { - "message": "Analyse wird zusammengestellt..." - }, "loadingProgress": { "message": "Ladefortschritt" }, - "thisMightTakeFewMinutes": { - "message": "Dies kann einige Minuten dauern." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Passwörter werden analysiert..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Berichte werden erstellt ..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Fertig!" }, "riskInsightsRunReport": { "message": "Bericht ausführen" @@ -5849,10 +5855,6 @@ "message": "Du kennst das Passwort nicht? Frage den Absender nach dem Passwort, das für dieses Send benötigt wird.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Dieses Send ist standardmäßig ausgeblendet. Du kannst die Sichtbarkeit mit dem Button unten umschalten.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Anhänge herunterladen" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Ich akzeptiere diese Risiken und Richtlinien-Aktualisierungen" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Persönlichen Tresor entfernen" }, @@ -6439,7 +6456,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "verifyYourEmailToViewThisSend": { - "message": "Verify your email to view this Send", + "message": "Verifiziere deine E-Mail, um dieses Send anzuzeigen", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "viewSendHiddenEmailWarning": { @@ -6687,7 +6704,7 @@ } }, "reinviteSuccessToast": { - "message": "1 invitation sent" + "message": "1 Einladung gesendet" }, "bulkReinviteSentToast": { "message": "$COUNT$ invitations sent", @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Ungültiger Verifizierungscode" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector-Domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Gib mehrere E-Mail-Adressen ein, indem du sie mit einem Komma trennst." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "benutzer@bitwarden.com, benutzer@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Ungültiges Send-Passwort" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Personen müssen das Passwort eingeben, um dieses Send anzusehen", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "Beim Aktualisieren deiner Zahlungsmethode ist ein Fehler aufgetreten." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/el/messages.json b/apps/web/src/locales/el/messages.json index e084687382a..9700ec80b68 100644 --- a/apps/web/src/locales/el/messages.json +++ b/apps/web/src/locales/el/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Επεξεργασία χρήστη $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Δεν γνωρίζετε τον κωδικό; Ζητήστε από τον αποστολέα τον κωδικό που απαιτείται για την πρόσβαση σε αυτό το Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Αυτό το send είναι κρυμμένο από προεπιλογή. Μπορείτε να αλλάξετε την ορατότητά του χρησιμοποιώντας το παρακάτω κουμπί.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Λήψη συνημμένων" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Προσωπική Ιδιοκτησία" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Μη έγκυρος κωδικός επαλήθευσης" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index 78242ac88dd..3920f2d2be6 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analysing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analysing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/en_IN/messages.json b/apps/web/src/locales/en_IN/messages.json index 5d1bf31a336..f9b75b283c3 100644 --- a/apps/web/src/locales/en_IN/messages.json +++ b/apps/web/src/locales/en_IN/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analysing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analysing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the Sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Personal Ownership" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/eo/messages.json b/apps/web/src/locales/eo/messages.json index 1c9f0473adf..4b39004d896 100644 --- a/apps/web/src/locales/eo/messages.json +++ b/apps/web/src/locales/eo/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Redaktiĝis uzanto $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Ĉu vi ne scias la pasvorton? Petu al la Sendinto la pasvorton bezonatan por aliri ĉi tiun Sendon.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Ĉi tiu sendado estas kaŝita defaŭlte. Vi povas ŝalti ĝian videblecon per la suba butono.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Persona Posedo" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/es/messages.json b/apps/web/src/locales/es/messages.json index 7e8f62b6a3c..d3b884663dd 100644 --- a/apps/web/src/locales/es/messages.json +++ b/apps/web/src/locales/es/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Usuario $ID$ editado.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "¿No conoce la contraseña? Pídele al remitente la contraseña necesaria para acceder a este enviar.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Este Send está oculto por defecto. Puede cambiar su visibilidad usando el botón de abajo.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Descargar archivos adjuntos" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Propiedad personal" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Código de verificación no válido" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/et/messages.json b/apps/web/src/locales/et/messages.json index e101d49d3cf..c312f096f1b 100644 --- a/apps/web/src/locales/et/messages.json +++ b/apps/web/src/locales/et/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Muutis kasutajat $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Sa ei tea parooli? Küsi seda konkreetse Sendi saatjalt.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "See Send on vaikeseades peidetud. Saad selle nähtavust alloleva nupu abil seadistada.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Lae manused alla" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Personaalne salvestamine" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Vale kinnituskood" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/eu/messages.json b/apps/web/src/locales/eu/messages.json index 75f55a0d0f0..aa8a65b1141 100644 --- a/apps/web/src/locales/eu/messages.json +++ b/apps/web/src/locales/eu/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "$ID$ erabiltzailea editatua.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Ez duzu pasahitza ezagutzen? Eskatu bidaltzaileari Send honetara sartzeko behar den pasahitza.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Send hau modu lehenetsian ezkutatuta dago. Beheko botoia sakatuz alda dezakezu ikusgarritasuna.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Ezabatu kutxa gotor pertsonala" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Egiaztatze-kodea ez da baliozkoa" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/fa/messages.json b/apps/web/src/locales/fa/messages.json index d1efb2a239f..215af9c7512 100644 --- a/apps/web/src/locales/fa/messages.json +++ b/apps/web/src/locales/fa/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "کاربر $ID$ ویرایش شد.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "کلمه عبور را نمی‌دانید؟ از فرستنده کلمه عبور لازم را برای دسترسی به این ارسال بخواهید.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "این ارسال به طور پیش‌فرض پنهان است. با استفاده از دکمه زیر می‌توانید نمایان بودن آن را تغییر دهید.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "بارگیری پیوست‌ها" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "حذف گاوصندوق شخصی" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "کد تأیید نامعتبر است" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "دامنه رابط کلید" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/fi/messages.json b/apps/web/src/locales/fi/messages.json index 4c6b3ef62d8..ff37dcbdfb1 100644 --- a/apps/web/src/locales/fi/messages.json +++ b/apps/web/src/locales/fi/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Muokkasi käyttäjää \"$ID$\".", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Etkö tiedä salasanaa? Pyydä lähettäjältä tämän Sendin avaukseen tarvittavaa salasanaa.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Send on oletusarvoisesti piilotettu. Voit vaihtaa sen näkyvyyttä alla olevalla painikkeella.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Lataa liitteet" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Poista yksityinen holvi" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Virheellinen todennuskoodi" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/fil/messages.json b/apps/web/src/locales/fil/messages.json index e94c5b4fea6..85e4d95320e 100644 --- a/apps/web/src/locales/fil/messages.json +++ b/apps/web/src/locales/fil/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Na-edit ang user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Hindi mo ba alam ang password Itanong sa nagpadala ang password na kailangan para ma-access ang Padala na ito.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Ang Send na ito ay nakatago bilang default. Maaari mong i toggle ang visibility nito gamit ang pindutan sa ibaba.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Alisin ang indibidwal na vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Maling verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index e4183ff5692..09c5fe03d34 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Utilisateur $ID$ automatiquement confirmé.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Utilisateur $ID$ modifié.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Génération de votre Intelligence d'Accès..." }, - "fetchingMemberData": { - "message": "Récupération des données des membres..." - }, - "analyzingPasswordHealth": { - "message": "Analyse de la santé du mot de passe..." - }, - "calculatingRiskScores": { - "message": "Calcul des niveaux de risque..." - }, - "generatingReportData": { - "message": "Génération des données du rapport..." - }, - "savingReport": { - "message": "Enregistrement du rapport..." - }, - "compilingInsights": { - "message": "Compilation des aperçus..." - }, "loadingProgress": { "message": "Chargement de la progression" }, - "thisMightTakeFewMinutes": { - "message": "Cela peut prendre quelques minutes." + "reviewingMemberData": { + "message": "Révision des données du membre..." + }, + "analyzingPasswords": { + "message": "Analyse des mots de passe..." + }, + "calculatingRisks": { + "message": "Calcul des risques..." + }, + "generatingReports": { + "message": "Génération du rapport..." + }, + "compilingInsightsProgress": { + "message": "Compilation des observations..." + }, + "reportGenerationDone": { + "message": "Fini !" }, "riskInsightsRunReport": { "message": "Exécuter le rapport" @@ -5849,10 +5855,6 @@ "message": "Vous ne connaissez pas le mot de passe ? Demandez à l'expéditeur le mot de passe nécessaire pour accéder à ce Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Ce Send est masqué par défaut. Vous pouvez changer sa visibilité en utilisant le bouton ci-dessous.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Télécharger les pièces jointes" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "J'accepte ces risques et mises à jour de la politique de sécurité" }, + "autoConfirmEnabledByAdmin": { + "message": "Activé le paramètre de confirmation automatique de l'utilisateur" + }, + "autoConfirmDisabledByAdmin": { + "message": "Désactivé le paramètre de confirmation automatique de l'utilisateur" + }, + "autoConfirmEnabledByPortal": { + "message": "Ajout de la politique de sécurité de confirmation automatique de l'utilisateur" + }, + "autoConfirmDisabledByPortal": { + "message": "Politique de sécurité de confirmation automatique de l'utilisateur retirée" + }, + "system": { + "message": "Système" + }, "personalOwnership": { "message": "Supprimer le coffre individuel" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Code de vérification invalide" }, + "invalidEmailOrVerificationCode": { + "message": "Courriel ou code de vérification invalide" + }, "keyConnectorDomain": { "message": "Domaine Key Connector" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Entrez plusieurs courriels en les séparant avec une virgule." }, + "emailsRequiredChangeAccessType": { + "message": "La vérification de courriel requiert au moins une adresse courriel. Pour retirer tous les courriels, changez le type d'accès ci-dessus." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Mot de passe Send invalide" }, + "vaultWelcomeDialogTitle": { + "message": "Vous y êtes! Bienvenue sur Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Enregistrez tous vos mots de passe et vos informations personnelles dans votre coffre de Bitwarden. Nous vous ferons faire la visite." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Commencer la visite" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Ignorer" + }, "sendPasswordHelperText": { "message": "Les personnes devront entrer le mot de passe pour afficher ce Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "Une erreur s'est produite lors de la mise à jour de votre mode de paiement." + }, + "sendPasswordInvalidAskOwner": { + "message": "Mot de passe invalide. Demandez à l'expéditeur le mot de passe nécessaire pour accéder à ce Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "Ce Send expire à $TIME$ le $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/gl/messages.json b/apps/web/src/locales/gl/messages.json index de6c7782c6d..dccfaa04c64 100644 --- a/apps/web/src/locales/gl/messages.json +++ b/apps/web/src/locales/gl/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/he/messages.json b/apps/web/src/locales/he/messages.json index 8829ed90e65..1dcbb3addcf 100644 --- a/apps/web/src/locales/he/messages.json +++ b/apps/web/src/locales/he/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "משתמש שנערך $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "הרץ דוח" @@ -5849,10 +5855,6 @@ "message": "לא יודע את הסיסמה? בקש מהשולח את הסיסמה הדרושה עבור סֵנְד זה.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "סֵנְד זה מוסתר כברירת מחדל. אתה יכול לשנות את מצב הנראות שלו באמצעות הלחצן למטה.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "הורד צרופות" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "אני מסכים לסיכונים ועדכוני מדיניות אלה" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "הסר כספת אישית" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "קוד אימות שגוי" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "דומיין של Key Connector" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/hi/messages.json b/apps/web/src/locales/hi/messages.json index 41d32fb9587..3ed164386a1 100644 --- a/apps/web/src/locales/hi/messages.json +++ b/apps/web/src/locales/hi/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/hr/messages.json b/apps/web/src/locales/hr/messages.json index 41eac503c59..7a7135cd2b2 100644 --- a/apps/web/src/locales/hr/messages.json +++ b/apps/web/src/locales/hr/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Uređen korisnik $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generiranje tvoje pristupne inteligencije..." }, - "fetchingMemberData": { - "message": "Dohvaćanje podataka o članu…" - }, - "analyzingPasswordHealth": { - "message": "Analiziranje zdravlja lozinke…" - }, - "calculatingRiskScores": { - "message": "Izračun ocjene rizika…" - }, - "generatingReportData": { - "message": "Generiranje izvješća…" - }, - "savingReport": { - "message": "Spremanje izvještaja…" - }, - "compilingInsights": { - "message": "Sastavljanje uvida…" - }, "loadingProgress": { "message": "Učitavanje napretka" }, - "thisMightTakeFewMinutes": { - "message": "Ovo može potrajati nekoliko minuta." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Pokreni izvješće" @@ -5849,10 +5855,6 @@ "message": "Ne znaš lozinku? Upitaj pošiljatelja za lozinku za pristup ovom Sendu.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Ovaj je Send zadano skriven. Moguće mu je promijeniti vidljivost.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Preuzmi privitke" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Prihavaćam ove rizike i ažurirana pravila" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Ukloni osobni trezor" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Nevažeći kôd za provjeru" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Domena konektora ključa" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index 9b8a98e5625..f6f580b7120 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "$ID$ felhasználó automatikusan megerősítésre került.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "$ID$ azonosítójú felhasználó módosításra került.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Hozzáférési intelligencia generálása..." }, - "fetchingMemberData": { - "message": "Tagi adatok lekérése..." - }, - "analyzingPasswordHealth": { - "message": "A jelszó állapot elemzése..." - }, - "calculatingRiskScores": { - "message": "Kockázati pontszámok kiszámítása..." - }, - "generatingReportData": { - "message": "Jelentés adatok generálása..." - }, - "savingReport": { - "message": "Jelentés mentése..." - }, - "compilingInsights": { - "message": "Betekintések összeállítása..." - }, "loadingProgress": { "message": "Feldolgozás betöltése" }, - "thisMightTakeFewMinutes": { - "message": "Ez eltarthat pár percig." + "reviewingMemberData": { + "message": "Tagi adatok lekérése..." + }, + "analyzingPasswords": { + "message": "A jelszavak elemzése..." + }, + "calculatingRisks": { + "message": "A kockázatok kiszámítása..." + }, + "generatingReports": { + "message": "Jelentések generálása..." + }, + "compilingInsightsProgress": { + "message": "Betekintések összeállítása..." + }, + "reportGenerationDone": { + "message": "Kész!" }, "riskInsightsRunReport": { "message": "Jelentés futtatása" @@ -5849,10 +5855,6 @@ "message": "Nem ismerjük a jelszót? Kérdezzünk rá a küldőnél a Send elérésére szükséges jelszóért.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Ez a Send alapértelmezésben rejtett. Az alábbi gombbal átváltható a láthatósága.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Mellékletek letöltése" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Elfogadom ezeket a kockázatokat és a szabályzat frissítéseit." }, + "autoConfirmEnabledByAdmin": { + "message": "Az automatikus felhasználó megerősítés beállítás bekapcsolásra került." + }, + "autoConfirmDisabledByAdmin": { + "message": "Az automatikus felhasználó megerősítés beállítás kikapcsolásra került." + }, + "autoConfirmEnabledByPortal": { + "message": "Az automatikus felhasználó megerősítés rendszabály hozzáadásra került." + }, + "autoConfirmDisabledByPortal": { + "message": "Az automatikus felhasználó megerősítés rendszabály eltávolításra került." + }, + "system": { + "message": "Rendszer" + }, "personalOwnership": { "message": "Személyes tulajdon" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Érvénytelen ellenőrző kód" }, + "invalidEmailOrVerificationCode": { + "message": "Az email cím vagy az ellenőrző kód érvénytelen." + }, "keyConnectorDomain": { "message": "Key Connector tartomány" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Írjunk be több email címet vesszővel elválasztva." }, + "emailsRequiredChangeAccessType": { + "message": "Az email cím ellenőrzéshez legalább egy email cím szükséges. Az összes email cím eltávolításához módosítsuk a fenti hozzáférési típust." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Érvénytelen a Send jelszó." }, + "vaultWelcomeDialogTitle": { + "message": "Megérkeztünk! Üdvözlet a Bitwardenben" + }, + "vaultWelcomeDialogDescription": { + "message": "Az összes jelszó és személyes adat tárolása a Bitwarden trezorban. Nézzünk körbe." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Túra elkezdése" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Kihagyás" + }, "sendPasswordHelperText": { "message": "A személyeknek meg kell adniuk a jelszót a Send elem megtekintéséhez.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "Hiba történt a fizetési mód frissítésekor." + }, + "sendPasswordInvalidAskOwner": { + "message": "A jelszó érvénytelen. Kérjük el a feladótól a Send elem eléréséhez szükséges jelszót.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "A Send elem lejár: $DATE$ - $TIME$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/id/messages.json b/apps/web/src/locales/id/messages.json index 30746054e41..fb6a4908684 100644 --- a/apps/web/src/locales/id/messages.json +++ b/apps/web/src/locales/id/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "$ID$ telah diedit.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Jalankan laporan" @@ -5849,10 +5855,6 @@ "message": "Tidak tahu sandinya? Tanyakan pengirim untuk sandi yang diperlukan untuk mengakses Kirim ini.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Pengiriman ini disembunyikan secara default. Anda dapat mengubah visibilitasnya menggunakan tombol di bawah ini.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Unduh lampiran" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Saya menerima risiko dan pembaruan kebijakan ini" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Kepemilikan Pribadi" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Kode verifikasi tidak valid" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/it/messages.json b/apps/web/src/locales/it/messages.json index 1e89c1624f6..f53262992fe 100644 --- a/apps/web/src/locales/it/messages.json +++ b/apps/web/src/locales/it/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Utente $ID$ modificato.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generazione del tuo Access Intelligence..." }, - "fetchingMemberData": { - "message": "Recupero dei dati dei membri..." - }, - "analyzingPasswordHealth": { - "message": "Analisi della salute della password..." - }, - "calculatingRiskScores": { - "message": "Calcolo dei punteggi di rischio..." - }, - "generatingReportData": { - "message": "Generazione dati del rapporto..." - }, - "savingReport": { - "message": "Salvataggio..." - }, - "compilingInsights": { - "message": "Compilazione dei dati..." - }, "loadingProgress": { "message": "Caricamento in corso" }, - "thisMightTakeFewMinutes": { - "message": "Attendi..." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Avvia report" @@ -5849,10 +5855,6 @@ "message": "Non conosci la password? Chiedi al mittente la password necessaria per accesso a questo Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Questo Send è nascosto per impostazione predefinita. Modifica la sua visibilità usando questo pulsante.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Scarica allegati" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Accetto questi rischi e aggiornamenti sulle politiche" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Rimuovi cassaforte individuale" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Codice di verifica non valido" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Dominio Key Connector" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Inserisci più indirizzi email separandoli con virgole." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Password del Send non valida" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index d86a7dce648..2b620ed1114 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "ユーザー「$ID$」の編集", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "パスワードがわかりませんか?このSendにアクセスするには送信者にパスワードをご確認ください。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "このSendはデフォルトでは非表示になっています。下のボタンで表示・非表示が切り替え可能です。", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "添付ファイルをダウンロード" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "個別の保管庫を削除" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "認証コードが間違っています" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/ka/messages.json b/apps/web/src/locales/ka/messages.json index 915ca5d6cba..3e527d955f3 100644 --- a/apps/web/src/locales/ka/messages.json +++ b/apps/web/src/locales/ka/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/km/messages.json b/apps/web/src/locales/km/messages.json index bc0e3b74cb6..d615154225d 100644 --- a/apps/web/src/locales/km/messages.json +++ b/apps/web/src/locales/km/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/kn/messages.json b/apps/web/src/locales/kn/messages.json index bd77a975193..51ed6353e01 100644 --- a/apps/web/src/locales/kn/messages.json +++ b/apps/web/src/locales/kn/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "ತಿದ್ಸಿದ ಬಳಕೆದಾರ $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "ಪಾಸ್ವರ್ಡ್ ತಿಳಿದಿಲ್ಲವೇ? ಈ ಕಳುಹಿಸುವಿಕೆಯನ್ನು ಪ್ರವೇಶಿಸಲು ಅಗತ್ಯವಿರುವ ಪಾಸ್‌ವರ್ಡ್‌ಗಾಗಿ ಕಳುಹಿಸುವವರನ್ನು ಕೇಳಿ.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "ಈ ಕಳುಹಿಸುವಿಕೆಯನ್ನು ಪೂರ್ವನಿಯೋಜಿತವಾಗಿ ಮರೆಮಾಡಲಾಗಿದೆ. ಕೆಳಗಿನ ಬಟನ್ ಬಳಸಿ ನೀವು ಅದರ ಗೋಚರತೆಯನ್ನು ಟಾಗಲ್ ಮಾಡಬಹುದು.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "ವೈಯಕ್ತಿಕ ಮಾಲೀಕತ್ವ" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/ko/messages.json b/apps/web/src/locales/ko/messages.json index 0af228dd821..a07134231bb 100644 --- a/apps/web/src/locales/ko/messages.json +++ b/apps/web/src/locales/ko/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "$ID$ 사용자를 편집했습니다.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "비밀번호를 모르시나요? 보낸 사람에게 Send에 접근할 수 있는 비밀번호를 요청하세요.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "이 Send는 기본적으로 숨겨져 있습니다. 아래의 버튼을 눌러 공개 여부를 전환할 수 있습니다.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "첨부 파일 다운로드" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "개인 소유권" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "유효하지 않은 확인 코드" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index f12dda40ea3..e50b76d1a4a 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -3339,13 +3339,13 @@ "message": "Abonements tika atjaunots." }, "resubscribe": { - "message": "Resubscribe" + "message": "Atsākt abonēšanu" }, "yourSubscriptionIsExpired": { - "message": "Your subscription is expired" + "message": "Abonements ir beidzies" }, "yourSubscriptionIsCanceled": { - "message": "Your subscription is canceled" + "message": "Abonements ir atcelts" }, "cancelConfirmation": { "message": "Vai tiešām atcelt? Tiks zaudēta piekļuve visām abonementa iespējām pēc pašreizējā norēķinu laika posma beigām." @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Labots lietotājs $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Izveido informāciju par Tavu piekļuvi…" }, - "fetchingMemberData": { - "message": "Iegūst dalībnieku datus…" - }, - "analyzingPasswordHealth": { - "message": "Izvērtē paroļu veselību…" - }, - "calculatingRiskScores": { - "message": "Aprēķina risku novērtējumu…" - }, - "generatingReportData": { - "message": "Izveido atskaites datus…" - }, - "savingReport": { - "message": "Saglabā atskaiti…" - }, - "compilingInsights": { - "message": "Apkopo ieskatus…" - }, "loadingProgress": { "message": "Ielādē virzību" }, - "thisMightTakeFewMinutes": { - "message": "Tas var aizņemt dažas minūtes." + "reviewingMemberData": { + "message": "Pārskata dalībnieku datus…" + }, + "analyzingPasswords": { + "message": "Izvērtē paroles…" + }, + "calculatingRisks": { + "message": "Aprēķina riskus…" + }, + "generatingReports": { + "message": "Izveido pārskatus…" + }, + "compilingInsightsProgress": { + "message": "Apkopo ieskatus…" + }, + "reportGenerationDone": { + "message": "Gatavs." }, "riskInsightsRunReport": { "message": "Izveidot atskaiti" @@ -5849,10 +5855,6 @@ "message": "Nezini paroli? Vaicā sūtītājam paroli, kas ir nepieciešama, lai piekļūtu šim Send!", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Šis Send ir paslēpts pēc noklusējuma. Tā redzamību var pārslēgt ar zemāk esošo pogu.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Lejupielādēt pielikumus" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Es pieņemu šos riskus un pamatnostādnes atjauninājumus" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Personīgās īpašumtiesības" }, @@ -6439,7 +6456,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "verifyYourEmailToViewThisSend": { - "message": "Verify your email to view this Send", + "message": "Jāapliecina sava e-pasta adrese, lai apskatītu šo Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "viewSendHiddenEmailWarning": { @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Nederīgs apliecinājuma kods" }, + "invalidEmailOrVerificationCode": { + "message": "Nederīga e-pasta adrese vai apliecinājuma kods" + }, "keyConnectorDomain": { "message": "Key Connector domēns" }, @@ -11910,10 +11930,10 @@ "message": "Šeit parādīsies arhivētie vienumi, un tie netiks iekļauti vispārējās meklēšanas iznākumos un automātiskās aizpildes ieteikumos." }, "itemArchiveToast": { - "message": "Item archived" + "message": "Vienums ievietots arhīvā" }, "itemUnarchivedToast": { - "message": "Item unarchived" + "message": "Vienums izņemts no arhīva" }, "bulkArchiveItems": { "message": "Vienumi tika arhivēti" @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "E-pasta apliecināšanai ir nepieciešama vismaz viena e-pasta adrese. Lai noņemtu visas e-pasta adreses, augstāk jānomaina piekļūšanas veids." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Cilvēkiem būs jāievada parole, lai apskatītu šo Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Nederīga parole. Jāvaicā nepieciešamā parole nosūtītājam, lai piekļūtu šim Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "Šī Send derīgums beigsies $DATE$ plkst. $TIME$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/ml/messages.json b/apps/web/src/locales/ml/messages.json index e68e7d25b85..d7e686fba73 100644 --- a/apps/web/src/locales/ml/messages.json +++ b/apps/web/src/locales/ml/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "പാസ്‌വേഡ് അറിയില്ലേ? ഈ അയയ്‌ക്കൽ ആക്‌സസ് ചെയ്യുന്നതിന് ആവശ്യമായ പാസ്‌വേഡിനായി അയച്ചയാളോട് ചോദിക്കുക.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "ഈ Send സ്ഥിരസ്ഥിതിയായി മറച്ചിരിക്കുന്നു. ചുവടെയുള്ള ബട്ടൺ ഉപയോഗിച്ചാൽ നിങ്ങൾക്ക് അതിന്റെ ദൃശ്യപരത ടോഗിൾ ചെയ്യാൻ കഴിയും.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "വ്യക്തിഗത ഉടമസ്ഥാവകാശം" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/mr/messages.json b/apps/web/src/locales/mr/messages.json index 2abd88c1169..4f030d2368d 100644 --- a/apps/web/src/locales/mr/messages.json +++ b/apps/web/src/locales/mr/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/my/messages.json b/apps/web/src/locales/my/messages.json index bc0e3b74cb6..d615154225d 100644 --- a/apps/web/src/locales/my/messages.json +++ b/apps/web/src/locales/my/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/nb/messages.json b/apps/web/src/locales/nb/messages.json index de97b70a119..103a439220e 100644 --- a/apps/web/src/locales/nb/messages.json +++ b/apps/web/src/locales/nb/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Redigerte brukeren $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Vet du ikke passordet? Be avsender om nødvendig tilgang til denne Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Denne Send-en er skjult som standard. Du kan veksle synlighet ved å bruke knappen nedenfor.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Personlig eierskap" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Ugyldig bekreftelseskode" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/ne/messages.json b/apps/web/src/locales/ne/messages.json index 26c942795b6..dbf4643236d 100644 --- a/apps/web/src/locales/ne/messages.json +++ b/apps/web/src/locales/ne/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index 2748009dba8..023a1b775a1 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatisch bevestigde gebruiker $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Gebruiker gewijzigd $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Je toegangsinlichtingen genereren..." }, - "fetchingMemberData": { - "message": "Ledengegevens ophalen..." - }, - "analyzingPasswordHealth": { - "message": "Wachtwoordgezondheid analyseren..." - }, - "calculatingRiskScores": { - "message": "Risicoscores berekenen..." - }, - "generatingReportData": { - "message": "Rapportgegevens genereren..." - }, - "savingReport": { - "message": "Rapport opslaan..." - }, - "compilingInsights": { - "message": "Inzichten compileren..." - }, "loadingProgress": { "message": "Voortgang laden" }, - "thisMightTakeFewMinutes": { - "message": "Dit kan een paar minuten duren." + "reviewingMemberData": { + "message": "Ledengegevens controleren..." + }, + "analyzingPasswords": { + "message": "Wachtwoorden analyseren..." + }, + "calculatingRisks": { + "message": "Risicoscores berekenen..." + }, + "generatingReports": { + "message": "Rapporteren genereren..." + }, + "compilingInsightsProgress": { + "message": "Inzichten compileren..." + }, + "reportGenerationDone": { + "message": "Klaar!" }, "riskInsightsRunReport": { "message": "Rapport uitvoeren" @@ -5849,10 +5855,6 @@ "message": "Weet je het wachtwoord niet? Vraag de afzender om het wachtwoord om toegang te krijgen tot deze Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Deze Send is standaard verborgen. Je kunt de zichtbaarheid ervan in- en uitschakelen met de knop hieronder.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Bijlagen downloaden" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Ik accepteer deze risico's en beleidsupdates" }, + "autoConfirmEnabledByAdmin": { + "message": "Automatisch gebruikersbevestigingsinstelling ingeschakeld" + }, + "autoConfirmDisabledByAdmin": { + "message": "Automatisch gebruikersbevestigingsinstelling uitgeschakeld" + }, + "autoConfirmEnabledByPortal": { + "message": "Automatisch gebruikersbevestigingsbeleid toegevoegd" + }, + "autoConfirmDisabledByPortal": { + "message": "Automatisch gebruikersbevestigingsbeleid verwijderd" + }, + "system": { + "message": "Systeem" + }, "personalOwnership": { "message": "Persoonlijk eigendom" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Ongeldige verificatiecode" }, + "invalidEmailOrVerificationCode": { + "message": "Ongeldig e-mailadres verificatiecode" + }, "keyConnectorDomain": { "message": "Key Connector-domein" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Voer meerdere e-mailadressen in door te scheiden met een komma." }, + "emailsRequiredChangeAccessType": { + "message": "E-mailverificatie vereist ten minste één e-mailadres. Om alle e-mailadressen te verwijderen, moet je het toegangstype hierboven wijzigen." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Ongeldig Send-wachtwoord" }, + "vaultWelcomeDialogTitle": { + "message": "Je bent erbij! Welkom bij Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Sla al je wachtwoorden en persoonlijke informatie op in je Bitwarden-kluis. We laten je zien hoe het werkt." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Rondleiding starten" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Overslaan" + }, "sendPasswordHelperText": { "message": "Individuen moeten het wachtwoord invoeren om deze Send te bekijken", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "Er is een fout opgetreden bij het bijwerken van je betaalmethode." + }, + "sendPasswordInvalidAskOwner": { + "message": "Onjuist wachtwoord. Vraag de afzender om het wachtwoord om toegang te krijgen tot deze Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "Deze Send verloopt om $TIME$ op $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/nn/messages.json b/apps/web/src/locales/nn/messages.json index 78a638ee05a..13f608da89d 100644 --- a/apps/web/src/locales/nn/messages.json +++ b/apps/web/src/locales/nn/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/or/messages.json b/apps/web/src/locales/or/messages.json index bc0e3b74cb6..d615154225d 100644 --- a/apps/web/src/locales/or/messages.json +++ b/apps/web/src/locales/or/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index 40ba151b7ad..7b5df541186 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Użytkownik $ID$ został zaktualizowany.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Nie znasz hasła? Poproś nadawcę o hasło, aby uzyskać dostęp do wysyłki.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Ta wysyłka jest domyślnie ukryta. Możesz zmienić jej widoczność za pomocą przycisku.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Pobierz załączniki" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Własność osobista" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Kod weryfikacyjny jest nieprawidłowy" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Domena Key Connector'a" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index f78d39715cc..eb8864d4e6c 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -15,7 +15,7 @@ "message": "Nenhum aplicativo crítico em risco" }, "critical": { - "message": "Critical ($COUNT$)", + "message": "Críticos ($COUNT$)", "placeholders": { "count": { "content": "$1", @@ -24,7 +24,7 @@ } }, "notCritical": { - "message": "Not critical ($COUNT$)", + "message": "Não críticos ($COUNT$)", "placeholders": { "count": { "content": "$1", @@ -33,13 +33,13 @@ } }, "criticalBadge": { - "message": "Critical" + "message": "Crítico" }, "accessIntelligence": { "message": "Inteligência de acesso" }, "noApplicationsMatchTheseFilters": { - "message": "No applications match these filters" + "message": "Nenhum aplicativo corresponde aos filtros" }, "passwordRisk": { "message": "Risco de senhas" @@ -48,7 +48,7 @@ "message": "Você não tem permissão para editar este item" }, "reviewAccessIntelligence": { - "message": "Review security reports to find and fix credential risks before they escalate." + "message": "Revise os relatórios de segurança para encontrar e corrigir riscos antes que cresçam." }, "reviewAtRiskLoginsPrompt": { "message": "Revisar credenciais em risco" @@ -269,7 +269,7 @@ } }, "numCriticalApplicationsMarkedSuccess": { - "message": "$COUNT$ applications marked critical", + "message": "$COUNT$ aplicativos marcados como críticos", "placeholders": { "count": { "content": "$1", @@ -278,7 +278,7 @@ } }, "numApplicationsUnmarkedCriticalSuccess": { - "message": "$COUNT$ applications marked not critical", + "message": "$COUNT$ aplicativos marcados como não críticos", "placeholders": { "count": { "content": "$1", @@ -287,7 +287,7 @@ } }, "markAppCountAsCritical": { - "message": "Mark $COUNT$ as critical", + "message": "Marcar $COUNT$ como críticos", "placeholders": { "count": { "content": "$1", @@ -296,7 +296,7 @@ } }, "markAppCountAsNotCritical": { - "message": "Mark $COUNT$ as not critical", + "message": "Marcar $COUNT$ como não críticos", "placeholders": { "count": { "content": "$1", @@ -311,7 +311,7 @@ "message": "Aplicativo" }, "applications": { - "message": "Applications" + "message": "Aplicativos" }, "atRiskPasswords": { "message": "Senhas em risco" @@ -650,7 +650,7 @@ "message": "E-mail" }, "emails": { - "message": "Emails" + "message": "E-mails" }, "phone": { "message": "Telefone" @@ -1284,7 +1284,7 @@ "message": "Selecionar tudo" }, "deselectAll": { - "message": "Deselect all" + "message": "Desselecionar tudo" }, "unselectAll": { "message": "Deselecionar tudo" @@ -1435,10 +1435,10 @@ "message": "Não" }, "noAuth": { - "message": "Anyone with the link" + "message": "Qualquer pessoa com o link" }, "anyOneWithPassword": { - "message": "Anyone with a password set by you" + "message": "Qualquer pessoa com uma senha configurada por você" }, "location": { "message": "Localização" @@ -3339,13 +3339,13 @@ "message": "A assinatura foi restabelecida." }, "resubscribe": { - "message": "Resubscribe" + "message": "Reinscrever-se" }, "yourSubscriptionIsExpired": { - "message": "Your subscription is expired" + "message": "Sua assinatura expirou" }, "yourSubscriptionIsCanceled": { - "message": "Your subscription is canceled" + "message": "Sua assinatura foi cancelada" }, "cancelConfirmation": { "message": "Você tem certeza que deseja cancelar? Você perderá o acesso a todos os recursos dessa assinatura no final deste ciclo de faturamento." @@ -3366,7 +3366,7 @@ "message": "Próxima cobrança" }, "nextChargeDate": { - "message": "Next charge date" + "message": "Próxima cobrança" }, "plan": { "message": "Plano" @@ -3857,7 +3857,7 @@ "message": "Editar conjunto" }, "viewCollection": { - "message": "View collection" + "message": "Ver conjunto" }, "collectionInfo": { "message": "Informações do conjunto" @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Usuário $ID$ foi confirmado automaticamente.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Editou o usuário $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Gerando sua Inteligência de Acesso..." }, - "fetchingMemberData": { - "message": "Buscando dados de membros..." - }, - "analyzingPasswordHealth": { - "message": "Analisando saúde das senhas..." - }, - "calculatingRiskScores": { - "message": "Calculando pontuações de risco..." - }, - "generatingReportData": { - "message": "Gerando dados do relatório..." - }, - "savingReport": { - "message": "Salvando relatório..." - }, - "compilingInsights": { - "message": "Compilando conhecimentos..." - }, "loadingProgress": { "message": "Progresso de carregamento" }, - "thisMightTakeFewMinutes": { - "message": "Isto pode levar alguns minutos." + "reviewingMemberData": { + "message": "Revisando os dados dos membros..." + }, + "analyzingPasswords": { + "message": "Analisando as senhas..." + }, + "calculatingRisks": { + "message": "Calculando os riscos..." + }, + "generatingReports": { + "message": "Gerando os relatórios..." + }, + "compilingInsightsProgress": { + "message": "Compilando conhecimentos..." + }, + "reportGenerationDone": { + "message": "Pronto!" }, "riskInsightsRunReport": { "message": "Executar relatório" @@ -5440,7 +5446,7 @@ "message": "Número mínimo de palavras" }, "passwordTypePolicyOverride": { - "message": "Password type", + "message": "Tipo da senha", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -5723,7 +5729,7 @@ } }, "sendCreatedDescriptionPassword": { - "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "message": "Copie e compartilhe este link do Send. O Send ficará disponível para qualquer um com o link e a senha por $TIME$.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "time": { @@ -5733,7 +5739,7 @@ } }, "sendCreatedDescriptionEmail": { - "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "message": "Copie e compartilhe este link do Send. Ele pode ser visto pelas pessoas que você especificou pelos próximos $TIME$.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "time": { @@ -5849,10 +5855,6 @@ "message": "Não sabe a senha? Peça ao remetente a senha necessária para acessar esse Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Este Send é oculto por padrão. Você pode alternar a visibilidade usando o botão abaixo.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Baixar anexos" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Eu aceito estes riscos e atualizações de política" }, + "autoConfirmEnabledByAdmin": { + "message": "Ativou a configuração de confirmação de usuários automática" + }, + "autoConfirmDisabledByAdmin": { + "message": "Desativou a configuração de confirmação de usuários automática" + }, + "autoConfirmEnabledByPortal": { + "message": "Adicionou a política de confirmação de usuários automática" + }, + "autoConfirmDisabledByPortal": { + "message": "Removeu a política de confirmação de usuários automática" + }, + "system": { + "message": "Sistema" + }, "personalOwnership": { "message": "Remover cofre individual" }, @@ -6439,7 +6456,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "verifyYourEmailToViewThisSend": { - "message": "Verify your email to view this Send", + "message": "Confirme seu e-mail para ver este Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "viewSendHiddenEmailWarning": { @@ -6687,10 +6704,10 @@ } }, "reinviteSuccessToast": { - "message": "1 invitation sent" + "message": "1 convite enviado" }, "bulkReinviteSentToast": { - "message": "$COUNT$ invitations sent", + "message": "$COUNT$ convites enviados", "placeholders": { "count": { "content": "$1", @@ -6716,7 +6733,7 @@ } }, "bulkReinviteProgressTitle": { - "message": "$COUNT$ of $TOTAL$ invitations sent...", + "message": "$COUNT$ dos $TOTAL$ convites foram enviados...", "placeholders": { "count": { "content": "$1", @@ -6729,10 +6746,10 @@ } }, "bulkReinviteProgressSubtitle": { - "message": "Keep this page open until all are sent." + "message": "Mantenha esta página aberta até que todos sejam enviados." }, "bulkReinviteFailuresTitle": { - "message": "$COUNT$ invitations didn't send", + "message": "$COUNT$ convites não foram enviados", "placeholders": { "count": { "content": "$1", @@ -6741,10 +6758,10 @@ } }, "bulkReinviteFailureTitle": { - "message": "1 invitation didn't send" + "message": "1 convite não foi enviado" }, "bulkReinviteFailureDescription": { - "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "message": "Ocorreu um erro ao enviar $COUNT$ convites para os $TOTAL$ membros. Tente enviar de novo, e se o problema continuar,", "placeholders": { "count": { "content": "$1", @@ -6757,7 +6774,7 @@ } }, "bulkResendInvitations": { - "message": "Try sending again" + "message": "Tentar enviar de novo" }, "bulkRemovedMessage": { "message": "Removido com sucesso" @@ -7092,16 +7109,16 @@ "message": "Uma ou mais políticas da organização impedem que você exporte seu cofre individual." }, "activateAutofillPolicy": { - "message": "Activate autofill" + "message": "Ativar preenchimento automático" }, "activateAutofillPolicyDescription": { - "message": "Activate the autofill on page load setting on the browser extension for all existing and new members." + "message": "Ative a configuração de preenchimento automático no carregamento da página na extensão do navegador para todos os membros existentes e novos." }, "autofillOnPageLoadExploitWarning": { - "message": "Compromised or untrusted websites can exploit autofill on page load." + "message": "Sites comprometidos ou não confiáveis podem explorar do preenchimento automático ao carregar a página." }, "learnMoreAboutAutofillPolicy": { - "message": "Learn more about autofill" + "message": "Saiba mais sobre preenchimento automático" }, "selectType": { "message": "Selecionar tipo de SSO" @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Código de verificação inválido" }, + "invalidEmailOrVerificationCode": { + "message": "Email ou código de verificação inválido" + }, "keyConnectorDomain": { "message": "Domínio do Key Connector" }, @@ -10198,7 +10218,7 @@ "message": "Atribuir tarefas" }, "allTasksAssigned": { - "message": "All tasks have been assigned" + "message": "Todas as tarefas foram atribuídas" }, "assignSecurityTasksToMembers": { "message": "Envie notificações para alteração de senhas" @@ -10608,7 +10628,7 @@ "message": "Falha ao salvar a integração. Tente novamente mais tarde." }, "mustBeOrganizationOwnerAdmin": { - "message": "You must be an Organization Owner or Admin to perform this action." + "message": "Você precisa ser proprietário ou administrador da organização para executar esta ação." }, "mustBeOrgOwnerToPerformAction": { "message": "Você precisa ser o proprietário da organização para executar esta ação." @@ -11539,13 +11559,13 @@ "message": "O Bitwarden tentará reivindicar o domínio 3 vezes durante as primeiras 72 horas. Se o domínio não poder ser reivindicado, confira o registro de DNS no seu servidor e reivindique manualmente. Se não for reivindicado, o domínio será removido da sua organização em 7 dias." }, "automaticDomainClaimProcess1": { - "message": "Bitwarden will attempt to claim the domain within 72 hours. If the domain can't be claimed, verify your DNS record and claim manually. Unclaimed domains are removed after 7 days." + "message": "Bitwarden tentará reivindicar o domínio dentro de 72 horas. Se o domínio não puder ser reivindicado, verifique o seu registro DNS e reivindique manualmente. Domínios não reivindicados são removidos após 7 dias." }, "automaticDomainClaimProcess2": { - "message": "Once claimed, existing members with claimed domains will be emailed about the " + "message": "Ao reivindicar, os membros existentes com domínios reivindicados serão enviados um e-mail sobre a " }, "accountOwnershipChange": { - "message": "account ownership change" + "message": "alteração de propriedade da conta" }, "automaticDomainClaimProcessEnd": { "message": "." @@ -11563,7 +11583,7 @@ "message": "Reivindicado" }, "domainStatusPending": { - "message": "Pending" + "message": "Pendente" }, "claimedDomainsDescription": { "message": "Reivindique um domínio para ser o proprietário das contas dos membros. A página do identificador do SSO será pulada durante a autenticação dos membros com os domínios reivindicados, e os administradores poderão apagar contas reivindicadas." @@ -11910,10 +11930,10 @@ "message": "Os itens arquivados aparecerão aqui e serão excluídos dos resultados gerais de busca e das sugestões de preenchimento automático." }, "itemArchiveToast": { - "message": "Item archived" + "message": "Item arquivado" }, "itemUnarchivedToast": { - "message": "Item unarchived" + "message": "Item desarquivado" }, "bulkArchiveItems": { "message": "Itens arquivados" @@ -12593,7 +12613,7 @@ "message": "Tem certeza que deseja continuar?" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "Erro: Não é possível descriptografar" }, "userVerificationFailed": { "message": "Falha na verificação do usuário." @@ -12858,16 +12878,19 @@ "message": "Você usou todos os $GB$ GB do seu armazenamento criptografado. Para continuar armazenando arquivos, adicione mais armazenamento." }, "whoCanView": { - "message": "Who can view" + "message": "Quem pode visualizar" }, "specificPeople": { - "message": "Specific people" + "message": "Pessoas específicas" }, "emailVerificationDesc": { - "message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send." + "message": "Após compartilhar este link de Send, indivíduos precisarão verificar seus e-mails com um código para visualizar este Send." }, "enterMultipleEmailsSeparatedByComma": { - "message": "Enter multiple emails by separating with a comma." + "message": "Digite vários e-mails, separados com uma vírgula." + }, + "emailsRequiredChangeAccessType": { + "message": "A verificação de e-mail requer pelo menos um endereço de e-mail. Para remover todos, altere o tipo de acesso acima." }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" @@ -12876,7 +12899,7 @@ "message": "Quando você remover o armazenamento, você receberá um crédito de conta proporcional que irá automaticamente para sua próxima fatura." }, "ownerBadgeA11yDescription": { - "message": "Owner, $OWNER$, show all items owned by $OWNER$", + "message": "Proprietário, $OWNER$, mostrar todos os itens pertencentes a $OWNER$", "placeholders": { "owner": { "content": "$1", @@ -12888,35 +12911,47 @@ "message": "Você tem o Premium" }, "emailProtected": { - "message": "E-mail protegido" + "message": "Protegido por e-mail" }, "invalidSendPassword": { - "message": "Invalid Send password" + "message": "Senha do Send inválida" + }, + "vaultWelcomeDialogTitle": { + "message": "Você entrou! Boas-vindas ao Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Armazene todas as suas senhas e informações pessoais no seu cofre do Bitwarden. Vamos te dar um guia." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Começar guia" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Pular" }, "sendPasswordHelperText": { - "message": "Individuals will need to enter the password to view this Send", + "message": "Os indivíduos precisarão digitar a senha para ver este Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "perUser": { - "message": "per user" + "message": "por usuário" }, "upgradeToTeams": { - "message": "Upgrade to Teams" + "message": "Fazer upgrade para o Equipes" }, "upgradeToEnterprise": { - "message": "Upgrade to Enterprise" + "message": "Fazer upgrade para o Empresarial" }, "upgradeShareEvenMore": { - "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + "message": "Compartilhe ainda mais com o Famílias, ou receba segurança poderosa e confiável de senhas com o Equipes ou o Empresarial" }, "organizationUpgradeTaxInformationMessage": { - "message": "Prices exclude tax and are billed annually." + "message": "Os preços excluem os impostos e são cobrados anualmente." }, "invoicePreviewErrorMessage": { - "message": "Encountered an error while generating the invoice preview." + "message": "Foi deparado um erro ao gerar a pré-visualização da fatura." }, "planProratedMembershipInMonths": { - "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "message": "Assinatura $PLAN$ rateada ($NUMOFMONTHS$)", "placeholders": { "plan": { "content": "$1", @@ -12929,16 +12964,16 @@ } }, "premiumSubscriptionCredit": { - "message": "Premium subscription credit" + "message": "Crédito da assinatura Premium" }, "enterpriseMembership": { - "message": "Enterprise membership" + "message": "Assinatura Empresarial" }, "teamsMembership": { - "message": "Teams membership" + "message": "Assinatura do Equipes" }, "plansUpdated": { - "message": "You've upgraded to $PLAN$!", + "message": "Você fez upgrade para o $PLAN$!", "placeholders": { "plan": { "content": "$1", @@ -12947,6 +12982,24 @@ } }, "paymentMethodUpdateError": { - "message": "There was an error updating your payment method." + "message": "Houve um erro ao atualizar seu método de pagamento." + }, + "sendPasswordInvalidAskOwner": { + "message": "Senha inválida. Peça ao remetente a senha necessária para acessar este Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "Este send expirá às $TIME$ em $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/pt_PT/messages.json b/apps/web/src/locales/pt_PT/messages.json index a20d884c321..eb4573b336c 100644 --- a/apps/web/src/locales/pt_PT/messages.json +++ b/apps/web/src/locales/pt_PT/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Utilizador $ID$ confirmado automaticamente.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Utilizador $ID$ editado.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "A gerar a sua Inteligência de Acesso..." }, - "fetchingMemberData": { - "message": "A obter dados dos membros..." - }, - "analyzingPasswordHealth": { - "message": "A analisar a segurança da palavra-passe..." - }, - "calculatingRiskScores": { - "message": "A calcular pontuações de risco..." - }, - "generatingReportData": { - "message": "A gerar dados do relatório..." - }, - "savingReport": { - "message": "A guardar relatório..." - }, - "compilingInsights": { - "message": "A compilar insights..." - }, "loadingProgress": { "message": "A carregar progresso" }, - "thisMightTakeFewMinutes": { - "message": "Isto pode demorar alguns minutos." + "reviewingMemberData": { + "message": "A rever os dados dos membros..." + }, + "analyzingPasswords": { + "message": "A analisar palavras-passe..." + }, + "calculatingRisks": { + "message": "A calcular riscos..." + }, + "generatingReports": { + "message": "A gerar relatórios..." + }, + "compilingInsightsProgress": { + "message": "A compilar insights..." + }, + "reportGenerationDone": { + "message": "Concluído!" }, "riskInsightsRunReport": { "message": "Executar relatório" @@ -5849,10 +5855,6 @@ "message": "Não sabe a palavra-passe? Peça ao remetente a palavra-passe necessária para aceder a este Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Este Send está oculto por defeito. Pode alternar a sua visibilidade utilizando o botão abaixo.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Transferir anexos" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Aceito estes riscos e atualizações da política" }, + "autoConfirmEnabledByAdmin": { + "message": "Definição de confirmação automática de utilizadores ativada" + }, + "autoConfirmDisabledByAdmin": { + "message": "Definição de confirmação automática de utilizadores desativada" + }, + "autoConfirmEnabledByPortal": { + "message": "Política de confirmação automática de utilizadores adicionada" + }, + "autoConfirmDisabledByPortal": { + "message": "Política de confirmação automática de utilizadores removida" + }, + "system": { + "message": "Sistema" + }, "personalOwnership": { "message": "Remover cofre pessoal" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Código de verificação inválido" }, + "invalidEmailOrVerificationCode": { + "message": "E-mail ou código de verificação inválido" + }, "keyConnectorDomain": { "message": "Domínio do Key Connector" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Introduza vários e-mails, separados por vírgula." }, + "emailsRequiredChangeAccessType": { + "message": "A verificação por e-mail requer pelo menos um endereço de e-mail. Para remover todos os e-mails, altere o tipo de acesso acima." + }, "emailPlaceholder": { "message": "utilizador@bitwarden.com , utilizador@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Palavra-passe do Send inválida" }, + "vaultWelcomeDialogTitle": { + "message": "Entrou com sucesso! Bem-vindo ao Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Armazene todas as suas palavras-passe e informações pessoais no seu cofre Bitwarden. Vamos mostrar-lhe como funciona." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Iniciar tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Saltar" + }, "sendPasswordHelperText": { "message": "Os indivíduos terão de introduzir a palavra-passe para ver este Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "Ocorreu um erro ao atualizar o seu método de pagamento." + }, + "sendPasswordInvalidAskOwner": { + "message": "Palavra-passe inválida. Peça ao remetente a palavra-passe necessária para aceder a este Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "Este Send expira às $TIME$ de $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/ro/messages.json b/apps/web/src/locales/ro/messages.json index 6dca68fa932..f0b67e015cc 100644 --- a/apps/web/src/locales/ro/messages.json +++ b/apps/web/src/locales/ro/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Utilizatorul $ID$ a fost editat.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Nu știți parola? Solicitați expeditorului parola necesară pentru a accesa acest Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Acest Send este ascuns în mod implicit. Puteți comuta vizibilitatea acestuia cu butonul de mai jos.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Înlăturați seiful personal" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Cod de verificare nevalid" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index 13aec65ba3e..0314006ab1e 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Автоматически подтвержденный пользователь $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Изменен пользователь $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Ваша информация о доступе генерируется..." }, - "fetchingMemberData": { - "message": "Получение данных о пользователях..." - }, - "analyzingPasswordHealth": { - "message": "Анализ здоровья пароля..." - }, - "calculatingRiskScores": { - "message": "Расчет показателей риска..." - }, - "generatingReportData": { - "message": "Генерация данных отчета..." - }, - "savingReport": { - "message": "Сохранение отчета..." - }, - "compilingInsights": { - "message": "Компиляция информации..." - }, "loadingProgress": { "message": "Прогресс загрузки" }, - "thisMightTakeFewMinutes": { - "message": "Это может занять несколько минут." + "reviewingMemberData": { + "message": "Проверка данных пользователя..." + }, + "analyzingPasswords": { + "message": "Анализ паролей..." + }, + "calculatingRisks": { + "message": "Расчет рисков..." + }, + "generatingReports": { + "message": "Формирование отчетов..." + }, + "compilingInsightsProgress": { + "message": "Компиляция информации..." + }, + "reportGenerationDone": { + "message": "Готово!" }, "riskInsightsRunReport": { "message": "Запустить отчет" @@ -5849,10 +5855,6 @@ "message": "Не знаете пароль? Для доступа к этой Send, запросите его у отправителя.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Эта Send по умолчанию скрыта. Вы можете переключить ее видимость с помощью кнопки ниже.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Скачать вложения" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Я принимаю эти риски и политики обновления" }, + "autoConfirmEnabledByAdmin": { + "message": "Включена настройка автоматического подтверждения пользователей" + }, + "autoConfirmDisabledByAdmin": { + "message": "Отключена настройка автоматического подтверждения пользователей" + }, + "autoConfirmEnabledByPortal": { + "message": "Добавлена политика автоматического подтверждения пользователей" + }, + "autoConfirmDisabledByPortal": { + "message": "Удалена политика автоматического подтверждения пользователей" + }, + "system": { + "message": "Система" + }, "personalOwnership": { "message": "Удалить личное хранилище" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Неверный код подтверждения" }, + "invalidEmailOrVerificationCode": { + "message": "Неверный email или код подтверждения" + }, "keyConnectorDomain": { "message": "Домен соединителя ключей" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Введите несколько email, разделяя их запятой." }, + "emailsRequiredChangeAccessType": { + "message": "Для проверки электронной почты требуется как минимум один адрес email. Чтобы удалить все адреса электронной почты, измените тип доступа выше." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Неверный пароль Send" }, + "vaultWelcomeDialogTitle": { + "message": "Вы с нами! Добро пожаловать в Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Сохраняйте все свои пароли и личную информацию в хранилище Bitwarden. Мы покажем как это работает." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Начать знакомство" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Пропустить" + }, "sendPasswordHelperText": { "message": "Пользователям необходимо будет ввести пароль для просмотра этой Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "Произошла ошибка при обновлении способа оплаты." + }, + "sendPasswordInvalidAskOwner": { + "message": "Неверный пароль. Для доступа к этой Send, запросите его у отправителя.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/si/messages.json b/apps/web/src/locales/si/messages.json index ab7c7e30566..3603a36246e 100644 --- a/apps/web/src/locales/si/messages.json +++ b/apps/web/src/locales/si/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index b1f38615a99..79e8b40f918 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Používateľ $ID$ upravený.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generuje sa prehľad o prístupe..." }, - "fetchingMemberData": { - "message": "Sťahujú sa dáta o členoch..." - }, - "analyzingPasswordHealth": { - "message": "Analyzuje sa odolnosť hesiel..." - }, - "calculatingRiskScores": { - "message": "Vypočítava sa úroveň ohrozenia..." - }, - "generatingReportData": { - "message": "Generujú sa dáta reportu..." - }, - "savingReport": { - "message": "Ukladá sa report..." - }, - "compilingInsights": { - "message": "Kompiluje sa prehľad..." - }, "loadingProgress": { "message": "Priebeh načítania" }, - "thisMightTakeFewMinutes": { - "message": "Môže to trvať niekoľko minút." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Generovať report" @@ -5849,10 +5855,6 @@ "message": "Neviete heslo? Požiadajte odosielateľa o heslo potrebné k prístupu k tomuto Sendu.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Tento Send je normálne skrytý. Tlačidlom nižšie môžete prepnúť jeho viditeľnosť.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Stiahnuť prílohy" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Akceptujem tieto riziká a aktualizácie pravidiel" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Zakázať osobný trezor" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Neplatný verifikačný kód" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Doména Key Connectora" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/sl/messages.json b/apps/web/src/locales/sl/messages.json index db65260f9b1..e5a622ca157 100644 --- a/apps/web/src/locales/sl/messages.json +++ b/apps/web/src/locales/sl/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/sr_CS/messages.json b/apps/web/src/locales/sr_CS/messages.json index fd18cb42a06..f869499d685 100644 --- a/apps/web/src/locales/sr_CS/messages.json +++ b/apps/web/src/locales/sr_CS/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/sr_CY/messages.json b/apps/web/src/locales/sr_CY/messages.json index eb6484db8df..e416df73247 100644 --- a/apps/web/src/locales/sr_CY/messages.json +++ b/apps/web/src/locales/sr_CY/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Корисник $ID$ промењен.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Преузимање података о члановима..." - }, - "analyzingPasswordHealth": { - "message": "Анализа здравља лозинки..." - }, - "calculatingRiskScores": { - "message": "Израчунавање резултата ризика..." - }, - "generatingReportData": { - "message": "Генерисање података извештаја..." - }, - "savingReport": { - "message": "Чување извештаја..." - }, - "compilingInsights": { - "message": "Састављање увида..." - }, "loadingProgress": { "message": "Учитавање напретка" }, - "thisMightTakeFewMinutes": { - "message": "Ово може потрајати неколико минута." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Покрените извештај" @@ -5849,10 +5855,6 @@ "message": "Не знате лозинку? Затражите од пошиљаоца лозинку потребну за приступ овом Слању.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Ово Слање је подразумевано скривено. Можете да пребацујете његову видљивост помоћу дугмета испод.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Преузмите прилоге" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Прихватам ове ризике и ажурирања политика" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Лично власништво" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Неисправан верификациони код" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Домен конектора кључа" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index 3cb3116b684..079dce0e3a7 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Redigerade användaren $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Genererar din Access Intelligence..." }, - "fetchingMemberData": { - "message": "Hämtar medlemsdata..." - }, - "analyzingPasswordHealth": { - "message": "Analyserar lösenordshälsa..." - }, - "calculatingRiskScores": { - "message": "Beräknar riskpoäng..." - }, - "generatingReportData": { - "message": "Genererar rapportdata..." - }, - "savingReport": { - "message": "Sparar rapport..." - }, - "compilingInsights": { - "message": "Sammanställer insikter..." - }, "loadingProgress": { "message": "Inläsningsförlopp" }, - "thisMightTakeFewMinutes": { - "message": "Detta kan ta några minuter." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Kör rapport" @@ -5849,10 +5855,6 @@ "message": "Vet du inte lösenordet? Fråga avsändaren om lösenordet som behövs för att komma åt denna Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Denna Send är dold som standard. Du kan växla dess synlighet med hjälp av knappen nedan.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Ladda ner bilagor" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Jag accepterar dessa risker och policyuppdateringar" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Radera individuellt valv" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Ogiltig verifieringskod" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector-domän" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Ange flera e-postadresser genom att separera dem med kommatecken." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "användare@bitwarden.com , användare@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Ogiltigt Send-lösenord" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individer måste ange lösenordet för att visa denna Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Ogiltigt lösenord. Fråga avsändaren om lösenordet som behövs för att komma åt denna Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/ta/messages.json b/apps/web/src/locales/ta/messages.json index 3789574201c..a1eb60d67c1 100644 --- a/apps/web/src/locales/ta/messages.json +++ b/apps/web/src/locales/ta/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "பயனர் $ID$ திருத்தப்பட்டார்.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "கடவுச்சொல் தெரியவில்லையா? இந்த Send-ஐ அணுகத் தேவையான கடவுச்சொல்லை அனுப்புநரிடம் கேட்கவும்.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "இந்த Send இயல்பாக மறைக்கப்பட்டுள்ளது. கீழே உள்ள பொத்தானைப் பயன்படுத்தி அதன் தெரிவுநிலையை மாற்றலாம்.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "இணைப்புகளைப் பதிவிறக்கவும்" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "தனிப்பட்ட வால்ட்டை அகற்று" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "தவறான சரிபார்ப்புக் குறியீடு" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "கீ கனெக்டர் டொமைன்" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/te/messages.json b/apps/web/src/locales/te/messages.json index bc0e3b74cb6..d615154225d 100644 --- a/apps/web/src/locales/te/messages.json +++ b/apps/web/src/locales/te/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/th/messages.json b/apps/web/src/locales/th/messages.json index eb6fa9011fc..ab53147de00 100644 --- a/apps/web/src/locales/th/messages.json +++ b/apps/web/src/locales/th/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/tr/messages.json b/apps/web/src/locales/tr/messages.json index 53348da8697..efd186d721a 100644 --- a/apps/web/src/locales/tr/messages.json +++ b/apps/web/src/locales/tr/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Kullanıcı düzenlendi: $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Access Intelligence’ınız oluşturuluyor..." }, - "fetchingMemberData": { - "message": "Üye verileri getiriliyor..." - }, - "analyzingPasswordHealth": { - "message": "Parola sağlığı analiz ediliyor..." - }, - "calculatingRiskScores": { - "message": "Risk puanları hesaplanıyor..." - }, - "generatingReportData": { - "message": "Rapor verileri oluşturuluyor..." - }, - "savingReport": { - "message": "Rapor kaydediliyor..." - }, - "compilingInsights": { - "message": "İçgörüler derleniyor..." - }, "loadingProgress": { "message": "Yükleme ilerlemesi" }, - "thisMightTakeFewMinutes": { - "message": "Bu işlem birkaç dakika sürebilir." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Raporu çalıştır" @@ -5849,10 +5855,6 @@ "message": "Parolayı bilmiyor musunuz? Bu Send'e erişmek için gereken parolayı dosyayı gönderen kişiye sorabilirsiniz.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Bu Send varsayılan olarak gizlidir. Aşağıdaki düğmeyi kullanarak görünürlüğünü değiştirebilirsiniz.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Ekleri indir" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Bu riskleri ve ilke güncellemelerini kabul ediyorum" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "Sistem" + }, "personalOwnership": { "message": "Kişisel kasayı kaldır" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Geçersiz doğrulama kodu" }, + "invalidEmailOrVerificationCode": { + "message": "E-posta veya doğrulama kodu geçersiz" + }, "keyConnectorDomain": { "message": "Key Connector alan adı" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "E-posta adreslerini virgülle ayırarak yazın." }, + "emailsRequiredChangeAccessType": { + "message": "E-posta doğrulaması için en az bir e-posta adresi gerekir. Tüm e-postaları silmek için yukarıdan erişim türünü değiştirin." + }, "emailPlaceholder": { "message": "kullanici@bitwarden.com , kullanici@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Geçersiz Send parolası" }, + "vaultWelcomeDialogTitle": { + "message": "Bitwarden'a hoş geldiniz" + }, + "vaultWelcomeDialogDescription": { + "message": "Tüm parolalarınızı ve kişisel bilgilerinizi Bitwarden kasanızda saklayabilirsiniz. Size etrafı gezdirelim." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Tura başla" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Geç" + }, "sendPasswordHelperText": { "message": "Bu Send'i görmek isteyen kişilerin parola girmesi gerekecektir", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "Ödeme yönteminizi güncellerken bir hata oluştu." + }, + "sendPasswordInvalidAskOwner": { + "message": "Parola geçersiz. Bu Send'e erişmek için gereken parolayı dosyayı gönderen kişiye sorabilirsiniz.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "Bu Send'in süresi $DATE$ $TIME$ tarihinde dolacaktır", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index dcb5d26aa1f..71780d2faa9 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Користувача $ID$ змінено.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5849,10 +5855,6 @@ "message": "Не знаєте пароль? Попросіть його у відправника для отримання доступу.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Це відправлення типово приховане. Ви можете змінити його видимість кнопкою нижче.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Завантажити вкладення" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Вилучити особисте сховище" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Недійсний код підтвердження" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Домен Key Connector" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Введіть декілька адрес е-пошти, розділяючи їх комою." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/vi/messages.json b/apps/web/src/locales/vi/messages.json index 64d0703bc07..369a87111d4 100644 --- a/apps/web/src/locales/vi/messages.json +++ b/apps/web/src/locales/vi/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Người dùng $ID$ đã được chỉnh sửa.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Đang tạo Access Intelligence của bạn..." }, - "fetchingMemberData": { - "message": "Đang lấy dữ liệu thành viên..." - }, - "analyzingPasswordHealth": { - "message": "Đang phân tích độ mạnh mật khẩu..." - }, - "calculatingRiskScores": { - "message": "Đang tính điểm rủi ro..." - }, - "generatingReportData": { - "message": "Đang tạo dữ liệu báo cáo..." - }, - "savingReport": { - "message": "Đang lưu báo cáo..." - }, - "compilingInsights": { - "message": "Đang biên soạn thông tin chi tiết..." - }, "loadingProgress": { "message": "Đang tải tiến trình" }, - "thisMightTakeFewMinutes": { - "message": "Quá trình này có thể mất vài phút." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Chạy báo cáo" @@ -5849,10 +5855,6 @@ "message": "Không biết mật khẩu? Hãy yêu cầu người gửi cung cấp mật khẩu cần thiết để truy cập vào Send này.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Send này sẽ bị ẩn theo mặc định. Bạn có thể bật/tắt tính năng này bằng cách nhấn vào nút bên dưới.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Tải xuống tập tin đính kèm" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Tôi chấp nhận những rủi ro và cập nhật chính sách này" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Xóa kho lưu trữ riêng lẻ" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "Mã xác minh không đúng" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Tên miền Key Connector" }, @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index 47c732a5a32..275e11a1c70 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -3339,13 +3339,13 @@ "message": "您的订阅已恢复。" }, "resubscribe": { - "message": "Resubscribe" + "message": "重新订阅" }, "yourSubscriptionIsExpired": { - "message": "Your subscription is expired" + "message": "您的订阅已过期" }, "yourSubscriptionIsCanceled": { - "message": "Your subscription is canceled" + "message": "您的订阅已取消" }, "cancelConfirmation": { "message": "确定要取消吗?在本次计费周期结束后,您将无法使用此订阅的所有功能。" @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "编辑了用户 $ID$。", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "正在生成 Access Intelligence..." }, - "fetchingMemberData": { - "message": "正在获取成员数据..." - }, - "analyzingPasswordHealth": { - "message": "正在分析密码健康度..." - }, - "calculatingRiskScores": { - "message": "正在计算风险评分..." - }, - "generatingReportData": { - "message": "正在生成报告数据..." - }, - "savingReport": { - "message": "正在保存报告..." - }, - "compilingInsights": { - "message": "正在编译洞察..." - }, "loadingProgress": { "message": "加载进度" }, - "thisMightTakeFewMinutes": { - "message": "这可能需要几分钟时间。" + "reviewingMemberData": { + "message": "正在审查成员数据..." + }, + "analyzingPasswords": { + "message": "正在分析密码..." + }, + "calculatingRisks": { + "message": "正在计算风险..." + }, + "generatingReports": { + "message": "正在生成报告..." + }, + "compilingInsightsProgress": { + "message": "正在编译洞察..." + }, + "reportGenerationDone": { + "message": "完成!" }, "riskInsightsRunReport": { "message": "运行报告" @@ -5849,10 +5855,6 @@ "message": "不知道密码吗?请向发送者索取访问此 Send 所需的密码。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "此 Send 默认隐藏。您可以使用下方的按钮切换其可见性。", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "下载附件" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "我接受这些风险和策略更新" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "禁用个人密码库" }, @@ -6439,7 +6456,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "verifyYourEmailToViewThisSend": { - "message": "Verify your email to view this Send", + "message": "验证您的电子邮箱以查看此 Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "viewSendHiddenEmailWarning": { @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "无效的验证码" }, + "invalidEmailOrVerificationCode": { + "message": "无效的电子邮箱或验证码" + }, "keyConnectorDomain": { "message": "Key Connector 域名" }, @@ -11910,10 +11930,10 @@ "message": "已归档的项目将显示在此处,并将被排除在一般搜索结果和自动填充建议之外。" }, "itemArchiveToast": { - "message": "Item archived" + "message": "项目已归档" }, "itemUnarchivedToast": { - "message": "Item unarchived" + "message": "项目已取消归档" }, "bulkArchiveItems": { "message": "项目已归档" @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "输入多个电子邮箱(使用逗号分隔)。" }, + "emailsRequiredChangeAccessType": { + "message": "电子邮箱验证要求至少有一个电子邮箱地址。要移除所有电子邮箱,请更改上面的访问类型。" + }, "emailPlaceholder": { "message": "user@bitwarden.com, user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "无效的 Send 密码" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "个人需要输入密码才能查看此 Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "更新您的付款方式时出错。" + }, + "sendPasswordInvalidAskOwner": { + "message": "无效的密码。请向发送者索取访问此 Send 所需的密码。", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "此 Send 有效期至 $DATE$ $TIME$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/zh_TW/messages.json b/apps/web/src/locales/zh_TW/messages.json index c006b37d612..aedc802f241 100644 --- a/apps/web/src/locales/zh_TW/messages.json +++ b/apps/web/src/locales/zh_TW/messages.json @@ -2803,7 +2803,7 @@ "message": "用戶端 ID" }, "twoFactorDuoClientSecret": { - "message": "用戶端秘密" + "message": "用戶端機密" }, "twoFactorDuoApiHostname": { "message": "API 主機名稱" @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "已編輯使用者 $ID$。", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "正在產生您的 Access Intelligence……" }, - "fetchingMemberData": { - "message": "正在擷取成員資料…" - }, - "analyzingPasswordHealth": { - "message": "正在分析密碼安全狀況…" - }, - "calculatingRiskScores": { - "message": "正在計算風險分數…" - }, - "generatingReportData": { - "message": "正在產生報告資料..." - }, - "savingReport": { - "message": "正在儲存報告..." - }, - "compilingInsights": { - "message": "正在整理洞察結果…" - }, "loadingProgress": { "message": "載入進度中" }, - "thisMightTakeFewMinutes": { - "message": "這可能需要幾分鐘。" + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "執行報告" @@ -5849,10 +5855,6 @@ "message": "不知道密碼?請向此 Send 的寄件者索取密碼。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "此 Send 預設為隱藏。您可使用下方的按鈕切換其可見度。", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "下載附件" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "我接受這些風險與原則更新" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "停用個人密碼庫" }, @@ -7400,6 +7417,9 @@ "invalidVerificationCode": { "message": "無效的驗證碼" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector 網域" }, @@ -9791,7 +9811,7 @@ } }, "secretsManagerForPlanDesc": { - "message": "提供工程與 DevOps 團隊一套在軟體開發生命週期中,管理秘密資訊的功能。" + "message": "提供工程與 DevOps 團隊一套在軟體開發生命週期中,管理機密資訊的功能。" }, "free2PersonOrganization": { "message": "免費的 2 人組織" @@ -9833,7 +9853,7 @@ "message": "訂閲機密管理員" }, "addSecretsManagerUpgradeDesc": { - "message": "將機密管理員加入您的升級方案,來維持先前方案建立的秘密資訊的存取權限。" + "message": "將機密管理員加入您的升級方案,來維持先前方案建立的機密資訊的存取權限。" }, "additionalServiceAccounts": { "message": "額外服務帳戶" @@ -10906,7 +10926,7 @@ "message": "已驗證" }, "viewSecret": { - "message": "檢視秘密" + "message": "檢視機密" }, "noClients": { "message": "沒有可列出的客戶" @@ -11910,7 +11930,7 @@ "message": "封存的項目會顯示在此處,且不會出現在一般搜尋結果或自動填入建議中。" }, "itemArchiveToast": { - "message": "Item archived" + "message": "項目已封存" }, "itemUnarchivedToast": { "message": "Item unarchived" @@ -12869,6 +12889,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "請以逗號分隔輸入多個電子郵件地址。" }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12893,6 +12916,18 @@ "invalidSendPassword": { "message": "Send 密碼無效" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "對方必須輸入密碼才能檢視此 Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12948,5 +12983,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } From a7c74c6f7614a0f0fdddd945d330e480f93c5e30 Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Fri, 20 Feb 2026 10:17:08 -0600 Subject: [PATCH 110/134] [PM-32372] Added testid for table and then fixed tech debt (#19066) --- .../common/base.events.component.ts | 26 +-- .../manage/events.component.html | 170 +++++++++--------- .../organizations/manage/events.component.ts | 7 +- .../providers/manage/events.component.html | 112 ++++++------ .../providers/manage/events.component.ts | 2 +- .../service-accounts-events.component.html | 82 +++++---- .../service-accounts-events.component.ts | 7 +- 7 files changed, 215 insertions(+), 191 deletions(-) diff --git a/apps/web/src/app/admin-console/common/base.events.component.ts b/apps/web/src/app/admin-console/common/base.events.component.ts index ba315dee7fb..dd1c393bc13 100644 --- a/apps/web/src/app/admin-console/common/base.events.component.ts +++ b/apps/web/src/app/admin-console/common/base.events.component.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Directive, OnDestroy } from "@angular/core"; +import { Directive, OnDestroy, signal } from "@angular/core"; import { FormControl, FormGroup } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; import { combineLatest, filter, map, Observable, Subject, switchMap, takeUntil } from "rxjs"; @@ -22,9 +22,9 @@ import { EventExportService } from "../../tools/event-export"; @Directive() export abstract class BaseEventsComponent implements OnDestroy { - loading = true; - loaded = false; - events: EventView[]; + readonly loading = signal(true); + readonly loaded = signal(false); + readonly events = signal([]); dirtyDates = true; continuationToken: string; canUseSM = false; @@ -115,7 +115,7 @@ export abstract class BaseEventsComponent implements OnDestroy { return; } - this.loading = true; + this.loading.set(true); const dates = this.parseDates(); if (dates == null) { @@ -131,7 +131,7 @@ export abstract class BaseEventsComponent implements OnDestroy { } promise = null; - this.loading = false; + this.loading.set(false); }; loadEvents = async (clearExisting: boolean) => { @@ -140,7 +140,7 @@ export abstract class BaseEventsComponent implements OnDestroy { return; } - this.loading = true; + this.loading.set(true); let events: EventView[] = []; let promise: Promise; promise = this.loadAndParseEvents( @@ -153,14 +153,16 @@ export abstract class BaseEventsComponent implements OnDestroy { this.continuationToken = result.continuationToken; events = result.events; - if (!clearExisting && this.events != null && this.events.length > 0) { - this.events = this.events.concat(events); + if (!clearExisting && this.events() != null && this.events().length > 0) { + this.events.update((current) => { + return [...current, ...events]; + }); } else { - this.events = events; + this.events.set(events); } this.dirtyDates = false; - this.loading = false; + this.loading.set(false); promise = null; }; @@ -227,7 +229,7 @@ export abstract class BaseEventsComponent implements OnDestroy { private async export(start: string, end: string) { let continuationToken = this.continuationToken; - let events = [].concat(this.events); + let events = [].concat(this.events()); while (continuationToken != null) { const result = await this.loadAndParseEvents(start, end, continuationToken); diff --git a/apps/web/src/app/admin-console/organizations/manage/events.component.html b/apps/web/src/app/admin-console/organizations/manage/events.component.html index 83665a4b99e..3e76c8c879b 100644 --- a/apps/web/src/app/admin-console/organizations/manage/events.component.html +++ b/apps/web/src/app/admin-console/organizations/manage/events.component.html @@ -1,14 +1,16 @@ @let usePlaceHolderEvents = !organization?.useEvents; + - - {{ "upgrade" | i18n }} - + @if (usePlaceHolderEvents) { + + {{ "upgrade" | i18n }} + + }
    @@ -61,79 +63,87 @@
    - - {{ "upgradeEventLogMessage" | i18n }} - - - - {{ "loading" | i18n }} - - - @let displayedEvents = organization?.useEvents ? events : placeholderEvents; +@if (loaded() && usePlaceHolderEvents) { + + {{ "upgradeEventLogMessage" | i18n }} + +} -

    {{ "noEventsInList" | i18n }}

    - - - - {{ "timestamp" | i18n }} - {{ "client" | i18n }} - {{ "member" | i18n }} - {{ "event" | i18n }} - - - - - - {{ i > 4 && usePlaceHolderEvents ? "******" : (e.date | date: "medium") }} - - - {{ e.appName }} - - - {{ e.userName }} - - - - - - -
    +@if (!loaded()) { + + + {{ "loading" | i18n }} + +} +@if (loaded()) { + + @let displayedEvents = organization?.useEvents ? events() : placeholderEvents; - -
    -
    - + @if (!displayedEvents || !displayedEvents.length) { +

    {{ "noEventsInList" | i18n }}

    + } -

    - {{ "upgradeEventLogTitleMessage" | i18n }} -

    -

    - {{ "upgradeForFullEventsMessage" | i18n }} -

    - - + } + +} + +@if (loaded() && usePlaceHolderEvents) { + +
    +
    + + +

    + {{ "upgradeEventLogTitleMessage" | i18n }} +

    +

    + {{ "upgradeForFullEventsMessage" | i18n }} +

    + + +
    -
    - + +} diff --git a/apps/web/src/app/admin-console/organizations/manage/events.component.ts b/apps/web/src/app/admin-console/organizations/manage/events.component.ts index fffe1c06ab8..01d6515c486 100644 --- a/apps/web/src/app/admin-console/organizations/manage/events.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/events.component.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Component, OnDestroy, OnInit } from "@angular/core"; +import { Component, OnDestroy, OnInit, ChangeDetectionStrategy } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { concatMap, firstValueFrom, lastValueFrom, map, of, switchMap, takeUntil, tap } from "rxjs"; @@ -47,9 +47,8 @@ const EVENT_SYSTEM_USER_TO_TRANSLATION: Record = { [EventSystemUser.BitwardenPortal]: "system", }; -// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush -// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, templateUrl: "events.component.html", imports: [SharedModule, HeaderModule], }) @@ -168,7 +167,7 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe } } await this.refreshEvents(); - this.loaded = true; + this.loaded.set(true); } protected requestEvents(startDate: string, endDate: string, continuationToken: string) { diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.html index 070505a53b2..c79b39a6feb 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.html @@ -50,56 +50,64 @@
    - - - {{ "loading" | i18n }} - - -

    {{ "noEventsInList" | i18n }}

    - - - - {{ "timestamp" | i18n }} - {{ "device" | i18n }} - {{ "user" | i18n }} - {{ "event" | i18n }} - - - - - {{ e.date | date: "medium" }} - - - {{ e.appName }}, {{ e.ip }} - - - {{ e.userName }} - - - - - - -
    + > + {{ "loading" | i18n }} + +} +@if (loaded()) { + + @if (!events() || !events().length) { +

    {{ "noEventsInList" | i18n }}

    + } + + @if (events() && events().length) { + + + + {{ "timestamp" | i18n }} + {{ "device" | i18n }} + {{ "user" | i18n }} + {{ "event" | i18n }} + + + + @for (e of events(); track i; let i = $index) { + + {{ e.date | date: "medium" }} + + + {{ e.appName }}, {{ e.ip }} + + + {{ e.userName }} + + + + } + + + } + @if (continuationToken) { + + } +
    +} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.ts index 3d00d897175..fe14e56bbaa 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.ts @@ -94,7 +94,7 @@ export class EventsComponent extends BaseEventsComponent implements OnInit { this.providerUsersUserIdMap.set(u.userId, { name: name, email: u.email }); }); await this.refreshEvents(); - this.loaded = true; + this.loaded.set(true); } protected requestEvents(startDate: string, endDate: string, continuationToken: string) { diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.html index a895ab058ec..d1f410abc33 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.html @@ -47,41 +47,47 @@
    - - - {{ "loading" | i18n }} - - -

    {{ "noEventsInList" | i18n }}

    - - - - {{ "timestamp" | i18n }} - {{ "client" | i18n }} - {{ "event" | i18n }} - - - - - {{ e.date | date: "medium" }} - - {{ e.appName }} - - - - - - -
    +@if (!loaded()) { + + + {{ "loading" | i18n }} + +} +@if (loaded()) { + + @if (!events() || !events().length) { +

    {{ "noEventsInList" | i18n }}

    + } + @if (events() && events().length) { + + + + {{ "timestamp" | i18n }} + {{ "client" | i18n }} + {{ "event" | i18n }} + + + + @for (e of events(); track i; let i = $index) { + + {{ e.date | date: "medium" }} + + {{ e.appName }} + + + + } + + + } + @if (continuationToken) { + + } +
    +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts index 5968933064d..525d658f233 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Component, OnDestroy, OnInit } from "@angular/core"; +import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { takeUntil } from "rxjs"; @@ -17,9 +17,8 @@ import { EventExportService } from "@bitwarden/web-vault/app/tools/event-export" import { ServiceAccountEventLogApiService } from "./service-account-event-log-api.service"; -// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush -// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, selector: "sm-service-accounts-events", templateUrl: "./service-accounts-events.component.html", standalone: false, @@ -69,7 +68,7 @@ export class ServiceAccountEventsComponent async load() { await this.refreshEvents(); - this.loaded = true; + this.loaded.set(true); } protected requestEvents(startDate: string, endDate: string, continuationToken: string) { From a610ce01a23acfffeb087dad9d2d03d8da608a78 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Fri, 20 Feb 2026 10:23:59 -0600 Subject: [PATCH 111/134] [PM-31433] Welcome Dialog with Extension Prompt (#18849) * add welcome prompt when extension is not installed * add feature flag * move prompt logic to internal service and add day prompt * rename dialog component * remove feature flag hardcode and add documentation * use i18n for image alt * move state into service * be more explicit when the account or creation date is not available * remove spaces * fix types caused by introducing a numeric feature flag type * add `typeof` for feature flag typing --- .../services/desktop-autofill.service.ts | 2 +- .../navigation-switcher.stories.ts | 2 +- .../product-switcher.stories.ts | 2 +- ...ult-extension-prompt-dialog.component.html | 34 +++ ...-extension-prompt-dialog.component.spec.ts | 86 ++++++ ...vault-extension-prompt-dialog.component.ts | 51 ++++ ...web-vault-extension-prompt.service.spec.ts | 269 ++++++++++++++++++ .../web-vault-extension-prompt.service.ts | 104 +++++++ .../services/web-vault-prompt.service.spec.ts | 21 +- .../services/web-vault-prompt.service.ts | 4 + .../src/images/vault/extension-mock-login.png | Bin 0 -> 170141 bytes apps/web/src/locales/en/messages.json | 15 + libs/common/src/enums/feature-flag.enum.ts | 4 + libs/state/src/core/state-definitions.ts | 7 + 14 files changed, 596 insertions(+), 5 deletions(-) create mode 100644 apps/web/src/app/vault/components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component.html create mode 100644 apps/web/src/app/vault/components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component.spec.ts create mode 100644 apps/web/src/app/vault/components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component.ts create mode 100644 apps/web/src/app/vault/services/web-vault-extension-prompt.service.spec.ts create mode 100644 apps/web/src/app/vault/services/web-vault-extension-prompt.service.ts create mode 100644 apps/web/src/images/vault/extension-mock-login.png diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts index cca0097d65e..473ce593cb6 100644 --- a/apps/desktop/src/autofill/services/desktop-autofill.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts @@ -51,7 +51,7 @@ import type { NativeWindowObject } from "./desktop-fido2-user-interface.service" export class DesktopAutofillService implements OnDestroy { private destroy$ = new Subject(); private registrationRequest: autofill.PasskeyRegistrationRequest; - private featureFlag?: FeatureFlag; + private featureFlag?: typeof FeatureFlag.MacOsNativeCredentialSync; private isEnabled: boolean = false; constructor( diff --git a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts index ba36063fb7b..7af255c6823 100644 --- a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts +++ b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts @@ -105,7 +105,7 @@ class MockBillingAccountProfileStateService implements Partial { getFeatureFlag$(key: Flag): Observable> { - return of(false); + return of(false as FeatureFlagValueType); } } diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts b/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts index ad18b2b3490..7378e619b1a 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts +++ b/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts @@ -100,7 +100,7 @@ class MockBillingAccountProfileStateService implements Partial { getFeatureFlag$(key: Flag): Observable> { - return of(false); + return of(false as FeatureFlagValueType); } } diff --git a/apps/web/src/app/vault/components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component.html b/apps/web/src/app/vault/components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component.html new file mode 100644 index 00000000000..e9932ac9022 --- /dev/null +++ b/apps/web/src/app/vault/components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component.html @@ -0,0 +1,34 @@ +
    + +
    +

    + {{ "extensionPromptHeading" | i18n }} +

    +

    + {{ "extensionPromptBody" | i18n }} +

    +
    + + + + {{ "downloadExtension" | i18n }} + + +
    +
    +
    diff --git a/apps/web/src/app/vault/components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component.spec.ts b/apps/web/src/app/vault/components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component.spec.ts new file mode 100644 index 00000000000..fdf218d8c35 --- /dev/null +++ b/apps/web/src/app/vault/components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component.spec.ts @@ -0,0 +1,86 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { provideNoopAnimations } from "@angular/platform-browser/animations"; +import { mock, MockProxy } from "jest-mock-extended"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { DeviceType } from "@bitwarden/common/enums"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; +import { DialogRef, DialogService } from "@bitwarden/components"; + +import { WebVaultExtensionPromptService } from "../../services/web-vault-extension-prompt.service"; + +import { WebVaultExtensionPromptDialogComponent } from "./web-vault-extension-prompt-dialog.component"; + +describe("WebVaultExtensionPromptDialogComponent", () => { + let component: WebVaultExtensionPromptDialogComponent; + let fixture: ComponentFixture; + let mockDialogRef: MockProxy>; + + const mockUserId = "test-user-id" as UserId; + + const getDevice = jest.fn(() => DeviceType.ChromeBrowser); + const mockUpdate = jest.fn().mockResolvedValue(undefined); + + const getDialogDismissedState = jest.fn().mockReturnValue({ + update: mockUpdate, + }); + + beforeEach(async () => { + const mockAccountService = mockAccountServiceWith(mockUserId); + mockDialogRef = mock>(); + + await TestBed.configureTestingModule({ + imports: [WebVaultExtensionPromptDialogComponent], + providers: [ + provideNoopAnimations(), + { + provide: PlatformUtilsService, + useValue: { getDevice }, + }, + { provide: I18nService, useValue: { t: (key: string) => key } }, + { provide: AccountService, useValue: mockAccountService }, + { provide: DialogRef, useValue: mockDialogRef }, + { provide: DialogService, useValue: mock() }, + { + provide: WebVaultExtensionPromptService, + useValue: { getDialogDismissedState }, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(WebVaultExtensionPromptDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + describe("ngOnInit", () => { + it("sets webStoreUrl", () => { + expect(getDevice).toHaveBeenCalled(); + + expect(component["webStoreUrl"]).toBe( + "https://chromewebstore.google.com/detail/bitwarden-password-manage/nngceckbapebfimnlniiiahkandclblb", + ); + }); + }); + + describe("dismissPrompt", () => { + it("calls webVaultExtensionPromptService.getDialogDismissedState and updates to true", async () => { + await component.dismissPrompt(); + + expect(getDialogDismissedState).toHaveBeenCalledWith(mockUserId); + expect(mockUpdate).toHaveBeenCalledWith(expect.any(Function)); + + const updateFn = mockUpdate.mock.calls[0][0]; + expect(updateFn()).toBe(true); + }); + + it("closes the dialog", async () => { + await component.dismissPrompt(); + + expect(mockDialogRef.close).toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/web/src/app/vault/components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component.ts b/apps/web/src/app/vault/components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component.ts new file mode 100644 index 00000000000..e5dcf5e3cf2 --- /dev/null +++ b/apps/web/src/app/vault/components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component.ts @@ -0,0 +1,51 @@ +import { CommonModule } from "@angular/common"; +import { Component, ChangeDetectionStrategy, OnInit } from "@angular/core"; +import { firstValueFrom } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { getWebStoreUrl } from "@bitwarden/common/vault/utils/get-web-store-url"; +import { + ButtonModule, + DialogModule, + DialogRef, + DialogService, + IconComponent, +} from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; + +import { WebVaultExtensionPromptService } from "../../services/web-vault-extension-prompt.service"; + +@Component({ + selector: "web-vault-extension-prompt-dialog", + templateUrl: "./web-vault-extension-prompt-dialog.component.html", + imports: [CommonModule, ButtonModule, DialogModule, I18nPipe, IconComponent], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class WebVaultExtensionPromptDialogComponent implements OnInit { + constructor( + private platformUtilsService: PlatformUtilsService, + private accountService: AccountService, + private dialogRef: DialogRef, + private webVaultExtensionPromptService: WebVaultExtensionPromptService, + ) {} + + /** Download Url for the extension based on the browser */ + protected webStoreUrl: string = ""; + + ngOnInit(): void { + this.webStoreUrl = getWebStoreUrl(this.platformUtilsService.getDevice()); + } + + async dismissPrompt() { + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + await this.webVaultExtensionPromptService.getDialogDismissedState(userId).update(() => true); + this.dialogRef.close(); + } + + /** Opens the web extension prompt generator dialog. */ + static open(dialogService: DialogService) { + return dialogService.open(WebVaultExtensionPromptDialogComponent); + } +} diff --git a/apps/web/src/app/vault/services/web-vault-extension-prompt.service.spec.ts b/apps/web/src/app/vault/services/web-vault-extension-prompt.service.spec.ts new file mode 100644 index 00000000000..4a8865990df --- /dev/null +++ b/apps/web/src/app/vault/services/web-vault-extension-prompt.service.spec.ts @@ -0,0 +1,269 @@ +import { TestBed } from "@angular/core/testing"; +import { BehaviorSubject } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { DialogService } from "@bitwarden/components"; +import { StateProvider } from "@bitwarden/state"; + +import { WebVaultExtensionPromptDialogComponent } from "../components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component"; + +import { WebBrowserInteractionService } from "./web-browser-interaction.service"; +import { WebVaultExtensionPromptService } from "./web-vault-extension-prompt.service"; + +describe("WebVaultExtensionPromptService", () => { + let service: WebVaultExtensionPromptService; + + const mockUserId = "user-123" as UserId; + const mockAccountCreationDate = new Date("2026-01-15"); + + const getFeatureFlag = jest.fn(); + const extensionInstalled$ = new BehaviorSubject(false); + const mockStateSubject = new BehaviorSubject(false); + const activeAccountSubject = new BehaviorSubject<{ id: UserId; creationDate: Date | null }>({ + id: mockUserId, + creationDate: mockAccountCreationDate, + }); + const getUser = jest.fn().mockReturnValue({ state$: mockStateSubject.asObservable() }); + + beforeEach(() => { + jest.clearAllMocks(); + getFeatureFlag.mockResolvedValue(false); + extensionInstalled$.next(false); + mockStateSubject.next(false); + activeAccountSubject.next({ id: mockUserId, creationDate: mockAccountCreationDate }); + + TestBed.configureTestingModule({ + providers: [ + WebVaultExtensionPromptService, + { + provide: StateProvider, + useValue: { + getUser, + }, + }, + { + provide: WebBrowserInteractionService, + useValue: { + extensionInstalled$: extensionInstalled$.asObservable(), + }, + }, + { + provide: AccountService, + useValue: { + activeAccount$: activeAccountSubject.asObservable(), + }, + }, + { + provide: ConfigService, + useValue: { + getFeatureFlag, + }, + }, + { + provide: DialogService, + useValue: { + open: jest.fn(), + }, + }, + ], + }); + + service = TestBed.inject(WebVaultExtensionPromptService); + }); + + describe("conditionallyPromptUserForExtension", () => { + it("returns false when feature flag is disabled", async () => { + getFeatureFlag.mockResolvedValueOnce(false); + + const result = await service.conditionallyPromptUserForExtension(mockUserId); + + expect(result).toBe(false); + expect(getFeatureFlag).toHaveBeenCalledWith( + FeatureFlag.PM29438_WelcomeDialogWithExtensionPrompt, + ); + }); + + it("returns false when dialog has been dismissed", async () => { + getFeatureFlag.mockResolvedValueOnce(true); + mockStateSubject.next(true); + extensionInstalled$.next(false); + + const result = await service.conditionallyPromptUserForExtension(mockUserId); + + expect(result).toBe(false); + }); + + it("returns false when profile is not within thresholds (too old)", async () => { + getFeatureFlag + .mockResolvedValueOnce(true) // Main feature flag + .mockResolvedValueOnce(0); // Min age days + mockStateSubject.next(false); + extensionInstalled$.next(false); + const oldAccountDate = new Date("2025-12-01"); // More than 30 days old + activeAccountSubject.next({ id: mockUserId, creationDate: oldAccountDate }); + + const result = await service.conditionallyPromptUserForExtension(mockUserId); + + expect(result).toBe(false); + }); + + it("returns false when profile is not within thresholds (too young)", async () => { + getFeatureFlag + .mockResolvedValueOnce(true) // Main feature flag + .mockResolvedValueOnce(10); // Min age days = 10 + mockStateSubject.next(false); + extensionInstalled$.next(false); + const youngAccountDate = new Date(); // Today + youngAccountDate.setDate(youngAccountDate.getDate() - 5); // 5 days old + activeAccountSubject.next({ id: mockUserId, creationDate: youngAccountDate }); + + const result = await service.conditionallyPromptUserForExtension(mockUserId); + + expect(result).toBe(false); + }); + + it("returns false when extension is installed", async () => { + getFeatureFlag + .mockResolvedValueOnce(true) // Main feature flag + .mockResolvedValueOnce(0); // Min age days + mockStateSubject.next(false); + extensionInstalled$.next(true); + + const result = await service.conditionallyPromptUserForExtension(mockUserId); + + expect(result).toBe(false); + }); + + it("returns true and opens dialog when all conditions are met", async () => { + getFeatureFlag + .mockResolvedValueOnce(true) // Main feature flag + .mockResolvedValueOnce(0); // Min age days + mockStateSubject.next(false); + extensionInstalled$.next(false); + + // Set account creation date to be within threshold (15 days old) + const validCreationDate = new Date(); + validCreationDate.setDate(validCreationDate.getDate() - 15); + activeAccountSubject.next({ id: mockUserId, creationDate: validCreationDate }); + + const dialogClosedSubject = new BehaviorSubject(undefined); + const openSpy = jest + .spyOn(WebVaultExtensionPromptDialogComponent, "open") + .mockReturnValue({ closed: dialogClosedSubject.asObservable() } as any); + + const resultPromise = service.conditionallyPromptUserForExtension(mockUserId); + + // Close the dialog + dialogClosedSubject.next(undefined); + + const result = await resultPromise; + + expect(openSpy).toHaveBeenCalledWith(expect.anything()); + expect(result).toBe(true); + }); + }); + + describe("profileIsWithinThresholds", () => { + it("returns false when account is younger than min threshold", async () => { + const minAgeDays = 7; + getFeatureFlag.mockResolvedValueOnce(minAgeDays); + + const recentDate = new Date(); + recentDate.setDate(recentDate.getDate() - 5); // 5 days old + activeAccountSubject.next({ id: mockUserId, creationDate: recentDate }); + + const result = await service["profileIsWithinThresholds"](); + + expect(result).toBe(false); + }); + + it("returns true when account is exactly at min threshold", async () => { + const minAgeDays = 7; + getFeatureFlag.mockResolvedValueOnce(minAgeDays); + + const exactDate = new Date(); + exactDate.setDate(exactDate.getDate() - 7); // Exactly 7 days old + activeAccountSubject.next({ id: mockUserId, creationDate: exactDate }); + + const result = await service["profileIsWithinThresholds"](); + + expect(result).toBe(true); + }); + + it("returns true when account is within the thresholds", async () => { + const minAgeDays = 0; + getFeatureFlag.mockResolvedValueOnce(minAgeDays); + + const validDate = new Date(); + validDate.setDate(validDate.getDate() - 15); // 15 days old (between 0 and 30) + activeAccountSubject.next({ id: mockUserId, creationDate: validDate }); + + const result = await service["profileIsWithinThresholds"](); + + expect(result).toBe(true); + }); + + it("returns false when account is older than max threshold (30 days)", async () => { + const minAgeDays = 0; + getFeatureFlag.mockResolvedValueOnce(minAgeDays); + + const oldDate = new Date(); + oldDate.setDate(oldDate.getDate() - 31); // 31 days old + activeAccountSubject.next({ id: mockUserId, creationDate: oldDate }); + + const result = await service["profileIsWithinThresholds"](); + + expect(result).toBe(false); + }); + + it("returns false when account is exactly 30 days old", async () => { + const minAgeDays = 0; + getFeatureFlag.mockResolvedValueOnce(minAgeDays); + + const exactDate = new Date(); + exactDate.setDate(exactDate.getDate() - 30); // Exactly 30 days old + activeAccountSubject.next({ id: mockUserId, creationDate: exactDate }); + + const result = await service["profileIsWithinThresholds"](); + + expect(result).toBe(false); + }); + + it("uses default min age of 0 when feature flag is null", async () => { + getFeatureFlag.mockResolvedValueOnce(null); + + const recentDate = new Date(); + recentDate.setDate(recentDate.getDate() - 5); // 5 days old + activeAccountSubject.next({ id: mockUserId, creationDate: recentDate }); + + const result = await service["profileIsWithinThresholds"](); + + expect(result).toBe(true); + }); + + it("defaults to false", async () => { + getFeatureFlag.mockResolvedValueOnce(0); + activeAccountSubject.next({ id: mockUserId, creationDate: null }); + + const result = await service["profileIsWithinThresholds"](); + + expect(result).toBe(false); + }); + }); + + describe("getDialogDismissedState", () => { + it("returns the SingleUserState for the dialog dismissed state", () => { + service.getDialogDismissedState(mockUserId); + + expect(getUser).toHaveBeenCalledWith( + mockUserId, + expect.objectContaining({ + key: "vaultWelcomeExtensionDialogDismissed", + }), + ); + }); + }); +}); diff --git a/apps/web/src/app/vault/services/web-vault-extension-prompt.service.ts b/apps/web/src/app/vault/services/web-vault-extension-prompt.service.ts new file mode 100644 index 00000000000..3e13935f94c --- /dev/null +++ b/apps/web/src/app/vault/services/web-vault-extension-prompt.service.ts @@ -0,0 +1,104 @@ +import { inject, Injectable } from "@angular/core"; +import { firstValueFrom, map } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { DialogService } from "@bitwarden/components"; +import { StateProvider, UserKeyDefinition, WELCOME_EXTENSION_DIALOG_DISK } from "@bitwarden/state"; + +import { WebVaultExtensionPromptDialogComponent } from "../components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component"; + +import { WebBrowserInteractionService } from "./web-browser-interaction.service"; + +export const WELCOME_EXTENSION_DIALOG_DISMISSED = new UserKeyDefinition( + WELCOME_EXTENSION_DIALOG_DISK, + "vaultWelcomeExtensionDialogDismissed", + { + deserializer: (dismissed) => dismissed, + clearOn: [], + }, +); + +@Injectable({ providedIn: "root" }) +export class WebVaultExtensionPromptService { + private stateProvider = inject(StateProvider); + private webBrowserInteractionService = inject(WebBrowserInteractionService); + private accountService = inject(AccountService); + private configService = inject(ConfigService); + private dialogService = inject(DialogService); + + /** + * Conditionally prompts the user to install the web extension + */ + async conditionallyPromptUserForExtension(userId: UserId) { + const featureFlagEnabled = await this.configService.getFeatureFlag( + FeatureFlag.PM29438_WelcomeDialogWithExtensionPrompt, + ); + + if (!featureFlagEnabled) { + return false; + } + + // Extension check takes time, trigger it early + const hasExtensionInstalled = firstValueFrom( + this.webBrowserInteractionService.extensionInstalled$, + ); + + const hasDismissedExtensionPrompt = await firstValueFrom( + this.getDialogDismissedState(userId).state$.pipe(map((dismissed) => dismissed ?? false)), + ); + if (hasDismissedExtensionPrompt) { + return false; + } + + const profileIsWithinThresholds = await this.profileIsWithinThresholds(); + if (!profileIsWithinThresholds) { + return false; + } + + if (await hasExtensionInstalled) { + return false; + } + + const dialogRef = WebVaultExtensionPromptDialogComponent.open(this.dialogService); + await firstValueFrom(dialogRef.closed); + + return true; + } + + /** Returns the SingleUserState for the dialog dismissed state */ + getDialogDismissedState(userId: UserId) { + return this.stateProvider.getUser(userId, WELCOME_EXTENSION_DIALOG_DISMISSED); + } + + /** + * Returns true if the user's profile is within the defined thresholds for showing the extension prompt, false otherwise. + * Thresholds are defined as account age between a configurable number of days and 30 days. + */ + private async profileIsWithinThresholds() { + const creationDate = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.creationDate)), + ); + + // When account or creationDate is not available for some reason, + // default to not showing the prompt to avoid disrupting the user. + if (!creationDate) { + return false; + } + + const minAccountAgeDays = await this.configService.getFeatureFlag( + FeatureFlag.PM29438_DialogWithExtensionPromptAccountAge, + ); + + const now = new Date(); + const accountAgeMs = now.getTime() - creationDate.getTime(); + const accountAgeDays = accountAgeMs / (1000 * 60 * 60 * 24); + + const minAgeDays = minAccountAgeDays ?? 0; + const maxAgeDays = 30; + + return accountAgeDays >= minAgeDays && accountAgeDays < maxAgeDays; + } +} diff --git a/apps/web/src/app/vault/services/web-vault-prompt.service.spec.ts b/apps/web/src/app/vault/services/web-vault-prompt.service.spec.ts index eb72c80fe04..14bbc1a86d5 100644 --- a/apps/web/src/app/vault/services/web-vault-prompt.service.spec.ts +++ b/apps/web/src/app/vault/services/web-vault-prompt.service.spec.ts @@ -20,6 +20,7 @@ import { } from "../../admin-console/organizations/policies"; import { UnifiedUpgradePromptService } from "../../billing/individual/upgrade/services"; +import { WebVaultExtensionPromptService } from "./web-vault-extension-prompt.service"; import { WebVaultPromptService } from "./web-vault-prompt.service"; import { WelcomeDialogService } from "./welcome-dialog.service"; @@ -43,6 +44,7 @@ describe("WebVaultPromptService", () => { const enforceOrganizationDataOwnership = jest.fn().mockResolvedValue(undefined); const conditionallyShowWelcomeDialog = jest.fn().mockResolvedValue(false); const logError = jest.fn(); + const conditionallyPromptUserForExtension = jest.fn().mockResolvedValue(false); let activeAccount$: BehaviorSubject; @@ -74,7 +76,14 @@ describe("WebVaultPromptService", () => { { provide: ConfigService, useValue: { getFeatureFlag$ } }, { provide: DialogService, useValue: { open } }, { provide: LogService, useValue: { error: logError } }, - { provide: WelcomeDialogService, useValue: { conditionallyShowWelcomeDialog } }, + { + provide: WebVaultExtensionPromptService, + useValue: { conditionallyPromptUserForExtension }, + }, + { + provide: WelcomeDialogService, + useValue: { conditionallyShowWelcomeDialog, conditionallyPromptUserForExtension }, + }, ], }); @@ -97,11 +106,19 @@ describe("WebVaultPromptService", () => { service["vaultItemTransferService"].enforceOrganizationDataOwnership, ).toHaveBeenCalledWith(mockUserId); }); + + it("calls conditionallyPromptUserForExtension with the userId", async () => { + await service.conditionallyPromptUser(); + + expect( + service["webVaultExtensionPromptService"].conditionallyPromptUserForExtension, + ).toHaveBeenCalledWith(mockUserId); + }); }); describe("setupAutoConfirm", () => { it("shows dialog when all conditions are met", fakeAsync(() => { - getFeatureFlag$.mockReturnValueOnce(of(true)); + getFeatureFlag$.mockReturnValue(of(true)); configurationAutoConfirm$.mockReturnValueOnce( of({ showSetupDialog: true, enabled: false, showBrowserNotification: false }), ); diff --git a/apps/web/src/app/vault/services/web-vault-prompt.service.ts b/apps/web/src/app/vault/services/web-vault-prompt.service.ts index 4c4e7a3fe78..aac30e7d0f4 100644 --- a/apps/web/src/app/vault/services/web-vault-prompt.service.ts +++ b/apps/web/src/app/vault/services/web-vault-prompt.service.ts @@ -20,6 +20,7 @@ import { } from "../../admin-console/organizations/policies"; import { UnifiedUpgradePromptService } from "../../billing/individual/upgrade/services"; +import { WebVaultExtensionPromptService } from "./web-vault-extension-prompt.service"; import { WelcomeDialogService } from "./welcome-dialog.service"; @Injectable() @@ -33,6 +34,7 @@ export class WebVaultPromptService { private configService = inject(ConfigService); private dialogService = inject(DialogService); private logService = inject(LogService); + private webVaultExtensionPromptService = inject(WebVaultExtensionPromptService); private welcomeDialogService = inject(WelcomeDialogService); private userId$ = this.accountService.activeAccount$.pipe(getUserId); @@ -57,6 +59,8 @@ export class WebVaultPromptService { await this.welcomeDialogService.conditionallyShowWelcomeDialog(); + await this.webVaultExtensionPromptService.conditionallyPromptUserForExtension(userId); + this.checkForAutoConfirm(); } diff --git a/apps/web/src/images/vault/extension-mock-login.png b/apps/web/src/images/vault/extension-mock-login.png new file mode 100644 index 0000000000000000000000000000000000000000..e002da6db2d494b90485d8008c59ffb241cbb576 GIT binary patch literal 170141 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�mG9faX5r{58M!1e0!_Ni%@Sp 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 literal 0 HcmV?d00001 diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 2d9cba6d409..ef8c109bc4b 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 6fdb146beb8..0cd97eb7f2e 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -70,6 +70,8 @@ export enum FeatureFlag { BrowserPremiumSpotlight = "pm-23384-browser-premium-spotlight", MigrateMyVaultToMyItems = "pm-20558-migrate-myvault-to-myitems", PM27632_SdkCipherCrudOperations = "pm-27632-cipher-crud-operations-to-sdk", + PM29438_WelcomeDialogWithExtensionPrompt = "pm-29438-welcome-dialog-with-extension-prompt", + PM29438_DialogWithExtensionPromptAccountAge = "pm-29438-dialog-with-extension-prompt-account-age", PM29437_WelcomeDialog = "pm-29437-welcome-dialog-no-ext-prompt", PM31039ItemActionInExtension = "pm-31039-item-action-in-extension", @@ -137,6 +139,8 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.BrowserPremiumSpotlight]: FALSE, [FeatureFlag.PM27632_SdkCipherCrudOperations]: FALSE, [FeatureFlag.MigrateMyVaultToMyItems]: FALSE, + [FeatureFlag.PM29438_WelcomeDialogWithExtensionPrompt]: FALSE, + [FeatureFlag.PM29438_DialogWithExtensionPromptAccountAge]: 5, [FeatureFlag.PM29437_WelcomeDialog]: FALSE, /* Auth */ diff --git a/libs/state/src/core/state-definitions.ts b/libs/state/src/core/state-definitions.ts index 30ee2be0592..33c9c780dec 100644 --- a/libs/state/src/core/state-definitions.ts +++ b/libs/state/src/core/state-definitions.ts @@ -220,6 +220,13 @@ export const VAULT_BROWSER_INTRO_CAROUSEL = new StateDefinition( "disk", ); export const VAULT_AT_RISK_PASSWORDS_MEMORY = new StateDefinition("vaultAtRiskPasswords", "memory"); +export const WELCOME_EXTENSION_DIALOG_DISK = new StateDefinition( + "vaultWelcomeExtensionDialogDismissed", + "disk", + { + web: "disk-local", + }, +); // KM From c6234076218944c120a4fe3f9e3d0707250d6d4d Mon Sep 17 00:00:00 2001 From: John Harrington <84741727+harr1424@users.noreply.github.com> Date: Fri, 20 Feb 2026 09:31:49 -0700 Subject: [PATCH 112/134] [PM-32471] [Defect] Importers have regressed during folder migration (#19079) * relax type-checking and add importer test coverage * satisfy lint --- .../models/request/folder-with-id.request.ts | 15 ++- .../bitwarden/bitwarden-csv-importer.spec.ts | 102 ++++++++++++++++++ .../src/services/import.service.spec.ts | 21 ++++ 3 files changed, 137 insertions(+), 1 deletion(-) diff --git a/libs/common/src/vault/models/request/folder-with-id.request.ts b/libs/common/src/vault/models/request/folder-with-id.request.ts index 8af890048ba..aecb12a05fc 100644 --- a/libs/common/src/vault/models/request/folder-with-id.request.ts +++ b/libs/common/src/vault/models/request/folder-with-id.request.ts @@ -1,12 +1,25 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore import { Folder } from "../domain/folder"; import { FolderRequest } from "./folder.request"; export class FolderWithIdRequest extends FolderRequest { + /** + * Declared as `string` (not `string | null`) to satisfy the + * {@link UserKeyRotationDataProvider}`` + * constraint on `FolderService`. + * + * At runtime this is `null` for new import folders. PR #17077 enforced strict type-checking on + * folder models, changing this assignment to `folder.id ?? ""` — causing the importer to send + * `{"id":""}` instead of `{"id":null}`, which the server rejected. + * The `|| null` below restores the pre-migration behavior while `@ts-strict-ignore` above + * allows the `null` assignment against the `string` declaration. + */ id: string; constructor(folder: Folder) { super(folder); - this.id = folder.id ?? ""; + this.id = folder.id || null; } } diff --git a/libs/importer/src/importers/bitwarden/bitwarden-csv-importer.spec.ts b/libs/importer/src/importers/bitwarden/bitwarden-csv-importer.spec.ts index 8f1a281050f..44ee35568d0 100644 --- a/libs/importer/src/importers/bitwarden/bitwarden-csv-importer.spec.ts +++ b/libs/importer/src/importers/bitwarden/bitwarden-csv-importer.spec.ts @@ -120,4 +120,106 @@ describe("BitwardenCsvImporter", () => { expect(result.ciphers.length).toBe(1); expect(result.ciphers[0].archivedDate).toBeUndefined(); }); + + describe("Individual vault imports with folders", () => { + beforeEach(() => { + importer.organizationId = null; + }); + + it("should parse folder and create a folder relationship", async () => { + const data = + `folder,favorite,type,name,login_uri,login_username,login_password` + + `\nSocial,0,login,Facebook,https://facebook.com,user@example.com,password`; + + const result = await importer.parse(data); + + expect(result.success).toBe(true); + expect(result.ciphers.length).toBe(1); + expect(result.folders.length).toBe(1); + expect(result.folders[0].name).toBe("Social"); + expect(result.folderRelationships).toHaveLength(1); + expect(result.folderRelationships[0]).toEqual([0, 0]); + }); + + it("should deduplicate folders when multiple items share the same folder", async () => { + const data = + `folder,favorite,type,name,login_uri,login_username,login_password` + + `\nSocial,0,login,Facebook,https://facebook.com,user1,pass1` + + `\nSocial,0,login,Twitter,https://twitter.com,user2,pass2`; + + const result = await importer.parse(data); + + expect(result.success).toBe(true); + expect(result.ciphers.length).toBe(2); + expect(result.folders.length).toBe(1); + expect(result.folders[0].name).toBe("Social"); + expect(result.folderRelationships).toHaveLength(2); + expect(result.folderRelationships[0]).toEqual([0, 0]); + expect(result.folderRelationships[1]).toEqual([1, 0]); + }); + + it("should create parent folders for nested folder paths", async () => { + const data = + `folder,favorite,type,name,login_uri,login_username,login_password` + + `\nWork/Email,0,login,Gmail,https://gmail.com,user@work.com,pass`; + + const result = await importer.parse(data); + + expect(result.success).toBe(true); + expect(result.folders.length).toBe(2); + expect(result.folders.map((f) => f.name)).toContain("Work/Email"); + expect(result.folders.map((f) => f.name)).toContain("Work"); + expect(result.folderRelationships).toHaveLength(1); + expect(result.folderRelationships[0]).toEqual([0, 0]); + }); + + it("should create no folder or relationship when folder column is empty", async () => { + const data = + `folder,favorite,type,name,login_uri,login_username,login_password` + + `\n,0,login,No Folder Item,https://example.com,user,pass`; + + const result = await importer.parse(data); + + expect(result.success).toBe(true); + expect(result.ciphers.length).toBe(1); + expect(result.folders.length).toBe(0); + expect(result.folderRelationships).toHaveLength(0); + }); + }); + + describe("organization collection import", () => { + it("should set collectionRelationships mapping ciphers to collections", async () => { + const data = + `collections,type,name,login_uri,login_username,login_password` + + `\ncol1,login,Item1,https://example.com,user1,pass1` + + `\ncol2,login,Item2,https://example.com,user2,pass2`; + + const result = await importer.parse(data); + + expect(result.success).toBe(true); + expect(result.ciphers.length).toBe(2); + expect(result.collections.length).toBe(2); + // Each cipher maps to its own collection + expect(result.collectionRelationships).toHaveLength(2); + expect(result.collectionRelationships[0]).toEqual([0, 0]); + expect(result.collectionRelationships[1]).toEqual([1, 1]); + }); + + it("should deduplicate collections and map both ciphers to the shared collection", async () => { + const data = + `collections,type,name,login_uri,login_username,login_password` + + `\nShared,login,Item1,https://example.com,user1,pass1` + + `\nShared,login,Item2,https://example.com,user2,pass2`; + + const result = await importer.parse(data); + + expect(result.success).toBe(true); + expect(result.ciphers.length).toBe(2); + expect(result.collections.length).toBe(1); + expect(result.collections[0].name).toBe("Shared"); + expect(result.collectionRelationships).toHaveLength(2); + expect(result.collectionRelationships[0]).toEqual([0, 0]); + expect(result.collectionRelationships[1]).toEqual([1, 0]); + }); + }); }); diff --git a/libs/importer/src/services/import.service.spec.ts b/libs/importer/src/services/import.service.spec.ts index 33a1e47a4ce..5f9b3c4b085 100644 --- a/libs/importer/src/services/import.service.spec.ts +++ b/libs/importer/src/services/import.service.spec.ts @@ -15,6 +15,8 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; +import { Folder } from "@bitwarden/common/vault/models/domain/folder"; +import { FolderWithIdRequest } from "@bitwarden/common/vault/models/request/folder-with-id.request"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; @@ -280,6 +282,25 @@ describe("ImportService", () => { }); }); +describe("FolderWithIdRequest", () => { + function makeFolder(id: string): Folder { + const folder = new Folder(); + folder.id = id; + return folder; + } + + it("preserves a real folder id", () => { + const guid = "f1a2b3c4-d5e6-7890-abcd-ef1234567890"; + const request = new FolderWithIdRequest(makeFolder(guid)); + expect(request.id).toBe(guid); + }); + + it("sends null when folder id is empty string (new import folder)", () => { + const request = new FolderWithIdRequest(makeFolder("")); + expect(request.id).toBeNull(); + }); +}); + function createCipher(options: Partial = {}) { const cipher = new CipherView(); From 99fdaaec91d57c13ce541e86d4754bcf7a57ad07 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Fri, 20 Feb 2026 11:03:46 -0600 Subject: [PATCH 113/134] cast feature flag mock (#19106) --- libs/components/src/a11y/router-focus-manager.service.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/components/src/a11y/router-focus-manager.service.spec.ts b/libs/components/src/a11y/router-focus-manager.service.spec.ts index 236a05ac038..f7b78296ccb 100644 --- a/libs/components/src/a11y/router-focus-manager.service.spec.ts +++ b/libs/components/src/a11y/router-focus-manager.service.spec.ts @@ -72,7 +72,7 @@ describe("RouterFocusManagerService", () => { return featureFlagSubject.asObservable(); } return new BehaviorSubject(false).asObservable(); - }), + }) as ConfigService["getFeatureFlag$"], }; // Spy on document.querySelector and console.warn From 38bcc9239858ad236f8fa33ecbb0a4cc293b6e76 Mon Sep 17 00:00:00 2001 From: John Harrington <84741727+harr1424@users.noreply.github.com> Date: Fri, 20 Feb 2026 11:03:13 -0700 Subject: [PATCH 114/134] reset otp state on back nav to email input (#19105) --- apps/web/src/app/tools/send/send-access/send-auth.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/src/app/tools/send/send-access/send-auth.component.ts b/apps/web/src/app/tools/send/send-access/send-auth.component.ts index 97b71778539..7617b0a502e 100644 --- a/apps/web/src/app/tools/send/send-access/send-auth.component.ts +++ b/apps/web/src/app/tools/send/send-access/send-auth.component.ts @@ -92,6 +92,7 @@ export class SendAuthComponent implements OnInit { onBackToEmail() { this.enterOtp.set(false); + this.otpSubmitted = false; this.updatePageTitle(); } From 0569ec95172a13de9b8340be37229119d49dd6f5 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 20 Feb 2026 19:09:52 +0100 Subject: [PATCH 115/134] [PM-31880 | BEEEP] Move random out of keyservice to PureCrypto (#18838) * Move random to PureCrypto * Rename and fix builds * Fix tests * Fix build * Prettier * Fix tests --- .../src/abstractions/key.service.ts | 1 - libs/key-management/src/key.service.ts | 35 -------- .../src/generator-services.module.ts | 2 +- .../core/src/engine/email-calculator.spec.ts | 4 + .../core/src/engine/email-randomizer.spec.ts | 4 + .../core/src/engine/forwarder-context.spec.ts | 4 + libs/tools/generator/core/src/engine/index.ts | 2 +- .../src/engine/password-randomizer.spec.ts | 4 + ....spec.ts => purecrypto-randomizer.spec.ts} | 87 ++++++++++--------- ...randomizer.ts => purecrypto-randomizer.ts} | 22 +++-- .../src/engine/username-randomizer.spec.ts | 4 + libs/tools/generator/core/src/factories.ts | 8 +- .../core/src/metadata/email/catchall.spec.ts | 4 + .../src/metadata/email/plus-address.spec.ts | 4 + .../metadata/password/eff-word-list.spec.ts | 4 + .../metadata/password/random-password.spec.ts | 4 + .../metadata/username/eff-word-list.spec.ts | 4 + .../available-algorithms-policy.spec.ts | 4 + ...ynamic-password-policy-constraints.spec.ts | 4 + .../passphrase-policy-constraints.spec.ts | 4 + .../providers/credential-preferences.spec.ts | 4 + .../generator-metadata-provider.spec.ts | 4 + ...fault-credential-generator.service.spec.ts | 4 + .../catchall-generator-strategy.spec.ts | 4 + .../eff-username-generator-strategy.spec.ts | 4 + .../forwarder-generator-strategy.spec.ts | 4 + .../passphrase-generator-strategy.spec.ts | 4 + .../password-generator-strategy.spec.ts | 4 + .../subaddress-generator-strategy.spec.ts | 4 + .../src/types/generated-credential.spec.ts | 4 + .../history/src/generated-credential.spec.ts | 4 + .../local-generator-history.service.spec.ts | 4 + ...eate-legacy-password-generation-service.ts | 4 +- ...eate-legacy-username-generation-service.ts | 4 +- ...legacy-password-generation.service.spec.ts | 4 + ...legacy-username-generation.service.spec.ts | 4 + ...fault-generator-navigation.service.spec.ts | 4 + .../generator-navigation-evaluator.spec.ts | 4 + 38 files changed, 187 insertions(+), 94 deletions(-) rename libs/tools/generator/core/src/engine/{key-service-randomizer.spec.ts => purecrypto-randomizer.spec.ts} (59%) rename libs/tools/generator/core/src/engine/{key-service-randomizer.ts => purecrypto-randomizer.ts} (73%) diff --git a/libs/key-management/src/abstractions/key.service.ts b/libs/key-management/src/abstractions/key.service.ts index 2dedc78a027..e9844ede4bb 100644 --- a/libs/key-management/src/abstractions/key.service.ts +++ b/libs/key-management/src/abstractions/key.service.ts @@ -316,7 +316,6 @@ export abstract class KeyService { * @throws Error when provided userId is null or undefined */ abstract clearKeys(userId: UserId): Promise; - abstract randomNumber(min: number, max: number): Promise; /** * Generates a new cipher key * @returns A new cipher key diff --git a/libs/key-management/src/key.service.ts b/libs/key-management/src/key.service.ts index 7b4e8d83127..7258857d889 100644 --- a/libs/key-management/src/key.service.ts +++ b/libs/key-management/src/key.service.ts @@ -493,41 +493,6 @@ export class DefaultKeyService implements KeyServiceAbstraction { await this.accountCryptographyStateService.clearAccountCryptographicState(userId); } - // EFForg/OpenWireless - // ref https://github.com/EFForg/OpenWireless/blob/master/app/js/diceware.js - async randomNumber(min: number, max: number): Promise { - let rval = 0; - const range = max - min + 1; - const bitsNeeded = Math.ceil(Math.log2(range)); - if (bitsNeeded > 53) { - throw new Error("We cannot generate numbers larger than 53 bits."); - } - - const bytesNeeded = Math.ceil(bitsNeeded / 8); - const mask = Math.pow(2, bitsNeeded) - 1; - // 7776 -> (2^13 = 8192) -1 == 8191 or 0x00001111 11111111 - - // Fill a byte array with N random numbers - const byteArray = new Uint8Array(await this.cryptoFunctionService.randomBytes(bytesNeeded)); - - let p = (bytesNeeded - 1) * 8; - for (let i = 0; i < bytesNeeded; i++) { - rval += byteArray[i] * Math.pow(2, p); - p -= 8; - } - - // Use & to apply the mask and reduce the number of recursive lookups - rval = rval & mask; - - if (rval >= range) { - // Integer out of acceptable range - return this.randomNumber(min, max); - } - - // Return an integer that falls within the range - return min + rval; - } - // ---HELPERS--- async validateUserKey(key: UserKey | MasterKey | null, userId: UserId): Promise { if (key == null) { diff --git a/libs/tools/generator/components/src/generator-services.module.ts b/libs/tools/generator/components/src/generator-services.module.ts index 935f7dc2d60..28e1a325e76 100644 --- a/libs/tools/generator/components/src/generator-services.module.ts +++ b/libs/tools/generator/components/src/generator-services.module.ts @@ -57,7 +57,7 @@ export const SYSTEM_SERVICE_PROVIDER = new SafeInjectionToken { diff --git a/libs/tools/generator/core/src/engine/email-randomizer.spec.ts b/libs/tools/generator/core/src/engine/email-randomizer.spec.ts index 2ebe50d12d4..41ffc8431cd 100644 --- a/libs/tools/generator/core/src/engine/email-randomizer.spec.ts +++ b/libs/tools/generator/core/src/engine/email-randomizer.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { mock } from "jest-mock-extended"; import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist"; diff --git a/libs/tools/generator/core/src/engine/forwarder-context.spec.ts b/libs/tools/generator/core/src/engine/forwarder-context.spec.ts index 9838dbcdbda..e7d848cfed5 100644 --- a/libs/tools/generator/core/src/engine/forwarder-context.spec.ts +++ b/libs/tools/generator/core/src/engine/forwarder-context.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { mock } from "jest-mock-extended"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; diff --git a/libs/tools/generator/core/src/engine/index.ts b/libs/tools/generator/core/src/engine/index.ts index f8008a866e4..1e9cdeb2d39 100644 --- a/libs/tools/generator/core/src/engine/index.ts +++ b/libs/tools/generator/core/src/engine/index.ts @@ -1,4 +1,4 @@ -export { KeyServiceRandomizer } from "./key-service-randomizer"; +export { PureCryptoRandomizer } from "./purecrypto-randomizer"; export { ForwarderConfiguration, AccountRequest } from "./forwarder-configuration"; export { ForwarderContext } from "./forwarder-context"; export * from "./settings"; diff --git a/libs/tools/generator/core/src/engine/password-randomizer.spec.ts b/libs/tools/generator/core/src/engine/password-randomizer.spec.ts index 1d9f58fddd7..83216ece3e3 100644 --- a/libs/tools/generator/core/src/engine/password-randomizer.spec.ts +++ b/libs/tools/generator/core/src/engine/password-randomizer.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { mock } from "jest-mock-extended"; import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist"; diff --git a/libs/tools/generator/core/src/engine/key-service-randomizer.spec.ts b/libs/tools/generator/core/src/engine/purecrypto-randomizer.spec.ts similarity index 59% rename from libs/tools/generator/core/src/engine/key-service-randomizer.spec.ts rename to libs/tools/generator/core/src/engine/purecrypto-randomizer.spec.ts index 459a05618f9..cc8d9d86196 100644 --- a/libs/tools/generator/core/src/engine/key-service-randomizer.spec.ts +++ b/libs/tools/generator/core/src/engine/purecrypto-randomizer.spec.ts @@ -1,19 +1,28 @@ -import { mock } from "jest-mock-extended"; +import { PureCryptoRandomizer } from "./purecrypto-randomizer"; -import { KeyService } from "@bitwarden/key-management"; +jest.mock("@bitwarden/sdk-internal", () => ({ + PureCrypto: { + random_number: jest.fn(), + }, +})); -import { KeyServiceRandomizer } from "./key-service-randomizer"; +jest.mock("@bitwarden/common/platform/abstractions/sdk/sdk-load.service", () => ({ + SdkLoadService: { + Ready: Promise.resolve(), + }, +})); -describe("KeyServiceRandomizer", () => { - const keyService = mock(); +const mockRandomNumber = jest.requireMock("@bitwarden/sdk-internal").PureCrypto + .random_number as jest.Mock; +describe("PureCryptoRandomizer", () => { afterEach(() => { jest.resetAllMocks(); }); describe("pick", () => { it.each([[null], [undefined], [[]]])("throws when the list is %p", async (list) => { - const randomizer = new KeyServiceRandomizer(keyService); + const randomizer = new PureCryptoRandomizer(); await expect(() => randomizer.pick(list)).rejects.toBeInstanceOf(Error); @@ -21,8 +30,8 @@ describe("KeyServiceRandomizer", () => { }); it("picks an item from the list", async () => { - const randomizer = new KeyServiceRandomizer(keyService); - keyService.randomNumber.mockResolvedValue(1); + const randomizer = new PureCryptoRandomizer(); + mockRandomNumber.mockReturnValue(1); const result = await randomizer.pick([0, 1]); @@ -32,7 +41,7 @@ describe("KeyServiceRandomizer", () => { describe("pickWord", () => { it.each([[null], [undefined], [[]]])("throws when the list is %p", async (list) => { - const randomizer = new KeyServiceRandomizer(keyService); + const randomizer = new PureCryptoRandomizer(); await expect(() => randomizer.pickWord(list)).rejects.toBeInstanceOf(Error); @@ -40,8 +49,8 @@ describe("KeyServiceRandomizer", () => { }); it("picks a word from the list", async () => { - const randomizer = new KeyServiceRandomizer(keyService); - keyService.randomNumber.mockResolvedValue(1); + const randomizer = new PureCryptoRandomizer(); + mockRandomNumber.mockReturnValue(1); const result = await randomizer.pickWord(["foo", "bar"]); @@ -49,8 +58,8 @@ describe("KeyServiceRandomizer", () => { }); it("capitalizes the word when options.titleCase is true", async () => { - const randomizer = new KeyServiceRandomizer(keyService); - keyService.randomNumber.mockResolvedValue(1); + const randomizer = new PureCryptoRandomizer(); + mockRandomNumber.mockReturnValue(1); const result = await randomizer.pickWord(["foo", "bar"], { titleCase: true }); @@ -58,9 +67,9 @@ describe("KeyServiceRandomizer", () => { }); it("appends a random number when options.number is true", async () => { - const randomizer = new KeyServiceRandomizer(keyService); - keyService.randomNumber.mockResolvedValueOnce(1); - keyService.randomNumber.mockResolvedValueOnce(2); + const randomizer = new PureCryptoRandomizer(); + mockRandomNumber.mockReturnValueOnce(1); + mockRandomNumber.mockReturnValueOnce(2); const result = await randomizer.pickWord(["foo", "bar"], { number: true }); @@ -70,7 +79,7 @@ describe("KeyServiceRandomizer", () => { describe("shuffle", () => { it.each([[null], [undefined], [[]]])("throws when the list is %p", async (list) => { - const randomizer = new KeyServiceRandomizer(keyService); + const randomizer = new PureCryptoRandomizer(); await expect(() => randomizer.shuffle(list)).rejects.toBeInstanceOf(Error); @@ -78,18 +87,18 @@ describe("KeyServiceRandomizer", () => { }); it("returns a copy of the list without shuffling it when theres only one entry", async () => { - const randomizer = new KeyServiceRandomizer(keyService); + const randomizer = new PureCryptoRandomizer(); const result = await randomizer.shuffle(["foo"]); expect(result).toEqual(["foo"]); expect(result).not.toBe(["foo"]); - expect(keyService.randomNumber).not.toHaveBeenCalled(); + expect(mockRandomNumber).not.toHaveBeenCalled(); }); it("shuffles the tail of the list", async () => { - const randomizer = new KeyServiceRandomizer(keyService); - keyService.randomNumber.mockResolvedValueOnce(0); + const randomizer = new PureCryptoRandomizer(); + mockRandomNumber.mockReturnValueOnce(0); const result = await randomizer.shuffle(["bar", "foo"]); @@ -97,9 +106,9 @@ describe("KeyServiceRandomizer", () => { }); it("shuffles the list", async () => { - const randomizer = new KeyServiceRandomizer(keyService); - keyService.randomNumber.mockResolvedValueOnce(0); - keyService.randomNumber.mockResolvedValueOnce(1); + const randomizer = new PureCryptoRandomizer(); + mockRandomNumber.mockReturnValueOnce(0); + mockRandomNumber.mockReturnValueOnce(1); const result = await randomizer.shuffle(["baz", "bar", "foo"]); @@ -107,8 +116,8 @@ describe("KeyServiceRandomizer", () => { }); it("returns the input list when options.copy is false", async () => { - const randomizer = new KeyServiceRandomizer(keyService); - keyService.randomNumber.mockResolvedValueOnce(0); + const randomizer = new PureCryptoRandomizer(); + mockRandomNumber.mockReturnValueOnce(0); const expectedResult = ["foo"]; const result = await randomizer.shuffle(expectedResult, { copy: false }); @@ -119,7 +128,7 @@ describe("KeyServiceRandomizer", () => { describe("chars", () => { it("returns an empty string when the length is 0", async () => { - const randomizer = new KeyServiceRandomizer(keyService); + const randomizer = new PureCryptoRandomizer(); const result = await randomizer.chars(0); @@ -127,8 +136,8 @@ describe("KeyServiceRandomizer", () => { }); it("returns an arbitrary lowercase ascii character", async () => { - const randomizer = new KeyServiceRandomizer(keyService); - keyService.randomNumber.mockResolvedValueOnce(0); + const randomizer = new PureCryptoRandomizer(); + mockRandomNumber.mockReturnValueOnce(0); const result = await randomizer.chars(1); @@ -136,38 +145,38 @@ describe("KeyServiceRandomizer", () => { }); it("returns a number of ascii characters based on the length", async () => { - const randomizer = new KeyServiceRandomizer(keyService); - keyService.randomNumber.mockResolvedValue(0); + const randomizer = new PureCryptoRandomizer(); + mockRandomNumber.mockReturnValue(0); const result = await randomizer.chars(2); expect(result).toEqual("aa"); - expect(keyService.randomNumber).toHaveBeenCalledTimes(2); + expect(mockRandomNumber).toHaveBeenCalledTimes(2); }); it("returns a new random character each time its called", async () => { - const randomizer = new KeyServiceRandomizer(keyService); - keyService.randomNumber.mockResolvedValueOnce(0); - keyService.randomNumber.mockResolvedValueOnce(1); + const randomizer = new PureCryptoRandomizer(); + mockRandomNumber.mockReturnValueOnce(0); + mockRandomNumber.mockReturnValueOnce(1); const resultA = await randomizer.chars(1); const resultB = await randomizer.chars(1); expect(resultA).toEqual("a"); expect(resultB).toEqual("b"); - expect(keyService.randomNumber).toHaveBeenCalledTimes(2); + expect(mockRandomNumber).toHaveBeenCalledTimes(2); }); }); describe("uniform", () => { it("forwards requests to the crypto service", async () => { - const randomizer = new KeyServiceRandomizer(keyService); - keyService.randomNumber.mockResolvedValue(5); + const randomizer = new PureCryptoRandomizer(); + mockRandomNumber.mockReturnValue(5); const result = await randomizer.uniform(0, 5); expect(result).toBe(5); - expect(keyService.randomNumber).toHaveBeenCalledWith(0, 5); + expect(mockRandomNumber).toHaveBeenCalledWith(0, 5); }); }); }); diff --git a/libs/tools/generator/core/src/engine/key-service-randomizer.ts b/libs/tools/generator/core/src/engine/purecrypto-randomizer.ts similarity index 73% rename from libs/tools/generator/core/src/engine/key-service-randomizer.ts rename to libs/tools/generator/core/src/engine/purecrypto-randomizer.ts index 5fc719042b7..bbd43b8a231 100644 --- a/libs/tools/generator/core/src/engine/key-service-randomizer.ts +++ b/libs/tools/generator/core/src/engine/purecrypto-randomizer.ts @@ -1,14 +1,18 @@ -import { KeyService } from "@bitwarden/key-management"; +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; +import { PureCrypto } from "@bitwarden/sdk-internal"; import { Randomizer } from "../abstractions"; import { WordOptions } from "../types"; -/** A randomizer backed by a KeyService. */ -export class KeyServiceRandomizer implements Randomizer { - /** instantiates the type. - * @param keyService generates random numbers +/** + * A randomizer backed by the SDK. + * Note: This should be replaced by higher level functions in the SDK eventually. + **/ +export class PureCryptoRandomizer implements Randomizer { + /** + * instantiates the type. */ - constructor(private keyService: KeyService) {} + constructor() {} async pick(list: Array): Promise { const length = list?.length ?? 0; @@ -28,7 +32,8 @@ export class KeyServiceRandomizer implements Randomizer { } if (options?.number ?? false) { - const num = await this.keyService.randomNumber(1, 9); + await SdkLoadService.Ready; + const num = PureCrypto.random_number(0, 9); word = word + num.toString(); } @@ -63,6 +68,7 @@ export class KeyServiceRandomizer implements Randomizer { } async uniform(min: number, max: number) { - return this.keyService.randomNumber(min, max); + await SdkLoadService.Ready; + return PureCrypto.random_number(min, max); } } diff --git a/libs/tools/generator/core/src/engine/username-randomizer.spec.ts b/libs/tools/generator/core/src/engine/username-randomizer.spec.ts index be0650fe16e..e8e288b74f9 100644 --- a/libs/tools/generator/core/src/engine/username-randomizer.spec.ts +++ b/libs/tools/generator/core/src/engine/username-randomizer.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { mock } from "jest-mock-extended"; import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist"; diff --git a/libs/tools/generator/core/src/factories.ts b/libs/tools/generator/core/src/factories.ts index 479545c78fe..0cd716f28af 100644 --- a/libs/tools/generator/core/src/factories.ts +++ b/libs/tools/generator/core/src/factories.ts @@ -1,11 +1,9 @@ // contains logic that constructs generator services dynamically given // a generator id. -import { KeyService } from "@bitwarden/key-management"; - import { Randomizer } from "./abstractions"; -import { KeyServiceRandomizer } from "./engine/key-service-randomizer"; +import { PureCryptoRandomizer } from "./engine/purecrypto-randomizer"; -export function createRandomizer(keyService: KeyService): Randomizer { - return new KeyServiceRandomizer(keyService); +export function createRandomizer(): Randomizer { + return new PureCryptoRandomizer(); } diff --git a/libs/tools/generator/core/src/metadata/email/catchall.spec.ts b/libs/tools/generator/core/src/metadata/email/catchall.spec.ts index 1099a6d59ea..4074b4639b6 100644 --- a/libs/tools/generator/core/src/metadata/email/catchall.spec.ts +++ b/libs/tools/generator/core/src/metadata/email/catchall.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { mock } from "jest-mock-extended"; import { EmailRandomizer } from "../../engine"; diff --git a/libs/tools/generator/core/src/metadata/email/plus-address.spec.ts b/libs/tools/generator/core/src/metadata/email/plus-address.spec.ts index befc900ceab..78054133020 100644 --- a/libs/tools/generator/core/src/metadata/email/plus-address.spec.ts +++ b/libs/tools/generator/core/src/metadata/email/plus-address.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { mock } from "jest-mock-extended"; import { EmailRandomizer } from "../../engine"; diff --git a/libs/tools/generator/core/src/metadata/password/eff-word-list.spec.ts b/libs/tools/generator/core/src/metadata/password/eff-word-list.spec.ts index bdf021c50f3..aacb82a51c4 100644 --- a/libs/tools/generator/core/src/metadata/password/eff-word-list.spec.ts +++ b/libs/tools/generator/core/src/metadata/password/eff-word-list.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { mock } from "jest-mock-extended"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; diff --git a/libs/tools/generator/core/src/metadata/password/random-password.spec.ts b/libs/tools/generator/core/src/metadata/password/random-password.spec.ts index 9efd5350c21..743a8d53a2b 100644 --- a/libs/tools/generator/core/src/metadata/password/random-password.spec.ts +++ b/libs/tools/generator/core/src/metadata/password/random-password.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { mock } from "jest-mock-extended"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; diff --git a/libs/tools/generator/core/src/metadata/username/eff-word-list.spec.ts b/libs/tools/generator/core/src/metadata/username/eff-word-list.spec.ts index beebb016504..0f6653360db 100644 --- a/libs/tools/generator/core/src/metadata/username/eff-word-list.spec.ts +++ b/libs/tools/generator/core/src/metadata/username/eff-word-list.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { mock } from "jest-mock-extended"; import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state-constraint"; diff --git a/libs/tools/generator/core/src/policies/available-algorithms-policy.spec.ts b/libs/tools/generator/core/src/policies/available-algorithms-policy.spec.ts index 7de8c708dcf..085ea72e5d5 100644 --- a/libs/tools/generator/core/src/policies/available-algorithms-policy.spec.ts +++ b/libs/tools/generator/core/src/policies/available-algorithms-policy.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { PolicyId } from "@bitwarden/common/types/guid"; diff --git a/libs/tools/generator/core/src/policies/dynamic-password-policy-constraints.spec.ts b/libs/tools/generator/core/src/policies/dynamic-password-policy-constraints.spec.ts index 0bebb0825bf..4d4d54a6e18 100644 --- a/libs/tools/generator/core/src/policies/dynamic-password-policy-constraints.spec.ts +++ b/libs/tools/generator/core/src/policies/dynamic-password-policy-constraints.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { ObjectKey } from "@bitwarden/common/tools/state/object-key"; import { BuiltIn, Profile } from "../metadata"; diff --git a/libs/tools/generator/core/src/policies/passphrase-policy-constraints.spec.ts b/libs/tools/generator/core/src/policies/passphrase-policy-constraints.spec.ts index 6306382c84e..c640b05da8e 100644 --- a/libs/tools/generator/core/src/policies/passphrase-policy-constraints.spec.ts +++ b/libs/tools/generator/core/src/policies/passphrase-policy-constraints.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { BuiltIn, Profile } from "../metadata"; import { PassphrasePolicyConstraints } from "./passphrase-policy-constraints"; diff --git a/libs/tools/generator/core/src/providers/credential-preferences.spec.ts b/libs/tools/generator/core/src/providers/credential-preferences.spec.ts index 6fd747f3823..cb1ed620c5b 100644 --- a/libs/tools/generator/core/src/providers/credential-preferences.spec.ts +++ b/libs/tools/generator/core/src/providers/credential-preferences.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { AlgorithmsByType, Type } from "../metadata"; import { CredentialPreference } from "../types"; diff --git a/libs/tools/generator/core/src/providers/generator-metadata-provider.spec.ts b/libs/tools/generator/core/src/providers/generator-metadata-provider.spec.ts index 39ff74ad901..58b7c1f6804 100644 --- a/libs/tools/generator/core/src/providers/generator-metadata-provider.spec.ts +++ b/libs/tools/generator/core/src/providers/generator-metadata-provider.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { mock } from "jest-mock-extended"; import { BehaviorSubject, ReplaySubject, firstValueFrom } from "rxjs"; diff --git a/libs/tools/generator/core/src/services/default-credential-generator.service.spec.ts b/libs/tools/generator/core/src/services/default-credential-generator.service.spec.ts index e459bb47f47..5c8389b7377 100644 --- a/libs/tools/generator/core/src/services/default-credential-generator.service.spec.ts +++ b/libs/tools/generator/core/src/services/default-credential-generator.service.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { BehaviorSubject, Subject, firstValueFrom, of } from "rxjs"; import { Account } from "@bitwarden/common/auth/abstractions/account.service"; diff --git a/libs/tools/generator/core/src/strategies/catchall-generator-strategy.spec.ts b/libs/tools/generator/core/src/strategies/catchall-generator-strategy.spec.ts index 99f618a4520..2de938c13dc 100644 --- a/libs/tools/generator/core/src/strategies/catchall-generator-strategy.spec.ts +++ b/libs/tools/generator/core/src/strategies/catchall-generator-strategy.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { mock } from "jest-mock-extended"; import { of, firstValueFrom } from "rxjs"; diff --git a/libs/tools/generator/core/src/strategies/eff-username-generator-strategy.spec.ts b/libs/tools/generator/core/src/strategies/eff-username-generator-strategy.spec.ts index d3582127ade..ba98eb75e23 100644 --- a/libs/tools/generator/core/src/strategies/eff-username-generator-strategy.spec.ts +++ b/libs/tools/generator/core/src/strategies/eff-username-generator-strategy.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { mock } from "jest-mock-extended"; import { of, firstValueFrom } from "rxjs"; diff --git a/libs/tools/generator/core/src/strategies/forwarder-generator-strategy.spec.ts b/libs/tools/generator/core/src/strategies/forwarder-generator-strategy.spec.ts index 99834a25417..3038d9bbdf6 100644 --- a/libs/tools/generator/core/src/strategies/forwarder-generator-strategy.spec.ts +++ b/libs/tools/generator/core/src/strategies/forwarder-generator-strategy.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { mock } from "jest-mock-extended"; import { of, firstValueFrom } from "rxjs"; diff --git a/libs/tools/generator/core/src/strategies/passphrase-generator-strategy.spec.ts b/libs/tools/generator/core/src/strategies/passphrase-generator-strategy.spec.ts index ee521d753ae..9c9bf92aa64 100644 --- a/libs/tools/generator/core/src/strategies/passphrase-generator-strategy.spec.ts +++ b/libs/tools/generator/core/src/strategies/passphrase-generator-strategy.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { mock } from "jest-mock-extended"; import { of, firstValueFrom } from "rxjs"; diff --git a/libs/tools/generator/core/src/strategies/password-generator-strategy.spec.ts b/libs/tools/generator/core/src/strategies/password-generator-strategy.spec.ts index 94e7c16be28..52d0d4fa272 100644 --- a/libs/tools/generator/core/src/strategies/password-generator-strategy.spec.ts +++ b/libs/tools/generator/core/src/strategies/password-generator-strategy.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { mock } from "jest-mock-extended"; import { of, firstValueFrom } from "rxjs"; diff --git a/libs/tools/generator/core/src/strategies/subaddress-generator-strategy.spec.ts b/libs/tools/generator/core/src/strategies/subaddress-generator-strategy.spec.ts index 0be5132c67f..fdd08dccb6b 100644 --- a/libs/tools/generator/core/src/strategies/subaddress-generator-strategy.spec.ts +++ b/libs/tools/generator/core/src/strategies/subaddress-generator-strategy.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { mock } from "jest-mock-extended"; import { of, firstValueFrom } from "rxjs"; diff --git a/libs/tools/generator/core/src/types/generated-credential.spec.ts b/libs/tools/generator/core/src/types/generated-credential.spec.ts index 3d8d2c9bd4d..bb4cce34052 100644 --- a/libs/tools/generator/core/src/types/generated-credential.spec.ts +++ b/libs/tools/generator/core/src/types/generated-credential.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { Type } from "../metadata"; import { GeneratedCredential } from "./generated-credential"; diff --git a/libs/tools/generator/extensions/history/src/generated-credential.spec.ts b/libs/tools/generator/extensions/history/src/generated-credential.spec.ts index 26a48cb83ea..3d3bd43126a 100644 --- a/libs/tools/generator/extensions/history/src/generated-credential.spec.ts +++ b/libs/tools/generator/extensions/history/src/generated-credential.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { Type } from "@bitwarden/generator-core"; import { GeneratedCredential } from "."; diff --git a/libs/tools/generator/extensions/history/src/local-generator-history.service.spec.ts b/libs/tools/generator/extensions/history/src/local-generator-history.service.spec.ts index 81335887f0d..5724b201f30 100644 --- a/libs/tools/generator/extensions/history/src/local-generator-history.service.spec.ts +++ b/libs/tools/generator/extensions/history/src/local-generator-history.service.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { mock } from "jest-mock-extended"; import { firstValueFrom, of } from "rxjs"; diff --git a/libs/tools/generator/extensions/legacy/src/create-legacy-password-generation-service.ts b/libs/tools/generator/extensions/legacy/src/create-legacy-password-generation-service.ts index 0048ce15499..6fbdf4afbed 100644 --- a/libs/tools/generator/extensions/legacy/src/create-legacy-password-generation-service.ts +++ b/libs/tools/generator/extensions/legacy/src/create-legacy-password-generation-service.ts @@ -13,7 +13,7 @@ import { LegacyPasswordGenerationService } from "./legacy-password-generation.se import { PasswordGenerationServiceAbstraction } from "./password-generation.service.abstraction"; const { PassphraseGeneratorStrategy, PasswordGeneratorStrategy } = strategies; -const { KeyServiceRandomizer, PasswordRandomizer } = engine; +const { PureCryptoRandomizer, PasswordRandomizer } = engine; const DefaultGeneratorService = services.DefaultGeneratorService; @@ -24,7 +24,7 @@ export function legacyPasswordGenerationServiceFactory( accountService: AccountService, stateProvider: StateProvider, ): PasswordGenerationServiceAbstraction { - const randomizer = new KeyServiceRandomizer(keyService); + const randomizer = new PureCryptoRandomizer(); const passwordRandomizer = new PasswordRandomizer(randomizer, Date.now); const passwords = new DefaultGeneratorService( diff --git a/libs/tools/generator/extensions/legacy/src/create-legacy-username-generation-service.ts b/libs/tools/generator/extensions/legacy/src/create-legacy-username-generation-service.ts index 36b4a20aec7..2507c5f7ecd 100644 --- a/libs/tools/generator/extensions/legacy/src/create-legacy-username-generation-service.ts +++ b/libs/tools/generator/extensions/legacy/src/create-legacy-username-generation-service.ts @@ -14,7 +14,7 @@ import { KeyService } from "@bitwarden/key-management"; import { LegacyUsernameGenerationService } from "./legacy-username-generation.service"; import { UsernameGenerationServiceAbstraction } from "./username-generation.service.abstraction"; -const { KeyServiceRandomizer, UsernameRandomizer, EmailRandomizer, EmailCalculator } = engine; +const { PureCryptoRandomizer, UsernameRandomizer, EmailRandomizer, EmailCalculator } = engine; const DefaultGeneratorService = services.DefaultGeneratorService; const { CatchallGeneratorStrategy, @@ -32,7 +32,7 @@ export function legacyUsernameGenerationServiceFactory( accountService: AccountService, stateProvider: StateProvider, ): UsernameGenerationServiceAbstraction { - const randomizer = new KeyServiceRandomizer(keyService); + const randomizer = new PureCryptoRandomizer(); const restClient = new RestClient(apiService, i18nService); const usernameRandomizer = new UsernameRandomizer(randomizer); const emailRandomizer = new EmailRandomizer(randomizer); diff --git a/libs/tools/generator/extensions/legacy/src/legacy-password-generation.service.spec.ts b/libs/tools/generator/extensions/legacy/src/legacy-password-generation.service.spec.ts index f575cd3b619..7846d78c77b 100644 --- a/libs/tools/generator/extensions/legacy/src/legacy-password-generation.service.spec.ts +++ b/libs/tools/generator/extensions/legacy/src/legacy-password-generation.service.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { mock } from "jest-mock-extended"; import { of } from "rxjs"; diff --git a/libs/tools/generator/extensions/legacy/src/legacy-username-generation.service.spec.ts b/libs/tools/generator/extensions/legacy/src/legacy-username-generation.service.spec.ts index 5a4dce4f4a5..de2059f86ef 100644 --- a/libs/tools/generator/extensions/legacy/src/legacy-username-generation.service.spec.ts +++ b/libs/tools/generator/extensions/legacy/src/legacy-username-generation.service.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { mock } from "jest-mock-extended"; diff --git a/libs/tools/generator/extensions/navigation/src/default-generator-navigation.service.spec.ts b/libs/tools/generator/extensions/navigation/src/default-generator-navigation.service.spec.ts index 37e8ec6e379..cd98ea30ace 100644 --- a/libs/tools/generator/extensions/navigation/src/default-generator-navigation.service.spec.ts +++ b/libs/tools/generator/extensions/navigation/src/default-generator-navigation.service.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { mock } from "jest-mock-extended"; import { firstValueFrom, of } from "rxjs"; diff --git a/libs/tools/generator/extensions/navigation/src/generator-navigation-evaluator.spec.ts b/libs/tools/generator/extensions/navigation/src/generator-navigation-evaluator.spec.ts index 82b9e29e91a..999edb2d25c 100644 --- a/libs/tools/generator/extensions/navigation/src/generator-navigation-evaluator.spec.ts +++ b/libs/tools/generator/extensions/navigation/src/generator-navigation-evaluator.spec.ts @@ -1,3 +1,7 @@ +/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally +import { TextEncoder, TextDecoder } from "util"; +Object.assign(global, { TextDecoder, TextEncoder }); + import { DefaultGeneratorNavigation } from "./default-generator-navigation"; import { GeneratorNavigationEvaluator } from "./generator-navigation-evaluator"; From 3a56f2e832ac373a038408466690449a77b3ea58 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 20 Feb 2026 19:22:05 +0100 Subject: [PATCH 116/134] [PM-30785|BEEEP] Remove deprecated master key login with device flow (#17943) * Remove deprecated master key login with device flow * Resolve conflicts / cleanup * Linting * Fix lint * Run prettier --- .../angular/login-via-auth-request/README.md | 31 ++------ .../login-via-auth-request.component.ts | 77 ++++--------------- .../auth-request.service.abstraction.ts | 25 +----- .../auth-request-login.strategy.spec.ts | 45 +---------- .../auth-request-login.strategy.ts | 15 +--- .../sso-login.strategy.spec.ts | 21 ----- .../login-strategies/sso-login.strategy.ts | 22 ++---- .../common/models/domain/login-credentials.ts | 7 +- .../auth-request/auth-request.service.spec.ts | 56 +------------- .../auth-request/auth-request.service.ts | 48 +----------- .../login-strategy.state.spec.ts | 2 - .../models/response/auth-request.response.ts | 4 +- .../crypto/abstractions/encrypt.service.ts | 7 -- .../encrypt.service.implementation.ts | 20 ----- 14 files changed, 34 insertions(+), 346 deletions(-) diff --git a/libs/auth/src/angular/login-via-auth-request/README.md b/libs/auth/src/angular/login-via-auth-request/README.md index 3396ba8698b..d1c4ddf3f0a 100644 --- a/libs/auth/src/angular/login-via-auth-request/README.md +++ b/libs/auth/src/angular/login-via-auth-request/README.md @@ -13,16 +13,9 @@ ## Standard Auth Request Flows -### Flow 1: Unauthed user requests approval from device; Approving device has a masterKey in memory +### Flow 1: This flow was removed -1. Unauthed user clicks "Login with device" -2. Navigates to `/login-with-device` which creates a `StandardAuthRequest` -3. Receives approval from a device with authRequestPublicKey(masterKey) -4. Decrypts masterKey -5. Decrypts userKey -6. Proceeds to vault - -### Flow 2: Unauthed user requests approval from device; Approving device does NOT have a masterKey in memory +### Flow 2: Unauthed user requests approval from device; Approving device does NOT need to have a masterKey in memory 1. Unauthed user clicks "Login with device" 2. Navigates to `/login-with-device` which creates a `StandardAuthRequest` @@ -33,28 +26,18 @@ **Note:** This flow is an uncommon scenario and relates to TDE off-boarding. The following describes how a user could get into this flow: -1. An SSO TD user logs into a device via an Admin auth request approval, therefore this device does NOT have a masterKey +1. An SSO TD user logs into a device via an Admin auth request approval, therefore this device does NOT need to have a masterKey in memory 2. The org admin: - Changes the member decryption options from "Trusted devices" to "Master password" AND - Turns off the "Require single sign-on authentication" policy 3. On another device, the user clicks "Login with device", which they can do because the org no longer requires SSO -4. The user approves from the device they had previously logged into with SSO TD, which does NOT have a masterKey in +4. The user approves from the device they had previously logged into with SSO TD, which does NOT need to have a masterKey in memory -### Flow 3: Authed SSO TD user requests approval from device; Approving device has a masterKey in memory +### Flow 3: This flow was removed -1. SSO TD user authenticates via SSO -2. Navigates to `/login-initiated` -3. Clicks "Approve from your other device" -4. Navigates to `/login-with-device` which creates a `StandardAuthRequest` -5. Receives approval from device with authRequestPublicKey(masterKey) -6. Decrypts masterKey -7. Decrypts userKey -8. Establishes trust (if required) -9. Proceeds to vault - -### Flow 4: Authed SSO TD user requests approval from device; Approving device does NOT have a masterKey in memory +### Flow 4: Authed SSO TD user requests approval from device; Approving device does NOT need to have a masterKey in memory 1. SSO TD user authenticates via SSO 2. Navigates to `/login-initiated` @@ -89,9 +72,7 @@ userKey. This is how admins are able to send over the authRequestPublicKey(userK | Flow | Auth Status | Clicks Button [active route] | Navigates to | Approving device has masterKey in memory\* | | --------------- | ----------- | ----------------------------------------------------- | --------------------------- | ------------------------------------------------- | -| Standard Flow 1 | unauthed | "Login with device" [`/login`] | `/login-with-device` | yes | | Standard Flow 2 | unauthed | "Login with device" [`/login`] | `/login-with-device` | no | -| Standard Flow 3 | authed | "Approve from your other device" [`/login-initiated`] | `/login-with-device` | yes | | Standard Flow 4 | authed | "Approve from your other device" [`/login-initiated`] | `/login-with-device` | no | | Admin Flow | authed | "Request admin approval"
    [`/login-initiated`] | `/admin-approval-requested` | NA - admin requests always send encrypted userKey | diff --git a/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts b/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts index fc91f220138..c7046d39022 100644 --- a/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts +++ b/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts @@ -605,10 +605,10 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { if (authRequestResponse.requestApproved) { const userHasAuthenticatedViaSSO = this.authStatus === AuthenticationStatus.Locked; if (userHasAuthenticatedViaSSO) { - // [Standard Flow 3-4] Handle authenticated SSO TD user flows + // [Standard Flow 4] Handle authenticated SSO TD user flows return await this.handleAuthenticatedFlows(authRequestResponse); } else { - // [Standard Flow 1-2] Handle unauthenticated user flows + // [Standard Flow 2] Handle unauthenticated user flows return await this.handleUnauthenticatedFlows(authRequestResponse, requestId); } } @@ -629,7 +629,7 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { } private async handleAuthenticatedFlows(authRequestResponse: AuthRequestResponse) { - // [Standard Flow 3-4] Handle authenticated SSO TD user flows + // [Standard Flow 4] Handle authenticated SSO TD user flows const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; if (!userId) { this.logService.error( @@ -654,7 +654,7 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { authRequestResponse: AuthRequestResponse, requestId: string, ) { - // [Standard Flow 1-2] Handle unauthenticated user flows + // [Standard Flow 2] Handle unauthenticated user flows const authRequestLoginCredentials = await this.buildAuthRequestLoginCredentials( requestId, authRequestResponse, @@ -679,27 +679,12 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { privateKey: Uint8Array, userId: UserId, ): Promise { - /** - * [Flow Type Detection] - * We determine the type of `key` based on the presence or absence of `masterPasswordHash`: - * - If `masterPasswordHash` exists: Standard Flow 1 or 3 (device has masterKey) - * - If no `masterPasswordHash`: Standard Flow 2, 4, or Admin Flow (device sends userKey) - */ - if (authRequestResponse.masterPasswordHash) { - // [Standard Flow 1 or 3] Device has masterKey - await this.authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash( - authRequestResponse, - privateKey, - userId, - ); - } else { - // [Standard Flow 2, 4, or Admin Flow] Device sends userKey - await this.authRequestService.setUserKeyAfterDecryptingSharedUserKey( - authRequestResponse, - privateKey, - userId, - ); - } + // [Standard Flow 2, 4, or Admin Flow] Device sends userKey + await this.authRequestService.setUserKeyAfterDecryptingSharedUserKey( + authRequestResponse, + privateKey, + userId, + ); // [Admin Flow Cleanup] Clear one-time use admin auth request // clear the admin auth request from state so it cannot be used again (it's a one time use) @@ -758,43 +743,13 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { /** * See verifyAndHandleApprovedAuthReq() for flow details. - * - * We determine the type of `key` based on the presence or absence of `masterPasswordHash`: - * - If `masterPasswordHash` has a value, we receive the `key` as an authRequestPublicKey(masterKey) [plus we have authRequestPublicKey(masterPasswordHash)] - * - If `masterPasswordHash` does not have a value, we receive the `key` as an authRequestPublicKey(userKey) */ - if (authRequestResponse.masterPasswordHash) { - // ...in Standard Auth Request Flow 1 - const { masterKey, masterKeyHash } = - await this.authRequestService.decryptPubKeyEncryptedMasterKeyAndHash( - authRequestResponse.key, - authRequestResponse.masterPasswordHash, - this.authRequestKeyPair.privateKey, - ); - - return new AuthRequestLoginCredentials( - this.email, - this.accessCode, - requestId, - null, // no userKey - masterKey, - masterKeyHash, - ); - } else { - // ...in Standard Auth Request Flow 2 - const userKey = await this.authRequestService.decryptPubKeyEncryptedUserKey( - authRequestResponse.key, - this.authRequestKeyPair.privateKey, - ); - return new AuthRequestLoginCredentials( - this.email, - this.accessCode, - requestId, - userKey, - null, // no masterKey - null, // no masterKeyHash - ); - } + // ...in Standard Auth Request Flow 2 + const userKey = await this.authRequestService.decryptPubKeyEncryptedUserKey( + authRequestResponse.key, + this.authRequestKeyPair.privateKey, + ); + return new AuthRequestLoginCredentials(this.email, this.accessCode, requestId, userKey); } private async clearExistingAdminAuthRequestAndStartNewRequest(userId: UserId) { diff --git a/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts b/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts index 1077bc024e9..04128768759 100644 --- a/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts +++ b/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts @@ -4,7 +4,7 @@ import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/a import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response"; import { UserId } from "@bitwarden/common/types/guid"; -import { UserKey, MasterKey } from "@bitwarden/common/types/key"; +import { UserKey } from "@bitwarden/common/types/key"; export abstract class AuthRequestServiceAbstraction { /** Emits an auth request id when an auth request has been approved. */ @@ -75,17 +75,6 @@ export abstract class AuthRequestServiceAbstraction { authReqPrivateKey: Uint8Array, userId: UserId, ): Promise; - /** - * Sets the `MasterKey` and `MasterKeyHash` from an auth request. Auth request must have a `MasterKey` and `MasterKeyHash`. - * @param authReqResponse The auth request. - * @param authReqPrivateKey The private key corresponding to the public key sent in the auth request. - * @param userId The ID of the user for whose account we will set the keys. - */ - abstract setKeysAfterDecryptingSharedMasterKeyAndHash( - authReqResponse: AuthRequestResponse, - authReqPrivateKey: Uint8Array, - userId: UserId, - ): Promise; /** * Decrypts a `UserKey` from a public key encrypted `UserKey`. * @param pubKeyEncryptedUserKey The public key encrypted `UserKey`. @@ -96,18 +85,6 @@ export abstract class AuthRequestServiceAbstraction { pubKeyEncryptedUserKey: string, privateKey: Uint8Array, ): Promise; - /** - * Decrypts a `MasterKey` and `MasterKeyHash` from a public key encrypted `MasterKey` and `MasterKeyHash`. - * @param pubKeyEncryptedMasterKey The public key encrypted `MasterKey`. - * @param pubKeyEncryptedMasterKeyHash The public key encrypted `MasterKeyHash`. - * @param privateKey The private key corresponding to the public key used to encrypt the `MasterKey` and `MasterKeyHash`. - * @returns The decrypted `MasterKey` and `MasterKeyHash`. - */ - abstract decryptPubKeyEncryptedMasterKeyAndHash( - pubKeyEncryptedMasterKey: string, - pubKeyEncryptedMasterKeyHash: string, - privateKey: Uint8Array, - ): Promise<{ masterKey: MasterKey; masterKeyHash: string }>; /** * Handles incoming auth request push server notifications. diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts index 275f2d97aa4..b07dc1202de 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts @@ -26,7 +26,7 @@ import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/sym import { makeEncString, FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { CsprngArray } from "@bitwarden/common/types/csprng"; import { UserId } from "@bitwarden/common/types/guid"; -import { MasterKey, UserKey } from "@bitwarden/common/types/key"; +import { UserKey } from "@bitwarden/common/types/key"; import { KdfConfigService, KeyService } from "@bitwarden/key-management"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; @@ -73,11 +73,7 @@ describe("AuthRequestLoginStrategy", () => { const email = "EMAIL"; const accessCode = "ACCESS_CODE"; const authRequestId = "AUTH_REQUEST_ID"; - const decMasterKey = new SymmetricCryptoKey( - new Uint8Array(64).buffer as CsprngArray, - ) as MasterKey; const decUserKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey; - const decMasterKeyHash = "LOCAL_PASSWORD_HASH"; beforeEach(async () => { keyService = mock(); @@ -150,42 +146,6 @@ describe("AuthRequestLoginStrategy", () => { ); }); - it("sets keys after a successful authentication when masterKey and masterKeyHash provided in login credentials", async () => { - credentials = new AuthRequestLoginCredentials( - email, - accessCode, - authRequestId, - null, - decMasterKey, - decMasterKeyHash, - ); - - const masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey; - const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey; - - masterPasswordService.masterKeySubject.next(masterKey); - masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); - tokenService.decodeAccessToken.mockResolvedValue({ sub: mockUserId }); - - await authRequestLoginStrategy.logIn(credentials); - - expect(masterPasswordService.mock.setMasterKey).toHaveBeenCalledWith(masterKey, mockUserId); - expect(masterPasswordService.mock.setMasterKeyHash).toHaveBeenCalledWith( - decMasterKeyHash, - mockUserId, - ); - expect(masterPasswordService.mock.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith( - tokenResponse.key, - mockUserId, - ); - expect(keyService.setUserKey).toHaveBeenCalledWith(userKey, mockUserId); - expect(deviceTrustService.trustDeviceIfRequired).toHaveBeenCalled(); - expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledWith( - { V1: { private_key: tokenResponse.privateKey } }, - mockUserId, - ); - }); - it("sets keys after a successful authentication when only userKey provided in login credentials", async () => { // Initialize credentials with only userKey credentials = new AuthRequestLoginCredentials( @@ -193,8 +153,6 @@ describe("AuthRequestLoginStrategy", () => { accessCode, authRequestId, decUserKey, // Pass userKey - null, // No masterKey - null, // No masterKeyHash ); // Call logIn @@ -240,7 +198,6 @@ describe("AuthRequestLoginStrategy", () => { }; apiService.postIdentityToken.mockResolvedValue(tokenResponse); - masterPasswordService.masterKeySubject.next(decMasterKey); masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(decUserKey); await authRequestLoginStrategy.logIn(credentials); diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts index 66b9ee83919..1f3eaf7c164 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts @@ -72,20 +72,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy { } protected override async setMasterKey(response: IdentityTokenResponse, userId: UserId) { - const authRequestCredentials = this.cache.value.authRequestCredentials; - if ( - authRequestCredentials.decryptedMasterKey && - authRequestCredentials.decryptedMasterKeyHash - ) { - await this.masterPasswordService.setMasterKey( - authRequestCredentials.decryptedMasterKey, - userId, - ); - await this.masterPasswordService.setMasterKeyHash( - authRequestCredentials.decryptedMasterKeyHash, - userId, - ); - } + // This login strategy does not use a master key } protected override async setUserKey( diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts index 4f0a6bbf73f..f7a97baba0a 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts @@ -416,24 +416,6 @@ describe("SsoLoginStrategy", () => { ); }); - it("sets the user key using master key and hash from approved admin request if exists", async () => { - apiService.postIdentityToken.mockResolvedValue(tokenResponse); - keyService.hasUserKey.mockResolvedValue(true); - const adminAuthResponse = { - id: "1", - publicKey: "PRIVATE" as any, - key: "KEY" as any, - masterPasswordHash: "HASH" as any, - requestApproved: true, - }; - apiService.getAuthRequest.mockResolvedValue(adminAuthResponse as AuthRequestResponse); - - await ssoLoginStrategy.logIn(credentials); - - expect(authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash).toHaveBeenCalled(); - expect(deviceTrustService.decryptUserKeyWithDeviceKey).not.toHaveBeenCalled(); - }); - it("sets the user key from approved admin request if exists", async () => { apiService.postIdentityToken.mockResolvedValue(tokenResponse); keyService.hasUserKey.mockResolvedValue(true); @@ -475,9 +457,6 @@ describe("SsoLoginStrategy", () => { await ssoLoginStrategy.logIn(credentials); expect(authRequestService.clearAdminAuthRequest).toHaveBeenCalled(); - expect( - authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash, - ).not.toHaveBeenCalled(); expect(authRequestService.setUserKeyAfterDecryptingSharedUserKey).not.toHaveBeenCalled(); expect(deviceTrustService.trustDeviceIfRequired).not.toHaveBeenCalled(); }); diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.ts index 6a57d11e29d..9c889d9d460 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.ts @@ -239,23 +239,11 @@ export class SsoLoginStrategy extends LoginStrategy { } if (adminAuthReqResponse?.requestApproved) { - // if masterPasswordHash has a value, we will always receive authReqResponse.key - // as authRequestPublicKey(masterKey) + authRequestPublicKey(masterPasswordHash) - if (adminAuthReqResponse.masterPasswordHash) { - await this.authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash( - adminAuthReqResponse, - adminAuthReqStorable.privateKey, - userId, - ); - } else { - // if masterPasswordHash is null, we will always receive authReqResponse.key - // as authRequestPublicKey(userKey) - await this.authRequestService.setUserKeyAfterDecryptingSharedUserKey( - adminAuthReqResponse, - adminAuthReqStorable.privateKey, - userId, - ); - } + await this.authRequestService.setUserKeyAfterDecryptingSharedUserKey( + adminAuthReqResponse, + adminAuthReqStorable.privateKey, + userId, + ); if (await this.keyService.hasUserKey(userId)) { // Now that we have a decrypted user key in memory, we can check if we diff --git a/libs/auth/src/common/models/domain/login-credentials.ts b/libs/auth/src/common/models/domain/login-credentials.ts index 96ee88945eb..608b36e908b 100644 --- a/libs/auth/src/common/models/domain/login-credentials.ts +++ b/libs/auth/src/common/models/domain/login-credentials.ts @@ -7,7 +7,7 @@ import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication- import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { WebAuthnLoginAssertionResponseRequest } from "@bitwarden/common/auth/services/webauthn-login/request/webauthn-login-assertion-response.request"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { UserKey, MasterKey } from "@bitwarden/common/types/key"; +import { UserKey } from "@bitwarden/common/types/key"; export class PasswordLoginCredentials { readonly type = AuthenticationType.Password; @@ -54,8 +54,6 @@ export class AuthRequestLoginCredentials { public accessCode: string, public authRequestId: string, public decryptedUserKey: UserKey | null, - public decryptedMasterKey: MasterKey | null, - public decryptedMasterKeyHash: string | null, public twoFactor?: TokenTwoFactorRequest, ) {} @@ -66,8 +64,6 @@ export class AuthRequestLoginCredentials { json.accessCode, json.authRequestId, null, - null, - json.decryptedMasterKeyHash, json.twoFactor ? new TokenTwoFactorRequest( json.twoFactor.provider, @@ -78,7 +74,6 @@ export class AuthRequestLoginCredentials { ), { decryptedUserKey: SymmetricCryptoKey.fromJSON(json.decryptedUserKey) as UserKey, - decryptedMasterKey: SymmetricCryptoKey.fromJSON(json.decryptedMasterKey) as MasterKey, }, ); } diff --git a/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts b/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts index 8cb0cc279ae..a3f79f45ad5 100644 --- a/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts +++ b/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts @@ -13,7 +13,7 @@ import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.ser import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { StateProvider } from "@bitwarden/common/platform/state"; import { UserId } from "@bitwarden/common/types/guid"; -import { MasterKey, UserKey } from "@bitwarden/common/types/key"; +import { UserKey } from "@bitwarden/common/types/key"; import { newGuid } from "@bitwarden/guid"; import { KeyService } from "@bitwarden/key-management"; @@ -154,60 +154,6 @@ describe("AuthRequestService", () => { }); }); - describe("setKeysAfterDecryptingSharedMasterKeyAndHash", () => { - it("decrypts and sets master key and hash and user key when given valid auth request response and private key", async () => { - // Arrange - const mockAuthReqResponse = { - key: "authReqPublicKeyEncryptedMasterKey", - masterPasswordHash: "authReqPublicKeyEncryptedMasterKeyHash", - } as AuthRequestResponse; - - const mockDecryptedMasterKey = {} as MasterKey; - const mockDecryptedMasterKeyHash = "mockDecryptedMasterKeyHash"; - const mockDecryptedUserKey = {} as UserKey; - - jest.spyOn(sut, "decryptPubKeyEncryptedMasterKeyAndHash").mockResolvedValueOnce({ - masterKey: mockDecryptedMasterKey, - masterKeyHash: mockDecryptedMasterKeyHash, - }); - - masterPasswordService.masterKeySubject.next(undefined); - masterPasswordService.masterKeyHashSubject.next(undefined); - masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue( - mockDecryptedUserKey, - ); - keyService.setUserKey.mockResolvedValueOnce(undefined); - - // Act - await sut.setKeysAfterDecryptingSharedMasterKeyAndHash( - mockAuthReqResponse, - mockPrivateKey, - mockUserId, - ); - - // Assert - expect(sut.decryptPubKeyEncryptedMasterKeyAndHash).toBeCalledWith( - mockAuthReqResponse.key, - mockAuthReqResponse.masterPasswordHash, - mockPrivateKey, - ); - expect(masterPasswordService.mock.setMasterKey).toHaveBeenCalledWith( - mockDecryptedMasterKey, - mockUserId, - ); - expect(masterPasswordService.mock.setMasterKeyHash).toHaveBeenCalledWith( - mockDecryptedMasterKeyHash, - mockUserId, - ); - expect(masterPasswordService.mock.decryptUserKeyWithMasterKey).toHaveBeenCalledWith( - mockDecryptedMasterKey, - mockUserId, - undefined, - ); - expect(keyService.setUserKey).toHaveBeenCalledWith(mockDecryptedUserKey, mockUserId); - }); - }); - describe("decryptAuthReqPubKeyEncryptedUserKey", () => { it("returns a decrypted user key when given valid public key encrypted user key and an auth req private key", async () => { // Arrange diff --git a/libs/auth/src/common/services/auth-request/auth-request.service.ts b/libs/auth/src/common/services/auth-request/auth-request.service.ts index ba4b9eaf174..f1ff8416d11 100644 --- a/libs/auth/src/common/services/auth-request/auth-request.service.ts +++ b/libs/auth/src/common/services/auth-request/auth-request.service.ts @@ -16,14 +16,13 @@ import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { AUTH_REQUEST_DISK_LOCAL, StateProvider, UserKeyDefinition, } from "@bitwarden/common/platform/state"; import { UserId } from "@bitwarden/common/types/guid"; -import { MasterKey, UserKey } from "@bitwarden/common/types/key"; +import { UserKey } from "@bitwarden/common/types/key"; import { KeyService } from "@bitwarden/key-management"; import { AuthRequestApiServiceAbstraction } from "../../abstractions/auth-request-api.service"; @@ -163,27 +162,6 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { await this.keyService.setUserKey(userKey, userId); } - async setKeysAfterDecryptingSharedMasterKeyAndHash( - authReqResponse: AuthRequestResponse, - authReqPrivateKey: Uint8Array, - userId: UserId, - ) { - const { masterKey, masterKeyHash } = await this.decryptPubKeyEncryptedMasterKeyAndHash( - authReqResponse.key, - authReqResponse.masterPasswordHash, - authReqPrivateKey, - ); - - // Decrypt and set user key in state - const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey, userId); - - // Set masterKey + masterKeyHash in state after decryption (in case decryption fails) - await this.masterPasswordService.setMasterKey(masterKey, userId); - await this.masterPasswordService.setMasterKeyHash(masterKeyHash, userId); - - await this.keyService.setUserKey(userKey, userId); - } - // Decryption helpers async decryptPubKeyEncryptedUserKey( pubKeyEncryptedUserKey: string, @@ -197,30 +175,6 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { return decryptedUserKey as UserKey; } - async decryptPubKeyEncryptedMasterKeyAndHash( - pubKeyEncryptedMasterKey: string, - pubKeyEncryptedMasterKeyHash: string, - privateKey: Uint8Array, - ): Promise<{ masterKey: MasterKey; masterKeyHash: string }> { - const decryptedMasterKeyArrayBuffer = await this.encryptService.rsaDecrypt( - new EncString(pubKeyEncryptedMasterKey), - privateKey, - ); - - const decryptedMasterKeyHashArrayBuffer = await this.encryptService.rsaDecrypt( - new EncString(pubKeyEncryptedMasterKeyHash), - privateKey, - ); - - const masterKey = new SymmetricCryptoKey(decryptedMasterKeyArrayBuffer) as MasterKey; - const masterKeyHash = Utils.fromBufferToUtf8(decryptedMasterKeyHashArrayBuffer); - - return { - masterKey, - masterKeyHash, - }; - } - sendAuthRequestPushNotification(notification: AuthRequestPushNotification): void { if (notification.id != null) { this.authRequestPushNotificationSubject.next(notification.id); diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.state.spec.ts b/libs/auth/src/common/services/login-strategies/login-strategy.state.spec.ts index 32c5fdcc4d5..195ae0dd721 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.state.spec.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.state.spec.ts @@ -93,8 +93,6 @@ describe("LOGIN_STRATEGY_CACHE_KEY", () => { "ACCESS_CODE", "AUTH_REQUEST_ID", new SymmetricCryptoKey(new Uint8Array(64)) as UserKey, - new SymmetricCryptoKey(new Uint8Array(64)) as MasterKey, - "MASTER_KEY_HASH", ); const result = sut.deserializer(JSON.parse(JSON.stringify(actual))); diff --git a/libs/common/src/auth/models/response/auth-request.response.ts b/libs/common/src/auth/models/response/auth-request.response.ts index 94c65000919..8d02e161e68 100644 --- a/libs/common/src/auth/models/response/auth-request.response.ts +++ b/libs/common/src/auth/models/response/auth-request.response.ts @@ -11,8 +11,7 @@ export class AuthRequestResponse extends BaseResponse { requestDeviceIdentifier: string; requestIpAddress: string; requestCountryName: string; - key: string; // could be either an encrypted MasterKey or an encrypted UserKey - masterPasswordHash: string; // if hash is present, the `key` above is an encrypted MasterKey (else `key` is an encrypted UserKey) + key: string; // Auth-request public-key encrypted user-key. Note: No sender authenticity provided! creationDate: string; requestApproved?: boolean; responseDate?: string; @@ -30,7 +29,6 @@ export class AuthRequestResponse extends BaseResponse { this.requestIpAddress = this.getResponseProperty("RequestIpAddress"); this.requestCountryName = this.getResponseProperty("RequestCountryName"); this.key = this.getResponseProperty("Key"); - this.masterPasswordHash = this.getResponseProperty("MasterPasswordHash"); this.creationDate = this.getResponseProperty("CreationDate"); this.requestApproved = this.getResponseProperty("RequestApproved"); this.responseDate = this.getResponseProperty("ResponseDate"); diff --git a/libs/common/src/key-management/crypto/abstractions/encrypt.service.ts b/libs/common/src/key-management/crypto/abstractions/encrypt.service.ts index 03cfd173a4d..ed1458b704e 100644 --- a/libs/common/src/key-management/crypto/abstractions/encrypt.service.ts +++ b/libs/common/src/key-management/crypto/abstractions/encrypt.service.ts @@ -161,13 +161,6 @@ export abstract class EncryptService { decapsulationKey: Uint8Array, ): Promise; - /** - * @deprecated Use @see {@link decapsulateKeyUnsigned} instead - * @param data - The ciphertext to decrypt - * @param privateKey - The privateKey to decrypt with - */ - abstract rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise; - /** * Generates a base64-encoded hash of the given value * @param value The value to hash diff --git a/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts b/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts index b14211b5b72..5fa5fa5eea0 100644 --- a/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts +++ b/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts @@ -219,24 +219,4 @@ export class EncryptServiceImplementation implements EncryptService { ); return new SymmetricCryptoKey(keyBytes); } - - async rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise { - if (data == null) { - throw new Error("[Encrypt service] rsaDecrypt: No data provided for decryption."); - } - - switch (data.encryptionType) { - case EncryptionType.Rsa2048_OaepSha1_B64: - case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64: - break; - default: - throw new Error("Invalid encryption type."); - } - - if (privateKey == null) { - throw new Error("[Encrypt service] rsaDecrypt: No private key provided for decryption."); - } - - return this.cryptoFunctionService.rsaDecrypt(data.dataBytes, privateKey, "sha1"); - } } From 84845024fdc264c4843cc797ba6d62053205b740 Mon Sep 17 00:00:00 2001 From: Alex Dragovich <46065570+itsadrago@users.noreply.github.com> Date: Fri, 20 Feb 2026 10:29:40 -0800 Subject: [PATCH 117/134] [PM-32502] fixed icon / copy value spacing in button on send access page (#19092) * [PM-32502] fixed icon / copy value spacing in button on send access page * [PM-32502] using more approriate button configuration on send access --- .../send/send-access/send-access-text.component.html | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/tools/send/send-access/send-access-text.component.html b/apps/web/src/app/tools/send/send-access/send-access-text.component.html index c7fa148169d..746dd5f0567 100644 --- a/apps/web/src/app/tools/send/send-access/send-access-text.component.html +++ b/apps/web/src/app/tools/send/send-access/send-access-text.component.html @@ -10,7 +10,14 @@ }

    -
    From 72be42ed681398561a57171126abf900760760a8 Mon Sep 17 00:00:00 2001 From: Brad <44413459+lastbestdev@users.noreply.github.com> Date: Fri, 20 Feb 2026 11:11:17 -0800 Subject: [PATCH 118/134] [PM-32127] Access Intelligence: persist selected applications through filter updates (#18990) This PR includes changes to the Access Intelligence table view, which keep Applications selected in the table as the user makes changes to filters (search bar, critical applications filter). This required updating logic to ensure only visible rows in the table are considered for updates to critical status with the "Mark # as critical" button, while still maintaining the full list of selected applications in the component's selectedUrls. The Applications table component is also refactored to use Angular output for checkbox state, emitting events on checkbox changes for individual table rows and "select all". The parent component handles these events by updating the set of selected Applications (selectedUrls) accordingly. Test cases are updated/added to cover the updated checkbox functionality. --- .../applications.component.html | 9 +- .../applications.component.spec.ts | 202 +++++++++++++++++- .../applications.component.ts | 85 +++++--- ...pp-table-row-scrollable-m11.component.html | 2 +- ...table-row-scrollable-m11.component.spec.ts | 74 ++++++- .../app-table-row-scrollable-m11.component.ts | 24 +-- 6 files changed, 339 insertions(+), 57 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.html index 743f8ff1b68..ec73c4f47e6 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.html @@ -22,7 +22,7 @@
    - @if (selectedUrls().size > 0) { + @if (visibleSelectedApps().size > 0) { @if (allSelectedAppsAreCritical()) { } @else { } } @@ -79,8 +79,9 @@ [dataSource]="dataSource" [selectedUrls]="selectedUrls()" [openApplication]="drawerDetails.invokerId || ''" - [checkboxChange]="onCheckboxChange" [showAppAtRiskMembers]="showAppAtRiskMembers" + (checkboxChange)="onCheckboxChange($event)" + (selectAllChange)="onSelectAllChange($event)" class="tw-mb-10" > diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.spec.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.spec.ts index b4cbbc5c436..b79f5160bf7 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.spec.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.spec.ts @@ -1,8 +1,9 @@ +import { Signal, WritableSignal } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { ReactiveFormsModule } from "@angular/forms"; -import { ActivatedRoute } from "@angular/router"; +import { ActivatedRoute, convertToParamMap } from "@angular/router"; import { mock, MockProxy } from "jest-mock-extended"; -import { BehaviorSubject } from "rxjs"; +import { BehaviorSubject, of } from "rxjs"; import { DrawerDetails, @@ -11,6 +12,7 @@ import { ReportStatus, RiskInsightsDataService, } from "@bitwarden/bit-common/dirt/reports/risk-insights"; +import { createNewSummaryData } from "@bitwarden/bit-common/dirt/reports/risk-insights/helpers"; import { RiskInsightsEnrichedData } from "@bitwarden/bit-common/dirt/reports/risk-insights/models/report-data-service.types"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -23,9 +25,18 @@ import { AccessIntelligenceSecurityTasksService } from "../shared/security-tasks import { ApplicationsComponent } from "./applications.component"; +// Mock ResizeObserver +global.ResizeObserver = class ResizeObserver { + observe() {} + unobserve() {} + disconnect() {} +}; + // Helper type to access protected members in tests type ComponentWithProtectedMembers = ApplicationsComponent & { dataSource: TableDataSource; + selectedUrls: WritableSignal>; + filteredTableData: Signal; }; describe("ApplicationsComponent", () => { @@ -83,7 +94,10 @@ describe("ApplicationsComponent", () => { { provide: RiskInsightsDataService, useValue: mockDataService }, { provide: ActivatedRoute, - useValue: { snapshot: { paramMap: { get: (): string | null => null } } }, + useValue: { + paramMap: of(convertToParamMap({})), + snapshot: { paramMap: convertToParamMap({}) }, + }, }, { provide: AccessIntelligenceSecurityTasksService, useValue: mockSecurityTasksService }, ], @@ -91,6 +105,7 @@ describe("ApplicationsComponent", () => { fixture = TestBed.createComponent(ApplicationsComponent); component = fixture.componentInstance; + fixture.detectChanges(); }); afterEach(() => { @@ -247,4 +262,185 @@ describe("ApplicationsComponent", () => { expect(capturedBlobData).not.toContain("Slack"); }); }); + + describe("checkbox selection", () => { + const mockApplicationData: ApplicationTableDataSource[] = [ + { + applicationName: "GitHub", + passwordCount: 10, + atRiskPasswordCount: 3, + memberCount: 5, + atRiskMemberCount: 2, + isMarkedAsCritical: true, + atRiskCipherIds: ["cipher1" as CipherId], + memberDetails: [] as MemberDetails[], + atRiskMemberDetails: [] as MemberDetails[], + cipherIds: ["cipher1" as CipherId], + iconCipher: undefined, + }, + { + applicationName: "Slack", + passwordCount: 8, + atRiskPasswordCount: 1, + memberCount: 4, + atRiskMemberCount: 1, + isMarkedAsCritical: false, + atRiskCipherIds: ["cipher2" as CipherId], + memberDetails: [] as MemberDetails[], + atRiskMemberDetails: [] as MemberDetails[], + cipherIds: ["cipher2" as CipherId], + iconCipher: undefined, + }, + { + applicationName: "Jira", + passwordCount: 12, + atRiskPasswordCount: 4, + memberCount: 6, + atRiskMemberCount: 3, + isMarkedAsCritical: false, + atRiskCipherIds: ["cipher3" as CipherId], + memberDetails: [] as MemberDetails[], + atRiskMemberDetails: [] as MemberDetails[], + cipherIds: ["cipher3" as CipherId], + iconCipher: undefined, + }, + ]; + + beforeEach(() => { + // Emit mock data through the data service observable to populate the table + enrichedReportData$.next({ + reportData: mockApplicationData, + summaryData: createNewSummaryData(), + applicationData: [], + creationDate: new Date(), + }); + }); + + describe("onCheckboxChange", () => { + it("should add application to selectedUrls when checked is true", () => { + // act + component.onCheckboxChange({ applicationName: "GitHub", checked: true }); + + // assert + const selectedUrls = (component as ComponentWithProtectedMembers).selectedUrls(); + expect(selectedUrls.has("GitHub")).toBe(true); + expect(selectedUrls.size).toBe(1); + }); + + it("should remove application from selectedUrls when checked is false", () => { + // arrange + (component as ComponentWithProtectedMembers).selectedUrls.set(new Set(["GitHub", "Slack"])); + + // act + component.onCheckboxChange({ applicationName: "GitHub", checked: false }); + + // assert + const selectedUrls = (component as ComponentWithProtectedMembers).selectedUrls(); + expect(selectedUrls.has("GitHub")).toBe(false); + expect(selectedUrls.has("Slack")).toBe(true); + expect(selectedUrls.size).toBe(1); + }); + + it("should handle multiple applications being selected", () => { + // act + component.onCheckboxChange({ applicationName: "GitHub", checked: true }); + component.onCheckboxChange({ applicationName: "Slack", checked: true }); + component.onCheckboxChange({ applicationName: "Jira", checked: true }); + + // assert + const selectedUrls = (component as ComponentWithProtectedMembers).selectedUrls(); + expect(selectedUrls.has("GitHub")).toBe(true); + expect(selectedUrls.has("Slack")).toBe(true); + expect(selectedUrls.has("Jira")).toBe(true); + expect(selectedUrls.size).toBe(3); + }); + }); + + describe("onSelectAllChange", () => { + it("should add all visible applications to selectedUrls when checked is true", () => { + // act + component.onSelectAllChange(true); + + // assert + const selectedUrls = (component as ComponentWithProtectedMembers).selectedUrls(); + expect(selectedUrls.has("GitHub")).toBe(true); + expect(selectedUrls.has("Slack")).toBe(true); + expect(selectedUrls.has("Jira")).toBe(true); + expect(selectedUrls.size).toBe(3); + }); + + it("should remove all applications from selectedUrls when checked is false", () => { + // arrange + (component as ComponentWithProtectedMembers).selectedUrls.set(new Set(["GitHub", "Slack"])); + + // act + component.onSelectAllChange(false); + + // assert + const selectedUrls = (component as ComponentWithProtectedMembers).selectedUrls(); + expect(selectedUrls.size).toBe(0); + }); + + it("should only add visible filtered applications when filter is applied", () => { + // arrange - apply filter to only show critical apps + (component as ComponentWithProtectedMembers).dataSource.filter = ( + app: ApplicationTableDataSource, + ) => app.isMarkedAsCritical; + fixture.detectChanges(); + + // act + component.onSelectAllChange(true); + + // assert - only GitHub is critical + const selectedUrls = (component as ComponentWithProtectedMembers).selectedUrls(); + expect(selectedUrls.has("GitHub")).toBe(true); + expect(selectedUrls.has("Slack")).toBe(false); + expect(selectedUrls.has("Jira")).toBe(false); + expect(selectedUrls.size).toBe(1); + }); + + it("should only remove visible filtered applications when unchecking with filter applied", () => { + // arrange - select all apps first, then apply filter to only show non-critical apps + (component as ComponentWithProtectedMembers).selectedUrls.set( + new Set(["GitHub", "Slack", "Jira"]), + ); + + (component as ComponentWithProtectedMembers).dataSource.filter = ( + app: ApplicationTableDataSource, + ) => !app.isMarkedAsCritical; + fixture.detectChanges(); + + // act - uncheck with filter applied + component.onSelectAllChange(false); + + // assert - GitHub (critical) should still be selected + const selectedUrls = (component as ComponentWithProtectedMembers).selectedUrls(); + expect(selectedUrls.has("GitHub")).toBe(true); + expect(selectedUrls.has("Slack")).toBe(false); + expect(selectedUrls.has("Jira")).toBe(false); + expect(selectedUrls.size).toBe(1); + }); + + it("should preserve existing selections when checking select all with filter", () => { + // arrange - select a non-visible app + (component as ComponentWithProtectedMembers).selectedUrls.set(new Set(["GitHub"])); + + // apply filter to hide GitHub + (component as ComponentWithProtectedMembers).dataSource.filter = ( + app: ApplicationTableDataSource, + ) => !app.isMarkedAsCritical; + fixture.detectChanges(); + + // act - select all visible (non-critical apps) + component.onSelectAllChange(true); + + // assert - GitHub should still be selected + visible apps + const selectedUrls = (component as ComponentWithProtectedMembers).selectedUrls(); + expect(selectedUrls.has("GitHub")).toBe(true); + expect(selectedUrls.has("Slack")).toBe(true); + expect(selectedUrls.has("Jira")).toBe(true); + expect(selectedUrls.size).toBe(3); + }); + }); + }); }); diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.ts index 962628584d3..659e099641c 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.ts @@ -89,6 +89,9 @@ export class ApplicationsComponent implements OnInit { // Standard properties protected readonly dataSource = new TableDataSource(); protected readonly searchControl = new FormControl("", { nonNullable: true }); + protected readonly filteredTableData = toSignal(this.dataSource.connect(), { + initialValue: [], + }); // Template driven properties protected readonly selectedUrls = signal(new Set()); @@ -118,13 +121,35 @@ export class ApplicationsComponent implements OnInit { }, ]); + // Computed property that returns only selected applications that are currently visible in filtered data + readonly visibleSelectedApps = computed(() => { + const filteredData = this.filteredTableData(); + const selected = this.selectedUrls(); + + if (!filteredData || selected.size === 0) { + return new Set(); + } + + const visibleSelected = new Set(); + filteredData.forEach((row) => { + if (selected.has(row.applicationName)) { + visibleSelected.add(row.applicationName); + } + }); + + return visibleSelected; + }); + readonly allSelectedAppsAreCritical = computed(() => { - if (!this.dataSource.filteredData || this.selectedUrls().size == 0) { + const visibleSelected = this.visibleSelectedApps(); + const filteredData = this.filteredTableData(); + + if (!filteredData || visibleSelected.size === 0) { return false; } - return this.dataSource.filteredData - .filter((row) => this.selectedUrls().has(row.applicationName)) + return filteredData + .filter((row) => visibleSelected.has(row.applicationName)) .every((row) => row.isMarkedAsCritical); }); @@ -202,15 +227,6 @@ export class ApplicationsComponent implements OnInit { this.dataSource.filter = (app) => filterFunction(app) && app.applicationName.toLowerCase().includes(searchText.toLowerCase()); - - // filter selectedUrls down to only applications showing with active filters - const filteredUrls = new Set(); - this.dataSource.filteredData?.forEach((row) => { - if (this.selectedUrls().has(row.applicationName)) { - filteredUrls.add(row.applicationName); - } - }); - this.selectedUrls.set(filteredUrls); }); } @@ -218,12 +234,13 @@ export class ApplicationsComponent implements OnInit { this.selectedFilter.set(value); } - markAppsAsCritical = async () => { + async markAppsAsCritical() { this.updatingCriticalApps.set(true); - const count = this.selectedUrls().size; + const visibleSelected = this.visibleSelectedApps(); + const count = visibleSelected.size; this.dataService - .saveCriticalApplications(Array.from(this.selectedUrls())) + .saveCriticalApplications(Array.from(visibleSelected)) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ next: (response) => { @@ -246,11 +263,11 @@ export class ApplicationsComponent implements OnInit { }); }, }); - }; + } - unmarkAppsAsCritical = async () => { + async unmarkAppsAsCritical() { this.updatingCriticalApps.set(true); - const appsToUnmark = this.selectedUrls(); + const appsToUnmark = this.visibleSelectedApps(); this.dataService .removeCriticalApplications(appsToUnmark) @@ -278,7 +295,7 @@ export class ApplicationsComponent implements OnInit { }); }, }); - }; + } async requestPasswordChange() { const orgId = this.organizationId(); @@ -310,24 +327,38 @@ export class ApplicationsComponent implements OnInit { } } - showAppAtRiskMembers = async (applicationName: string) => { + async showAppAtRiskMembers(applicationName: string) { await this.dataService.setDrawerForAppAtRiskMembers(applicationName); - }; + } - onCheckboxChange = (applicationName: string, event: Event) => { - const isChecked = (event.target as HTMLInputElement).checked; + onCheckboxChange({ applicationName, checked }: { applicationName: string; checked: boolean }) { this.selectedUrls.update((selectedUrls) => { const nextSelected = new Set(selectedUrls); - if (isChecked) { + if (checked) { nextSelected.add(applicationName); } else { nextSelected.delete(applicationName); } return nextSelected; }); - }; + } - downloadApplicationsCSV = () => { + onSelectAllChange(checked: boolean) { + const filteredData = this.filteredTableData(); + if (!filteredData) { + return; + } + + this.selectedUrls.update((selectedUrls) => { + const nextSelected = new Set(selectedUrls); + filteredData.forEach((row) => + checked ? nextSelected.add(row.applicationName) : nextSelected.delete(row.applicationName), + ); + return nextSelected; + }); + } + + downloadApplicationsCSV() { try { const data = this.dataSource.filteredData; if (!data || data.length === 0) { @@ -360,5 +391,5 @@ export class ApplicationsComponent implements OnInit { } catch (error) { this.logService.error("Failed to download applications CSV", error); } - }; + } } diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.html index 05dec048328..ddbc977fc13 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.html @@ -33,7 +33,7 @@ bitCheckbox type="checkbox" [checked]="selectedUrls().has(row.applicationName)" - (change)="checkboxChange()(row.applicationName, $event)" + (change)="checkboxChanged($event.target, row.applicationName)" /> { selectAllCheckboxEl = fixture.debugElement.query(By.css('[data-testid="selectAll"]')); }); - it("should check all rows in table when checked", () => { + it("should emit selectAllChange event with true when checked", () => { // arrange const selectedUrls = new Set(); const dataSource = new TableDataSource(); @@ -121,18 +121,19 @@ describe("AppTableRowScrollableM11Component", () => { fixture.componentRef.setInput("dataSource", dataSource); fixture.detectChanges(); + const selectAllChangeSpy = jest.fn(); + fixture.componentInstance.selectAllChange.subscribe(selectAllChangeSpy); + // act selectAllCheckboxEl.nativeElement.click(); fixture.detectChanges(); // assert - expect(selectedUrls.has("google.com")).toBe(true); - expect(selectedUrls.has("facebook.com")).toBe(true); - expect(selectedUrls.has("twitter.com")).toBe(true); - expect(selectedUrls.size).toBe(3); + expect(selectAllChangeSpy).toHaveBeenCalledWith(true); + expect(selectAllChangeSpy).toHaveBeenCalledTimes(1); }); - it("should uncheck all rows in table when unchecked", () => { + it("should emit selectAllChange event with false when unchecked", () => { // arrange const selectedUrls = new Set(["google.com", "facebook.com", "twitter.com"]); const dataSource = new TableDataSource(); @@ -142,12 +143,16 @@ describe("AppTableRowScrollableM11Component", () => { fixture.componentRef.setInput("dataSource", dataSource); fixture.detectChanges(); + const selectAllChangeSpy = jest.fn(); + fixture.componentInstance.selectAllChange.subscribe(selectAllChangeSpy); + // act selectAllCheckboxEl.nativeElement.click(); fixture.detectChanges(); // assert - expect(selectedUrls.size).toBe(0); + expect(selectAllChangeSpy).toHaveBeenCalledWith(false); + expect(selectAllChangeSpy).toHaveBeenCalledTimes(1); }); it("should become checked when all rows in table are checked", () => { @@ -178,4 +183,59 @@ describe("AppTableRowScrollableM11Component", () => { expect(selectAllCheckboxEl.nativeElement.checked).toBe(false); }); }); + + describe("individual row checkbox", () => { + it("should emit checkboxChange event with correct parameters when checkboxChanged is called", () => { + // arrange + const checkboxChangeSpy = jest.fn(); + fixture.componentInstance.checkboxChange.subscribe(checkboxChangeSpy); + + const mockTarget = { checked: true } as HTMLInputElement; + + // act + fixture.componentInstance.checkboxChanged(mockTarget, "google.com"); + + // assert + expect(checkboxChangeSpy).toHaveBeenCalledWith({ + applicationName: "google.com", + checked: true, + }); + expect(checkboxChangeSpy).toHaveBeenCalledTimes(1); + }); + + it("should emit checkboxChange with checked=false when checkbox is unchecked", () => { + // arrange + const checkboxChangeSpy = jest.fn(); + fixture.componentInstance.checkboxChange.subscribe(checkboxChangeSpy); + + const mockTarget = { checked: false } as HTMLInputElement; + + // act + fixture.componentInstance.checkboxChanged(mockTarget, "google.com"); + + // assert + expect(checkboxChangeSpy).toHaveBeenCalledWith({ + applicationName: "google.com", + checked: false, + }); + expect(checkboxChangeSpy).toHaveBeenCalledTimes(1); + }); + + it("should emit checkboxChange with correct applicationName for different applications", () => { + // arrange + const checkboxChangeSpy = jest.fn(); + fixture.componentInstance.checkboxChange.subscribe(checkboxChangeSpy); + + const mockTarget = { checked: true } as HTMLInputElement; + + // act + fixture.componentInstance.checkboxChanged(mockTarget, "facebook.com"); + + // assert + expect(checkboxChangeSpy).toHaveBeenCalledWith({ + applicationName: "facebook.com", + checked: true, + }); + }); + }); }); diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.ts index a23d1855ba5..65cfb8d092e 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from "@angular/common"; -import { ChangeDetectionStrategy, Component, input } from "@angular/core"; +import { ChangeDetectionStrategy, Component, input, output } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { MenuModule, TableDataSource, TableModule, TooltipDirective } from "@bitwarden/components"; @@ -30,7 +30,8 @@ export class AppTableRowScrollableM11Component { readonly selectedUrls = input>(); readonly openApplication = input(""); readonly showAppAtRiskMembers = input<(applicationName: string) => void>(); - readonly checkboxChange = input<(applicationName: string, $event: Event) => void>(); + readonly checkboxChange = output<{ applicationName: string; checked: boolean }>(); + readonly selectAllChange = output(); allAppsSelected(): boolean { const tableData = this.dataSource()?.filteredData; @@ -43,20 +44,13 @@ export class AppTableRowScrollableM11Component { return tableData.length > 0 && tableData.every((row) => selectedUrls.has(row.applicationName)); } + checkboxChanged(target: HTMLInputElement, applicationName: string) { + const checked = target.checked; + this.checkboxChange.emit({ applicationName, checked }); + } + selectAllChanged(target: HTMLInputElement) { const checked = target.checked; - - const tableData = this.dataSource()?.filteredData; - const selectedUrls = this.selectedUrls(); - - if (!tableData || !selectedUrls) { - return false; - } - - if (checked) { - tableData.forEach((row) => selectedUrls.add(row.applicationName)); - } else { - selectedUrls.clear(); - } + this.selectAllChange.emit(checked); } } From 531a9df6b01565f931fd3f89093e8d351c170fb0 Mon Sep 17 00:00:00 2001 From: Github Actions Date: Fri, 20 Feb 2026 19:25:42 +0000 Subject: [PATCH 119/134] Bumped Desktop client to 2026.2.1 --- apps/desktop/package.json | 2 +- apps/desktop/src/package-lock.json | 4 ++-- apps/desktop/src/package.json | 2 +- package-lock.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index cd2147d21e4..5718c752a7c 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/desktop", "description": "A secure and free password manager for all of your devices.", - "version": "2026.2.0", + "version": "2026.2.1", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index 0aa188eba2f..01c429ab3d0 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2026.2.0", + "version": "2026.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2026.2.0", + "version": "2026.2.1", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-napi": "file:../desktop_native/napi" diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index 0076981ab60..fac797b5344 100644 --- a/apps/desktop/src/package.json +++ b/apps/desktop/src/package.json @@ -2,7 +2,7 @@ "name": "@bitwarden/desktop", "productName": "Bitwarden", "description": "A secure and free password manager for all of your devices.", - "version": "2026.2.0", + "version": "2026.2.1", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/package-lock.json b/package-lock.json index f5ac6ccbc0c..79541971b12 100644 --- a/package-lock.json +++ b/package-lock.json @@ -232,7 +232,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2026.2.0", + "version": "2026.2.1", "hasInstallScript": true, "license": "GPL-3.0" }, From cae1ae649162c71a87efe388cce24d2419a730f9 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Fri, 20 Feb 2026 13:45:30 -0600 Subject: [PATCH 120/134] Revert "Split NAPI modules [PM-31598] (#18722)" (#19112) This reverts commit fd90efabe4e31b169031902f29baf906785b2772. --- .github/CODEOWNERS | 3 - .../desktop_native/napi/src/autofill.rs | 332 ----- .../desktop_native/napi/src/autostart.rs | 9 - .../desktop_native/napi/src/autotype.rs | 20 - .../desktop_native/napi/src/biometrics.rs | 100 -- .../desktop_native/napi/src/biometrics_v2.rs | 116 -- .../napi/src/chromium_importer.rs | 116 -- .../desktop_native/napi/src/clipboards.rs | 15 - apps/desktop/desktop_native/napi/src/ipc.rs | 106 -- apps/desktop/desktop_native/napi/src/lib.rs | 1260 ++++++++++++++++- .../desktop_native/napi/src/logging.rs | 131 -- .../napi/src/passkey_authenticator.rs | 9 - .../desktop_native/napi/src/passwords.rs | 46 - .../desktop_native/napi/src/powermonitors.rs | 26 - .../napi/src/processisolations.rs | 23 - .../desktop_native/napi/src/sshagent.rs | 163 --- .../napi/src/windows_registry.rs | 16 - 17 files changed, 1241 insertions(+), 1250 deletions(-) delete mode 100644 apps/desktop/desktop_native/napi/src/autofill.rs delete mode 100644 apps/desktop/desktop_native/napi/src/autostart.rs delete mode 100644 apps/desktop/desktop_native/napi/src/autotype.rs delete mode 100644 apps/desktop/desktop_native/napi/src/biometrics.rs delete mode 100644 apps/desktop/desktop_native/napi/src/biometrics_v2.rs delete mode 100644 apps/desktop/desktop_native/napi/src/chromium_importer.rs delete mode 100644 apps/desktop/desktop_native/napi/src/clipboards.rs delete mode 100644 apps/desktop/desktop_native/napi/src/ipc.rs delete mode 100644 apps/desktop/desktop_native/napi/src/logging.rs delete mode 100644 apps/desktop/desktop_native/napi/src/passkey_authenticator.rs delete mode 100644 apps/desktop/desktop_native/napi/src/passwords.rs delete mode 100644 apps/desktop/desktop_native/napi/src/powermonitors.rs delete mode 100644 apps/desktop/desktop_native/napi/src/processisolations.rs delete mode 100644 apps/desktop/desktop_native/napi/src/sshagent.rs delete mode 100644 apps/desktop/desktop_native/napi/src/windows_registry.rs diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c2e04d94f95..8f416e09511 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -163,9 +163,6 @@ apps/desktop/desktop_native/windows_plugin_authenticator @bitwarden/team-autofil apps/desktop/desktop_native/autotype @bitwarden/team-autofill-desktop-dev apps/desktop/desktop_native/core/src/ssh_agent @bitwarden/team-autofill-desktop-dev @bitwarden/wg-ssh-keys apps/desktop/desktop_native/ssh_agent @bitwarden/team-autofill-desktop-dev @bitwarden/wg-ssh-keys -apps/desktop/desktop_native/napi/src/autofill.rs @bitwarden/team-autofill-desktop-dev -apps/desktop/desktop_native/napi/src/autotype.rs @bitwarden/team-autofill-desktop-dev -apps/desktop/desktop_native/napi/src/sshagent.rs @bitwarden/team-autofill-desktop-dev # DuckDuckGo integration apps/desktop/native-messaging-test-runner @bitwarden/team-autofill-desktop-dev apps/desktop/src/services/duckduckgo-message-handler.service.ts @bitwarden/team-autofill-desktop-dev diff --git a/apps/desktop/desktop_native/napi/src/autofill.rs b/apps/desktop/desktop_native/napi/src/autofill.rs deleted file mode 100644 index 7717b22ccef..00000000000 --- a/apps/desktop/desktop_native/napi/src/autofill.rs +++ /dev/null @@ -1,332 +0,0 @@ -#[napi] -pub mod autofill { - use desktop_core::ipc::server::{Message, MessageType}; - use napi::{ - bindgen_prelude::FnArgs, - threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}, - }; - use serde::{de::DeserializeOwned, Deserialize, Serialize}; - use tracing::error; - - #[napi] - pub async fn run_command(value: String) -> napi::Result { - desktop_core::autofill::run_command(value) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[derive(Debug, serde::Serialize, serde:: Deserialize)] - pub enum BitwardenError { - Internal(String), - } - - #[napi(string_enum)] - #[derive(Debug, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub enum UserVerification { - #[napi(value = "preferred")] - Preferred, - #[napi(value = "required")] - Required, - #[napi(value = "discouraged")] - Discouraged, - } - - #[derive(Serialize, Deserialize)] - #[serde(bound = "T: Serialize + DeserializeOwned")] - pub struct PasskeyMessage { - pub sequence_number: u32, - pub value: Result, - } - - #[napi(object)] - #[derive(Debug, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct Position { - pub x: i32, - pub y: i32, - } - - #[napi(object)] - #[derive(Debug, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct PasskeyRegistrationRequest { - pub rp_id: String, - pub user_name: String, - pub user_handle: Vec, - pub client_data_hash: Vec, - pub user_verification: UserVerification, - pub supported_algorithms: Vec, - pub window_xy: Position, - pub excluded_credentials: Vec>, - } - - #[napi(object)] - #[derive(Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct PasskeyRegistrationResponse { - pub rp_id: String, - pub client_data_hash: Vec, - pub credential_id: Vec, - pub attestation_object: Vec, - } - - #[napi(object)] - #[derive(Debug, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct PasskeyAssertionRequest { - pub rp_id: String, - pub client_data_hash: Vec, - pub user_verification: UserVerification, - pub allowed_credentials: Vec>, - pub window_xy: Position, - //extension_input: Vec, TODO: Implement support for extensions - } - - #[napi(object)] - #[derive(Debug, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct PasskeyAssertionWithoutUserInterfaceRequest { - pub rp_id: String, - pub credential_id: Vec, - pub user_name: String, - pub user_handle: Vec, - pub record_identifier: Option, - pub client_data_hash: Vec, - pub user_verification: UserVerification, - pub window_xy: Position, - } - - #[napi(object)] - #[derive(Debug, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct NativeStatus { - pub key: String, - pub value: String, - } - - #[napi(object)] - #[derive(Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct PasskeyAssertionResponse { - pub rp_id: String, - pub user_handle: Vec, - pub signature: Vec, - pub client_data_hash: Vec, - pub authenticator_data: Vec, - pub credential_id: Vec, - } - - #[napi] - pub struct AutofillIpcServer { - server: desktop_core::ipc::server::Server, - } - - // FIXME: Remove unwraps! They panic and terminate the whole application. - #[allow(clippy::unwrap_used)] - #[napi] - impl AutofillIpcServer { - /// Create and start the IPC server without blocking. - /// - /// @param name The endpoint name to listen on. This name uniquely identifies the IPC - /// connection and must be the same for both the server and client. @param callback - /// This function will be called whenever a message is received from a client. - #[allow(clippy::unused_async)] // FIXME: Remove unused async! - #[napi(factory)] - pub async fn listen( - name: String, - // Ideally we'd have a single callback that has an enum containing the request values, - // but NAPI doesn't support that just yet - #[napi( - ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void" - )] - registration_callback: ThreadsafeFunction< - FnArgs<(u32, u32, PasskeyRegistrationRequest)>, - >, - #[napi( - ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void" - )] - assertion_callback: ThreadsafeFunction< - FnArgs<(u32, u32, PasskeyAssertionRequest)>, - >, - #[napi( - ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionWithoutUserInterfaceRequest) => void" - )] - assertion_without_user_interface_callback: ThreadsafeFunction< - FnArgs<(u32, u32, PasskeyAssertionWithoutUserInterfaceRequest)>, - >, - #[napi( - ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: NativeStatus) => void" - )] - native_status_callback: ThreadsafeFunction<(u32, u32, NativeStatus)>, - ) -> napi::Result { - let (send, mut recv) = tokio::sync::mpsc::channel::(32); - tokio::spawn(async move { - while let Some(Message { - client_id, - kind, - message, - }) = recv.recv().await - { - match kind { - // TODO: We're ignoring the connection and disconnection messages for now - MessageType::Connected | MessageType::Disconnected => continue, - MessageType::Message => { - let Some(message) = message else { - error!("Message is empty"); - continue; - }; - - match serde_json::from_str::>( - &message, - ) { - Ok(msg) => { - let value = msg - .value - .map(|value| (client_id, msg.sequence_number, value).into()) - .map_err(|e| napi::Error::from_reason(format!("{e:?}"))); - - assertion_callback - .call(value, ThreadsafeFunctionCallMode::NonBlocking); - continue; - } - Err(e) => { - error!(error = %e, "Error deserializing message1"); - } - } - - match serde_json::from_str::< - PasskeyMessage, - >(&message) - { - Ok(msg) => { - let value = msg - .value - .map(|value| (client_id, msg.sequence_number, value).into()) - .map_err(|e| napi::Error::from_reason(format!("{e:?}"))); - - assertion_without_user_interface_callback - .call(value, ThreadsafeFunctionCallMode::NonBlocking); - continue; - } - Err(e) => { - error!(error = %e, "Error deserializing message1"); - } - } - - match serde_json::from_str::>( - &message, - ) { - Ok(msg) => { - let value = msg - .value - .map(|value| (client_id, msg.sequence_number, value).into()) - .map_err(|e| napi::Error::from_reason(format!("{e:?}"))); - registration_callback - .call(value, ThreadsafeFunctionCallMode::NonBlocking); - continue; - } - Err(e) => { - error!(error = %e, "Error deserializing message2"); - } - } - - match serde_json::from_str::>(&message) { - Ok(msg) => { - let value = msg - .value - .map(|value| (client_id, msg.sequence_number, value)) - .map_err(|e| napi::Error::from_reason(format!("{e:?}"))); - native_status_callback - .call(value, ThreadsafeFunctionCallMode::NonBlocking); - continue; - } - Err(error) => { - error!(%error, "Unable to deserialze native status."); - } - } - - error!(message, "Received an unknown message2"); - } - } - } - }); - - let path = desktop_core::ipc::path(&name); - - let server = desktop_core::ipc::server::Server::start(&path, send).map_err(|e| { - napi::Error::from_reason(format!( - "Error listening to server - Path: {path:?} - Error: {e} - {e:?}" - )) - })?; - - Ok(AutofillIpcServer { server }) - } - - /// Return the path to the IPC server. - #[napi] - pub fn get_path(&self) -> String { - self.server.path.to_string_lossy().to_string() - } - - /// Stop the IPC server. - #[napi] - pub fn stop(&self) -> napi::Result<()> { - self.server.stop(); - Ok(()) - } - - #[napi] - pub fn complete_registration( - &self, - client_id: u32, - sequence_number: u32, - response: PasskeyRegistrationResponse, - ) -> napi::Result { - let message = PasskeyMessage { - sequence_number, - value: Ok(response), - }; - self.send(client_id, serde_json::to_string(&message).unwrap()) - } - - #[napi] - pub fn complete_assertion( - &self, - client_id: u32, - sequence_number: u32, - response: PasskeyAssertionResponse, - ) -> napi::Result { - let message = PasskeyMessage { - sequence_number, - value: Ok(response), - }; - self.send(client_id, serde_json::to_string(&message).unwrap()) - } - - #[napi] - pub fn complete_error( - &self, - client_id: u32, - sequence_number: u32, - error: String, - ) -> napi::Result { - let message: PasskeyMessage<()> = PasskeyMessage { - sequence_number, - value: Err(BitwardenError::Internal(error)), - }; - self.send(client_id, serde_json::to_string(&message).unwrap()) - } - - // TODO: Add a way to send a message to a specific client? - fn send(&self, _client_id: u32, message: String) -> napi::Result { - self.server - .send(message) - .map_err(|e| { - napi::Error::from_reason(format!("Error sending message - Error: {e} - {e:?}")) - }) - // NAPI doesn't support u64 or usize, so we need to convert to u32 - .map(|u| u32::try_from(u).unwrap_or_default()) - } - } -} diff --git a/apps/desktop/desktop_native/napi/src/autostart.rs b/apps/desktop/desktop_native/napi/src/autostart.rs deleted file mode 100644 index 3068226809e..00000000000 --- a/apps/desktop/desktop_native/napi/src/autostart.rs +++ /dev/null @@ -1,9 +0,0 @@ -#[napi] -pub mod autostart { - #[napi] - pub async fn set_autostart(autostart: bool, params: Vec) -> napi::Result<()> { - desktop_core::autostart::set_autostart(autostart, params) - .await - .map_err(|e| napi::Error::from_reason(format!("Error setting autostart - {e} - {e:?}"))) - } -} diff --git a/apps/desktop/desktop_native/napi/src/autotype.rs b/apps/desktop/desktop_native/napi/src/autotype.rs deleted file mode 100644 index b63c95ceb5c..00000000000 --- a/apps/desktop/desktop_native/napi/src/autotype.rs +++ /dev/null @@ -1,20 +0,0 @@ -#[napi] -pub mod autotype { - #[napi] - pub fn get_foreground_window_title() -> napi::Result { - autotype::get_foreground_window_title().map_err(|_| { - napi::Error::from_reason( - "Autotype Error: failed to get foreground window title".to_string(), - ) - }) - } - - #[napi] - pub fn type_input( - input: Vec, - keyboard_shortcut: Vec, - ) -> napi::Result<(), napi::Status> { - autotype::type_input(&input, &keyboard_shortcut) - .map_err(|e| napi::Error::from_reason(format!("Autotype Error: {e}"))) - } -} diff --git a/apps/desktop/desktop_native/napi/src/biometrics.rs b/apps/desktop/desktop_native/napi/src/biometrics.rs deleted file mode 100644 index bca802d5884..00000000000 --- a/apps/desktop/desktop_native/napi/src/biometrics.rs +++ /dev/null @@ -1,100 +0,0 @@ -#[napi] -pub mod biometrics { - use desktop_core::biometric::{Biometric, BiometricTrait}; - - // Prompt for biometric confirmation - #[napi] - pub async fn prompt( - hwnd: napi::bindgen_prelude::Buffer, - message: String, - ) -> napi::Result { - Biometric::prompt(hwnd.into(), message) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[napi] - pub async fn available() -> napi::Result { - Biometric::available() - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[napi] - pub async fn set_biometric_secret( - service: String, - account: String, - secret: String, - key_material: Option, - iv_b64: String, - ) -> napi::Result { - Biometric::set_biometric_secret( - &service, - &account, - &secret, - key_material.map(|m| m.into()), - &iv_b64, - ) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - /// Retrieves the biometric secret for the given service and account. - /// Throws Error with message [`passwords::PASSWORD_NOT_FOUND`] if the secret does not exist. - #[napi] - pub async fn get_biometric_secret( - service: String, - account: String, - key_material: Option, - ) -> napi::Result { - Biometric::get_biometric_secret(&service, &account, key_material.map(|m| m.into())) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - /// Derives key material from biometric data. Returns a string encoded with a - /// base64 encoded key and the base64 encoded challenge used to create it - /// separated by a `|` character. - /// - /// If the iv is provided, it will be used as the challenge. Otherwise a random challenge will - /// be generated. - /// - /// `format!("|")` - #[allow(clippy::unused_async)] // FIXME: Remove unused async! - #[napi] - pub async fn derive_key_material(iv: Option) -> napi::Result { - Biometric::derive_key_material(iv.as_deref()) - .map(|k| k.into()) - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[napi(object)] - pub struct KeyMaterial { - pub os_key_part_b64: String, - pub client_key_part_b64: Option, - } - - impl From for desktop_core::biometric::KeyMaterial { - fn from(km: KeyMaterial) -> Self { - desktop_core::biometric::KeyMaterial { - os_key_part_b64: km.os_key_part_b64, - client_key_part_b64: km.client_key_part_b64, - } - } - } - - #[napi(object)] - pub struct OsDerivedKey { - pub key_b64: String, - pub iv_b64: String, - } - - impl From for OsDerivedKey { - fn from(km: desktop_core::biometric::OsDerivedKey) -> Self { - OsDerivedKey { - key_b64: km.key_b64, - iv_b64: km.iv_b64, - } - } - } -} diff --git a/apps/desktop/desktop_native/napi/src/biometrics_v2.rs b/apps/desktop/desktop_native/napi/src/biometrics_v2.rs deleted file mode 100644 index 2df3a6a07be..00000000000 --- a/apps/desktop/desktop_native/napi/src/biometrics_v2.rs +++ /dev/null @@ -1,116 +0,0 @@ -#[napi] -pub mod biometrics_v2 { - use desktop_core::biometric_v2::BiometricTrait; - - #[napi] - pub struct BiometricLockSystem { - inner: desktop_core::biometric_v2::BiometricLockSystem, - } - - #[napi] - pub fn init_biometric_system() -> napi::Result { - Ok(BiometricLockSystem { - inner: desktop_core::biometric_v2::BiometricLockSystem::new(), - }) - } - - #[napi] - pub async fn authenticate( - biometric_lock_system: &BiometricLockSystem, - hwnd: napi::bindgen_prelude::Buffer, - message: String, - ) -> napi::Result { - biometric_lock_system - .inner - .authenticate(hwnd.into(), message) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[napi] - pub async fn authenticate_available( - biometric_lock_system: &BiometricLockSystem, - ) -> napi::Result { - biometric_lock_system - .inner - .authenticate_available() - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[napi] - pub async fn enroll_persistent( - biometric_lock_system: &BiometricLockSystem, - user_id: String, - key: napi::bindgen_prelude::Buffer, - ) -> napi::Result<()> { - biometric_lock_system - .inner - .enroll_persistent(&user_id, &key) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[napi] - pub async fn provide_key( - biometric_lock_system: &BiometricLockSystem, - user_id: String, - key: napi::bindgen_prelude::Buffer, - ) -> napi::Result<()> { - biometric_lock_system - .inner - .provide_key(&user_id, &key) - .await; - Ok(()) - } - - #[napi] - pub async fn unlock( - biometric_lock_system: &BiometricLockSystem, - user_id: String, - hwnd: napi::bindgen_prelude::Buffer, - ) -> napi::Result { - biometric_lock_system - .inner - .unlock(&user_id, hwnd.into()) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - .map(|v| v.into()) - } - - #[napi] - pub async fn unlock_available( - biometric_lock_system: &BiometricLockSystem, - user_id: String, - ) -> napi::Result { - biometric_lock_system - .inner - .unlock_available(&user_id) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[napi] - pub async fn has_persistent( - biometric_lock_system: &BiometricLockSystem, - user_id: String, - ) -> napi::Result { - biometric_lock_system - .inner - .has_persistent(&user_id) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[napi] - pub async fn unenroll( - biometric_lock_system: &BiometricLockSystem, - user_id: String, - ) -> napi::Result<()> { - biometric_lock_system - .inner - .unenroll(&user_id) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } -} diff --git a/apps/desktop/desktop_native/napi/src/chromium_importer.rs b/apps/desktop/desktop_native/napi/src/chromium_importer.rs deleted file mode 100644 index da295984a47..00000000000 --- a/apps/desktop/desktop_native/napi/src/chromium_importer.rs +++ /dev/null @@ -1,116 +0,0 @@ -#[napi] -pub mod chromium_importer { - use std::collections::HashMap; - - use chromium_importer::{ - chromium::{ - DefaultInstalledBrowserRetriever, LoginImportResult as _LoginImportResult, - ProfileInfo as _ProfileInfo, - }, - metadata::NativeImporterMetadata as _NativeImporterMetadata, - }; - - #[napi(object)] - pub struct ProfileInfo { - pub id: String, - pub name: String, - } - - #[napi(object)] - pub struct Login { - pub url: String, - pub username: String, - pub password: String, - pub note: String, - } - - #[napi(object)] - pub struct LoginImportFailure { - pub url: String, - pub username: String, - pub error: String, - } - - #[napi(object)] - pub struct LoginImportResult { - pub login: Option, - pub failure: Option, - } - - #[napi(object)] - pub struct NativeImporterMetadata { - pub id: String, - pub loaders: Vec, - pub instructions: String, - } - - impl From<_LoginImportResult> for LoginImportResult { - fn from(l: _LoginImportResult) -> Self { - match l { - _LoginImportResult::Success(l) => LoginImportResult { - login: Some(Login { - url: l.url, - username: l.username, - password: l.password, - note: l.note, - }), - failure: None, - }, - _LoginImportResult::Failure(l) => LoginImportResult { - login: None, - failure: Some(LoginImportFailure { - url: l.url, - username: l.username, - error: l.error, - }), - }, - } - } - } - - impl From<_ProfileInfo> for ProfileInfo { - fn from(p: _ProfileInfo) -> Self { - ProfileInfo { - id: p.folder, - name: p.name, - } - } - } - - impl From<_NativeImporterMetadata> for NativeImporterMetadata { - fn from(m: _NativeImporterMetadata) -> Self { - NativeImporterMetadata { - id: m.id, - loaders: m.loaders, - instructions: m.instructions, - } - } - } - - #[napi] - /// Returns OS aware metadata describing supported Chromium based importers as a JSON string. - pub fn get_metadata() -> HashMap { - chromium_importer::metadata::get_supported_importers::() - .into_iter() - .map(|(browser, metadata)| (browser, NativeImporterMetadata::from(metadata))) - .collect() - } - - #[napi] - pub fn get_available_profiles(browser: String) -> napi::Result> { - chromium_importer::chromium::get_available_profiles(&browser) - .map(|profiles| profiles.into_iter().map(ProfileInfo::from).collect()) - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[napi] - pub async fn import_logins( - browser: String, - profile_id: String, - ) -> napi::Result> { - chromium_importer::chromium::import_logins(&browser, &profile_id) - .await - .map(|logins| logins.into_iter().map(LoginImportResult::from).collect()) - .map_err(|e| napi::Error::from_reason(e.to_string())) - } -} diff --git a/apps/desktop/desktop_native/napi/src/clipboards.rs b/apps/desktop/desktop_native/napi/src/clipboards.rs deleted file mode 100644 index 810e457dd60..00000000000 --- a/apps/desktop/desktop_native/napi/src/clipboards.rs +++ /dev/null @@ -1,15 +0,0 @@ -#[napi] -pub mod clipboards { - #[allow(clippy::unused_async)] // FIXME: Remove unused async! - #[napi] - pub async fn read() -> napi::Result { - desktop_core::clipboard::read().map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[allow(clippy::unused_async)] // FIXME: Remove unused async! - #[napi] - pub async fn write(text: String, password: bool) -> napi::Result<()> { - desktop_core::clipboard::write(&text, password) - .map_err(|e| napi::Error::from_reason(e.to_string())) - } -} diff --git a/apps/desktop/desktop_native/napi/src/ipc.rs b/apps/desktop/desktop_native/napi/src/ipc.rs deleted file mode 100644 index ba72b1dce2b..00000000000 --- a/apps/desktop/desktop_native/napi/src/ipc.rs +++ /dev/null @@ -1,106 +0,0 @@ -#[napi] -pub mod ipc { - use desktop_core::ipc::server::{Message, MessageType}; - use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}; - - #[napi(object)] - pub struct IpcMessage { - pub client_id: u32, - pub kind: IpcMessageType, - pub message: Option, - } - - impl From for IpcMessage { - fn from(message: Message) -> Self { - IpcMessage { - client_id: message.client_id, - kind: message.kind.into(), - message: message.message, - } - } - } - - #[napi] - pub enum IpcMessageType { - Connected, - Disconnected, - Message, - } - - impl From for IpcMessageType { - fn from(message_type: MessageType) -> Self { - match message_type { - MessageType::Connected => IpcMessageType::Connected, - MessageType::Disconnected => IpcMessageType::Disconnected, - MessageType::Message => IpcMessageType::Message, - } - } - } - - #[napi] - pub struct NativeIpcServer { - server: desktop_core::ipc::server::Server, - } - - #[napi] - impl NativeIpcServer { - /// Create and start the IPC server without blocking. - /// - /// @param name The endpoint name to listen on. This name uniquely identifies the IPC - /// connection and must be the same for both the server and client. @param callback - /// This function will be called whenever a message is received from a client. - #[allow(clippy::unused_async)] // FIXME: Remove unused async! - #[napi(factory)] - pub async fn listen( - name: String, - #[napi(ts_arg_type = "(error: null | Error, message: IpcMessage) => void")] - callback: ThreadsafeFunction, - ) -> napi::Result { - let (send, mut recv) = tokio::sync::mpsc::channel::(32); - tokio::spawn(async move { - while let Some(message) = recv.recv().await { - callback.call(Ok(message.into()), ThreadsafeFunctionCallMode::NonBlocking); - } - }); - - let path = desktop_core::ipc::path(&name); - - let server = desktop_core::ipc::server::Server::start(&path, send).map_err(|e| { - napi::Error::from_reason(format!( - "Error listening to server - Path: {path:?} - Error: {e} - {e:?}" - )) - })?; - - Ok(NativeIpcServer { server }) - } - - /// Return the path to the IPC server. - #[napi] - pub fn get_path(&self) -> String { - self.server.path.to_string_lossy().to_string() - } - - /// Stop the IPC server. - #[napi] - pub fn stop(&self) -> napi::Result<()> { - self.server.stop(); - Ok(()) - } - - /// Send a message over the IPC server to all the connected clients - /// - /// @return The number of clients that the message was sent to. Note that the number of - /// messages actually received may be less, as some clients could disconnect before - /// receiving the message. - #[napi] - pub fn send(&self, message: String) -> napi::Result { - self.server - .send(message) - .map_err(|e| { - napi::Error::from_reason(format!("Error sending message - Error: {e} - {e:?}")) - }) - // NAPI doesn't support u64 or usize, so we need to convert to u32 - .map(|u| u32::try_from(u).unwrap_or_default()) - } - } -} diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index e3abfd50e7a..588f757631c 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -4,22 +4,1244 @@ extern crate napi_derive; mod passkey_authenticator_internal; mod registry; -// NAPI namespaces -// In each of these modules, the types are defined within a nested namespace of -// the same name so that NAPI can export the TypeScript types within a -// namespace. -pub mod autofill; -pub mod autostart; -pub mod autotype; -pub mod biometrics; -pub mod biometrics_v2; -pub mod chromium_importer; -pub mod clipboards; -pub mod ipc; -pub mod logging; -pub mod passkey_authenticator; -pub mod passwords; -pub mod powermonitors; -pub mod processisolations; -pub mod sshagent; -pub mod windows_registry; +#[napi] +pub mod passwords { + /// The error message returned when a password is not found during retrieval or deletion. + #[napi] + pub const PASSWORD_NOT_FOUND: &str = desktop_core::password::PASSWORD_NOT_FOUND; + + /// Fetch the stored password from the keychain. + /// Throws {@link Error} with message {@link PASSWORD_NOT_FOUND} if the password does not exist. + #[napi] + pub async fn get_password(service: String, account: String) -> napi::Result { + desktop_core::password::get_password(&service, &account) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + /// Save the password to the keychain. Adds an entry if none exists otherwise updates the + /// existing entry. + #[napi] + pub async fn set_password( + service: String, + account: String, + password: String, + ) -> napi::Result<()> { + desktop_core::password::set_password(&service, &account, &password) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + /// Delete the stored password from the keychain. + /// Throws {@link Error} with message {@link PASSWORD_NOT_FOUND} if the password does not exist. + #[napi] + pub async fn delete_password(service: String, account: String) -> napi::Result<()> { + desktop_core::password::delete_password(&service, &account) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + /// Checks if the os secure storage is available + #[napi] + pub async fn is_available() -> napi::Result { + desktop_core::password::is_available() + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } +} + +#[napi] +pub mod biometrics { + use desktop_core::biometric::{Biometric, BiometricTrait}; + + // Prompt for biometric confirmation + #[napi] + pub async fn prompt( + hwnd: napi::bindgen_prelude::Buffer, + message: String, + ) -> napi::Result { + Biometric::prompt(hwnd.into(), message) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[napi] + pub async fn available() -> napi::Result { + Biometric::available() + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[napi] + pub async fn set_biometric_secret( + service: String, + account: String, + secret: String, + key_material: Option, + iv_b64: String, + ) -> napi::Result { + Biometric::set_biometric_secret( + &service, + &account, + &secret, + key_material.map(|m| m.into()), + &iv_b64, + ) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + /// Retrieves the biometric secret for the given service and account. + /// Throws Error with message [`passwords::PASSWORD_NOT_FOUND`] if the secret does not exist. + #[napi] + pub async fn get_biometric_secret( + service: String, + account: String, + key_material: Option, + ) -> napi::Result { + Biometric::get_biometric_secret(&service, &account, key_material.map(|m| m.into())) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + /// Derives key material from biometric data. Returns a string encoded with a + /// base64 encoded key and the base64 encoded challenge used to create it + /// separated by a `|` character. + /// + /// If the iv is provided, it will be used as the challenge. Otherwise a random challenge will + /// be generated. + /// + /// `format!("|")` + #[allow(clippy::unused_async)] // FIXME: Remove unused async! + #[napi] + pub async fn derive_key_material(iv: Option) -> napi::Result { + Biometric::derive_key_material(iv.as_deref()) + .map(|k| k.into()) + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[napi(object)] + pub struct KeyMaterial { + pub os_key_part_b64: String, + pub client_key_part_b64: Option, + } + + impl From for desktop_core::biometric::KeyMaterial { + fn from(km: KeyMaterial) -> Self { + desktop_core::biometric::KeyMaterial { + os_key_part_b64: km.os_key_part_b64, + client_key_part_b64: km.client_key_part_b64, + } + } + } + + #[napi(object)] + pub struct OsDerivedKey { + pub key_b64: String, + pub iv_b64: String, + } + + impl From for OsDerivedKey { + fn from(km: desktop_core::biometric::OsDerivedKey) -> Self { + OsDerivedKey { + key_b64: km.key_b64, + iv_b64: km.iv_b64, + } + } + } +} + +#[napi] +pub mod biometrics_v2 { + use desktop_core::biometric_v2::BiometricTrait; + + #[napi] + pub struct BiometricLockSystem { + inner: desktop_core::biometric_v2::BiometricLockSystem, + } + + #[napi] + pub fn init_biometric_system() -> napi::Result { + Ok(BiometricLockSystem { + inner: desktop_core::biometric_v2::BiometricLockSystem::new(), + }) + } + + #[napi] + pub async fn authenticate( + biometric_lock_system: &BiometricLockSystem, + hwnd: napi::bindgen_prelude::Buffer, + message: String, + ) -> napi::Result { + biometric_lock_system + .inner + .authenticate(hwnd.into(), message) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[napi] + pub async fn authenticate_available( + biometric_lock_system: &BiometricLockSystem, + ) -> napi::Result { + biometric_lock_system + .inner + .authenticate_available() + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[napi] + pub async fn enroll_persistent( + biometric_lock_system: &BiometricLockSystem, + user_id: String, + key: napi::bindgen_prelude::Buffer, + ) -> napi::Result<()> { + biometric_lock_system + .inner + .enroll_persistent(&user_id, &key) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[napi] + pub async fn provide_key( + biometric_lock_system: &BiometricLockSystem, + user_id: String, + key: napi::bindgen_prelude::Buffer, + ) -> napi::Result<()> { + biometric_lock_system + .inner + .provide_key(&user_id, &key) + .await; + Ok(()) + } + + #[napi] + pub async fn unlock( + biometric_lock_system: &BiometricLockSystem, + user_id: String, + hwnd: napi::bindgen_prelude::Buffer, + ) -> napi::Result { + biometric_lock_system + .inner + .unlock(&user_id, hwnd.into()) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + .map(|v| v.into()) + } + + #[napi] + pub async fn unlock_available( + biometric_lock_system: &BiometricLockSystem, + user_id: String, + ) -> napi::Result { + biometric_lock_system + .inner + .unlock_available(&user_id) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[napi] + pub async fn has_persistent( + biometric_lock_system: &BiometricLockSystem, + user_id: String, + ) -> napi::Result { + biometric_lock_system + .inner + .has_persistent(&user_id) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[napi] + pub async fn unenroll( + biometric_lock_system: &BiometricLockSystem, + user_id: String, + ) -> napi::Result<()> { + biometric_lock_system + .inner + .unenroll(&user_id) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } +} + +#[napi] +pub mod clipboards { + #[allow(clippy::unused_async)] // FIXME: Remove unused async! + #[napi] + pub async fn read() -> napi::Result { + desktop_core::clipboard::read().map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[allow(clippy::unused_async)] // FIXME: Remove unused async! + #[napi] + pub async fn write(text: String, password: bool) -> napi::Result<()> { + desktop_core::clipboard::write(&text, password) + .map_err(|e| napi::Error::from_reason(e.to_string())) + } +} + +#[napi] +pub mod sshagent { + use std::sync::Arc; + + use napi::{ + bindgen_prelude::Promise, + threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}, + }; + use tokio::{self, sync::Mutex}; + use tracing::error; + + #[napi] + pub struct SshAgentState { + state: desktop_core::ssh_agent::BitwardenDesktopAgent, + } + + #[napi(object)] + pub struct PrivateKey { + pub private_key: String, + pub name: String, + pub cipher_id: String, + } + + #[napi(object)] + pub struct SshKey { + pub private_key: String, + pub public_key: String, + pub key_fingerprint: String, + } + + #[napi(object)] + pub struct SshUIRequest { + pub cipher_id: Option, + pub is_list: bool, + pub process_name: String, + pub is_forwarding: bool, + pub namespace: Option, + } + + #[allow(clippy::unused_async)] // FIXME: Remove unused async! + #[napi] + pub async fn serve( + callback: ThreadsafeFunction>, + ) -> napi::Result { + let (auth_request_tx, mut auth_request_rx) = + tokio::sync::mpsc::channel::(32); + let (auth_response_tx, auth_response_rx) = + tokio::sync::broadcast::channel::<(u32, bool)>(32); + let auth_response_tx_arc = Arc::new(Mutex::new(auth_response_tx)); + // Wrap callback in Arc so it can be shared across spawned tasks + let callback = Arc::new(callback); + tokio::spawn(async move { + let _ = auth_response_rx; + + while let Some(request) = auth_request_rx.recv().await { + let cloned_response_tx_arc = auth_response_tx_arc.clone(); + let cloned_callback = callback.clone(); + tokio::spawn(async move { + let auth_response_tx_arc = cloned_response_tx_arc; + let callback = cloned_callback; + // In NAPI v3, obtain the JS callback return as a Promise and await it + // in Rust + let (tx, rx) = std::sync::mpsc::channel::>(); + let status = callback.call_with_return_value( + Ok(SshUIRequest { + cipher_id: request.cipher_id, + is_list: request.is_list, + process_name: request.process_name, + is_forwarding: request.is_forwarding, + namespace: request.namespace, + }), + ThreadsafeFunctionCallMode::Blocking, + move |ret: Result, napi::Error>, _env| { + if let Ok(p) = ret { + let _ = tx.send(p); + } + Ok(()) + }, + ); + + let result = if status == napi::Status::Ok { + match rx.recv() { + Ok(promise) => match promise.await { + Ok(v) => v, + Err(e) => { + error!(error = %e, "UI callback promise rejected"); + false + } + }, + Err(e) => { + error!(error = %e, "Failed to receive UI callback promise"); + false + } + } + } else { + error!(error = ?status, "Calling UI callback failed"); + false + }; + + let _ = auth_response_tx_arc + .lock() + .await + .send((request.request_id, result)) + .expect("should be able to send auth response to agent"); + }); + } + }); + + match desktop_core::ssh_agent::BitwardenDesktopAgent::start_server( + auth_request_tx, + Arc::new(Mutex::new(auth_response_rx)), + ) { + Ok(state) => Ok(SshAgentState { state }), + Err(e) => Err(napi::Error::from_reason(e.to_string())), + } + } + + #[napi] + pub fn stop(agent_state: &mut SshAgentState) -> napi::Result<()> { + let bitwarden_agent_state = &mut agent_state.state; + bitwarden_agent_state.stop(); + Ok(()) + } + + #[napi] + pub fn is_running(agent_state: &mut SshAgentState) -> bool { + let bitwarden_agent_state = agent_state.state.clone(); + bitwarden_agent_state.is_running() + } + + #[napi] + pub fn set_keys( + agent_state: &mut SshAgentState, + new_keys: Vec, + ) -> napi::Result<()> { + let bitwarden_agent_state = &mut agent_state.state; + bitwarden_agent_state + .set_keys( + new_keys + .iter() + .map(|k| (k.private_key.clone(), k.name.clone(), k.cipher_id.clone())) + .collect(), + ) + .map_err(|e| napi::Error::from_reason(e.to_string()))?; + Ok(()) + } + + #[napi] + pub fn lock(agent_state: &mut SshAgentState) -> napi::Result<()> { + let bitwarden_agent_state = &mut agent_state.state; + bitwarden_agent_state + .lock() + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[napi] + pub fn clear_keys(agent_state: &mut SshAgentState) -> napi::Result<()> { + let bitwarden_agent_state = &mut agent_state.state; + bitwarden_agent_state + .clear_keys() + .map_err(|e| napi::Error::from_reason(e.to_string())) + } +} + +#[napi] +pub mod processisolations { + #[allow(clippy::unused_async)] // FIXME: Remove unused async! + #[napi] + pub async fn disable_coredumps() -> napi::Result<()> { + desktop_core::process_isolation::disable_coredumps() + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[allow(clippy::unused_async)] // FIXME: Remove unused async! + #[napi] + pub async fn is_core_dumping_disabled() -> napi::Result { + desktop_core::process_isolation::is_core_dumping_disabled() + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[allow(clippy::unused_async)] // FIXME: Remove unused async! + #[napi] + pub async fn isolate_process() -> napi::Result<()> { + desktop_core::process_isolation::isolate_process() + .map_err(|e| napi::Error::from_reason(e.to_string())) + } +} + +#[napi] +pub mod powermonitors { + use napi::{ + threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}, + tokio, + }; + + #[napi] + pub async fn on_lock(callback: ThreadsafeFunction<()>) -> napi::Result<()> { + let (tx, mut rx) = tokio::sync::mpsc::channel::<()>(32); + desktop_core::powermonitor::on_lock(tx) + .await + .map_err(|e| napi::Error::from_reason(e.to_string()))?; + tokio::spawn(async move { + while let Some(()) = rx.recv().await { + callback.call(Ok(()), ThreadsafeFunctionCallMode::NonBlocking); + } + }); + Ok(()) + } + + #[napi] + pub async fn is_lock_monitor_available() -> napi::Result { + Ok(desktop_core::powermonitor::is_lock_monitor_available().await) + } +} + +#[napi] +pub mod windows_registry { + #[allow(clippy::unused_async)] // FIXME: Remove unused async! + #[napi] + pub async fn create_key(key: String, subkey: String, value: String) -> napi::Result<()> { + crate::registry::create_key(&key, &subkey, &value) + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[allow(clippy::unused_async)] // FIXME: Remove unused async! + #[napi] + pub async fn delete_key(key: String, subkey: String) -> napi::Result<()> { + crate::registry::delete_key(&key, &subkey) + .map_err(|e| napi::Error::from_reason(e.to_string())) + } +} + +#[napi] +pub mod ipc { + use desktop_core::ipc::server::{Message, MessageType}; + use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}; + + #[napi(object)] + pub struct IpcMessage { + pub client_id: u32, + pub kind: IpcMessageType, + pub message: Option, + } + + impl From for IpcMessage { + fn from(message: Message) -> Self { + IpcMessage { + client_id: message.client_id, + kind: message.kind.into(), + message: message.message, + } + } + } + + #[napi] + pub enum IpcMessageType { + Connected, + Disconnected, + Message, + } + + impl From for IpcMessageType { + fn from(message_type: MessageType) -> Self { + match message_type { + MessageType::Connected => IpcMessageType::Connected, + MessageType::Disconnected => IpcMessageType::Disconnected, + MessageType::Message => IpcMessageType::Message, + } + } + } + + #[napi] + pub struct NativeIpcServer { + server: desktop_core::ipc::server::Server, + } + + #[napi] + impl NativeIpcServer { + /// Create and start the IPC server without blocking. + /// + /// @param name The endpoint name to listen on. This name uniquely identifies the IPC + /// connection and must be the same for both the server and client. @param callback + /// This function will be called whenever a message is received from a client. + #[allow(clippy::unused_async)] // FIXME: Remove unused async! + #[napi(factory)] + pub async fn listen( + name: String, + #[napi(ts_arg_type = "(error: null | Error, message: IpcMessage) => void")] + callback: ThreadsafeFunction, + ) -> napi::Result { + let (send, mut recv) = tokio::sync::mpsc::channel::(32); + tokio::spawn(async move { + while let Some(message) = recv.recv().await { + callback.call(Ok(message.into()), ThreadsafeFunctionCallMode::NonBlocking); + } + }); + + let path = desktop_core::ipc::path(&name); + + let server = desktop_core::ipc::server::Server::start(&path, send).map_err(|e| { + napi::Error::from_reason(format!( + "Error listening to server - Path: {path:?} - Error: {e} - {e:?}" + )) + })?; + + Ok(NativeIpcServer { server }) + } + + /// Return the path to the IPC server. + #[napi] + pub fn get_path(&self) -> String { + self.server.path.to_string_lossy().to_string() + } + + /// Stop the IPC server. + #[napi] + pub fn stop(&self) -> napi::Result<()> { + self.server.stop(); + Ok(()) + } + + /// Send a message over the IPC server to all the connected clients + /// + /// @return The number of clients that the message was sent to. Note that the number of + /// messages actually received may be less, as some clients could disconnect before + /// receiving the message. + #[napi] + pub fn send(&self, message: String) -> napi::Result { + self.server + .send(message) + .map_err(|e| { + napi::Error::from_reason(format!("Error sending message - Error: {e} - {e:?}")) + }) + // NAPI doesn't support u64 or usize, so we need to convert to u32 + .map(|u| u32::try_from(u).unwrap_or_default()) + } + } +} + +#[napi] +pub mod autostart { + #[napi] + pub async fn set_autostart(autostart: bool, params: Vec) -> napi::Result<()> { + desktop_core::autostart::set_autostart(autostart, params) + .await + .map_err(|e| napi::Error::from_reason(format!("Error setting autostart - {e} - {e:?}"))) + } +} + +#[napi] +pub mod autofill { + use desktop_core::ipc::server::{Message, MessageType}; + use napi::{ + bindgen_prelude::FnArgs, + threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}, + }; + use serde::{de::DeserializeOwned, Deserialize, Serialize}; + use tracing::error; + + #[napi] + pub async fn run_command(value: String) -> napi::Result { + desktop_core::autofill::run_command(value) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[derive(Debug, serde::Serialize, serde:: Deserialize)] + pub enum BitwardenError { + Internal(String), + } + + #[napi(string_enum)] + #[derive(Debug, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub enum UserVerification { + #[napi(value = "preferred")] + Preferred, + #[napi(value = "required")] + Required, + #[napi(value = "discouraged")] + Discouraged, + } + + #[derive(Serialize, Deserialize)] + #[serde(bound = "T: Serialize + DeserializeOwned")] + pub struct PasskeyMessage { + pub sequence_number: u32, + pub value: Result, + } + + #[napi(object)] + #[derive(Debug, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct Position { + pub x: i32, + pub y: i32, + } + + #[napi(object)] + #[derive(Debug, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct PasskeyRegistrationRequest { + pub rp_id: String, + pub user_name: String, + pub user_handle: Vec, + pub client_data_hash: Vec, + pub user_verification: UserVerification, + pub supported_algorithms: Vec, + pub window_xy: Position, + pub excluded_credentials: Vec>, + } + + #[napi(object)] + #[derive(Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct PasskeyRegistrationResponse { + pub rp_id: String, + pub client_data_hash: Vec, + pub credential_id: Vec, + pub attestation_object: Vec, + } + + #[napi(object)] + #[derive(Debug, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct PasskeyAssertionRequest { + pub rp_id: String, + pub client_data_hash: Vec, + pub user_verification: UserVerification, + pub allowed_credentials: Vec>, + pub window_xy: Position, + //extension_input: Vec, TODO: Implement support for extensions + } + + #[napi(object)] + #[derive(Debug, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct PasskeyAssertionWithoutUserInterfaceRequest { + pub rp_id: String, + pub credential_id: Vec, + pub user_name: String, + pub user_handle: Vec, + pub record_identifier: Option, + pub client_data_hash: Vec, + pub user_verification: UserVerification, + pub window_xy: Position, + } + + #[napi(object)] + #[derive(Debug, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct NativeStatus { + pub key: String, + pub value: String, + } + + #[napi(object)] + #[derive(Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct PasskeyAssertionResponse { + pub rp_id: String, + pub user_handle: Vec, + pub signature: Vec, + pub client_data_hash: Vec, + pub authenticator_data: Vec, + pub credential_id: Vec, + } + + #[napi] + pub struct AutofillIpcServer { + server: desktop_core::ipc::server::Server, + } + + // FIXME: Remove unwraps! They panic and terminate the whole application. + #[allow(clippy::unwrap_used)] + #[napi] + impl AutofillIpcServer { + /// Create and start the IPC server without blocking. + /// + /// @param name The endpoint name to listen on. This name uniquely identifies the IPC + /// connection and must be the same for both the server and client. @param callback + /// This function will be called whenever a message is received from a client. + #[allow(clippy::unused_async)] // FIXME: Remove unused async! + #[napi(factory)] + pub async fn listen( + name: String, + // Ideally we'd have a single callback that has an enum containing the request values, + // but NAPI doesn't support that just yet + #[napi( + ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void" + )] + registration_callback: ThreadsafeFunction< + FnArgs<(u32, u32, PasskeyRegistrationRequest)>, + >, + #[napi( + ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void" + )] + assertion_callback: ThreadsafeFunction< + FnArgs<(u32, u32, PasskeyAssertionRequest)>, + >, + #[napi( + ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionWithoutUserInterfaceRequest) => void" + )] + assertion_without_user_interface_callback: ThreadsafeFunction< + FnArgs<(u32, u32, PasskeyAssertionWithoutUserInterfaceRequest)>, + >, + #[napi( + ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: NativeStatus) => void" + )] + native_status_callback: ThreadsafeFunction<(u32, u32, NativeStatus)>, + ) -> napi::Result { + let (send, mut recv) = tokio::sync::mpsc::channel::(32); + tokio::spawn(async move { + while let Some(Message { + client_id, + kind, + message, + }) = recv.recv().await + { + match kind { + // TODO: We're ignoring the connection and disconnection messages for now + MessageType::Connected | MessageType::Disconnected => continue, + MessageType::Message => { + let Some(message) = message else { + error!("Message is empty"); + continue; + }; + + match serde_json::from_str::>( + &message, + ) { + Ok(msg) => { + let value = msg + .value + .map(|value| (client_id, msg.sequence_number, value).into()) + .map_err(|e| napi::Error::from_reason(format!("{e:?}"))); + + assertion_callback + .call(value, ThreadsafeFunctionCallMode::NonBlocking); + continue; + } + Err(e) => { + error!(error = %e, "Error deserializing message1"); + } + } + + match serde_json::from_str::< + PasskeyMessage, + >(&message) + { + Ok(msg) => { + let value = msg + .value + .map(|value| (client_id, msg.sequence_number, value).into()) + .map_err(|e| napi::Error::from_reason(format!("{e:?}"))); + + assertion_without_user_interface_callback + .call(value, ThreadsafeFunctionCallMode::NonBlocking); + continue; + } + Err(e) => { + error!(error = %e, "Error deserializing message1"); + } + } + + match serde_json::from_str::>( + &message, + ) { + Ok(msg) => { + let value = msg + .value + .map(|value| (client_id, msg.sequence_number, value).into()) + .map_err(|e| napi::Error::from_reason(format!("{e:?}"))); + registration_callback + .call(value, ThreadsafeFunctionCallMode::NonBlocking); + continue; + } + Err(e) => { + error!(error = %e, "Error deserializing message2"); + } + } + + match serde_json::from_str::>(&message) { + Ok(msg) => { + let value = msg + .value + .map(|value| (client_id, msg.sequence_number, value)) + .map_err(|e| napi::Error::from_reason(format!("{e:?}"))); + native_status_callback + .call(value, ThreadsafeFunctionCallMode::NonBlocking); + continue; + } + Err(error) => { + error!(%error, "Unable to deserialze native status."); + } + } + + error!(message, "Received an unknown message2"); + } + } + } + }); + + let path = desktop_core::ipc::path(&name); + + let server = desktop_core::ipc::server::Server::start(&path, send).map_err(|e| { + napi::Error::from_reason(format!( + "Error listening to server - Path: {path:?} - Error: {e} - {e:?}" + )) + })?; + + Ok(AutofillIpcServer { server }) + } + + /// Return the path to the IPC server. + #[napi] + pub fn get_path(&self) -> String { + self.server.path.to_string_lossy().to_string() + } + + /// Stop the IPC server. + #[napi] + pub fn stop(&self) -> napi::Result<()> { + self.server.stop(); + Ok(()) + } + + #[napi] + pub fn complete_registration( + &self, + client_id: u32, + sequence_number: u32, + response: PasskeyRegistrationResponse, + ) -> napi::Result { + let message = PasskeyMessage { + sequence_number, + value: Ok(response), + }; + self.send(client_id, serde_json::to_string(&message).unwrap()) + } + + #[napi] + pub fn complete_assertion( + &self, + client_id: u32, + sequence_number: u32, + response: PasskeyAssertionResponse, + ) -> napi::Result { + let message = PasskeyMessage { + sequence_number, + value: Ok(response), + }; + self.send(client_id, serde_json::to_string(&message).unwrap()) + } + + #[napi] + pub fn complete_error( + &self, + client_id: u32, + sequence_number: u32, + error: String, + ) -> napi::Result { + let message: PasskeyMessage<()> = PasskeyMessage { + sequence_number, + value: Err(BitwardenError::Internal(error)), + }; + self.send(client_id, serde_json::to_string(&message).unwrap()) + } + + // TODO: Add a way to send a message to a specific client? + fn send(&self, _client_id: u32, message: String) -> napi::Result { + self.server + .send(message) + .map_err(|e| { + napi::Error::from_reason(format!("Error sending message - Error: {e} - {e:?}")) + }) + // NAPI doesn't support u64 or usize, so we need to convert to u32 + .map(|u| u32::try_from(u).unwrap_or_default()) + } + } +} + +#[napi] +pub mod passkey_authenticator { + #[napi] + pub fn register() -> napi::Result<()> { + crate::passkey_authenticator_internal::register().map_err(|e| { + napi::Error::from_reason(format!("Passkey registration failed - Error: {e} - {e:?}")) + }) + } +} + +#[napi] +pub mod logging { + //! `logging` is the interface between the native desktop's usage of the `tracing` crate + //! for logging, to intercept events and write to the JS space. + //! + //! # Example + //! + //! [Elec] 14:34:03.517 › [NAPI] [INFO] desktop_core::ssh_agent::platform_ssh_agent: Starting + //! SSH Agent server {socket=/Users/foo/.bitwarden-ssh-agent.sock} + + use std::{fmt::Write, sync::OnceLock}; + + use napi::{ + bindgen_prelude::FnArgs, + threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}, + }; + use tracing::Level; + use tracing_subscriber::{ + filter::EnvFilter, + fmt::format::{DefaultVisitor, Writer}, + layer::SubscriberExt, + util::SubscriberInitExt, + Layer, + }; + + struct JsLogger(OnceLock>>); + static JS_LOGGER: JsLogger = JsLogger(OnceLock::new()); + + #[napi] + pub enum LogLevel { + Trace, + Debug, + Info, + Warn, + Error, + } + + impl From<&Level> for LogLevel { + fn from(level: &Level) -> Self { + match *level { + Level::TRACE => LogLevel::Trace, + Level::DEBUG => LogLevel::Debug, + Level::INFO => LogLevel::Info, + Level::WARN => LogLevel::Warn, + Level::ERROR => LogLevel::Error, + } + } + } + + // JsLayer lets us intercept events and write them to the JS Logger. + struct JsLayer; + + impl Layer for JsLayer + where + S: tracing::Subscriber, + { + // This function builds a log message buffer from the event data and + // calls the JS logger with it. + // + // For example, this log call: + // + // ``` + // mod supreme { + // mod module { + // let foo = "bar"; + // info!(best_variable_name = %foo, "Foo done it again."); + // } + // } + // ``` + // + // , results in the following string: + // + // [INFO] supreme::module: Foo done it again. {best_variable_name=bar} + fn on_event( + &self, + event: &tracing::Event<'_>, + _ctx: tracing_subscriber::layer::Context<'_, S>, + ) { + let mut buffer = String::new(); + + // create the preamble text that precedes the message and vars. e.g.: + // [INFO] desktop_core::ssh_agent::platform_ssh_agent: + let level = event.metadata().level().as_str(); + let module_path = event.metadata().module_path().unwrap_or_default(); + + write!(&mut buffer, "[{level}] {module_path}:") + .expect("Failed to write tracing event to buffer"); + + let writer = Writer::new(&mut buffer); + + // DefaultVisitor adds the message and variables to the buffer + let mut visitor = DefaultVisitor::new(writer, false); + event.record(&mut visitor); + + let msg = (event.metadata().level().into(), buffer); + + if let Some(logger) = JS_LOGGER.0.get() { + let _ = logger.call(Ok(msg.into()), ThreadsafeFunctionCallMode::NonBlocking); + }; + } + } + + #[napi] + pub fn init_napi_log(js_log_fn: ThreadsafeFunction>) { + let _ = JS_LOGGER.0.set(js_log_fn); + + // the log level hierarchy is determined by: + // - if RUST_LOG is detected at runtime + // - if RUST_LOG is provided at compile time + // - default to INFO + let filter = EnvFilter::builder() + .with_default_directive( + option_env!("RUST_LOG") + .unwrap_or("info") + .parse() + .expect("should provide valid log level at compile time."), + ) + // parse directives from the RUST_LOG environment variable, + // overriding the default directive for matching targets. + .from_env_lossy(); + + // With the `tracing-log` feature enabled for the `tracing_subscriber`, + // the registry below will initialize a log compatibility layer, which allows + // the subscriber to consume log::Records as though they were tracing Events. + // https://docs.rs/tracing-subscriber/latest/tracing_subscriber/util/trait.SubscriberInitExt.html#method.init + tracing_subscriber::registry() + .with(filter) + .with(JsLayer) + .init(); + } +} + +#[napi] +pub mod chromium_importer { + use std::collections::HashMap; + + use chromium_importer::{ + chromium::{ + DefaultInstalledBrowserRetriever, LoginImportResult as _LoginImportResult, + ProfileInfo as _ProfileInfo, + }, + metadata::NativeImporterMetadata as _NativeImporterMetadata, + }; + + #[napi(object)] + pub struct ProfileInfo { + pub id: String, + pub name: String, + } + + #[napi(object)] + pub struct Login { + pub url: String, + pub username: String, + pub password: String, + pub note: String, + } + + #[napi(object)] + pub struct LoginImportFailure { + pub url: String, + pub username: String, + pub error: String, + } + + #[napi(object)] + pub struct LoginImportResult { + pub login: Option, + pub failure: Option, + } + + #[napi(object)] + pub struct NativeImporterMetadata { + pub id: String, + pub loaders: Vec, + pub instructions: String, + } + + impl From<_LoginImportResult> for LoginImportResult { + fn from(l: _LoginImportResult) -> Self { + match l { + _LoginImportResult::Success(l) => LoginImportResult { + login: Some(Login { + url: l.url, + username: l.username, + password: l.password, + note: l.note, + }), + failure: None, + }, + _LoginImportResult::Failure(l) => LoginImportResult { + login: None, + failure: Some(LoginImportFailure { + url: l.url, + username: l.username, + error: l.error, + }), + }, + } + } + } + + impl From<_ProfileInfo> for ProfileInfo { + fn from(p: _ProfileInfo) -> Self { + ProfileInfo { + id: p.folder, + name: p.name, + } + } + } + + impl From<_NativeImporterMetadata> for NativeImporterMetadata { + fn from(m: _NativeImporterMetadata) -> Self { + NativeImporterMetadata { + id: m.id, + loaders: m.loaders, + instructions: m.instructions, + } + } + } + + #[napi] + /// Returns OS aware metadata describing supported Chromium based importers as a JSON string. + pub fn get_metadata() -> HashMap { + chromium_importer::metadata::get_supported_importers::() + .into_iter() + .map(|(browser, metadata)| (browser, NativeImporterMetadata::from(metadata))) + .collect() + } + + #[napi] + pub fn get_available_profiles(browser: String) -> napi::Result> { + chromium_importer::chromium::get_available_profiles(&browser) + .map(|profiles| profiles.into_iter().map(ProfileInfo::from).collect()) + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[napi] + pub async fn import_logins( + browser: String, + profile_id: String, + ) -> napi::Result> { + chromium_importer::chromium::import_logins(&browser, &profile_id) + .await + .map(|logins| logins.into_iter().map(LoginImportResult::from).collect()) + .map_err(|e| napi::Error::from_reason(e.to_string())) + } +} + +#[napi] +pub mod autotype { + #[napi] + pub fn get_foreground_window_title() -> napi::Result { + autotype::get_foreground_window_title().map_err(|_| { + napi::Error::from_reason( + "Autotype Error: failed to get foreground window title".to_string(), + ) + }) + } + + #[napi] + pub fn type_input( + input: Vec, + keyboard_shortcut: Vec, + ) -> napi::Result<(), napi::Status> { + autotype::type_input(&input, &keyboard_shortcut) + .map_err(|e| napi::Error::from_reason(format!("Autotype Error: {e}"))) + } +} diff --git a/apps/desktop/desktop_native/napi/src/logging.rs b/apps/desktop/desktop_native/napi/src/logging.rs deleted file mode 100644 index e5791065e4e..00000000000 --- a/apps/desktop/desktop_native/napi/src/logging.rs +++ /dev/null @@ -1,131 +0,0 @@ -#[napi] -pub mod logging { - //! `logging` is the interface between the native desktop's usage of the `tracing` crate - //! for logging, to intercept events and write to the JS space. - //! - //! # Example - //! - //! [Elec] 14:34:03.517 › [NAPI] [INFO] desktop_core::ssh_agent::platform_ssh_agent: Starting - //! SSH Agent server {socket=/Users/foo/.bitwarden-ssh-agent.sock} - - use std::{fmt::Write, sync::OnceLock}; - - use napi::{ - bindgen_prelude::FnArgs, - threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}, - }; - use tracing::Level; - use tracing_subscriber::{ - filter::EnvFilter, - fmt::format::{DefaultVisitor, Writer}, - layer::SubscriberExt, - util::SubscriberInitExt, - Layer, - }; - - struct JsLogger(OnceLock>>); - static JS_LOGGER: JsLogger = JsLogger(OnceLock::new()); - - #[napi] - pub enum LogLevel { - Trace, - Debug, - Info, - Warn, - Error, - } - - impl From<&Level> for LogLevel { - fn from(level: &Level) -> Self { - match *level { - Level::TRACE => LogLevel::Trace, - Level::DEBUG => LogLevel::Debug, - Level::INFO => LogLevel::Info, - Level::WARN => LogLevel::Warn, - Level::ERROR => LogLevel::Error, - } - } - } - - // JsLayer lets us intercept events and write them to the JS Logger. - struct JsLayer; - - impl Layer for JsLayer - where - S: tracing::Subscriber, - { - // This function builds a log message buffer from the event data and - // calls the JS logger with it. - // - // For example, this log call: - // - // ``` - // mod supreme { - // mod module { - // let foo = "bar"; - // info!(best_variable_name = %foo, "Foo done it again."); - // } - // } - // ``` - // - // , results in the following string: - // - // [INFO] supreme::module: Foo done it again. {best_variable_name=bar} - fn on_event( - &self, - event: &tracing::Event<'_>, - _ctx: tracing_subscriber::layer::Context<'_, S>, - ) { - let mut buffer = String::new(); - - // create the preamble text that precedes the message and vars. e.g.: - // [INFO] desktop_core::ssh_agent::platform_ssh_agent: - let level = event.metadata().level().as_str(); - let module_path = event.metadata().module_path().unwrap_or_default(); - - write!(&mut buffer, "[{level}] {module_path}:") - .expect("Failed to write tracing event to buffer"); - - let writer = Writer::new(&mut buffer); - - // DefaultVisitor adds the message and variables to the buffer - let mut visitor = DefaultVisitor::new(writer, false); - event.record(&mut visitor); - - let msg = (event.metadata().level().into(), buffer); - - if let Some(logger) = JS_LOGGER.0.get() { - let _ = logger.call(Ok(msg.into()), ThreadsafeFunctionCallMode::NonBlocking); - }; - } - } - - #[napi] - pub fn init_napi_log(js_log_fn: ThreadsafeFunction>) { - let _ = JS_LOGGER.0.set(js_log_fn); - - // the log level hierarchy is determined by: - // - if RUST_LOG is detected at runtime - // - if RUST_LOG is provided at compile time - // - default to INFO - let filter = EnvFilter::builder() - .with_default_directive( - option_env!("RUST_LOG") - .unwrap_or("info") - .parse() - .expect("should provide valid log level at compile time."), - ) - // parse directives from the RUST_LOG environment variable, - // overriding the default directive for matching targets. - .from_env_lossy(); - - // With the `tracing-log` feature enabled for the `tracing_subscriber`, - // the registry below will initialize a log compatibility layer, which allows - // the subscriber to consume log::Records as though they were tracing Events. - // https://docs.rs/tracing-subscriber/latest/tracing_subscriber/util/trait.SubscriberInitExt.html#method.init - tracing_subscriber::registry() - .with(filter) - .with(JsLayer) - .init(); - } -} diff --git a/apps/desktop/desktop_native/napi/src/passkey_authenticator.rs b/apps/desktop/desktop_native/napi/src/passkey_authenticator.rs deleted file mode 100644 index 37796353b80..00000000000 --- a/apps/desktop/desktop_native/napi/src/passkey_authenticator.rs +++ /dev/null @@ -1,9 +0,0 @@ -#[napi] -pub mod passkey_authenticator { - #[napi] - pub fn register() -> napi::Result<()> { - crate::passkey_authenticator_internal::register().map_err(|e| { - napi::Error::from_reason(format!("Passkey registration failed - Error: {e} - {e:?}")) - }) - } -} diff --git a/apps/desktop/desktop_native/napi/src/passwords.rs b/apps/desktop/desktop_native/napi/src/passwords.rs deleted file mode 100644 index 763f338b0cb..00000000000 --- a/apps/desktop/desktop_native/napi/src/passwords.rs +++ /dev/null @@ -1,46 +0,0 @@ -#[napi] -pub mod passwords { - - /// The error message returned when a password is not found during retrieval or deletion. - #[napi] - pub const PASSWORD_NOT_FOUND: &str = desktop_core::password::PASSWORD_NOT_FOUND; - - /// Fetch the stored password from the keychain. - /// Throws {@link Error} with message {@link PASSWORD_NOT_FOUND} if the password does not exist. - #[napi] - pub async fn get_password(service: String, account: String) -> napi::Result { - desktop_core::password::get_password(&service, &account) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - /// Save the password to the keychain. Adds an entry if none exists otherwise updates the - /// existing entry. - #[napi] - pub async fn set_password( - service: String, - account: String, - password: String, - ) -> napi::Result<()> { - desktop_core::password::set_password(&service, &account, &password) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - /// Delete the stored password from the keychain. - /// Throws {@link Error} with message {@link PASSWORD_NOT_FOUND} if the password does not exist. - #[napi] - pub async fn delete_password(service: String, account: String) -> napi::Result<()> { - desktop_core::password::delete_password(&service, &account) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - /// Checks if the os secure storage is available - #[napi] - pub async fn is_available() -> napi::Result { - desktop_core::password::is_available() - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } -} diff --git a/apps/desktop/desktop_native/napi/src/powermonitors.rs b/apps/desktop/desktop_native/napi/src/powermonitors.rs deleted file mode 100644 index eb673bdbe68..00000000000 --- a/apps/desktop/desktop_native/napi/src/powermonitors.rs +++ /dev/null @@ -1,26 +0,0 @@ -#[napi] -pub mod powermonitors { - use napi::{ - threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}, - tokio, - }; - - #[napi] - pub async fn on_lock(callback: ThreadsafeFunction<()>) -> napi::Result<()> { - let (tx, mut rx) = tokio::sync::mpsc::channel::<()>(32); - desktop_core::powermonitor::on_lock(tx) - .await - .map_err(|e| napi::Error::from_reason(e.to_string()))?; - tokio::spawn(async move { - while let Some(()) = rx.recv().await { - callback.call(Ok(()), ThreadsafeFunctionCallMode::NonBlocking); - } - }); - Ok(()) - } - - #[napi] - pub async fn is_lock_monitor_available() -> napi::Result { - Ok(desktop_core::powermonitor::is_lock_monitor_available().await) - } -} diff --git a/apps/desktop/desktop_native/napi/src/processisolations.rs b/apps/desktop/desktop_native/napi/src/processisolations.rs deleted file mode 100644 index 6ab4a2a645d..00000000000 --- a/apps/desktop/desktop_native/napi/src/processisolations.rs +++ /dev/null @@ -1,23 +0,0 @@ -#[napi] -pub mod processisolations { - #[allow(clippy::unused_async)] // FIXME: Remove unused async! - #[napi] - pub async fn disable_coredumps() -> napi::Result<()> { - desktop_core::process_isolation::disable_coredumps() - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[allow(clippy::unused_async)] // FIXME: Remove unused async! - #[napi] - pub async fn is_core_dumping_disabled() -> napi::Result { - desktop_core::process_isolation::is_core_dumping_disabled() - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[allow(clippy::unused_async)] // FIXME: Remove unused async! - #[napi] - pub async fn isolate_process() -> napi::Result<()> { - desktop_core::process_isolation::isolate_process() - .map_err(|e| napi::Error::from_reason(e.to_string())) - } -} diff --git a/apps/desktop/desktop_native/napi/src/sshagent.rs b/apps/desktop/desktop_native/napi/src/sshagent.rs deleted file mode 100644 index 83eec090302..00000000000 --- a/apps/desktop/desktop_native/napi/src/sshagent.rs +++ /dev/null @@ -1,163 +0,0 @@ -#[napi] -pub mod sshagent { - use std::sync::Arc; - - use napi::{ - bindgen_prelude::Promise, - threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}, - }; - use tokio::{self, sync::Mutex}; - use tracing::error; - - #[napi] - pub struct SshAgentState { - state: desktop_core::ssh_agent::BitwardenDesktopAgent, - } - - #[napi(object)] - pub struct PrivateKey { - pub private_key: String, - pub name: String, - pub cipher_id: String, - } - - #[napi(object)] - pub struct SshKey { - pub private_key: String, - pub public_key: String, - pub key_fingerprint: String, - } - - #[napi(object)] - pub struct SshUIRequest { - pub cipher_id: Option, - pub is_list: bool, - pub process_name: String, - pub is_forwarding: bool, - pub namespace: Option, - } - - #[allow(clippy::unused_async)] // FIXME: Remove unused async! - #[napi] - pub async fn serve( - callback: ThreadsafeFunction>, - ) -> napi::Result { - let (auth_request_tx, mut auth_request_rx) = - tokio::sync::mpsc::channel::(32); - let (auth_response_tx, auth_response_rx) = - tokio::sync::broadcast::channel::<(u32, bool)>(32); - let auth_response_tx_arc = Arc::new(Mutex::new(auth_response_tx)); - // Wrap callback in Arc so it can be shared across spawned tasks - let callback = Arc::new(callback); - tokio::spawn(async move { - let _ = auth_response_rx; - - while let Some(request) = auth_request_rx.recv().await { - let cloned_response_tx_arc = auth_response_tx_arc.clone(); - let cloned_callback = callback.clone(); - tokio::spawn(async move { - let auth_response_tx_arc = cloned_response_tx_arc; - let callback = cloned_callback; - // In NAPI v3, obtain the JS callback return as a Promise and await it - // in Rust - let (tx, rx) = std::sync::mpsc::channel::>(); - let status = callback.call_with_return_value( - Ok(SshUIRequest { - cipher_id: request.cipher_id, - is_list: request.is_list, - process_name: request.process_name, - is_forwarding: request.is_forwarding, - namespace: request.namespace, - }), - ThreadsafeFunctionCallMode::Blocking, - move |ret: Result, napi::Error>, _env| { - if let Ok(p) = ret { - let _ = tx.send(p); - } - Ok(()) - }, - ); - - let result = if status == napi::Status::Ok { - match rx.recv() { - Ok(promise) => match promise.await { - Ok(v) => v, - Err(e) => { - error!(error = %e, "UI callback promise rejected"); - false - } - }, - Err(e) => { - error!(error = %e, "Failed to receive UI callback promise"); - false - } - } - } else { - error!(error = ?status, "Calling UI callback failed"); - false - }; - - let _ = auth_response_tx_arc - .lock() - .await - .send((request.request_id, result)) - .expect("should be able to send auth response to agent"); - }); - } - }); - - match desktop_core::ssh_agent::BitwardenDesktopAgent::start_server( - auth_request_tx, - Arc::new(Mutex::new(auth_response_rx)), - ) { - Ok(state) => Ok(SshAgentState { state }), - Err(e) => Err(napi::Error::from_reason(e.to_string())), - } - } - - #[napi] - pub fn stop(agent_state: &mut SshAgentState) -> napi::Result<()> { - let bitwarden_agent_state = &mut agent_state.state; - bitwarden_agent_state.stop(); - Ok(()) - } - - #[napi] - pub fn is_running(agent_state: &mut SshAgentState) -> bool { - let bitwarden_agent_state = agent_state.state.clone(); - bitwarden_agent_state.is_running() - } - - #[napi] - pub fn set_keys( - agent_state: &mut SshAgentState, - new_keys: Vec, - ) -> napi::Result<()> { - let bitwarden_agent_state = &mut agent_state.state; - bitwarden_agent_state - .set_keys( - new_keys - .iter() - .map(|k| (k.private_key.clone(), k.name.clone(), k.cipher_id.clone())) - .collect(), - ) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; - Ok(()) - } - - #[napi] - pub fn lock(agent_state: &mut SshAgentState) -> napi::Result<()> { - let bitwarden_agent_state = &mut agent_state.state; - bitwarden_agent_state - .lock() - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[napi] - pub fn clear_keys(agent_state: &mut SshAgentState) -> napi::Result<()> { - let bitwarden_agent_state = &mut agent_state.state; - bitwarden_agent_state - .clear_keys() - .map_err(|e| napi::Error::from_reason(e.to_string())) - } -} diff --git a/apps/desktop/desktop_native/napi/src/windows_registry.rs b/apps/desktop/desktop_native/napi/src/windows_registry.rs deleted file mode 100644 index e22e2ce46f5..00000000000 --- a/apps/desktop/desktop_native/napi/src/windows_registry.rs +++ /dev/null @@ -1,16 +0,0 @@ -#[napi] -pub mod windows_registry { - #[allow(clippy::unused_async)] // FIXME: Remove unused async! - #[napi] - pub async fn create_key(key: String, subkey: String, value: String) -> napi::Result<()> { - crate::registry::create_key(&key, &subkey, &value) - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[allow(clippy::unused_async)] // FIXME: Remove unused async! - #[napi] - pub async fn delete_key(key: String, subkey: String) -> napi::Result<()> { - crate::registry::delete_key(&key, &subkey) - .map_err(|e| napi::Error::from_reason(e.to_string())) - } -} From 8c6a5775a9612674df47bc9574e17b16cec9655d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 14:40:15 -0600 Subject: [PATCH 121/134] [deps] SM: Update jest (#17554) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: cd-bitwarden <106776772+cd-bitwarden@users.noreply.github.com> --- package-lock.json | 16 ++++++++-------- package.json | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 79541971b12..47ba7456b0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -151,7 +151,7 @@ "jest-diff": "30.2.0", "jest-junit": "16.0.0", "jest-mock-extended": "3.0.7", - "jest-preset-angular": "14.6.1", + "jest-preset-angular": "14.6.2", "json5": "2.2.3", "lint-staged": "16.0.0", "mini-css-extract-plugin": "2.9.4", @@ -169,7 +169,7 @@ "storybook": "9.1.17", "style-loader": "4.0.0", "tailwindcss": "3.4.18", - "ts-jest": "29.4.5", + "ts-jest": "29.4.6", "ts-loader": "9.5.4", "tsconfig-paths-webpack-plugin": "4.2.0", "type-fest": "2.19.0", @@ -28537,9 +28537,9 @@ } }, "node_modules/jest-preset-angular": { - "version": "14.6.1", - "resolved": "https://registry.npmjs.org/jest-preset-angular/-/jest-preset-angular-14.6.1.tgz", - "integrity": "sha512-7q5x42wKrsF2ykOwGVzcXpr9p1X4FQJMU/DnH1tpvCmeOm5XqENdwD/xDZug+nP6G8SJPdioauwdsK/PMY/MpQ==", + "version": "14.6.2", + "resolved": "https://registry.npmjs.org/jest-preset-angular/-/jest-preset-angular-14.6.2.tgz", + "integrity": "sha512-QWnjfXrnYJX65D+iZXBrdQ0ABHSo6DGvcmL3dGYOdF+V2ZhDlqJwKTmt7nyiOcORPdCL+20P8y+Q1mmnjZTHKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -41234,9 +41234,9 @@ "license": "Apache-2.0" }, "node_modules/ts-jest": { - "version": "29.4.5", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.5.tgz", - "integrity": "sha512-HO3GyiWn2qvTQA4kTgjDcXiMwYQt68a1Y8+JuLRVpdIzm+UOLSHgl/XqR4c6nzJkq5rOkjc02O2I7P7l/Yof0Q==", + "version": "29.4.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", + "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 1795e93cf83..ecb49605114 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "jest-diff": "30.2.0", "jest-junit": "16.0.0", "jest-mock-extended": "3.0.7", - "jest-preset-angular": "14.6.1", + "jest-preset-angular": "14.6.2", "json5": "2.2.3", "lint-staged": "16.0.0", "mini-css-extract-plugin": "2.9.4", @@ -136,7 +136,7 @@ "storybook": "9.1.17", "style-loader": "4.0.0", "tailwindcss": "3.4.18", - "ts-jest": "29.4.5", + "ts-jest": "29.4.6", "ts-loader": "9.5.4", "tsconfig-paths-webpack-plugin": "4.2.0", "type-fest": "2.19.0", From c01ce9f99d2e1751c6a61d262c662d8c236f14fb Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Fri, 20 Feb 2026 13:41:47 -0800 Subject: [PATCH 122/134] check for falsy orgnanizationId in cipher bulk collection assignment (#19088) --- apps/web/src/app/vault/individual-vault/vault.component.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 1f80748ab29..a6b80291647 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -1205,8 +1205,7 @@ export class VaultComponent implements OnInit, OnDestr let availableCollections: CollectionView[] = []; const orgId = - this.activeFilter.organizationId || - ciphers.find((c) => c.organizationId !== undefined)?.organizationId; + this.activeFilter.organizationId || ciphers.find((c) => !!c.organizationId)?.organizationId; if (orgId && orgId !== "MyVault") { const organization = this.allOrganizations.find((o) => o.id === orgId); From 60e97a4968b379d4916b5f085ea0237a10a28812 Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Fri, 20 Feb 2026 16:47:44 -0500 Subject: [PATCH 123/134] [PM-32341] use an initial cache value in default cipher form (#19091) * update default cipher form cache with initial cache value --- .../services/default-cipher-form-cache.service.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/libs/vault/src/cipher-form/services/default-cipher-form-cache.service.ts b/libs/vault/src/cipher-form/services/default-cipher-form-cache.service.ts index d525dcd9afa..f83c2bbb15b 100644 --- a/libs/vault/src/cipher-form/services/default-cipher-form-cache.service.ts +++ b/libs/vault/src/cipher-form/services/default-cipher-form-cache.service.ts @@ -18,6 +18,12 @@ export class CipherFormCacheService { */ initializedWithValue: boolean; + /** + * The cipher form will overwrite the cache from various components when initialized + * To prevent this, we store the initial value of the cache when the service is initialized + */ + initialCacheValue: CipherView | null; + private cipherCache = this.viewCacheService.signal({ key: CIPHER_FORM_CACHE_KEY, initialValue: null, @@ -26,6 +32,7 @@ export class CipherFormCacheService { constructor() { this.initializedWithValue = !!this.cipherCache(); + this.initialCacheValue = this.cipherCache(); } /** @@ -42,13 +49,14 @@ export class CipherFormCacheService { * Returns the cached CipherView when available. */ getCachedCipherView(): CipherView | null { - return this.cipherCache(); + return this.initialCacheValue; } /** * Clear the cached CipherView. */ clearCache(): void { + this.initialCacheValue = null; this.cipherCache.set(null); } } From ef7df6b841ae87c069a6a45bf882e528fb5b2b26 Mon Sep 17 00:00:00 2001 From: Jackson Engstrom Date: Fri, 20 Feb 2026 14:28:54 -0800 Subject: [PATCH 124/134] [PM-30521] Add Autofill button to View Login screen for extension (#18766) * adds autofill button for cipher view * adds tests * changes autofill function for non login types * adds top margin to autofill button * adds more top margin to autofill button * only shows autofill button when autofill is allowed (not in a popout) * add button type * updates _domainMatched to take a tab param, updates how the component is passed through to slot * fixes tests from rename * adds comment about autofill tab checking behavior * removes diff markers --- .../item-more-options.component.ts | 2 + .../components/vault/view/view.component.html | 16 +- .../vault/view/view.component.spec.ts | 470 +++++++++++++++++- .../components/vault/view/view.component.ts | 127 ++++- libs/common/src/enums/feature-flag.enum.ts | 2 + .../cipher-view/cipher-view.component.html | 1 + .../item-details-v2.component.html | 1 + 7 files changed, 615 insertions(+), 4 deletions(-) diff --git a/apps/browser/src/vault/popup/components/vault/item-more-options/item-more-options.component.ts b/apps/browser/src/vault/popup/components/vault/item-more-options/item-more-options.component.ts index f7fe9ee1494..e564ca0ceea 100644 --- a/apps/browser/src/vault/popup/components/vault/item-more-options/item-more-options.component.ts +++ b/apps/browser/src/vault/popup/components/vault/item-more-options/item-more-options.component.ts @@ -218,6 +218,8 @@ export class ItemMoreOptionsComponent { return; } + //this tab checking should be moved into the vault-popup-autofill service in case the current tab is changed + //ticket: https://bitwarden.atlassian.net/browse/PM-32467 const currentTab = await firstValueFrom(this.vaultPopupAutofillService.currentAutofillTab$); if (!currentTab?.url) { diff --git a/apps/browser/src/vault/popup/components/vault/view/view.component.html b/apps/browser/src/vault/popup/components/vault/view/view.component.html index a3d65522022..0e07497cea9 100644 --- a/apps/browser/src/vault/popup/components/vault/view/view.component.html +++ b/apps/browser/src/vault/popup/components/vault/view/view.component.html @@ -11,7 +11,21 @@ @if (cipher) { - + + @if (showAutofillButton()) { + + } + } diff --git a/apps/browser/src/vault/popup/components/vault/view/view.component.spec.ts b/apps/browser/src/vault/popup/components/vault/view/view.component.spec.ts index 5c94af0205d..af31dee7550 100644 --- a/apps/browser/src/vault/popup/components/vault/view/view.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault/view/view.component.spec.ts @@ -2,7 +2,7 @@ import { ComponentFixture, fakeAsync, flush, TestBed, tick } from "@angular/core import { By } from "@angular/platform-browser"; import { ActivatedRoute, Router } from "@angular/router"; import { mock } from "jest-mock-extended"; -import { of, Subject } from "rxjs"; +import { BehaviorSubject, of, Subject } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -18,6 +18,7 @@ import { import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { EventType } from "@bitwarden/common/enums"; +import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -33,6 +34,7 @@ import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folde import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { TaskService } from "@bitwarden/common/vault/tasks"; import { DialogService, ToastService } from "@bitwarden/components"; @@ -47,6 +49,10 @@ import BrowserPopupUtils from "../../../../../platform/browser/browser-popup-uti import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service"; import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service"; import { VaultPopupScrollPositionService } from "../../../services/vault-popup-scroll-position.service"; +import { + AutofillConfirmationDialogComponent, + AutofillConfirmationDialogResult, +} from "../autofill-confirmation-dialog/autofill-confirmation-dialog.component"; import { ViewComponent } from "./view.component"; @@ -62,6 +68,7 @@ describe("ViewComponent", () => { const mockNavigate = jest.fn(); const collect = jest.fn().mockResolvedValue(null); const doAutofill = jest.fn().mockResolvedValue(true); + const doAutofillAndSave = jest.fn().mockResolvedValue(true); const copy = jest.fn().mockResolvedValue(true); const back = jest.fn().mockResolvedValue(null); const openSimpleDialog = jest.fn().mockResolvedValue(true); @@ -69,6 +76,8 @@ describe("ViewComponent", () => { const showToast = jest.fn(); const showPasswordPrompt = jest.fn().mockResolvedValue(true); const getFeatureFlag$ = jest.fn().mockReturnValue(of(true)); + const getFeatureFlag = jest.fn().mockResolvedValue(true); + const currentAutofillTab$ = of({ url: "https://example.com", id: 1 }); const mockCipher = { id: "122-333-444", @@ -87,8 +96,12 @@ describe("ViewComponent", () => { const mockPasswordRepromptService = { showPasswordPrompt, }; + const autofillAllowed$ = new BehaviorSubject(true); const mockVaultPopupAutofillService = { doAutofill, + doAutofillAndSave, + currentAutofillTab$, + autofillAllowed$, }; const mockCopyCipherFieldService = { copy, @@ -112,12 +125,15 @@ describe("ViewComponent", () => { mockNavigate.mockClear(); collect.mockClear(); doAutofill.mockClear(); + doAutofillAndSave.mockClear(); copy.mockClear(); stop.mockClear(); openSimpleDialog.mockClear(); back.mockClear(); showToast.mockClear(); showPasswordPrompt.mockClear(); + getFeatureFlag.mockClear(); + autofillAllowed$.next(true); cipherArchiveService.hasArchiveFlagEnabled$ = of(true); cipherArchiveService.userCanArchive$.mockReturnValue(of(false)); cipherArchiveService.archiveWithServer.mockResolvedValue({ id: "122-333-444" } as CipherData); @@ -137,7 +153,7 @@ describe("ViewComponent", () => { { provide: VaultPopupScrollPositionService, useValue: { stop } }, { provide: VaultPopupAutofillService, useValue: mockVaultPopupAutofillService }, { provide: ToastService, useValue: { showToast } }, - { provide: ConfigService, useValue: { getFeatureFlag$ } }, + { provide: ConfigService, useValue: { getFeatureFlag$, getFeatureFlag } }, { provide: I18nService, useValue: { @@ -203,6 +219,8 @@ describe("ViewComponent", () => { provide: DomainSettingsService, useValue: { showFavicons$: of(true), + resolvedDefaultUriMatchStrategy$: of(UriMatchStrategy.Domain), + getUrlEquivalentDomains: jest.fn().mockReturnValue(of([])), }, }, { @@ -697,4 +715,452 @@ describe("ViewComponent", () => { expect(badge).toBeFalsy(); }); }); + + describe("showAutofillButton", () => { + beforeEach(() => { + component.cipher = { ...mockCipher, type: CipherType.Login } as CipherView; + }); + + it("returns true when feature flag is enabled, cipher is a login, and not archived/deleted", fakeAsync(() => { + getFeatureFlag$.mockReturnValue(of(true)); + autofillAllowed$.next(true); + + // Recreate component to pick up the signal values + fixture = TestBed.createComponent(ViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + component.cipher = { + ...mockCipher, + type: CipherType.Login, + isArchived: false, + isDeleted: false, + } as CipherView; + + flush(); + + const result = component.showAutofillButton(); + + expect(result).toBe(true); + })); + + it("returns true for Card type when conditions are met", fakeAsync(() => { + getFeatureFlag$.mockReturnValue(of(true)); + autofillAllowed$.next(true); + + // Recreate component to pick up the signal values + fixture = TestBed.createComponent(ViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + component.cipher = { + ...mockCipher, + type: CipherType.Card, + isArchived: false, + isDeleted: false, + } as CipherView; + + flush(); + + const result = component.showAutofillButton(); + + expect(result).toBe(true); + })); + + it("returns true for Identity type when conditions are met", fakeAsync(() => { + getFeatureFlag$.mockReturnValue(of(true)); + autofillAllowed$.next(true); + + // Recreate component to pick up the signal values + fixture = TestBed.createComponent(ViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + component.cipher = { + ...mockCipher, + type: CipherType.Identity, + isArchived: false, + isDeleted: false, + } as CipherView; + + flush(); + + const result = component.showAutofillButton(); + + expect(result).toBe(true); + })); + + it("returns false when feature flag is disabled", fakeAsync(() => { + getFeatureFlag$.mockReturnValue(of(false)); + + // Recreate component to pick up the new feature flag value + fixture = TestBed.createComponent(ViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + component.cipher = { + ...mockCipher, + type: CipherType.Login, + isArchived: false, + isDeleted: false, + } as CipherView; + + flush(); + + const result = component.showAutofillButton(); + + expect(result).toBe(false); + })); + + it("returns false when autofill is not allowed", fakeAsync(() => { + getFeatureFlag$.mockReturnValue(of(true)); + autofillAllowed$.next(false); + + // Recreate component to pick up the new autofillAllowed value + fixture = TestBed.createComponent(ViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + component.cipher = { + ...mockCipher, + type: CipherType.Login, + isArchived: false, + isDeleted: false, + } as CipherView; + + flush(); + + const result = component.showAutofillButton(); + + expect(result).toBe(false); + })); + + it("returns false for SecureNote type", fakeAsync(() => { + getFeatureFlag$.mockReturnValue(of(true)); + autofillAllowed$.next(true); + + // Recreate component to pick up the signal values + fixture = TestBed.createComponent(ViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + component.cipher = { + ...mockCipher, + type: CipherType.SecureNote, + isArchived: false, + isDeleted: false, + } as CipherView; + + flush(); + + const result = component.showAutofillButton(); + + expect(result).toBe(false); + })); + + it("returns false for SshKey type", fakeAsync(() => { + getFeatureFlag$.mockReturnValue(of(true)); + autofillAllowed$.next(true); + + // Recreate component to pick up the signal values + fixture = TestBed.createComponent(ViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + component.cipher = { + ...mockCipher, + type: CipherType.SshKey, + isArchived: false, + isDeleted: false, + } as CipherView; + + flush(); + + const result = component.showAutofillButton(); + + expect(result).toBe(false); + })); + + it("returns false when cipher is archived", fakeAsync(() => { + getFeatureFlag$.mockReturnValue(of(true)); + autofillAllowed$.next(true); + + // Recreate component to pick up the signal values + fixture = TestBed.createComponent(ViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + component.cipher = { + ...mockCipher, + type: CipherType.Login, + isArchived: true, + isDeleted: false, + } as CipherView; + + flush(); + + const result = component.showAutofillButton(); + + expect(result).toBe(false); + })); + + it("returns false when cipher is deleted", fakeAsync(() => { + getFeatureFlag$.mockReturnValue(of(true)); + autofillAllowed$.next(true); + + // Recreate component to pick up the signal values + fixture = TestBed.createComponent(ViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + component.cipher = { + ...mockCipher, + type: CipherType.Login, + isArchived: false, + isDeleted: true, + } as CipherView; + + flush(); + + const result = component.showAutofillButton(); + + expect(result).toBe(false); + })); + }); + + describe("doAutofill", () => { + let dialogService: DialogService; + const originalCurrentAutofillTab$ = currentAutofillTab$; + + beforeEach(() => { + dialogService = TestBed.inject(DialogService); + + component.cipher = { + ...mockCipher, + type: CipherType.Login, + login: { + username: "test", + password: "test", + uris: [ + { + uri: "https://example.com", + match: null, + } as LoginUriView, + ], + }, + edit: true, + } as CipherView; + }); + + afterEach(() => { + // Restore original observable to prevent test pollution + mockVaultPopupAutofillService.currentAutofillTab$ = originalCurrentAutofillTab$; + }); + + it("returns early when feature flag is disabled", async () => { + getFeatureFlag.mockResolvedValue(false); + + await component.doAutofill(); + + expect(doAutofill).not.toHaveBeenCalled(); + expect(openSimpleDialog).not.toHaveBeenCalled(); + }); + + it("shows exact match dialog when no URIs and default strategy is Exact", async () => { + getFeatureFlag.mockResolvedValue(true); + component.cipher.login.uris = []; + (component as any).uriMatchStrategy$ = of(UriMatchStrategy.Exact); + + await component.doAutofill(); + + expect(openSimpleDialog).toHaveBeenCalledWith({ + title: { key: "cannotAutofill" }, + content: { key: "cannotAutofillExactMatch" }, + type: "info", + acceptButtonText: { key: "okay" }, + cancelButtonText: null, + }); + expect(doAutofill).not.toHaveBeenCalled(); + }); + + it("shows exact match dialog when all URIs have exact match strategy", async () => { + getFeatureFlag.mockResolvedValue(true); + component.cipher.login.uris = [ + { uri: "https://example.com", match: UriMatchStrategy.Exact } as LoginUriView, + { uri: "https://example2.com", match: UriMatchStrategy.Exact } as LoginUriView, + ]; + + await component.doAutofill(); + + expect(openSimpleDialog).toHaveBeenCalledWith({ + title: { key: "cannotAutofill" }, + content: { key: "cannotAutofillExactMatch" }, + type: "info", + acceptButtonText: { key: "okay" }, + cancelButtonText: null, + }); + expect(doAutofill).not.toHaveBeenCalled(); + }); + + it("shows error dialog when current tab URL is unavailable", async () => { + getFeatureFlag.mockResolvedValue(true); + mockVaultPopupAutofillService.currentAutofillTab$ = of({ url: null, id: 1 }); + + await component.doAutofill(); + + expect(openSimpleDialog).toHaveBeenCalledWith({ + title: { key: "error" }, + content: { key: "errorGettingAutoFillData" }, + type: "danger", + }); + expect(doAutofill).not.toHaveBeenCalled(); + }); + + it("autofills directly when domain matches", async () => { + getFeatureFlag.mockResolvedValue(true); + jest.spyOn(component as any, "_domainMatched").mockResolvedValue(true); + + await component.doAutofill(); + + expect(doAutofill).toHaveBeenCalledWith(component.cipher, true, true); + }); + + it("shows confirmation dialog when domain does not match", async () => { + getFeatureFlag.mockResolvedValue(true); + jest.spyOn(component as any, "_domainMatched").mockResolvedValue(false); + + const mockDialogRef = { + closed: of(AutofillConfirmationDialogResult.Canceled), + }; + jest.spyOn(AutofillConfirmationDialogComponent, "open").mockReturnValue(mockDialogRef as any); + + await component.doAutofill(); + + expect(AutofillConfirmationDialogComponent.open).toHaveBeenCalledWith(dialogService, { + data: { + currentUrl: "https://example.com", + savedUrls: ["https://example.com"], + viewOnly: false, + }, + }); + }); + + it("does not autofill when user cancels confirmation dialog", async () => { + getFeatureFlag.mockResolvedValue(true); + jest.spyOn(component as any, "_domainMatched").mockResolvedValue(false); + + const mockDialogRef = { + closed: of(AutofillConfirmationDialogResult.Canceled), + }; + jest.spyOn(AutofillConfirmationDialogComponent, "open").mockReturnValue(mockDialogRef as any); + + await component.doAutofill(); + + expect(doAutofill).not.toHaveBeenCalled(); + expect(doAutofillAndSave).not.toHaveBeenCalled(); + }); + + it("autofills only when user selects AutofilledOnly", async () => { + getFeatureFlag.mockResolvedValue(true); + jest.spyOn(component as any, "_domainMatched").mockResolvedValue(false); + + const mockDialogRef = { + closed: of(AutofillConfirmationDialogResult.AutofilledOnly), + }; + jest.spyOn(AutofillConfirmationDialogComponent, "open").mockReturnValue(mockDialogRef as any); + + await component.doAutofill(); + + expect(doAutofill).toHaveBeenCalledWith(component.cipher, true, true); + expect(doAutofillAndSave).not.toHaveBeenCalled(); + }); + + it("autofills and saves URL when user selects AutofillAndUrlAdded", async () => { + getFeatureFlag.mockResolvedValue(true); + jest.spyOn(component as any, "_domainMatched").mockResolvedValue(false); + + const mockDialogRef = { + closed: of(AutofillConfirmationDialogResult.AutofillAndUrlAdded), + }; + jest.spyOn(AutofillConfirmationDialogComponent, "open").mockReturnValue(mockDialogRef as any); + + await component.doAutofill(); + + expect(doAutofillAndSave).toHaveBeenCalledWith(component.cipher, true, true); + expect(doAutofill).not.toHaveBeenCalled(); + }); + + it("passes viewOnly as true when cipher is not editable", async () => { + getFeatureFlag.mockResolvedValue(true); + jest.spyOn(component as any, "_domainMatched").mockResolvedValue(false); + component.cipher.edit = false; + + const mockDialogRef = { + closed: of(AutofillConfirmationDialogResult.Canceled), + }; + const openSpy = jest + .spyOn(AutofillConfirmationDialogComponent, "open") + .mockReturnValue(mockDialogRef as any); + + await component.doAutofill(); + + expect(openSpy).toHaveBeenCalledWith(dialogService, { + data: { + currentUrl: "https://example.com", + savedUrls: ["https://example.com"], + viewOnly: true, + }, + }); + }); + + it("filters out URIs without uri property", async () => { + getFeatureFlag.mockResolvedValue(true); + jest.spyOn(component as any, "_domainMatched").mockResolvedValue(false); + component.cipher.login.uris = [ + { uri: "https://example.com" } as LoginUriView, + { uri: null } as LoginUriView, + { uri: "https://example2.com" } as LoginUriView, + ]; + + const mockDialogRef = { + closed: of(AutofillConfirmationDialogResult.Canceled), + }; + const openSpy = jest + .spyOn(AutofillConfirmationDialogComponent, "open") + .mockReturnValue(mockDialogRef as any); + + await component.doAutofill(); + + expect(openSpy).toHaveBeenCalledWith(dialogService, { + data: { + currentUrl: "https://example.com", + savedUrls: ["https://example.com", "https://example2.com"], + viewOnly: false, + }, + }); + }); + + it("handles cipher with no login uris gracefully", async () => { + getFeatureFlag.mockResolvedValue(true); + jest.spyOn(component as any, "_domainMatched").mockResolvedValue(false); + component.cipher.login.uris = null; + + const mockDialogRef = { + closed: of(AutofillConfirmationDialogResult.Canceled), + }; + const openSpy = jest + .spyOn(AutofillConfirmationDialogComponent, "open") + .mockReturnValue(mockDialogRef as any); + + await component.doAutofill(); + + expect(openSpy).toHaveBeenCalledWith(dialogService, { + data: { + currentUrl: "https://example.com", + savedUrls: [], + viewOnly: false, + }, + }); + }); + }); }); diff --git a/apps/browser/src/vault/popup/components/vault/view/view.component.ts b/apps/browser/src/vault/popup/components/vault/view/view.component.ts index 48402a957d6..5166dbcf8db 100644 --- a/apps/browser/src/vault/popup/components/vault/view/view.component.ts +++ b/apps/browser/src/vault/popup/components/vault/view/view.component.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { CommonModule } from "@angular/common"; import { Component } from "@angular/core"; -import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { takeUntilDestroyed, toSignal } from "@angular/core/rxjs-interop"; import { FormsModule } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { firstValueFrom, Observable, switchMap, of, map } from "rxjs"; @@ -21,7 +21,11 @@ import { SHOW_AUTOFILL_BUTTON, UPDATE_PASSWORD, } from "@bitwarden/common/autofill/constants"; +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { EventType } from "@bitwarden/common/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { UserId } from "@bitwarden/common/types/guid"; @@ -32,6 +36,7 @@ import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; +import { CipherViewLikeUtils } from "@bitwarden/common/vault/utils/cipher-view-like-utils"; import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities"; import { AsyncActionsModule, @@ -66,6 +71,10 @@ import { VaultPopupAutofillService } from "../../../services/vault-popup-autofil import { VaultPopupScrollPositionService } from "../../../services/vault-popup-scroll-position.service"; import { closeViewVaultItemPopout, VaultPopoutType } from "../../../utils/vault-popout-window"; import { ROUTES_AFTER_EDIT_DELETION } from "../add-edit/add-edit.component"; +import { + AutofillConfirmationDialogComponent, + AutofillConfirmationDialogResult, +} from "../autofill-confirmation-dialog/autofill-confirmation-dialog.component"; /** * The types of actions that can be triggered when loading the view vault item popout via the @@ -118,6 +127,13 @@ export class ViewComponent { senderTabId?: number; routeAfterDeletion?: ROUTES_AFTER_EDIT_DELETION; + //feature flag + private readonly pm30521FeatureFlag = toSignal( + this.configService.getFeatureFlag$(FeatureFlag.PM30521_AutofillButtonViewLoginScreen), + ); + + private readonly autofillAllowed = toSignal(this.vaultPopupAutofillService.autofillAllowed$); + private uriMatchStrategy$ = this.domainSettingsService.resolvedDefaultUriMatchStrategy$; protected showFooter$: Observable; protected userCanArchive$ = this.accountService.activeAccount$ .pipe(getUserId) @@ -142,6 +158,8 @@ export class ViewComponent { private popupScrollPositionService: VaultPopupScrollPositionService, private archiveService: CipherArchiveService, private archiveCipherUtilsService: ArchiveCipherUtilitiesService, + private domainSettingsService: DomainSettingsService, + private configService: ConfigService, ) { this.subscribeToParams(); } @@ -322,6 +340,113 @@ export class ViewComponent { : this.cipherService.softDeleteWithServer(this.cipher.id, this.activeUserId); } + showAutofillButton(): boolean { + //feature flag + if (!this.pm30521FeatureFlag()) { + return false; + } + + if (!this.autofillAllowed()) { + return false; + } + + const validAutofillType = ( + [CipherType.Login, CipherType.Card, CipherType.Identity] as CipherType[] + ).includes(CipherViewLikeUtils.getType(this.cipher)); + + return validAutofillType && !(this.cipher.isArchived || this.cipher.isDeleted); + } + + async doAutofill() { + //feature flag + if ( + !(await this.configService.getFeatureFlag(FeatureFlag.PM30521_AutofillButtonViewLoginScreen)) + ) { + return; + } + + //for non login types that are still auto-fillable + if (CipherViewLikeUtils.getType(this.cipher) !== CipherType.Login) { + await this.vaultPopupAutofillService.doAutofill(this.cipher, true, true); + return; + } + + const uris = this.cipher.login?.uris ?? []; + const uriMatchStrategy = await firstValueFrom(this.uriMatchStrategy$); + + const showExactMatchDialog = + uris.length === 0 + ? uriMatchStrategy === UriMatchStrategy.Exact + : // all saved URIs are exact match + uris.every((u) => (u.match ?? uriMatchStrategy) === UriMatchStrategy.Exact); + + if (showExactMatchDialog) { + await this.dialogService.openSimpleDialog({ + title: { key: "cannotAutofill" }, + content: { key: "cannotAutofillExactMatch" }, + type: "info", + acceptButtonText: { key: "okay" }, + cancelButtonText: null, + }); + return; + } + + //this tab checking should be moved into the vault-popup-autofill service in case the current tab is changed + //ticket: https://bitwarden.atlassian.net/browse/PM-32467 + const currentTab = await firstValueFrom(this.vaultPopupAutofillService.currentAutofillTab$); + + if (!currentTab?.url) { + await this.dialogService.openSimpleDialog({ + title: { key: "error" }, + content: { key: "errorGettingAutoFillData" }, + type: "danger", + }); + return; + } + + if (await this._domainMatched(currentTab)) { + await this.vaultPopupAutofillService.doAutofill(this.cipher, true, true); + return; + } + + const ref = AutofillConfirmationDialogComponent.open(this.dialogService, { + data: { + currentUrl: currentTab?.url || "", + savedUrls: this.cipher.login?.uris?.filter((u) => u.uri).map((u) => u.uri!) ?? [], + viewOnly: !this.cipher.edit, + }, + }); + + const result = await firstValueFrom(ref.closed); + + switch (result) { + case AutofillConfirmationDialogResult.Canceled: + return; + case AutofillConfirmationDialogResult.AutofilledOnly: + await this.vaultPopupAutofillService.doAutofill(this.cipher, true, true); + return; + case AutofillConfirmationDialogResult.AutofillAndUrlAdded: + await this.vaultPopupAutofillService.doAutofillAndSave(this.cipher, true, true); + return; + } + } + + private async _domainMatched(currentTab: chrome.tabs.Tab): Promise { + const equivalentDomains = await firstValueFrom( + this.domainSettingsService.getUrlEquivalentDomains(currentTab?.url), + ); + const defaultMatch = await firstValueFrom( + this.domainSettingsService.resolvedDefaultUriMatchStrategy$, + ); + + return CipherViewLikeUtils.matchesUri( + this.cipher, + currentTab?.url, + equivalentDomains, + defaultMatch, + ); + } + /** * Handles the load action for the view vault item popout. These actions are typically triggered * via the extension context menu. It is necessary to render the view for items that have password diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 0cd97eb7f2e..c9e2fa17dd6 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -70,6 +70,7 @@ export enum FeatureFlag { BrowserPremiumSpotlight = "pm-23384-browser-premium-spotlight", MigrateMyVaultToMyItems = "pm-20558-migrate-myvault-to-myitems", PM27632_SdkCipherCrudOperations = "pm-27632-cipher-crud-operations-to-sdk", + PM30521_AutofillButtonViewLoginScreen = "pm-30521-autofill-button-view-login-screen", PM29438_WelcomeDialogWithExtensionPrompt = "pm-29438-welcome-dialog-with-extension-prompt", PM29438_DialogWithExtensionPromptAccountAge = "pm-29438-dialog-with-extension-prompt-account-age", PM29437_WelcomeDialog = "pm-29437-welcome-dialog-no-ext-prompt", @@ -139,6 +140,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.BrowserPremiumSpotlight]: FALSE, [FeatureFlag.PM27632_SdkCipherCrudOperations]: FALSE, [FeatureFlag.MigrateMyVaultToMyItems]: FALSE, + [FeatureFlag.PM30521_AutofillButtonViewLoginScreen]: FALSE, [FeatureFlag.PM29438_WelcomeDialogWithExtensionPrompt]: FALSE, [FeatureFlag.PM29438_DialogWithExtensionPromptAccountAge]: 5, [FeatureFlag.PM29437_WelcomeDialog]: FALSE, diff --git a/libs/vault/src/cipher-view/cipher-view.component.html b/libs/vault/src/cipher-view/cipher-view.component.html index 05d2ecede72..813d1452225 100644 --- a/libs/vault/src/cipher-view/cipher-view.component.html +++ b/libs/vault/src/cipher-view/cipher-view.component.html @@ -37,6 +37,7 @@ [folder]="folder()" [hideOwner]="isAdminConsole()" > + diff --git a/libs/vault/src/cipher-view/item-details/item-details-v2.component.html b/libs/vault/src/cipher-view/item-details/item-details-v2.component.html index edf17f0921c..5687da0a212 100644 --- a/libs/vault/src/cipher-view/item-details/item-details-v2.component.html +++ b/libs/vault/src/cipher-view/item-details/item-details-v2.component.html @@ -89,5 +89,6 @@ }
    + From 2e284c5e5ad02a70035a9f44269fa92d7187279b Mon Sep 17 00:00:00 2001 From: Sola Date: Mon, 23 Feb 2026 16:50:13 +0800 Subject: [PATCH 125/134] Fix biometric authentication in sandboxed environments (Flatpak, Snap, etc.) (#18625) Biometric authentication was failing in Flatpak with the error "Unix process subject does not have uid set". This occurred because polkit could not validate the sandboxed PID against the host PID namespace. Use polkit's system-bus-name subject type instead of unix-process. This allows polkit to query D-Bus for the connection owner's host PID and credentials, bypassing the PID namespace issue. Includes fallback to unix-process for edge cases where D-Bus unique name is unavailable. --- .../desktop_native/core/src/biometric/unix.rs | 27 ++++++++++++++++++- .../core/src/biometric_v2/linux.rs | 27 ++++++++++++++++++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/apps/desktop/desktop_native/core/src/biometric/unix.rs b/apps/desktop/desktop_native/core/src/biometric/unix.rs index 3f4f10a1fcf..f120408a9e5 100644 --- a/apps/desktop/desktop_native/core/src/biometric/unix.rs +++ b/apps/desktop/desktop_native/core/src/biometric/unix.rs @@ -21,7 +21,32 @@ impl super::BiometricTrait for Biometric { async fn prompt(_hwnd: Vec, _message: String) -> Result { let connection = Connection::system().await?; let proxy = AuthorityProxy::new(&connection).await?; - let subject = Subject::new_for_owner(std::process::id(), None, None)?; + + // Use system-bus-name instead of unix-process to avoid PID namespace issues in + // sandboxed environments (e.g., Flatpak). When using unix-process with a PID from + // inside the sandbox, polkit cannot validate it against the host PID namespace. + // + // By using system-bus-name, polkit queries D-Bus for the connection's credentials, + // which includes the correct host PID and UID, avoiding namespace mismatches. + // + // If D-Bus unique name is not available, fall back to the traditional unix-process + // approach for compatibility with non-sandboxed environments. + let subject = if let Some(bus_name) = connection.unique_name() { + use zbus::zvariant::{OwnedValue, Str}; + let mut subject_details = std::collections::HashMap::new(); + subject_details.insert( + "name".to_string(), + OwnedValue::from(Str::from(bus_name.as_str())), + ); + Subject { + subject_kind: "system-bus-name".to_string(), + subject_details, + } + } else { + // Fallback: use unix-process with PID (may not work in sandboxed environments) + Subject::new_for_owner(std::process::id(), None, None)? + }; + let details = std::collections::HashMap::new(); let result = proxy .check_authorization( diff --git a/apps/desktop/desktop_native/core/src/biometric_v2/linux.rs b/apps/desktop/desktop_native/core/src/biometric_v2/linux.rs index ef6527e7b26..2656bd3fdf9 100644 --- a/apps/desktop/desktop_native/core/src/biometric_v2/linux.rs +++ b/apps/desktop/desktop_native/core/src/biometric_v2/linux.rs @@ -96,7 +96,32 @@ async fn polkit_authenticate_bitwarden_policy() -> Result { let connection = Connection::system().await?; let proxy = AuthorityProxy::new(&connection).await?; - let subject = Subject::new_for_owner(std::process::id(), None, None)?; + + // Use system-bus-name instead of unix-process to avoid PID namespace issues in + // sandboxed environments (e.g., Flatpak). When using unix-process with a PID from + // inside the sandbox, polkit cannot validate it against the host PID namespace. + // + // By using system-bus-name, polkit queries D-Bus for the connection's credentials, + // which includes the correct host PID and UID, avoiding namespace mismatches. + // + // If D-Bus unique name is not available, fall back to the traditional unix-process + // approach for compatibility with non-sandboxed environments. + let subject = if let Some(bus_name) = connection.unique_name() { + use zbus::zvariant::{OwnedValue, Str}; + let mut subject_details = std::collections::HashMap::new(); + subject_details.insert( + "name".to_string(), + OwnedValue::from(Str::from(bus_name.as_str())), + ); + Subject { + subject_kind: "system-bus-name".to_string(), + subject_details, + } + } else { + // Fallback: use unix-process with PID (may not work in sandboxed environments) + Subject::new_for_owner(std::process::id(), None, None)? + }; + let details = std::collections::HashMap::new(); let authorization_result = proxy .check_authorization( From 760b426c22d575631a8b6c0908bfc683093e9bc5 Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 04:34:43 -0600 Subject: [PATCH 126/134] Autosync the updated translations (#19129) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/cs/messages.json | 4 +-- apps/desktop/src/locales/de/messages.json | 6 ++-- apps/desktop/src/locales/it/messages.json | 30 ++++++++++---------- apps/desktop/src/locales/zh_CN/messages.json | 6 ++-- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index 0772645a8d4..e5d009cbe2c 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -1024,7 +1024,7 @@ "message": "Neplatný ověřovací kód" }, "invalidEmailOrVerificationCode": { - "message": "Invalid email or verification code" + "message": "Neplatný e-mail nebo ověřovací kód" }, "continue": { "message": "Pokračovat" @@ -1058,7 +1058,7 @@ "message": "Ověřovací aplikace" }, "authenticatorAppDescV2": { - "message": "Zadejte kód vygenerovaný ověřovací aplikací, jako je Autentikátor Bitwarden.", + "message": "Zadejte kód vygenerovaný ověřovací aplikací, jako je Bitwarden Authenticator.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index fb045e30489..6d7f8843a32 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -1024,7 +1024,7 @@ "message": "Ungültiger Verifizierungscode" }, "invalidEmailOrVerificationCode": { - "message": "Invalid email or verification code" + "message": "E-Mail oder Verifizierungscode ungültig" }, "continue": { "message": "Weiter" @@ -4619,12 +4619,12 @@ "message": "Gib mehrere E-Mail-Adressen ein, indem du sie mit einem Komma trennst." }, "emailsRequiredChangeAccessType": { - "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + "message": "E-Mail-Verifizierung erfordert mindestens eine E-Mail-Adresse. Ändere den Zugriffstyp oben, um alle E-Mails zu entfernen." }, "emailPlaceholder": { "message": "benutzer@bitwarden.com, benutzer@acme.com" }, "userVerificationFailed": { - "message": "User verification failed." + "message": "Benutzerverifizierung fehlgeschlagen." } } diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index c394dd84a6f..ba1bb9e5346 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -1024,7 +1024,7 @@ "message": "Codice di verifica non valido" }, "invalidEmailOrVerificationCode": { - "message": "Invalid email or verification code" + "message": "Codice di verifica non valido" }, "continue": { "message": "Continua" @@ -4391,10 +4391,10 @@ "message": "Gli elementi archiviati appariranno qui e saranno esclusi dai risultati di ricerca e dal riempimento automatico." }, "itemArchiveToast": { - "message": "Item archived" + "message": "Elemento archiviato" }, "itemUnarchivedToast": { - "message": "Item unarchived" + "message": "Elemento estratto dall'archivio" }, "archiveItem": { "message": "Archivia elemento" @@ -4490,7 +4490,7 @@ "message": "Azione al timeout" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "Errore: impossibile decrittare" }, "sessionTimeoutHeader": { "message": "Timeout della sessione" @@ -4591,40 +4591,40 @@ "message": "Perché vedo questo avviso?" }, "sendPasswordHelperText": { - "message": "Individuals will need to enter the password to view this Send", + "message": "I destinatari dovranno inserire la password per visualizzare questo Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "emailProtected": { - "message": "Email protected" + "message": "Email protetta" }, "emails": { - "message": "Emails" + "message": "Indirizzi email" }, "noAuth": { - "message": "Anyone with the link" + "message": "Chiunque abbia il link" }, "anyOneWithPassword": { - "message": "Anyone with a password set by you" + "message": "Chiunque abbia una password impostata da te" }, "whoCanView": { - "message": "Who can view" + "message": "Chi può visualizzare" }, "specificPeople": { - "message": "Specific people" + "message": "Persone specifiche" }, "emailVerificationDesc": { - "message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send." + "message": "I destinatari dovranno verificare il loro indirizzo email con un codice per poter visualizzare il Send." }, "enterMultipleEmailsSeparatedByComma": { - "message": "Enter multiple emails by separating with a comma." + "message": "Inserisci più indirizzi email separandoli con virgole." }, "emailsRequiredChangeAccessType": { - "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + "message": "La verifica via email richiede almeno un indirizzo email. Per rimuovere tutte le email, modifica il tipo di accesso qui sopra." }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, "userVerificationFailed": { - "message": "User verification failed." + "message": "Verifica dell'utente non riuscita." } } diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index ad09c8f032e..bd3a897ffab 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -1024,7 +1024,7 @@ "message": "无效的验证码" }, "invalidEmailOrVerificationCode": { - "message": "Invalid email or verification code" + "message": "无效的电子邮箱或验证码" }, "continue": { "message": "继续" @@ -4619,12 +4619,12 @@ "message": "输入多个电子邮箱(使用逗号分隔)。" }, "emailsRequiredChangeAccessType": { - "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + "message": "电子邮箱验证要求至少有一个电子邮箱地址。要移除所有电子邮箱,请更改上面的访问类型。" }, "emailPlaceholder": { "message": "user@bitwarden.com, user@acme.com" }, "userVerificationFailed": { - "message": "User verification failed." + "message": "用户验证失败。" } } From b4235110b00e6c950ef945d893e4c176784b4a3a Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 10:34:48 +0000 Subject: [PATCH 127/134] Autosync the updated translations (#19131) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/web/src/locales/af/messages.json | 15 +++++++++ apps/web/src/locales/ar/messages.json | 15 +++++++++ apps/web/src/locales/az/messages.json | 27 ++++++++++++---- apps/web/src/locales/be/messages.json | 15 +++++++++ apps/web/src/locales/bg/messages.json | 35 +++++++++++++++------ apps/web/src/locales/bn/messages.json | 15 +++++++++ apps/web/src/locales/bs/messages.json | 15 +++++++++ apps/web/src/locales/ca/messages.json | 15 +++++++++ apps/web/src/locales/cs/messages.json | 21 +++++++++++-- apps/web/src/locales/cy/messages.json | 15 +++++++++ apps/web/src/locales/da/messages.json | 15 +++++++++ apps/web/src/locales/de/messages.json | 21 +++++++++++-- apps/web/src/locales/el/messages.json | 15 +++++++++ apps/web/src/locales/en_GB/messages.json | 15 +++++++++ apps/web/src/locales/en_IN/messages.json | 15 +++++++++ apps/web/src/locales/eo/messages.json | 15 +++++++++ apps/web/src/locales/es/messages.json | 15 +++++++++ apps/web/src/locales/et/messages.json | 15 +++++++++ apps/web/src/locales/eu/messages.json | 15 +++++++++ apps/web/src/locales/fa/messages.json | 15 +++++++++ apps/web/src/locales/fi/messages.json | 15 +++++++++ apps/web/src/locales/fil/messages.json | 15 +++++++++ apps/web/src/locales/fr/messages.json | 15 +++++++++ apps/web/src/locales/gl/messages.json | 15 +++++++++ apps/web/src/locales/he/messages.json | 15 +++++++++ apps/web/src/locales/hi/messages.json | 15 +++++++++ apps/web/src/locales/hr/messages.json | 15 +++++++++ apps/web/src/locales/hu/messages.json | 15 +++++++++ apps/web/src/locales/id/messages.json | 15 +++++++++ apps/web/src/locales/it/messages.json | 15 +++++++++ apps/web/src/locales/ja/messages.json | 15 +++++++++ apps/web/src/locales/ka/messages.json | 15 +++++++++ apps/web/src/locales/km/messages.json | 15 +++++++++ apps/web/src/locales/kn/messages.json | 15 +++++++++ apps/web/src/locales/ko/messages.json | 15 +++++++++ apps/web/src/locales/lv/messages.json | 15 +++++++++ apps/web/src/locales/ml/messages.json | 15 +++++++++ apps/web/src/locales/mr/messages.json | 15 +++++++++ apps/web/src/locales/my/messages.json | 15 +++++++++ apps/web/src/locales/nb/messages.json | 15 +++++++++ apps/web/src/locales/ne/messages.json | 15 +++++++++ apps/web/src/locales/nl/messages.json | 15 +++++++++ apps/web/src/locales/nn/messages.json | 15 +++++++++ apps/web/src/locales/or/messages.json | 15 +++++++++ apps/web/src/locales/pl/messages.json | 15 +++++++++ apps/web/src/locales/pt_BR/messages.json | 15 +++++++++ apps/web/src/locales/pt_PT/messages.json | 15 +++++++++ apps/web/src/locales/ro/messages.json | 15 +++++++++ apps/web/src/locales/ru/messages.json | 15 +++++++++ apps/web/src/locales/si/messages.json | 15 +++++++++ apps/web/src/locales/sk/messages.json | 15 +++++++++ apps/web/src/locales/sl/messages.json | 15 +++++++++ apps/web/src/locales/sr_CS/messages.json | 15 +++++++++ apps/web/src/locales/sr_CY/messages.json | 15 +++++++++ apps/web/src/locales/sv/messages.json | 15 +++++++++ apps/web/src/locales/ta/messages.json | 15 +++++++++ apps/web/src/locales/te/messages.json | 15 +++++++++ apps/web/src/locales/th/messages.json | 15 +++++++++ apps/web/src/locales/tr/messages.json | 15 +++++++++ apps/web/src/locales/uk/messages.json | 15 +++++++++ apps/web/src/locales/vi/messages.json | 15 +++++++++ apps/web/src/locales/zh_CN/messages.json | 39 ++++++++++++++++-------- apps/web/src/locales/zh_TW/messages.json | 15 +++++++++ 63 files changed, 979 insertions(+), 34 deletions(-) diff --git a/apps/web/src/locales/af/messages.json b/apps/web/src/locales/af/messages.json index 72666452d86..1e12ff7be2d 100644 --- a/apps/web/src/locales/af/messages.json +++ b/apps/web/src/locales/af/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/ar/messages.json b/apps/web/src/locales/ar/messages.json index 67fb72af9f9..9ceeb66bf59 100644 --- a/apps/web/src/locales/ar/messages.json +++ b/apps/web/src/locales/ar/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index a97c11ea852..7d25a8a86e0 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -4609,16 +4609,16 @@ "message": "İrəliləyiş yüklənir" }, "reviewingMemberData": { - "message": "Reviewing member data..." + "message": "Üzv veriləri incələnir..." }, "analyzingPasswords": { - "message": "Analyzing passwords..." + "message": "Parollar təhlil edilir..." }, "calculatingRisks": { - "message": "Calculating risks..." + "message": "Risklər hesablanır..." }, "generatingReports": { - "message": "Generating reports..." + "message": "Hesabatlar yaradılır..." }, "compilingInsightsProgress": { "message": "Compiling insights..." @@ -6456,7 +6456,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "verifyYourEmailToViewThisSend": { - "message": "Verify your email to view this Send", + "message": "Bu \"Send\"ə baxmaq üçün e-poçtunuzu doğrulayın", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "viewSendHiddenEmailWarning": { @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "Bütün $GB$ GB-lıq şifrələnmiş anbar sahənizi istifadə etmisiniz. Faylları saxlaya bilmək üçün daha çox anbar sahəsi əlavə edin." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Kimlər baxa bilər" }, @@ -12989,7 +13004,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresOn": { - "message": "This Send expires at $TIME$ on $DATE$", + "message": "Bu \"Send\"in müddəti bitir: $TIME$ $DATE$", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "time": { diff --git a/apps/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index 91fce4817d0..a0d13b0f5db 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index 484e6fd5a1b..caef0b4869b 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -4338,7 +4338,7 @@ } }, "automaticallyConfirmedUserId": { - "message": "Automatically confirmed user $ID$.", + "message": "Автоматично потвърден потребител: $ID$.", "placeholders": { "id": { "content": "$1", @@ -6148,19 +6148,19 @@ "message": "Приемам тези рискове и промени в политиката" }, "autoConfirmEnabledByAdmin": { - "message": "Turned on Automatic user confirmation setting" + "message": "Настройката за автоматично потвърждаване на потребители е включена" }, "autoConfirmDisabledByAdmin": { - "message": "Turned off Automatic user confirmation setting" + "message": "Настройката за автоматично потвърждаване на потребители е изключена" }, "autoConfirmEnabledByPortal": { - "message": "Added Automatic user confirmation policy" + "message": "Добавена е политика за автоматично потвърждаване на потребителите" }, "autoConfirmDisabledByPortal": { - "message": "Removed Automatic user confirmation policy" + "message": "Премахната е политика за автоматично потвърждаване на потребителите" }, "system": { - "message": "System" + "message": "Система" }, "personalOwnership": { "message": "Индивидуално притежание" @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "Използвали сте всичките си $GB$ GB от наличното си място за съхранение на шифровани данни. Ако искате да продължите да добавяте файлове, добавете повече място за съхранение." }, + "extensionPromptHeading": { + "message": "Инсталирайте добавката за по-лесен достъп до трезора си" + }, + "extensionPromptBody": { + "message": "Когато добавката е инсталирана, Битуорден ще бъде винаги лесно достъпен във всички уеб сайтове. С нея можете да попълвате паролите автоматично и да се вписвате с едно щракване на мишката." + }, + "extensionPromptImageAlt": { + "message": "Уеб браузър показващ добавката на Битуорден с елементи за автоматично попълване за текущата уеб страница." + }, + "skip": { + "message": "Пропускане" + }, + "downloadExtension": { + "message": "Сваляне на добавката" + }, "whoCanView": { "message": "Кой може да преглежда" }, @@ -12917,16 +12932,16 @@ "message": "Неправилна парола за Изпращане" }, "vaultWelcomeDialogTitle": { - "message": "You're in! Welcome to Bitwarden" + "message": "Влязохте! Добре дошли в Битуорден!" }, "vaultWelcomeDialogDescription": { - "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + "message": "Съхранявайте всичките си пароли и лични данни в трезора си в Битуорден. Нека Ви разведем!" }, "vaultWelcomeDialogPrimaryCta": { - "message": "Start tour" + "message": "Начало на обиколката" }, "vaultWelcomeDialogDismissCta": { - "message": "Skip" + "message": "Пропускане" }, "sendPasswordHelperText": { "message": "Хората ще трябва да въведат паролата, за да видят това Изпращане", diff --git a/apps/web/src/locales/bn/messages.json b/apps/web/src/locales/bn/messages.json index 3cc5f01c689..7dd3573e960 100644 --- a/apps/web/src/locales/bn/messages.json +++ b/apps/web/src/locales/bn/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/bs/messages.json b/apps/web/src/locales/bs/messages.json index effcfd3062b..8aea40fb633 100644 --- a/apps/web/src/locales/bs/messages.json +++ b/apps/web/src/locales/bs/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index 52a0f9cdd44..9af40c9f946 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index 300b1b583b7..469ccc0a072 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -1925,7 +1925,7 @@ "message": "Ověřovací aplikace" }, "authenticatorAppDescV2": { - "message": "Zadejte kód vygenerovaný ověřovací aplikací, jako je Autentikátor Bitwarden.", + "message": "Zadejte kód vygenerovaný ověřovací aplikací, jako je Bitwarden Authenticator.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { @@ -2704,7 +2704,7 @@ "message": "Pokračovat na bitwarden.com?" }, "twoStepContinueToBitwardenUrlDesc": { - "message": "Autentikátor Bitwarden umožňuje ukládat ověřovací klíče a generovat TOTP kódy pro 2-fázové ověřování. Další informace naleznete na stránkách bitwarden.com" + "message": "Bitwarden Authenticator umožňuje ukládat ověřovací klíče a generovat TOTP kódy pro 2-fázové ověřování. Další informace naleznete na stránkách bitwarden.com" }, "twoStepAuthenticatorScanCodeV2": { "message": "Naskenujte QR kód pomocí Vaší ověřovací aplikace nebo zadejte klíč." @@ -7418,7 +7418,7 @@ "message": "Neplatný ověřovací kód" }, "invalidEmailOrVerificationCode": { - "message": "Invalid email or verification code" + "message": "Neplatný e-mail nebo ověřovací kód" }, "keyConnectorDomain": { "message": "Doména Key Connectoru" @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "Využili jste celých $GB$ GB Vašeho šifrovaného úložiště. Chcete-li pokračovat v ukládání souborů, přidejte další úložiště." }, + "extensionPromptHeading": { + "message": "Získejte rozšíření pro snadný přístup k trezoru" + }, + "extensionPromptBody": { + "message": "S nainstalovaným rozšířením prohlížeče budete mít Bitwarden všude online. Budou se vyplňovat hesla, takže se můžete přihlásit do svých účtů jediným klepnutím." + }, + "extensionPromptImageAlt": { + "message": "Webový prohlížeč zobrazující rozšíření Bitwarden s položkami automatického vyplňování aktuální webové stránky." + }, + "skip": { + "message": "Přeskočit" + }, + "downloadExtension": { + "message": "Nainstalovat rozšíření" + }, "whoCanView": { "message": "Kdo může zobrazit" }, diff --git a/apps/web/src/locales/cy/messages.json b/apps/web/src/locales/cy/messages.json index 9fd6bb263ed..d1252309bfc 100644 --- a/apps/web/src/locales/cy/messages.json +++ b/apps/web/src/locales/cy/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index 1f89b9f62e5..11b3dc87b89 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index e76dc238b5e..6ea7e3b91a2 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -7418,7 +7418,7 @@ "message": "Ungültiger Verifizierungscode" }, "invalidEmailOrVerificationCode": { - "message": "Invalid email or verification code" + "message": "E-Mail oder Verifizierungscode ungültig" }, "keyConnectorDomain": { "message": "Key Connector-Domain" @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "Du hast die gesamten $GB$ GB deines verschlüsselten Speichers verwendet. Um mit dem Speichern von Dateien fortzufahren, füge mehr Speicher hinzu." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Wer kann das sehen" }, @@ -12890,7 +12905,7 @@ "message": "Gib mehrere E-Mail-Adressen ein, indem du sie mit einem Komma trennst." }, "emailsRequiredChangeAccessType": { - "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + "message": "E-Mail-Verifizierung erfordert mindestens eine E-Mail-Adresse. Ändere den Zugriffstyp oben, um alle E-Mails zu entfernen." }, "emailPlaceholder": { "message": "benutzer@bitwarden.com, benutzer@acme.com" @@ -12985,7 +13000,7 @@ "message": "Beim Aktualisieren deiner Zahlungsmethode ist ein Fehler aufgetreten." }, "sendPasswordInvalidAskOwner": { - "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "message": "Ungültiges Passwort. Frage den Absender nach dem Passwort, das benötigt wird, um auf dieses Send zuzugreifen.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresOn": { diff --git a/apps/web/src/locales/el/messages.json b/apps/web/src/locales/el/messages.json index 9700ec80b68..8b4bc6fc581 100644 --- a/apps/web/src/locales/el/messages.json +++ b/apps/web/src/locales/el/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index 3920f2d2be6..4990efea695 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/en_IN/messages.json b/apps/web/src/locales/en_IN/messages.json index f9b75b283c3..24414fca4e6 100644 --- a/apps/web/src/locales/en_IN/messages.json +++ b/apps/web/src/locales/en_IN/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/eo/messages.json b/apps/web/src/locales/eo/messages.json index 4b39004d896..2d3f8d29a29 100644 --- a/apps/web/src/locales/eo/messages.json +++ b/apps/web/src/locales/eo/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/es/messages.json b/apps/web/src/locales/es/messages.json index d3b884663dd..6d4d75e20fc 100644 --- a/apps/web/src/locales/es/messages.json +++ b/apps/web/src/locales/es/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/et/messages.json b/apps/web/src/locales/et/messages.json index c312f096f1b..448103ed594 100644 --- a/apps/web/src/locales/et/messages.json +++ b/apps/web/src/locales/et/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/eu/messages.json b/apps/web/src/locales/eu/messages.json index aa8a65b1141..3376ef33f63 100644 --- a/apps/web/src/locales/eu/messages.json +++ b/apps/web/src/locales/eu/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/fa/messages.json b/apps/web/src/locales/fa/messages.json index 215af9c7512..25b2a61c256 100644 --- a/apps/web/src/locales/fa/messages.json +++ b/apps/web/src/locales/fa/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/fi/messages.json b/apps/web/src/locales/fi/messages.json index ff37dcbdfb1..f562a98c60c 100644 --- a/apps/web/src/locales/fi/messages.json +++ b/apps/web/src/locales/fi/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/fil/messages.json b/apps/web/src/locales/fil/messages.json index 85e4d95320e..af1a0105c83 100644 --- a/apps/web/src/locales/fil/messages.json +++ b/apps/web/src/locales/fil/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index 09c5fe03d34..65a62b5a12d 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "Vous avez utilisé tous les $GB$ Go de votre stockage chiffré. Pour continuer à stocker des fichiers, ajoutez plus de stockage." }, + "extensionPromptHeading": { + "message": "Obtenir l'extension pour un accès facile au coffre" + }, + "extensionPromptBody": { + "message": "Avec l'extension de navigateur installée, vous emmènerez Bitwarden partout en ligne. Il remplira les mots de passe, faisant en sorte que vous puissiez vous connecter à vos comptes en un seul clic." + }, + "extensionPromptImageAlt": { + "message": "Un navigateur web montrant l'extension Bitwarden avec des éléments de saisie automatique pour la page web actuelle." + }, + "skip": { + "message": "Ignorer" + }, + "downloadExtension": { + "message": "Télécharger l'extension" + }, "whoCanView": { "message": "Qui peut afficher" }, diff --git a/apps/web/src/locales/gl/messages.json b/apps/web/src/locales/gl/messages.json index dccfaa04c64..17426524033 100644 --- a/apps/web/src/locales/gl/messages.json +++ b/apps/web/src/locales/gl/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/he/messages.json b/apps/web/src/locales/he/messages.json index 1dcbb3addcf..7685aaae5a1 100644 --- a/apps/web/src/locales/he/messages.json +++ b/apps/web/src/locales/he/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/hi/messages.json b/apps/web/src/locales/hi/messages.json index 3ed164386a1..1fad1304650 100644 --- a/apps/web/src/locales/hi/messages.json +++ b/apps/web/src/locales/hi/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/hr/messages.json b/apps/web/src/locales/hr/messages.json index 7a7135cd2b2..c67344da799 100644 --- a/apps/web/src/locales/hr/messages.json +++ b/apps/web/src/locales/hr/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index f6f580b7120..ed38bd4b0e7 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "A titkosított tárhely összes $GB$ mérete felhasználásra került. A fájlok tárolásának folytatásához adjunk hozzá további tárhelyet." }, + "extensionPromptHeading": { + "message": "Szerezzük be a kiterjesztést a széf könnyű eléréséhez." + }, + "extensionPromptBody": { + "message": "A böngésző kiterjesztés telepítésével a Bitwardent mindenhová magunkkal vihetjük. Kitölti a jelszavakat, így egyetlen kattintással bejelentkezhetünk a fiókjainkba." + }, + "extensionPromptImageAlt": { + "message": "Egy webböngésző, amely a Bitwarden kiterjesztést jeleníti meg az aktuális weboldal automatikus kitöltési elemeivel." + }, + "skip": { + "message": "Kihagyás" + }, + "downloadExtension": { + "message": "Kiterjesztés letöltése" + }, "whoCanView": { "message": "Ki láthatja" }, diff --git a/apps/web/src/locales/id/messages.json b/apps/web/src/locales/id/messages.json index fb6a4908684..c1967096299 100644 --- a/apps/web/src/locales/id/messages.json +++ b/apps/web/src/locales/id/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/it/messages.json b/apps/web/src/locales/it/messages.json index f53262992fe..ac04390751d 100644 --- a/apps/web/src/locales/it/messages.json +++ b/apps/web/src/locales/it/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "Hai usato tutti i $GB$ GB del tuo spazio di archiviazione crittografato. Per archiviare altri file, aggiungi altro spazio." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Chi può visualizzare" }, diff --git a/apps/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index 2b620ed1114..b3b5a975ec0 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/ka/messages.json b/apps/web/src/locales/ka/messages.json index 3e527d955f3..39a28d2a0c7 100644 --- a/apps/web/src/locales/ka/messages.json +++ b/apps/web/src/locales/ka/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/km/messages.json b/apps/web/src/locales/km/messages.json index d615154225d..4e6a8265ad6 100644 --- a/apps/web/src/locales/km/messages.json +++ b/apps/web/src/locales/km/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/kn/messages.json b/apps/web/src/locales/kn/messages.json index 51ed6353e01..85e79eba91d 100644 --- a/apps/web/src/locales/kn/messages.json +++ b/apps/web/src/locales/kn/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/ko/messages.json b/apps/web/src/locales/ko/messages.json index a07134231bb..273bdddadab 100644 --- a/apps/web/src/locales/ko/messages.json +++ b/apps/web/src/locales/ko/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index e50b76d1a4a..26f960a29d7 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/ml/messages.json b/apps/web/src/locales/ml/messages.json index d7e686fba73..c700f329c65 100644 --- a/apps/web/src/locales/ml/messages.json +++ b/apps/web/src/locales/ml/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/mr/messages.json b/apps/web/src/locales/mr/messages.json index 4f030d2368d..1fc1ef73f4b 100644 --- a/apps/web/src/locales/mr/messages.json +++ b/apps/web/src/locales/mr/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/my/messages.json b/apps/web/src/locales/my/messages.json index d615154225d..4e6a8265ad6 100644 --- a/apps/web/src/locales/my/messages.json +++ b/apps/web/src/locales/my/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/nb/messages.json b/apps/web/src/locales/nb/messages.json index 103a439220e..6c463a61e64 100644 --- a/apps/web/src/locales/nb/messages.json +++ b/apps/web/src/locales/nb/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/ne/messages.json b/apps/web/src/locales/ne/messages.json index dbf4643236d..9ef82cc799c 100644 --- a/apps/web/src/locales/ne/messages.json +++ b/apps/web/src/locales/ne/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index 023a1b775a1..e50966e3ca1 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "Je hebt alle $GB$ GB aan versleutelde opslag gebruikt. Voeg meer opslagruimte toe om door te gaan met het opslaan van bestanden." }, + "extensionPromptHeading": { + "message": "Gebruik de extensie voor eenvoudige toegang tot je kluis" + }, + "extensionPromptBody": { + "message": "Met de browserextensie kun je Bitwarden overal online gebruiken. Het invullen van wachtwoorden, zodat je met één klik op je accounts kunt inloggen." + }, + "extensionPromptImageAlt": { + "message": "Een webbrowser die de Bitwarden-extensie toont met automatisch invullen voor de huidige webpagina." + }, + "skip": { + "message": "Overslaan" + }, + "downloadExtension": { + "message": "Extensie downloaden" + }, "whoCanView": { "message": "Wie kan weergeven" }, diff --git a/apps/web/src/locales/nn/messages.json b/apps/web/src/locales/nn/messages.json index 13f608da89d..f87cf97aa65 100644 --- a/apps/web/src/locales/nn/messages.json +++ b/apps/web/src/locales/nn/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/or/messages.json b/apps/web/src/locales/or/messages.json index d615154225d..4e6a8265ad6 100644 --- a/apps/web/src/locales/or/messages.json +++ b/apps/web/src/locales/or/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index 7b5df541186..82f18f25c31 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index eb8864d4e6c..73b3994fab3 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "Você usou todos os $GB$ GB do seu armazenamento criptografado. Para continuar armazenando arquivos, adicione mais armazenamento." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Quem pode visualizar" }, diff --git a/apps/web/src/locales/pt_PT/messages.json b/apps/web/src/locales/pt_PT/messages.json index eb4573b336c..55b35c60155 100644 --- a/apps/web/src/locales/pt_PT/messages.json +++ b/apps/web/src/locales/pt_PT/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "Utilizou os $GB$ GB do seu armazenamento encriptado. Para continuar a guardar ficheiros, adicione mais espaço de armazenamento." }, + "extensionPromptHeading": { + "message": "Obtenha a extensão para aceder facilmente ao seu cofre" + }, + "extensionPromptBody": { + "message": "Com a extensão do navegador instalada, terá o Bitwarden sempre disponível online. Esta preencherá automaticamente as palavras-passe, para que possa iniciar sessão nas suas contas com um único clique." + }, + "extensionPromptImageAlt": { + "message": "Um navegador web a apresentar a extensão Bitwarden com itens de preenchimento automático para a página atual." + }, + "skip": { + "message": "Saltar" + }, + "downloadExtension": { + "message": "Transferir extensão" + }, "whoCanView": { "message": "Quem pode ver" }, diff --git a/apps/web/src/locales/ro/messages.json b/apps/web/src/locales/ro/messages.json index f0b67e015cc..8fe372fbc52 100644 --- a/apps/web/src/locales/ro/messages.json +++ b/apps/web/src/locales/ro/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index 0314006ab1e..6687b59fc86 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "Вы использовали все $GB$ вашего зашифрованного хранилища. Чтобы продолжить хранение файлов, добавьте дополнительное хранилище." }, + "extensionPromptHeading": { + "message": "Установите расширение для удобного доступа к хранилищу" + }, + "extensionPromptBody": { + "message": "Установив расширение для браузера, вы сможете использовать Bitwarden везде, где есть интернет. Оно будет вводить пароли, так что вы сможете входить в свои аккаунты одним щелчком мыши." + }, + "extensionPromptImageAlt": { + "message": "Браузер, отображающий расширение Bitwarden с элементами автозаполнения для текущей веб-страницы." + }, + "skip": { + "message": "Пропустить" + }, + "downloadExtension": { + "message": "Скачать расширение" + }, "whoCanView": { "message": "Кто может просматривать" }, diff --git a/apps/web/src/locales/si/messages.json b/apps/web/src/locales/si/messages.json index 3603a36246e..3bc5fa153d9 100644 --- a/apps/web/src/locales/si/messages.json +++ b/apps/web/src/locales/si/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index 79e8b40f918..6a25d6d0f3f 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "Použili ste všetkých $GB$ GB vášho šifrovaného úložiska. Ak chcete uložiť ďalšie súbory, pridajte viac úložiska." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/sl/messages.json b/apps/web/src/locales/sl/messages.json index e5a622ca157..4553f68c54b 100644 --- a/apps/web/src/locales/sl/messages.json +++ b/apps/web/src/locales/sl/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/sr_CS/messages.json b/apps/web/src/locales/sr_CS/messages.json index f869499d685..24ff0eb2f9f 100644 --- a/apps/web/src/locales/sr_CS/messages.json +++ b/apps/web/src/locales/sr_CS/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/sr_CY/messages.json b/apps/web/src/locales/sr_CY/messages.json index e416df73247..f0878f973c2 100644 --- a/apps/web/src/locales/sr_CY/messages.json +++ b/apps/web/src/locales/sr_CY/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index 079dce0e3a7..77771bb166d 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "Du har använt alla $GB$ GB av din krypterade lagring. För att fortsätta lagra filer, lägg till mer lagringsutrymme." }, + "extensionPromptHeading": { + "message": "Skaffa tillägget för enkel åtkomst till valv" + }, + "extensionPromptBody": { + "message": "Med webbläsartillägget installerat tar du Bitwarden överallt på nätet. Det fyller i lösenord, så att du kan logga in på dina konton med ett enda klick." + }, + "extensionPromptImageAlt": { + "message": "En webbläsare som visar Bitwarden-tillägget med autofyll objekt för den aktuella webbsidan." + }, + "skip": { + "message": "Hoppa över" + }, + "downloadExtension": { + "message": "Ladda ner tillägg" + }, "whoCanView": { "message": "Vem kan se" }, diff --git a/apps/web/src/locales/ta/messages.json b/apps/web/src/locales/ta/messages.json index a1eb60d67c1..757a8158097 100644 --- a/apps/web/src/locales/ta/messages.json +++ b/apps/web/src/locales/ta/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/te/messages.json b/apps/web/src/locales/te/messages.json index d615154225d..4e6a8265ad6 100644 --- a/apps/web/src/locales/te/messages.json +++ b/apps/web/src/locales/te/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/th/messages.json b/apps/web/src/locales/th/messages.json index ab53147de00..2eb0a00f8fc 100644 --- a/apps/web/src/locales/th/messages.json +++ b/apps/web/src/locales/th/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/tr/messages.json b/apps/web/src/locales/tr/messages.json index efd186d721a..260514462cb 100644 --- a/apps/web/src/locales/tr/messages.json +++ b/apps/web/src/locales/tr/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Kim görebilir" }, diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index 71780d2faa9..7dc407ad5e6 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Хто може переглядати" }, diff --git a/apps/web/src/locales/vi/messages.json b/apps/web/src/locales/vi/messages.json index 369a87111d4..3671c9e8ab5 100644 --- a/apps/web/src/locales/vi/messages.json +++ b/apps/web/src/locales/vi/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index 275e11a1c70..ff23e62c5d8 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -4338,7 +4338,7 @@ } }, "automaticallyConfirmedUserId": { - "message": "Automatically confirmed user $ID$.", + "message": "自动确认了用户 $ID$。", "placeholders": { "id": { "content": "$1", @@ -6148,19 +6148,19 @@ "message": "我接受这些风险和策略更新" }, "autoConfirmEnabledByAdmin": { - "message": "Turned on Automatic user confirmation setting" + "message": "启用了自动用户确认设置" }, "autoConfirmDisabledByAdmin": { - "message": "Turned off Automatic user confirmation setting" + "message": "停用了自动用户确认设置" }, "autoConfirmEnabledByPortal": { - "message": "Added Automatic user confirmation policy" + "message": "添加了自动用户确认策略" }, "autoConfirmDisabledByPortal": { - "message": "Removed Automatic user confirmation policy" + "message": "禁用了自动用户确认策略" }, "system": { - "message": "System" + "message": "系统" }, "personalOwnership": { "message": "禁用个人密码库" @@ -6761,7 +6761,7 @@ "message": "1 份邀请未发送" }, "bulkReinviteFailureDescription": { - "message": "向 $TOTAL$ 位成员中的 $COUNT$ 位发送邀请时发生错误。请尝试重新发送,如果问题仍然存在,", + "message": "向 $TOTAL$ 位成员中的 $COUNT$ 位发送邀请时发生错误。请尝试再次发送,如果问题仍然存在,", "placeholders": { "count": { "content": "$1", @@ -9171,7 +9171,7 @@ "message": "查看全部" }, "showingPortionOfTotal": { - "message": "显示 $PORTION$ / $TOTAL$", + "message": "显示 $TOTAL$ 中的 $PORTION$", "placeholders": { "portion": { "content": "$1", @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "您已使用了全部的 $GB$ GB 加密存储空间。要继续存储文件,请添加更多存储空间。" }, + "extensionPromptHeading": { + "message": "获取扩展以便轻松访问密码库" + }, + "extensionPromptBody": { + "message": "安装浏览器扩展后,您可以随时随地在线使用 Bitwarden。它会自动填写密码,只需单击一下即可登录您的账户。" + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "跳过" + }, + "downloadExtension": { + "message": "下载扩展" + }, "whoCanView": { "message": "谁可以查看" }, @@ -12917,16 +12932,16 @@ "message": "无效的 Send 密码" }, "vaultWelcomeDialogTitle": { - "message": "You're in! Welcome to Bitwarden" + "message": "您已成功加入!欢迎使用 Bitwarden" }, "vaultWelcomeDialogDescription": { - "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + "message": "将您的所有密码和个人信息存储在你的 Bitwarden 密码库中。我们将带您熟悉一下。" }, "vaultWelcomeDialogPrimaryCta": { - "message": "Start tour" + "message": "开始导览" }, "vaultWelcomeDialogDismissCta": { - "message": "Skip" + "message": "跳过" }, "sendPasswordHelperText": { "message": "个人需要输入密码才能查看此 Send", diff --git a/apps/web/src/locales/zh_TW/messages.json b/apps/web/src/locales/zh_TW/messages.json index aedc802f241..31099edc763 100644 --- a/apps/web/src/locales/zh_TW/messages.json +++ b/apps/web/src/locales/zh_TW/messages.json @@ -12877,6 +12877,21 @@ "storageFullDescription": { "message": "您已用完全部 $GB$ GB 的加密儲存空間。如需繼續儲存檔案,請增加儲存空間。" }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "誰可以檢視" }, From a90d74c32c6e93aee4780b0a7175af4e24e24013 Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 04:35:06 -0600 Subject: [PATCH 128/134] Autosync the updated translations (#19130) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/src/_locales/az/messages.json | 8 ++--- apps/browser/src/_locales/bg/messages.json | 2 +- apps/browser/src/_locales/cs/messages.json | 2 +- apps/browser/src/_locales/de/messages.json | 4 +-- apps/browser/src/_locales/it/messages.json | 32 ++++++++--------- apps/browser/src/_locales/uk/messages.json | 34 +++++++++---------- apps/browser/src/_locales/zh_CN/messages.json | 8 ++--- 7 files changed, 45 insertions(+), 45 deletions(-) diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 6572c2d09d9..eb3135599f4 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -3087,7 +3087,7 @@ } }, "durationTimeHours": { - "message": "$HOURS$ hours", + "message": "$HOURS$ saat", "placeholders": { "hours": { "content": "$1", @@ -3096,7 +3096,7 @@ } }, "sendCreatedDescriptionV2": { - "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "message": "Bu Send keçidini kopyala və paylaş. Send, keçidə sahib olan hər kəs üçün növbəti $TIME$ ərzində əlçatan olacaq.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "time": { @@ -3106,7 +3106,7 @@ } }, "sendCreatedDescriptionPassword": { - "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "message": "Bu Send keçidini kopyala və paylaş. Send, keçidə və ayarladığınız parola sahib olan hər kəs üçün növbəti $TIME$ ərzində əlçatan olacaq.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "time": { @@ -3116,7 +3116,7 @@ } }, "sendCreatedDescriptionEmail": { - "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "message": "Bu Send keçidini kopyala və paylaş. Qeyd etdiyiniz şəxslər buna növbəti $TIME$ ərzində baxa bilər.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "time": { diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 4f2f26bade8..275dd21d0e9 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -2861,7 +2861,7 @@ "message": "Илюстрация на списък с елементи за вписване, които са в риск." }, "welcomeDialogGraphicAlt": { - "message": "Illustration of the layout of the Bitwarden vault page." + "message": "Илюстрация на оформлението на страницата с трезора в Битуорден." }, "generatePasswordSlideDesc": { "message": "Генерирайте бързо сложна и уникална парола от менюто за автоматично попълване на Битуорден, на уеб сайта, който е в риск.", diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index 9106d0518db..efadf781fc8 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -1691,7 +1691,7 @@ "message": "Ověřovací aplikace" }, "authenticatorAppDescV2": { - "message": "Zadejte kód vygenerovaný ověřovací aplikací, jako je Autentikátor Bitwarden.", + "message": "Zadejte kód vygenerovaný ověřovací aplikací, jako je Bitwarden Authenticator.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 6d301950e03..8c33da9ae79 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -897,7 +897,7 @@ "message": "Ungültiger Verifizierungscode" }, "invalidEmailOrVerificationCode": { - "message": "Invalid email or verification code" + "message": "E-Mail oder Verifizierungscode ungültig" }, "valueCopied": { "message": "$VALUE$ kopiert", @@ -6167,7 +6167,7 @@ "message": "Gib mehrere E-Mail-Adressen ein, indem du sie mit einem Komma trennst." }, "emailsRequiredChangeAccessType": { - "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + "message": "E-Mail-Verifizierung erfordert mindestens eine E-Mail-Adresse. Ändere den Zugriffstyp oben, um alle E-Mails zu entfernen." }, "emailPlaceholder": { "message": "benutzer@bitwarden.com, benutzer@acme.com" diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index d66dfe7bfba..f1d704c6d48 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -574,10 +574,10 @@ "message": "Gli elementi archiviati compariranno qui e saranno esclusi dai risultati di ricerca e suggerimenti di autoriempimento." }, "itemArchiveToast": { - "message": "Item archived" + "message": "Elemento archiviato" }, "itemUnarchivedToast": { - "message": "Item unarchived" + "message": "Elemento estratto dall'archivio" }, "archiveItem": { "message": "Archivia elemento" @@ -897,7 +897,7 @@ "message": "Codice di verifica non valido" }, "invalidEmailOrVerificationCode": { - "message": "Invalid email or verification code" + "message": "Codice di verifica non valido" }, "valueCopied": { "message": "$VALUE$ copiata", @@ -2861,7 +2861,7 @@ "message": "Illustrazione di una lista di login a rischio." }, "welcomeDialogGraphicAlt": { - "message": "Illustration of the layout of the Bitwarden vault page." + "message": "Illustrazione del layout di pagina della cassaforte Bitwarden." }, "generatePasswordSlideDesc": { "message": "Genera rapidamente una parola d'accesso forte e unica con il menu' di riempimento automatico Bitwarden nel sito a rischio.", @@ -3087,7 +3087,7 @@ } }, "durationTimeHours": { - "message": "$HOURS$ hours", + "message": "$HOURS$ ore", "placeholders": { "hours": { "content": "$1", @@ -3096,7 +3096,7 @@ } }, "sendCreatedDescriptionV2": { - "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "message": "Copia e condividi questo link di Send. Sarà disponibile a chiunque abbia il link per $TIME$.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "time": { @@ -3106,7 +3106,7 @@ } }, "sendCreatedDescriptionPassword": { - "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "message": "Copia e condividi questo link di Send. Sarà disponibile a chiunque abbia link e password per $TIME$.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "time": { @@ -3116,7 +3116,7 @@ } }, "sendCreatedDescriptionEmail": { - "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "message": "Copia e condividi questo link Send: potrà essere visualizzato dalle persone che hai specificato per $TIME$.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "time": { @@ -6006,7 +6006,7 @@ "message": "Numero di carta" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "Errore: impossibile decrittare" }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "La tua organizzazione non utilizza più le password principali per accedere a Bitwarden. Per continuare, verifica l'organizzazione e il dominio." @@ -6146,10 +6146,10 @@ "message": "Perché vedo questo avviso?" }, "items": { - "message": "Items" + "message": "Elementi" }, "searchResults": { - "message": "Search results" + "message": "Risultati di ricerca" }, "resizeSideNavigation": { "message": "Ridimensiona la navigazione laterale" @@ -6167,22 +6167,22 @@ "message": "Inserisci più indirizzi email separandoli con virgole." }, "emailsRequiredChangeAccessType": { - "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + "message": "La verifica via email richiede almeno un indirizzo email. Per rimuovere tutte le email, modifica il tipo di accesso qui sopra." }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, "downloadBitwardenApps": { - "message": "Download Bitwarden apps" + "message": "Scarica l'app Bitwarden" }, "emailProtected": { - "message": "Email protected" + "message": "Email protetta" }, "sendPasswordHelperText": { - "message": "Individuals will need to enter the password to view this Send", + "message": "I destinatari dovranno inserire la password per visualizzare questo Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "userVerificationFailed": { - "message": "User verification failed." + "message": "Verifica dell'utente non riuscita." } } diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index 541a60b8ff8..143dc8037fd 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -897,7 +897,7 @@ "message": "Недійсний код підтвердження" }, "invalidEmailOrVerificationCode": { - "message": "Invalid email or verification code" + "message": "Недійсна е-пошта або код підтвердження" }, "valueCopied": { "message": "$VALUE$ скопійовано", @@ -1144,7 +1144,7 @@ "message": "Натисніть на запис у режимі перегляду сховища для автозаповнення" }, "clickToAutofill": { - "message": "Натисніть запис у пропозиціях для автозаповнення" + "message": "Натиснути запис у пропозиціях для автозаповнення" }, "clearClipboard": { "message": "Очистити буфер обміну", @@ -2055,7 +2055,7 @@ "message": "Е-пошта" }, "emails": { - "message": "Е-пошти" + "message": "Адреси е-пошти" }, "phone": { "message": "Телефон" @@ -2861,7 +2861,7 @@ "message": "Ілюстрація списку ризикованих записів." }, "welcomeDialogGraphicAlt": { - "message": "Illustration of the layout of the Bitwarden vault page." + "message": "Ілюстрація макету сторінки сховища Bitwarden." }, "generatePasswordSlideDesc": { "message": "Швидко згенеруйте надійний, унікальний пароль через меню автозаповнення Bitwarden на сайті з ризикованим паролем.", @@ -3087,7 +3087,7 @@ } }, "durationTimeHours": { - "message": "$HOURS$ hours", + "message": "$HOURS$ годин", "placeholders": { "hours": { "content": "$1", @@ -3096,7 +3096,7 @@ } }, "sendCreatedDescriptionV2": { - "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "message": "Скопіюйте посилання на це відправлення і поділіться ним. Відправлення буде доступне за посиланням усім протягом $TIME$.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "time": { @@ -3106,7 +3106,7 @@ } }, "sendCreatedDescriptionPassword": { - "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "message": "Скопіюйте посилання на це відправлення і поділіться ним. Відправлення буде доступне за посиланням і встановленим вами паролем усім протягом $TIME$.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "time": { @@ -3116,7 +3116,7 @@ } }, "sendCreatedDescriptionEmail": { - "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "message": "Скопіюйте посилання на це відправлення і поділіться ним. Його зможуть переглядати зазначені вами користувачі протягом $TIME$.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "time": { @@ -3398,7 +3398,7 @@ "message": "Не вдалося розблокувати за допомогою ключа доступу. Повторіть спробу або скористайтеся іншим способом розблокування." }, "noPrfCredentialsAvailable": { - "message": "Немає ключів доступу з підтримкою PRF, доступних для розблокування. Спочатку увійдіть з ключем доступу." + "message": "Немає ключів доступу з підтримкою PRF, доступних для розблокування. Спочатку ввійдіть з ключем доступу." }, "decryptionError": { "message": "Помилка розшифрування" @@ -4734,7 +4734,7 @@ "message": "Запропоновані записи" }, "autofillSuggestionsTip": { - "message": "Зберегти дані входу цього сайту для автозаповнення" + "message": "Збережіть дані входу цього сайту для автозаповнення" }, "yourVaultIsEmpty": { "message": "Ваше сховище порожнє" @@ -4776,7 +4776,7 @@ } }, "moreOptionsLabelNoPlaceholder": { - "message": "Більше опцій" + "message": "Інші варіанти" }, "moreOptionsTitle": { "message": "Інші можливості – $ITEMNAME$", @@ -5710,7 +5710,7 @@ "message": "Дуже широке" }, "narrow": { - "message": "Вузький" + "message": "Вузьке" }, "sshKeyWrongPassword": { "message": "Ви ввели неправильний пароль." @@ -6158,16 +6158,16 @@ "message": "Хто може переглядати" }, "specificPeople": { - "message": "Певні люди" + "message": "Певні користувачі" }, "emailVerificationDesc": { - "message": "Після того, як ви поділитеся цим посиланням на відправлення, особам необхідно буде підтвердити свої е-пошти за допомогою коду, щоб переглянути це відправлення." + "message": "Після того, як ви поділитеся посиланням на це відправлення, користувачі мають підтвердити свою е-пошту за допомогою коду, щоб переглянути його." }, "enterMultipleEmailsSeparatedByComma": { "message": "Введіть декілька адрес е-пошти, розділяючи їх комою." }, "emailsRequiredChangeAccessType": { - "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + "message": "Для підтвердження адреси електронної пошти потрібна щонайменше одна адреса. Щоб вилучити всі адреси електронної пошти, змініть тип доступу вище." }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" @@ -6179,10 +6179,10 @@ "message": "Е-пошту захищено" }, "sendPasswordHelperText": { - "message": "Особам необхідно ввести пароль для перегляду цього відправлення", + "message": "Користувачі мають ввести пароль для перегляду цього відправлення", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "userVerificationFailed": { - "message": "User verification failed." + "message": "Не вдалося перевірити користувача." } } diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index 5fc0b632676..c27d1a8bb24 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -897,7 +897,7 @@ "message": "无效的验证码" }, "invalidEmailOrVerificationCode": { - "message": "Invalid email or verification code" + "message": "无效的电子邮箱或验证码" }, "valueCopied": { "message": "$VALUE$ 已复制", @@ -2861,7 +2861,7 @@ "message": "存在风险的登录列表示意图。" }, "welcomeDialogGraphicAlt": { - "message": "Illustration of the layout of the Bitwarden vault page." + "message": "Bitwarden 密码库页面布局示意图。" }, "generatePasswordSlideDesc": { "message": "在存在风险的网站上,使用 Bitwarden 自动填充菜单快速生成强大且唯一的密码。", @@ -6167,7 +6167,7 @@ "message": "输入多个电子邮箱(使用逗号分隔)。" }, "emailsRequiredChangeAccessType": { - "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + "message": "电子邮箱验证要求至少有一个电子邮箱地址。要移除所有电子邮箱,请更改上面的访问类型。" }, "emailPlaceholder": { "message": "user@bitwarden.com, user@acme.com" @@ -6183,6 +6183,6 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "userVerificationFailed": { - "message": "User verification failed." + "message": "用户验证失败。" } } From e6c4998b7ccd1568c4518e43de6586afa3d1f227 Mon Sep 17 00:00:00 2001 From: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> Date: Mon, 23 Feb 2026 11:48:40 +0000 Subject: [PATCH 129/134] Bumped client version(s) --- apps/browser/package.json | 2 +- apps/browser/src/manifest.json | 2 +- apps/browser/src/manifest.v3.json | 2 +- apps/cli/package.json | 2 +- apps/web/package.json | 2 +- package-lock.json | 6 +++--- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/browser/package.json b/apps/browser/package.json index 53103643374..fa3da23991e 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2026.1.1", + "version": "2026.2.0", "scripts": { "build": "npm run build:chrome", "build:bit": "npm run build:bit:chrome", diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index c2e0b422985..54a10e34b12 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extName__", "short_name": "Bitwarden", - "version": "2026.1.1", + "version": "2026.2.0", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index 603d3e06ba7..206a300236c 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -3,7 +3,7 @@ "minimum_chrome_version": "102.0", "name": "__MSG_extName__", "short_name": "Bitwarden", - "version": "2026.1.1", + "version": "2026.2.0", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/cli/package.json b/apps/cli/package.json index 6c27267054f..a5b3a00ec4e 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/cli", "description": "A secure and free password manager for all of your devices.", - "version": "2026.1.0", + "version": "2026.2.0", "keywords": [ "bitwarden", "password", diff --git a/apps/web/package.json b/apps/web/package.json index ad778b03778..844ac1f12b5 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/web-vault", - "version": "2026.2.0", + "version": "2026.2.1", "scripts": { "build:oss": "webpack", "build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js", diff --git a/package-lock.json b/package-lock.json index 47ba7456b0a..0c2a38a9cc2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -191,11 +191,11 @@ }, "apps/browser": { "name": "@bitwarden/browser", - "version": "2026.1.1" + "version": "2026.2.0" }, "apps/cli": { "name": "@bitwarden/cli", - "version": "2026.1.0", + "version": "2026.2.0", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@koa/multer": "4.0.0", @@ -445,7 +445,7 @@ }, "apps/web": { "name": "@bitwarden/web-vault", - "version": "2026.2.0" + "version": "2026.2.1" }, "libs/admin-console": { "name": "@bitwarden/admin-console", From cf32250d7b947804fa196f719574d913888fd3f6 Mon Sep 17 00:00:00 2001 From: bmbitwarden Date: Mon, 23 Feb 2026 09:09:05 -0500 Subject: [PATCH 130/134] PM-7853 implemented hide send based on config setting (#18831) --- apps/web/src/app/layouts/user-layout.component.html | 4 +++- apps/web/src/app/layouts/user-layout.component.ts | 8 +++++++- apps/web/src/app/oss-routing.module.ts | 9 +++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/layouts/user-layout.component.html b/apps/web/src/app/layouts/user-layout.component.html index 10f569e2558..57b8cf047c4 100644 --- a/apps/web/src/app/layouts/user-layout.component.html +++ b/apps/web/src/app/layouts/user-layout.component.html @@ -3,7 +3,9 @@ - + @if (sendEnabled$ | async) { + + } diff --git a/apps/web/src/app/layouts/user-layout.component.ts b/apps/web/src/app/layouts/user-layout.component.ts index 33bce661c65..6af7b0639e5 100644 --- a/apps/web/src/app/layouts/user-layout.component.ts +++ b/apps/web/src/app/layouts/user-layout.component.ts @@ -4,12 +4,13 @@ import { CommonModule } from "@angular/common"; import { Component, OnInit, Signal } from "@angular/core"; import { toSignal } from "@angular/core/rxjs-interop"; import { RouterModule } from "@angular/router"; -import { Observable, switchMap } from "rxjs"; +import { map, Observable, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { PasswordManagerLogo } from "@bitwarden/assets/svg"; import { canAccessEmergencyAccess } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; @@ -42,6 +43,11 @@ export class UserLayoutComponent implements OnInit { protected hasFamilySponsorshipAvailable$: Observable; protected showSponsoredFamilies$: Observable; protected showSubscription$: Observable; + protected readonly sendEnabled$: Observable = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.policyService.policyAppliesToUser$(PolicyType.DisableSend, userId)), + map((isDisabled) => !isDisabled), + ); protected consolidatedSessionTimeoutComponent$: Observable; constructor( diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 932d0b8119b..a5fe3f5d627 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -1,5 +1,6 @@ import { NgModule } from "@angular/core"; import { Route, RouterModule, Routes } from "@angular/router"; +import { map } from "rxjs"; import { organizationPolicyGuard } from "@bitwarden/angular/admin-console/guards"; import { AuthenticationTimeoutComponent } from "@bitwarden/angular/auth/components/authentication-timeout.component"; @@ -50,6 +51,7 @@ import { NewDeviceVerificationComponent, } from "@bitwarden/auth/angular"; import { canAccessEmergencyAccess } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { AnonLayoutWrapperComponent, AnonLayoutWrapperData } from "@bitwarden/components"; import { LockComponent, RemovePasswordComponent } from "@bitwarden/key-management-ui"; @@ -641,6 +643,13 @@ const routes: Routes = [ path: "sends", component: SendComponent, data: { titleId: "send" } satisfies RouteDataProperties, + canActivate: [ + organizationPolicyGuard((userId, _configService, policyService) => + policyService + .policyAppliesToUser$(PolicyType.DisableSend, userId) + .pipe(map((policyApplies) => !policyApplies)), + ), + ], }, { path: "sm-landing", From 4fea6300734573aa447fbe16e939f7ccb6b211c9 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 23 Feb 2026 15:16:30 +0100 Subject: [PATCH 131/134] Fix user crypto management module not being imported correctly (#19133) --- apps/web/src/app/core/core.module.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index b3afb8ca984..d270162f99d 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -126,6 +126,7 @@ import { SessionTimeoutSettingsComponentService, } from "@bitwarden/key-management-ui"; import { SerializedMemoryStorageService } from "@bitwarden/storage-core"; +import { UserCryptoManagementModule } from "@bitwarden/user-crypto-management"; import { DefaultSshImportPromptService, SshImportPromptService } from "@bitwarden/vault"; import { WebOrganizationInviteService } from "@bitwarden/web-vault/app/auth/core/services/organization-invite/web-organization-invite.service"; import { WebVaultPremiumUpgradePromptService } from "@bitwarden/web-vault/app/vault/services/web-premium-upgrade-prompt.service"; @@ -497,7 +498,7 @@ const safeProviders: SafeProvider[] = [ @NgModule({ declarations: [], - imports: [CommonModule, JslibServicesModule, GeneratorServicesModule], + imports: [CommonModule, JslibServicesModule, UserCryptoManagementModule, GeneratorServicesModule], // Do not register your dependency here! Add it to the typesafeProviders array using the helper function providers: safeProviders, }) From 494dd7d329adc102e34678ca59734877b2faaf9b Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Mon, 23 Feb 2026 08:17:46 -0600 Subject: [PATCH 132/134] [PM-31833] Split mark as critical and assign tasks (#18843) --- .../new-applications-dialog.component.ts | 145 ++++++++++-------- 1 file changed, 80 insertions(+), 65 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/application-review-dialog/new-applications-dialog.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/application-review-dialog/new-applications-dialog.component.ts index 5b9cea436a0..13018ba6884 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/application-review-dialog/new-applications-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/application-review-dialog/new-applications-dialog.component.ts @@ -9,8 +9,8 @@ import { Signal, signal, } from "@angular/core"; -import { takeUntilDestroyed, toSignal } from "@angular/core/rxjs-interop"; -import { catchError, EMPTY, from, switchMap, take } from "rxjs"; +import { toSignal } from "@angular/core/rxjs-interop"; +import { firstValueFrom } from "rxjs"; import { ApplicationHealthReportDetail, @@ -238,6 +238,12 @@ export class NewApplicationsDialogComponent { // Checks if there are selected applications and proceeds to assign tasks async handleMarkAsCritical() { + if (this.markingAsCritical()) { + return; // Prevent double-click + } + + this.markingAsCritical.set(true); + if (this.selectedApplications().size === 0) { const confirmed = await this.dialogService.openSimpleDialog({ title: { key: "confirmNoSelectedCriticalApplicationsTitle" }, @@ -246,25 +252,11 @@ export class NewApplicationsDialogComponent { }); if (!confirmed) { + this.markingAsCritical.set(false); return; } } - // Skip the assign tasks view if there are no new unassigned at-risk cipher IDs - if (this.newUnassignedAtRiskCipherIds().length === 0) { - this.handleAssignTasks(); - } else { - this.currentView.set(DialogView.AssignTasks); - } - } - - // Saves the application review and assigns tasks for unassigned at-risk ciphers - protected handleAssignTasks() { - if (this.saving()) { - return; // Prevent double-click - } - this.saving.set(true); - const reviewedDate = new Date(); const updatedApplications = this.dialogParams.newApplications.map((app) => { const isCritical = this.selectedApplications().has(app.applicationName); @@ -276,56 +268,79 @@ export class NewApplicationsDialogComponent { }); // Save the application review dates and critical markings - this.dataService - .saveApplicationReviewStatus(updatedApplications) - .pipe( - takeUntilDestroyed(this.destroyRef), // Satisfy eslint rule - take(1), - switchMap(() => { - // Assign password change tasks for unassigned at-risk ciphers for critical applications - return from( - this.securityTasksService.requestPasswordChangeForCriticalApplications( - this.dialogParams.organizationId, - this.newUnassignedAtRiskCipherIds(), - ), - ); - }), - catchError((error: unknown) => { - if (error instanceof ErrorResponse && error.statusCode === 404) { - this.toastService.showToast({ - message: this.i18nService.t("mustBeOrganizationOwnerAdmin"), - variant: "error", - title: this.i18nService.t("error"), - }); + try { + await firstValueFrom(this.dataService.saveApplicationReviewStatus(updatedApplications)); - this.saving.set(false); - return EMPTY; - } - - this.logService.error( - "[NewApplicationsDialog] Failed to save application review or assign tasks", - error, - ); - this.saving.set(false); - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorSavingReviewStatus"), - message: this.i18nService.t("pleaseTryAgain"), - }); - - this.saving.set(false); - return EMPTY; - }), - ) - .subscribe(() => { - this.toastService.showToast({ - variant: "success", - title: this.i18nService.t("applicationReviewSaved"), - message: this.i18nService.t("newApplicationsReviewed"), - }); - this.saving.set(false); - this.handleAssigningCompleted(); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("applicationReviewSaved"), + message: this.i18nService.t("newApplicationsReviewed"), }); + + // If there are no unassigned at-risk ciphers, we can complete immediately. Otherwise, navigate to the assign tasks view. + if (this.newUnassignedAtRiskCipherIds().length === 0) { + this.handleAssigningCompleted(); + } else { + this.currentView.set(DialogView.AssignTasks); + } + } catch (error: unknown) { + this.logService.error( + "[NewApplicationsDialog] Failed to save application review status", + error, + ); + + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorSavingReviewStatus"), + message: this.i18nService.t("pleaseTryAgain"), + }); + } finally { + this.markingAsCritical.set(false); + } + } + + // Saves the application review and assigns tasks for unassigned at-risk ciphers + protected async handleAssignTasks() { + if (this.saving()) { + return; // Prevent double-click + } + this.saving.set(true); + + try { + await this.securityTasksService.requestPasswordChangeForCriticalApplications( + this.dialogParams.organizationId, + this.newUnassignedAtRiskCipherIds(), + ); + + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("success"), + message: this.i18nService.t("notifiedMembers"), + }); + + // close the dialog + this.handleAssigningCompleted(); + } catch (error: unknown) { + if (error instanceof ErrorResponse && error.statusCode === 404) { + this.toastService.showToast({ + message: this.i18nService.t("mustBeOrganizationOwnerAdmin"), + variant: "error", + title: this.i18nService.t("error"), + }); + + return; + } + + this.logService.error("[NewApplicationsDialog] Failed to assign tasks", error); + + this.toastService.showToast({ + message: this.i18nService.t("unexpectedError"), + variant: "error", + title: this.i18nService.t("error"), + }); + } finally { + this.saving.set(false); + } } /** From 2af9396766a962b56705094bfeb2dd4e642295ea Mon Sep 17 00:00:00 2001 From: Leslie Tilton <23057410+Banrion@users.noreply.github.com> Date: Mon, 23 Feb 2026 08:47:16 -0600 Subject: [PATCH 133/134] Initial bitwarden team core docs (#19048) --- .../bit-common/src/dirt/docs/README.md | 73 ++++ .../src/dirt/docs/documentation-structure.md | 277 +++++++++++++ .../src/dirt/docs/getting-started.md | 96 +++++ .../src/dirt/docs/integration-guide.md | 378 ++++++++++++++++++ 4 files changed, 824 insertions(+) create mode 100644 bitwarden_license/bit-common/src/dirt/docs/README.md create mode 100644 bitwarden_license/bit-common/src/dirt/docs/documentation-structure.md create mode 100644 bitwarden_license/bit-common/src/dirt/docs/getting-started.md create mode 100644 bitwarden_license/bit-common/src/dirt/docs/integration-guide.md diff --git a/bitwarden_license/bit-common/src/dirt/docs/README.md b/bitwarden_license/bit-common/src/dirt/docs/README.md new file mode 100644 index 00000000000..f07f5c8b44c --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/docs/README.md @@ -0,0 +1,73 @@ +# DIRT Team Documentation + +**Location:** `bitwarden_license/bit-common/src/dirt/docs/` +**Purpose:** Overview of DIRT team documentation with navigation to detailed guides + +--- + +## 🎯 Start Here + +**New to the DIRT team?** → [Getting Started](./getting-started.md) + +**Looking for something specific?** + +- **"What should I read for my task?"** → [Getting Started](./getting-started.md) +- **"How are docs organized?"** → [Documentation Structure](./documentation-structure.md) +- **"How do I implement a feature?"** → [Playbooks](./playbooks/) +- **"What are the coding standards?"** → [Standards](./standards/) +- **"How do services integrate with components?"** → [Integration Guide](./integration-guide.md) + +--- + +## 📁 What's in This Folder + +| Document/Folder | Purpose | +| -------------------------------------------------------------- | ------------------------------------------------- | +| **[getting-started.md](./getting-started.md)** | Navigation hub - what to read for your task | +| **[documentation-structure.md](./documentation-structure.md)** | Complete structure guide - how docs are organized | +| **[integration-guide.md](./integration-guide.md)** | Service ↔ Component integration patterns | +| **[playbooks/](./playbooks/)** | Step-by-step implementation guides | +| **[standards/](./standards/)** | Team coding and documentation standards | +| **[access-intelligence/](./access-intelligence/)** | Migration guides and architecture comparisons | + +--- + +## 🏗️ DIRT Team Features + +The DIRT team (Data, Insights, Reporting & Tooling) owns: + +- **Access Intelligence** - Organization security reporting and password health +- **Organization Integrations** - Third-party integrations +- **External Reports** - Organization reports (weak passwords, member access, etc.) +- **Phishing Detection** - Browser-based phishing detection + +**Documentation is organized by package:** + +- **bit-common** - Platform-agnostic services (work on all platforms) +- **bit-web** - Angular web components (web client only) +- **bit-browser** - Browser extension components + +For detailed feature documentation locations, see [Getting Started](./getting-started.md). + +--- + +## 📝 Creating New Documentation + +**Before creating new docs, follow these steps:** + +1. **Read the standards:** [Documentation Standards](./standards/documentation-standards.md) +2. **Check for overlaps:** Review existing docs to avoid duplication +3. **Follow the playbook:** [Documentation Playbook](./playbooks/documentation-playbook.md) +4. **Update navigation:** Add to [getting-started.md](./getting-started.md) if it's a primary entry point +5. **Update this README:** If adding a new category or top-level document + +**For detailed guidance on where to place docs, see:** + +- [Documentation Standards § Document Location Rules](./standards/documentation-standards.md#document-location-rules) +- [Documentation Structure](./documentation-structure.md) + +--- + +**Document Version:** 1.0 +**Last Updated:** 2026-02-17 +**Maintainer:** DIRT Team diff --git a/bitwarden_license/bit-common/src/dirt/docs/documentation-structure.md b/bitwarden_license/bit-common/src/dirt/docs/documentation-structure.md new file mode 100644 index 00000000000..7d7e20b5d31 --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/docs/documentation-structure.md @@ -0,0 +1,277 @@ +# DIRT Team Documentation Structure + +**Purpose:** Navigation guide for all DIRT team documentation organized by team/feature hierarchy + +--- + +## 📁 Documentation Organization + +DIRT team documentation follows a **team/feature** hierarchy organized across multiple locations based on separation of concerns: + +### Team-Level Documentation + +**Location:** `bitwarden_license/bit-common/src/dirt/docs/` + +**Scope:** Applies to all DIRT features (Access Intelligence, Phishing Detection, etc.) + +**Contains:** + +- Team playbooks (service, component, documentation) +- Team coding standards +- Integration guides +- Getting started guide + +### Feature-Level Documentation + +**Pattern:** Feature docs live **next to the feature code**, not in the team `docs/` folder. + +**Location:** `dirt/[feature]/docs/` + +**Examples:** + +- **Access Intelligence:** `dirt/access-intelligence/v2/docs/` (or `dirt/access-intelligence/docs/` for current version) +- **Phishing Detection (future):** `dirt/phishing-detection/docs/` + +**Feature docs contain:** + +- Feature-specific architecture +- Feature-specific implementation guides +- Feature-specific patterns + +**Exception:** Migration/transition documentation can live in team `docs/` as **team-level knowledge**. Example: `docs/access-intelligence/` contains migration guides from v1 to v2, which is team-level context about the transition, not feature-specific architecture. + +### 1. Services & Architecture (Platform-Agnostic) + +**Pattern:** `bitwarden_license/bit-common/src/dirt/[feature]/docs/` + +**Purpose:** Feature-specific documentation lives next to the feature code + +**Example for Access Intelligence:** + +- Location: `dirt/access-intelligence/v2/docs/` (for v2 architecture) +- Contains: Architecture docs, implementation guides specific to that version + +**Note:** Team-level migration docs may live in `docs/access-intelligence/` as team knowledge about the transition between versions. + +### 2. Components (Angular-Specific) + +**Pattern:** `bitwarden_license/bit-web/src/app/dirt/[feature]/docs/` + +**Purpose:** Angular-specific UI components for web client only + +**Example for Access Intelligence:** + +- Location: `dirt/access-intelligence/docs/` +- Contains: Component inventory, migration guides, Storybook + +--- + +## 🎯 Where to Start? + +**For navigation guidance (what to read), see:** [getting-started.md](./getting-started.md) + +This document focuses on **how** the documentation is organized, not **what** to read. + +--- + +## 🗂️ Complete File Structure + +``` +# ============================================================================ +# SERVICES & ARCHITECTURE (bit-common) +# Platform-agnostic - Used by web, desktop, browser, CLI +# ============================================================================ + +bitwarden_license/bit-common/src/dirt/ +├── docs/ ← TEAM-LEVEL documentation only +│ ├── README.md ← Team docs overview +│ ├── getting-started.md ← Entry point for team +│ ├── documentation-structure.md ← This file +│ ├── integration-guide.md ← Service ↔ Component integration +│ │ +│ ├── playbooks/ ← Team playbooks (service, component, docs) +│ │ └── README.md ← Playbook navigation +│ │ +│ ├── standards/ ← Team coding standards +│ │ └── standards.md ← Core standards +│ │ +│ └── access-intelligence/ ← EXCEPTION: Migration guides (team knowledge) +│ ├── README.md ← Migration overview +│ ├── ... ← Migration analysis files +│ ├── architecture/ ← Migration architecture comparison +│ │ └── ... ← Architecture comparison files +│ └── implementation/ ← Implementation guides +│ └── ... ← Integration guides +│ +└── [feature]/ ← FEATURE CODE + FEATURE DOCS + └── docs/ ← Feature-specific documentation + ├── README.md ← Feature docs navigation + ├── architecture/ ← Feature architecture (lives with code) + │ └── ... ← Architecture files + └── implementation/ ← Feature implementation guides + └── ... ← Implementation guide files + +# Example for Access Intelligence v2: +bitwarden_license/bit-common/src/dirt/access-intelligence/ +├── v2/ ← V2 implementation +│ ├── services/ ← V2 services +│ ├── models/ ← V2 models +│ └── docs/ ← V2-SPECIFIC documentation +│ ├── README.md ← V2 docs overview +│ ├── architecture/ ← V2 architecture +│ │ └── ... ← Architecture files +│ └── implementation/ ← V2 implementation guides +│ └── ... ← Implementation guide files +└── v1/ ← V1 implementation (legacy) + +# ============================================================================ +# COMPONENTS (bit-web) +# Angular-specific - Web client only +# ============================================================================ + +bitwarden_license/bit-web/src/app/dirt/[feature]/ +├── docs/ ← Component documentation +│ └── README.md ← Component docs navigation +├── [component folders]/ ← Angular components +└── v2/ ← V2 components (if applicable) + +# Example for Access Intelligence: +bitwarden_license/bit-web/src/app/dirt/access-intelligence/ +├── docs/ ← Component documentation +│ ├── README.md ← Component docs navigation +│ └── ... ← Component guides +├── [components]/ ← Angular components +└── v2/ ← V2 components (if applicable) + └── ... ← V2 component files +``` + +--- + +## 🔄 When to Update This Structure + +Update this document when: + +- [ ] Adding new documentation categories +- [ ] Changing file locations +- [ ] Restructuring documentation organization + +--- + +## 📝 Architecture Decisions + +**Where decisions are tracked:** + +- **Company-wide ADRs:** Stored in the `contributing-docs` repository +- **Feature-specific decisions:** Tracked in Confluence (link to be added) +- **Local decision notes (optional):** `~/Documents/bitwarden-notes/dirt/decisions/[feature]/` for personal reference before moving to Confluence + - Example: `~/Documents/bitwarden-notes/dirt/decisions/access-intelligence/` + +**What goes in repo architecture docs:** + +- Current architecture state +- Migration plans and roadmaps +- Technical constraints +- Implementation patterns + +**What goes in Confluence:** + +- Decision discussions and rationale +- Alternative approaches considered +- Stakeholder input +- Links to Slack discussions + +--- + +## ✏️ Creating New Documentation + +**Before creating new documentation, see:** [docs/README.md](./README.md) § Documentation Best Practices + +**Key principles:** + +- **Single responsibility** - Each document should answer one question +- **Check for overlaps** - Read related docs first +- **Follow naming conventions** - See [documentation-standards.md](./standards/documentation-standards.md) +- **Cross-reference standards** - See [documentation-standards.md § Cross-Reference Standards](./standards/documentation-standards.md#cross-reference-standards) +- **Update navigation** - Add to getting-started.md if it's a primary entry point + +--- + +## 📊 Why This Structure? + +### Documentation Placement Principles + +**Team-Level Documentation (`docs/`):** + +- Applies to all DIRT features +- Playbooks, standards, getting-started guides +- Migration guides and transition documentation (team knowledge about rewrites) +- Cross-feature integration patterns + +**Feature-Level Documentation (`dirt/[feature]/docs/`):** + +- Lives **next to the feature code** +- Feature-specific architecture +- Version-specific implementation details +- Feature-specific patterns + +**Rationale:** + +- **Discoverability:** Architecture docs are found where the code lives +- **Versioning:** v1 and v2 can have separate docs directories +- **Maintainability:** Update feature docs without touching team docs +- **Clarity:** Clear separation between "what applies to all features" vs "what applies to this feature" + +### Separation of Concerns + +**Platform-Agnostic (bit-common):** + +- Services work on all platforms (web, desktop, browser, CLI) +- Domain models are platform-independent +- Architecture decisions affect all clients +- **Feature docs live with feature code:** `dirt/[feature]/docs/` + +**Angular-Specific (bit-web):** + +- Components only used in web client +- Storybook is web-only +- Angular-specific patterns (OnPush, Signals, etc.) +- **Component docs live with components:** `dirt/[feature]/docs/` + +### Benefits + +1. **Clarity:** Developers know where to look based on what they're working on +2. **Separation:** Team docs vs feature docs, Angular code vs platform-agnostic code +3. **Discoverability:** Feature docs are near feature code +4. **Maintainability:** Easier to update feature docs without affecting team docs +5. **Scalability:** Can add versioned docs (v1/, v2/) next to versioned code +6. **Migration clarity:** Team `docs/` can hold migration guides while feature `docs/` hold version-specific architecture + +--- + +## 🆘 Need Help? + +### Can't Find Documentation? + +1. **Start with getting-started.md:** [getting-started.md](./getting-started.md) + - Navigation hub for all DIRT team documentation + - Links to all major documentation categories + +2. **Check README files:** + - [Team Documentation README](./README.md) + - [Component README](/bitwarden_license/bit-web/src/app/dirt/access-intelligence/docs/README.md) + +3. **Check feature-specific docs:** + - Look in `dirt/[feature]/docs/` next to the feature code + - Example: `dirt/access-intelligence/v2/docs/` + +### Links Broken? + +- Check if file was moved +- Update cross-references following [documentation-standards.md § Cross-Reference Standards](./standards/documentation-standards.md#cross-reference-standards) +- Update navigation in README.md files + +--- + +**Document Version:** 1.0 +**Last Updated:** 2026-02-17 +**Maintainer:** DIRT Team diff --git a/bitwarden_license/bit-common/src/dirt/docs/getting-started.md b/bitwarden_license/bit-common/src/dirt/docs/getting-started.md new file mode 100644 index 00000000000..0077019fe02 --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/docs/getting-started.md @@ -0,0 +1,96 @@ +# DIRT Team - Getting Started + +**Purpose:** Navigation hub showing what documentation is available for your work + +--- + +## 🎯 DIRT Team Features + +The **DIRT team** (Data, Insights, Reporting & Tooling) owns: + +- **Access Intelligence** (formerly Risk Insights) + - Organization security reporting and password health analysis + - Location: `dirt/reports/risk-insights/` (v1 services), `bit-web/.../access-intelligence/` (UI) + - Note: `risk-insights` is the v1 codebase name for Access Intelligence + +- **Organization Integrations** + - Third-party organization integrations + - Location: `dirt/organization-integrations/` + +- **External Reports** + - Various organization reports (weak password report, member access report, etc.) + - Documentation: Coming soon + +- **Phishing Detection** + - Documentation: Coming soon + +**Note:** Access Intelligence has the most documentation as it's the first feature we're documenting comprehensively. + +--- + +## 📚 What's Available + +### Development Resources + +| Resource Type | What It Provides | Where to Find It | +| ----------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| **Playbooks** | Step-by-step implementation guides for common dev tasks | [Playbooks Hub](./playbooks/) | +| **Standards** | Coding conventions, patterns, and best practices | [Standards Hub](./standards/README.md) | +| **Architecture** | Feature architecture reviews and migration plans | [Access Intelligence Architecture](./access-intelligence/architecture/) | +| **Integration Guides** | How services and components work together | [Generic Guide](./integration-guide.md), [Access Intelligence](./access-intelligence/service-component-integration.md) | +| **Documentation Guide** | How docs are organized and where to find things | [Documentation Structure](./documentation-structure.md) | + +### Standards by Area + +| Area | Standard Document | +| ---------------------- | -------------------------------------------------------------------------- | +| **General Coding** | [Standards Hub](./standards/README.md) | +| **Services** | [Service Standards](./standards/service-standards.md) | +| **Domain Models** | [Model Standards](./standards/model-standards.md) | +| **Service Testing** | [Service Testing Standards](./standards/testing-standards-services.md) | +| **Angular Components** | [Angular Standards](./standards/angular-standards.md) | +| **Component Testing** | [Component Testing Standards](./standards/testing-standards-components.md) | +| **RxJS Patterns** | [RxJS Standards](./standards/rxjs-standards.md) | +| **Code Organization** | [Code Organization Standards](./standards/code-organization-standards.md) | +| **Documentation** | [Documentation Standards](./standards/documentation-standards.md) | + +### Playbooks by Task + +| Task | Playbook | +| ------------------------------------ | --------------------------------------------------------------------------------- | +| **Implement or refactor a service** | [Service Implementation Playbook](./playbooks/service-implementation-playbook.md) | +| **Migrate or create a UI component** | [Component Migration Playbook](./playbooks/component-migration-playbook.md) | +| **Create or update documentation** | [Documentation Playbook](./playbooks/documentation-playbook.md) | +| **Browse all playbooks** | [Playbooks Hub](./playbooks/) | + +--- + +## 🚀 Quick Reference by Task + +| What are you working on? | Start here | +| ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Services** (implementation, architecture, testing) | [Service Playbook](./playbooks/service-implementation-playbook.md) + [Service Standards](./standards/service-standards.md) | +| **Domain Models** (view models, query methods) | [Service Playbook](./playbooks/service-implementation-playbook.md) + [Model Standards](./standards/model-standards.md) | +| **UI Components** (Angular, migration, testing) | [Component Playbook](./playbooks/component-migration-playbook.md) + [Angular Standards](./standards/angular-standards.md) | +| **Storybook** (create or update stories) | [Component Playbook](./playbooks/component-migration-playbook.md) + [Component Testing Standards § Storybook](./standards/testing-standards-components.md#storybook-as-living-documentation) | +| **Component Tests** (Jest, OnPush, Signals) | [Component Playbook](./playbooks/component-migration-playbook.md) + [Component Testing Standards](./standards/testing-standards-components.md) | +| **Service Tests** (mocks, observables, RxJS) | [Service Playbook](./playbooks/service-implementation-playbook.md) + [Service Testing Standards](./standards/testing-standards-services.md) | +| **Documentation** (create, update, organize) | [Documentation Playbook](./playbooks/documentation-playbook.md) + [Documentation Standards](./standards/documentation-standards.md) | +| **Architecture Review** (feature planning) | [Access Intelligence Architecture](./access-intelligence/architecture/) | +| **Feature Architecture Decisions** | Document in [docs/[feature]/architecture/](./documentation-structure.md#feature-level-documentation) (decisions tracked in Confluence) | + +--- + +## 🆘 Need Help? + +**Can't find what you're looking for?** + +- **Understand how docs are organized:** See [Documentation Structure](./documentation-structure.md) +- **Browse all team documentation:** See [Team Docs README](./README.md) +- **Component-specific docs:** See [Component Docs](/bitwarden_license/bit-web/src/app/dirt/access-intelligence/docs/README.md) + +--- + +**Document Version:** 1.0 +**Last Updated:** 2026-02-17 +**Maintainer:** DIRT Team diff --git a/bitwarden_license/bit-common/src/dirt/docs/integration-guide.md b/bitwarden_license/bit-common/src/dirt/docs/integration-guide.md new file mode 100644 index 00000000000..0d7bf3db847 --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/docs/integration-guide.md @@ -0,0 +1,378 @@ +# Service ↔ Component Integration Guide + +**Purpose:** Coordination guide for features that span both platform-agnostic services (bit-common) and Angular UI components (bit-web/bit-browser) + +**Scope:** This guide applies to **any DIRT feature** requiring work in both service and component layers. For feature-specific integration patterns and detailed examples, see the feature's documentation: + +- [Access Intelligence Integration](/bitwarden_license/bit-common/src/dirt/docs/access-intelligence/service-component-integration.md) + +**Focus:** This document focuses on **coordination and handoffs** between service and component developers. For code patterns and standards, see [Standards Documentation](/bitwarden_license/bit-common/src/dirt/docs/standards/standards.md). + +--- + +## 📋 When You Need Both + +Many DIRT features require coordinated work across service AND component layers: + +| Feature Type | Service Work | Component Work | +| -------------------------- | ----------------------------- | --------------------------------- | +| **New report/data type** | Generate, persist, load data | Display data, filters, navigation | +| **New data visualization** | Aggregate/query data | Charts, cards, tables | +| **User actions** | Business logic on models | UI interactions, forms | +| **Settings/preferences** | Persist settings | Settings UI | +| **Integrations** | API communication, sync logic | Configuration UI, status display | + +--- + +## 🔄 Integration Pattern + +``` +┌─────────────────────────────────────────────────┐ +│ Component (bit-web/bit-browser) │ +│ - User interactions │ +│ - Display logic │ +│ - Converts Observables → Signals (toSignal()) │ +│ - OnPush + Signal inputs/outputs │ +├─────────────────────────────────────────────────┤ +│ Data Service (Feature-specific) │ +│ - Exposes Observable streams │ +│ - Coordinates feature data │ +│ - Delegates business logic to models │ +│ - Delegates persistence to services │ +├─────────────────────────────────────────────────┤ +│ Domain Services (bit-common) │ +│ - Business logic orchestration │ +│ - Pure transformation │ +│ - Platform-agnostic │ +├─────────────────────────────────────────────────┤ +│ View Models │ +│ - Smart models (CipherView pattern) │ +│ - Query methods: getData(), filter(), etc. │ +│ - Mutation methods: update(), delete(), etc. │ +└─────────────────────────────────────────────────┘ +``` + +**Key principle:** Services do the work, components coordinate the UI. Business logic lives in view models, not components. + +--- + +## 🔀 Service → Component Handoff + +**When:** Service implementation is complete, ready for UI integration + +### Readiness Checklist + +Before handing off to component developer, ensure: + +- [ ] **Service is complete and tested** + - [ ] Abstract defined with JSDoc + - [ ] Implementation complete + - [ ] Tests passing (`npm run test`) + - [ ] Types validated (`npm run test:types`) + +- [ ] **View models have required methods** + - [ ] Query methods for component data needs (documented) + - [ ] Mutation methods for user actions (documented) + - [ ] Methods follow naming conventions + +- [ ] **Data service exposes observables** + - [ ] Observable(s) are public and documented + - [ ] Observable emits correct view models + - [ ] Observable handles errors gracefully + +- [ ] **Component requirements documented** + - [ ] What data the component needs + - [ ] What user actions the component handles + - [ ] What the component should display + - [ ] Any performance considerations + +### Handoff Communication Template + +When handing off to component developer, provide: + +1. **What service to inject** + - Example: `FeatureDataService` + +2. **What observable(s) to use** + - Example: `data$: Observable` + - Type signature and nullability + +3. **What model methods are available** + - Query methods: `feature.getData()`, `feature.filter(criteria)` + - Mutation methods: `feature.update(data)`, `feature.delete(id)` + - Link to model documentation or JSDoc + +4. **How to integrate in component** + - Reference [Standards: Observable to Signal Conversion](/bitwarden_license/bit-common/src/dirt/docs/standards/standards.md) + - Basic pattern: inject service → convert observable to signal → use in template + +5. **Any gotchas or special considerations** + - Performance notes (large datasets, expensive operations) + - Error handling requirements + - Special states (loading, empty, error) + +### Communication Methods + +- **Slack:** Quick handoff for simple integrations +- **Jira comment:** Document handoff details on feature ticket +- **Documentation:** Update feature docs with integration examples +- **Pair session:** For complex integrations, schedule pairing + +--- + +## 🔀 Component → Service Handoff + +**When:** Component needs new data/functionality not yet available in services + +### Discovery Checklist + +Before creating a service request, identify: + +- [ ] **What's missing** + - [ ] New query method needed on view model? + - [ ] New mutation method needed on view model? + - [ ] New service needed entirely? + - [ ] New data needs to be loaded/persisted? + +- [ ] **Document the requirement clearly** + - [ ] What data the component needs (shape, type) + - [ ] What format the data should be in + - [ ] What user action triggers this need + - [ ] Performance requirements (dataset size, frequency) + +- [ ] **Assess scope** + - [ ] Is this a new method on existing model? (small change) + - [ ] Is this a new service? (medium-large change) + - [ ] Does this require API changes? (involves backend team) + +- [ ] **File appropriate ticket** + - [ ] Link to component/feature that needs it + - [ ] Link to design/mockup if applicable + - [ ] Tag service developer or tech lead + +### Handoff Communication Template + +When requesting service work, provide: + +1. **What the component needs** + - Clear description: "Component needs list of filtered items based on user criteria" + +2. **Proposed API (if you have one)** + - Example: `model.getFilteredItems(criteria): Item[]` + - This is negotiable, service developer may suggest better approach + +3. **Why (user story/context)** + - Example: "User clicks 'Show only critical' filter, UI should update to show subset" + +4. **Data format expected** + - Example: "Array of `{ id: string, name: string, isCritical: boolean }`" + - Or reference existing model type if reusing + +5. **Performance/scale considerations** + - Example: "Could be 1000+ items for large organizations" + - Helps service developer optimize + +6. **Timeline/priority** + - Is this blocking component work? + - Can component proceed with stub/mock for now? + +### Communication Methods + +- **Jira ticket:** For non-trivial work requiring tracking +- **Slack:** For quick questions or small additions +- **Planning session:** For large features requiring design discussion +- **ADR:** If architectural decision needed + +--- + +## 🤝 Collaboration Patterns + +### Pattern 1: Parallel Development + +**When to use:** Service and component work can be developed simultaneously + +**How:** + +1. Service developer creates interface/abstract first +2. Component developer uses interface with mock data +3. Both develop in parallel +4. Integration happens at the end + +**Benefits:** Faster delivery, clear contracts + +### Pattern 2: Sequential Development (Service First) + +**When to use:** Component needs complete service implementation + +**How:** + +1. Service developer implements fully +2. Service developer documents integration +3. Component developer integrates +4. Component developer provides feedback + +**Benefits:** Fewer integration issues, clearer requirements + +### Pattern 3: Sequential Development (Component First) + +**When to use:** UI/UX needs to be proven before service investment + +**How:** + +1. Component developer builds with mock data +2. Component developer documents data needs +3. Service developer implements to match needs +4. Integration and refinement + +**Benefits:** User-driven design, avoids unused service work + +### Pattern 4: Paired Development + +**When to use:** Complex integration, unclear requirements, new patterns + +**How:** + +1. Service and component developer pair on design +2. Develop together or in short iterations +3. Continuous feedback and adjustment + +**Benefits:** Fastest problem solving, shared understanding + +--- + +## 🧪 Testing Integration Points + +### Service Layer Testing + +**Service developers should test:** + +- Services return correct view models +- Observables emit expected data +- Error handling works correctly +- Performance is acceptable for expected dataset sizes + +**Reference:** [Service Implementation Playbook - Testing](/bitwarden_license/bit-common/src/dirt/docs/playbooks/service-implementation-playbook.md) + +### Component Layer Testing + +**Component developers should test:** + +- Services are correctly injected +- Observables are correctly converted to signals +- View model methods are called appropriately +- Data is displayed correctly +- User interactions trigger correct model methods + +**Reference:** [Component Migration Playbook - Testing](/bitwarden_license/bit-common/src/dirt/docs/playbooks/component-migration-playbook.md) + +### Integration Testing + +**Both should coordinate on:** + +- Full user flows work end-to-end +- Data flows correctly from service → component +- UI updates when data changes +- Error states are handled gracefully + +--- + +## 🚨 Common Integration Pitfalls + +### 1. Component Bypasses Data Service + +**Problem:** Component directly calls API services or persistence layers + +**Why it's bad:** Breaks abstraction, duplicates logic, harder to test + +**Solution:** Always go through feature's data service layer + +**Reference:** [Standards: Service Layer Pattern](/bitwarden_license/bit-common/src/dirt/docs/standards/standards.md) + +### 2. Service Returns Plain Objects + +**Problem:** Service returns `{ ... }` instead of view model instances + +**Why it's bad:** Loses model methods, breaks encapsulation, business logic leaks to components + +**Solution:** Always return view model instances with query/mutation methods + +**Reference:** [Standards: View Models](/bitwarden_license/bit-common/src/dirt/docs/standards/standards.md) + +### 3. Business Logic in Components + +**Problem:** Component implements filtering, calculations, state changes + +**Why it's bad:** Logic not reusable, harder to test, violates separation of concerns + +**Solution:** Business logic belongs in view models or domain services + +**Reference:** [Standards: Component Responsibilities](/bitwarden_license/bit-common/src/dirt/docs/standards/standards.md) + +### 4. Manual Observable Subscriptions + +**Problem:** Component uses `.subscribe()` instead of `toSignal()` + +**Why it's bad:** Memory leaks, manual cleanup needed, doesn't leverage Angular signals + +**Solution:** Use `toSignal()` for automatic cleanup and signal integration + +**Reference:** [Standards: Observable to Signal Conversion](/bitwarden_license/bit-common/src/dirt/docs/standards/standards.md) + +### 5. Unclear Handoff + +**Problem:** Service developer finishes work but doesn't communicate to component developer + +**Why it's bad:** Delays integration, component developer doesn't know work is ready + +**Solution:** Use handoff communication templates above, update Jira tickets, notify in Slack + +--- + +## 📞 Who to Contact + +### Service Questions + +- Check: [Service Implementation Playbook](/bitwarden_license/bit-common/src/dirt/docs/playbooks/service-implementation-playbook.md) +- Check: [Standards](/bitwarden_license/bit-common/src/dirt/docs/standards/standards.md) +- Ask: DIRT team service developers + +### Component Questions + +- Check: [Component Migration Playbook](/bitwarden_license/bit-common/src/dirt/docs/playbooks/component-migration-playbook.md) +- Check: [Standards](/bitwarden_license/bit-common/src/dirt/docs/standards/standards.md) +- Ask: DIRT team component developers + +### Architecture Questions + +- Check: [Architecture Docs](/bitwarden_license/bit-common/src/dirt/docs/access-intelligence/architecture/) +- Check: [Getting Started](/bitwarden_license/bit-common/src/dirt/docs/getting-started.md) +- Ask: DIRT team tech lead + +### Coordination/Process Questions + +- Ask: DIRT team lead or scrum master + +--- + +## 📚 Related Documentation + +### General Guides + +- [Getting Started](/bitwarden_license/bit-common/src/dirt/docs/getting-started.md) +- [Standards](/bitwarden_license/bit-common/src/dirt/docs/standards/standards.md) +- [Documentation Structure](/bitwarden_license/bit-common/src/dirt/docs/documentation-structure.md) + +### Implementation Playbooks + +- [Service Implementation Playbook](/bitwarden_license/bit-common/src/dirt/docs/playbooks/service-implementation-playbook.md) +- [Component Migration Playbook](/bitwarden_license/bit-common/src/dirt/docs/playbooks/component-migration-playbook.md) + +### Feature-Specific Integration Guides + +- [Access Intelligence Integration](/bitwarden_license/bit-common/src/dirt/docs/access-intelligence/service-component-integration.md) + +--- + +**Document Version:** 1.0 +**Last Updated:** 2026-02-17 +**Maintainer:** DIRT Team From 74aec0b80c96b61566149888d2e31090084079e1 Mon Sep 17 00:00:00 2001 From: Leslie Tilton <23057410+Banrion@users.noreply.github.com> Date: Mon, 23 Feb 2026 09:05:26 -0600 Subject: [PATCH 134/134] [PM-26487][PM-20112] Member Access Report - Member Cipher Client Mapping (#18774) * Added v2 version of member access reports that aggregate data client side instead of using endpoint that times out. Added feature flag. * Remove feature flag * Added avatar color to the member access report * Update icon usage * Add story book for member access report * Add icon module to member access report component * Fix test case * Update member access report service to match export of v1 version. Update test cases * Fix billing error in member access report * Add timeout to fetch organization ciphers * Handle group naming * Add cached permission text * Add memberAccessReportLoadError message * Fix member cipher mapping to deduplicate data in memory * Update log * Update storybook with deterministic data and test type * Fix avatar color default * Fix types * Address timeout cleanup --- apps/web/src/locales/en/messages.json | 3 + .../member-access-report.component.html | 20 +- .../member-access-report.component.stories.ts | 268 +++++++ .../member-access-report.component.ts | 102 ++- .../member-access-report.service.spec.ts | 670 +++++++++++++++++- .../services/member-access-report.service.ts | 464 +++++++++++- .../view/member-access-report.view.ts | 1 + 7 files changed, 1490 insertions(+), 38 deletions(-) create mode 100644 bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/member-access-report.component.stories.ts diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index ef8c109bc4b..7ea2abb5d08 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -10977,6 +10977,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Off" }, + "memberAccessReportLoadError": { + "message": "Failed to load the member access report. This may be due to a large organization size or network issue. Please try again or contact support if the problem persists." + }, "kdfIterationRecommends": { "message": "We recommend 600,000 or more" }, diff --git a/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/member-access-report.component.html b/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/member-access-report.component.html index 440e955a226..6769998e2c8 100644 --- a/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/member-access-report.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/member-access-report.component.html @@ -9,7 +9,7 @@ > } @@ -22,11 +22,11 @@ @if (isLoading) {
    - +

    {{ "loading" | i18n }}

    } @else { @@ -42,7 +42,13 @@
    - +