From d5f5052c118226cf80ea6218944fc7214455b587 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Mon, 20 Oct 2025 02:41:44 +0200 Subject: [PATCH 01/12] [PM-3162] Use system notification for update alert (#15606) --- apps/desktop/src/main.ts | 2 +- apps/desktop/src/main/updater.main.ts | 67 +++++++++++++++++++++++++-- 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index d5484213a90..fbb83a1bf56 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -221,7 +221,7 @@ export class Main { ); this.messagingMain = new MessagingMain(this, this.desktopSettingsService); - this.updaterMain = new UpdaterMain(this.i18nService, this.windowMain); + this.updaterMain = new UpdaterMain(this.i18nService, this.logService, this.windowMain); const messageSubject = new Subject>>(); this.messagingService = MessageSender.combine( diff --git a/apps/desktop/src/main/updater.main.ts b/apps/desktop/src/main/updater.main.ts index 51d5073911e..60b4f282405 100644 --- a/apps/desktop/src/main/updater.main.ts +++ b/apps/desktop/src/main/updater.main.ts @@ -1,8 +1,9 @@ -import { dialog, shell } from "electron"; +import { dialog, shell, Notification } from "electron"; import log from "electron-log"; import { autoUpdater, UpdateDownloadedEvent, VerifyUpdateSupport } from "electron-updater"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/logging"; import { isAppImage, isDev, isMacAppStore, isWindowsPortable, isWindowsStore } from "../utils"; @@ -11,6 +12,8 @@ import { WindowMain } from "./window.main"; const UpdaterCheckInitialDelay = 5 * 1000; // 5 seconds const UpdaterCheckInterval = 12 * 60 * 60 * 1000; // 12 hours +const MaxTimeBeforeBlockingUpdateNotification = 7 * 24 * 60 * 60 * 1000; // 7 days + export class UpdaterMain { private doingUpdateCheck = false; private doingUpdateCheckWithFeedback = false; @@ -18,8 +21,19 @@ export class UpdaterMain { private updateDownloaded: UpdateDownloadedEvent = null; private originalRolloutFunction: VerifyUpdateSupport = null; + // This needs to be tracked to avoid the Notification being garbage collected, + // which would break the click handler. + private openedNotification: Notification | null = null; + + // This is used to set when the initial update notification was shown. + // The system notifications can be easy to miss or be disabled, so we want to + // ensure the user is eventually made aware of the update. If the user does not + // interact with the notification in a reasonable time, we will prompt them again. + private initialUpdateNotificationTime: number | null = null; + constructor( private i18nService: I18nService, + private logService: LogService, private windowMain: WindowMain, ) { autoUpdater.logger = log; @@ -43,6 +57,8 @@ export class UpdaterMain { }); autoUpdater.on("update-available", async () => { + this.initialUpdateNotificationTime ??= Date.now(); + if (this.doingUpdateCheckWithFeedback) { if (this.windowMain.win == null) { this.reset(); @@ -87,7 +103,7 @@ export class UpdaterMain { } this.updateDownloaded = info; - await this.promptRestartUpdate(info); + await this.promptRestartUpdate(info, this.doingUpdateCheckWithFeedback); }); autoUpdater.on("error", (error) => { @@ -108,7 +124,7 @@ export class UpdaterMain { } if (this.updateDownloaded && withFeedback) { - await this.promptRestartUpdate(this.updateDownloaded); + await this.promptRestartUpdate(this.updateDownloaded, true); return; } @@ -144,7 +160,50 @@ export class UpdaterMain { this.updateDownloaded = null; } - private async promptRestartUpdate(info: UpdateDownloadedEvent) { + private async promptRestartUpdate(info: UpdateDownloadedEvent, blocking: boolean) { + // If we have an initial notification, and it's from a long time ago, + // we will block the user with a dialog to ensure they see it. + const longTimeSinceInitialNotification = + this.initialUpdateNotificationTime != null && + Date.now() - this.initialUpdateNotificationTime > MaxTimeBeforeBlockingUpdateNotification; + + if (!longTimeSinceInitialNotification && !blocking && Notification.isSupported()) { + // If the prompt doesn't have to block and we support notifications, + // we will show a notification instead of a blocking dialog, which won't steal focus. + await this.promptRestartUpdateUsingSystemNotification(info); + } else { + // If we are blocking, or notifications are not supported, we will show a blocking dialog. + // This will steal the user's focus, so we should only do this for user initiated actions + // or when there are no other options. + await this.promptRestartUpdateUsingDialog(info); + } + } + + private async promptRestartUpdateUsingSystemNotification(info: UpdateDownloadedEvent) { + if (this.openedNotification != null) { + this.openedNotification.close(); + } + + this.openedNotification = new Notification({ + title: this.i18nService.t("bitwarden") + " - " + this.i18nService.t("restartToUpdate"), + body: this.i18nService.t("restartToUpdateDesc", info.version), + timeoutType: "never", + silent: false, + }); + + // If the user clicks the notification, prompt again to restart, this time with a blocking dialog. + this.openedNotification.on("click", () => { + void this.promptRestartUpdate(info, true); + }); + // If the notification fails to show, fall back to the blocking dialog as well. + this.openedNotification.on("failed", (error) => { + this.logService.error("Update notification failed", error); + void this.promptRestartUpdate(info, true); + }); + this.openedNotification.show(); + } + + private async promptRestartUpdateUsingDialog(info: UpdateDownloadedEvent) { const result = await dialog.showMessageBox(this.windowMain.win, { type: "info", title: this.i18nService.t("bitwarden") + " - " + this.i18nService.t("restartToUpdate"), From fa584f76b491be613de49f1735ef2de10fd3559e Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 20 Oct 2025 12:37:19 +0200 Subject: [PATCH 02/12] [PM-24683] Move change kdf service to SDK implementation (#16001) * Add new mp service api * Fix tests * Add test coverage * Add newline * Fix type * Rename to "unwrapUserKeyFromMasterPasswordUnlockData" * Fix build * Fix build on cli * Fix linting * Re-sort spec * Add tests * Fix test and build issues * Fix build * Clean up * Remove introduced function * Clean up comments * Fix abstract class types * Fix comments * Cleanup * Cleanup * Update libs/common/src/key-management/master-password/types/master-password.types.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/master-password/services/master-password.service.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/master-password/types/master-password.types.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Add comments * Fix build * Add arg null check * Cleanup * Fix build * Fix build on browser * Implement KDF change service * Deprecate encryptUserKeyWithMasterKey * Update libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Add tests for null params * Fix builds * Cleanup and deprecate more functions * Fix formatting * Prettier * Clean up * Update libs/key-management/src/abstractions/key.service.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Make emailToSalt private and expose abstract saltForUser * Add tests * Add docs * Fix build * Fix tests * Fix tests * Address feedback and fix primitive obsession * Consolidate active account checks in change kdf confirmation component * Update libs/common/src/key-management/kdf/services/change-kdf-service.spec.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Add defensive parameter checks * Add tests * Add comment for follow-up epic * Move change kdf service, remove abstraction and add api service * Fix test * Drop redundant null check * Address feedback * Add throw on empty password * Fix tests * Mark change kdf service as internal * Add abstract classes * Switch to abstraction * Move change kdf to sdk * Update tests * Fix tests * Clean up sdk mapping * Clean up tests * Check the argument to make_update_kdf * Fix mock data * Fix tests * Fix relative imports --------- Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> --- .../src/services/jslib-services.module.ts | 2 +- .../kdf/change-kdf-service.spec.ts | 159 ++++++++++-------- .../key-management/kdf/change-kdf-service.ts | 65 +++---- .../types/master-password.types.ts | 33 +++- libs/key-management/src/index.ts | 1 + libs/key-management/src/models/kdf-config.ts | 14 ++ package-lock.json | 8 +- package.json | 2 +- 8 files changed, 175 insertions(+), 109 deletions(-) diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 954598de9ca..f5642f45b2e 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1277,7 +1277,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: ChangeKdfService, useClass: DefaultChangeKdfService, - deps: [MasterPasswordServiceAbstraction, KeyService, KdfConfigService, ChangeKdfApiService], + deps: [ChangeKdfApiService, SdkService], }), safeProvider({ provide: AuthRequestServiceAbstraction, diff --git a/libs/common/src/key-management/kdf/change-kdf-service.spec.ts b/libs/common/src/key-management/kdf/change-kdf-service.spec.ts index 07cba1cc7eb..c7df90f4790 100644 --- a/libs/common/src/key-management/kdf/change-kdf-service.spec.ts +++ b/libs/common/src/key-management/kdf/change-kdf-service.spec.ts @@ -1,14 +1,14 @@ import { mock } from "jest-mock-extended"; import { of } from "rxjs"; -import { KdfRequest } from "@bitwarden/common/models/request/kdf.request"; -import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { UserId } from "@bitwarden/common/types/guid"; -import { UserKey } from "@bitwarden/common/types/key"; // eslint-disable-next-line no-restricted-imports -import { KdfConfigService, KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management"; +import { PBKDF2KdfConfig } from "@bitwarden/key-management"; -import { MasterPasswordServiceAbstraction } from "../master-password/abstractions/master-password.service.abstraction"; +import { makeEncString } from "../../../spec"; +import { KdfRequest } from "../../models/request/kdf.request"; +import { SdkService } from "../../platform/abstractions/sdk/sdk.service"; +import { UserId } from "../../types/guid"; +import { EncString } from "../crypto/models/enc-string"; import { MasterKeyWrappedUserKey, MasterPasswordAuthenticationHash, @@ -21,35 +21,63 @@ import { DefaultChangeKdfService } from "./change-kdf-service"; describe("ChangeKdfService", () => { const changeKdfApiService = mock(); - const masterPasswordService = mock(); - const keyService = mock(); - const kdfConfigService = mock(); + const sdkService = mock(); - let sut: DefaultChangeKdfService = mock(); + let sut: DefaultChangeKdfService; - const mockUserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey; - const mockOldKdfConfig = new PBKDF2KdfConfig(100000); const mockNewKdfConfig = new PBKDF2KdfConfig(200000); + const mockOldKdfConfig = new PBKDF2KdfConfig(100000); const mockOldHash = "oldHash" as MasterPasswordAuthenticationHash; const mockNewHash = "newHash" as MasterPasswordAuthenticationHash; const mockUserId = "00000000-0000-0000-0000-000000000000" as UserId; const mockSalt = "test@bitwarden.com" as MasterPasswordSalt; - const mockWrappedUserKey = "wrappedUserKey"; + const mockWrappedUserKey: EncString = makeEncString("wrappedUserKey"); + + const mockSdkClient = { + crypto: jest.fn().mockReturnValue({ + make_update_kdf: jest.fn(), + }), + }; + const mockRef = { + value: mockSdkClient, + [Symbol.dispose]: jest.fn(), + }; + const mockSdk = { + take: jest.fn().mockReturnValue(mockRef), + }; beforeEach(() => { - sut = new DefaultChangeKdfService( - masterPasswordService, - keyService, - kdfConfigService, - changeKdfApiService, - ); + sdkService.userClient$ = jest.fn((userId: UserId) => of(mockSdk)) as any; + sut = new DefaultChangeKdfService(changeKdfApiService, sdkService); }); afterEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); describe("updateUserKdfParams", () => { + const mockUpdateKdfResult = { + masterPasswordAuthenticationData: { + kdf: mockNewKdfConfig.toSdkConfig(), + salt: mockSalt, + masterPasswordAuthenticationHash: mockNewHash, + }, + masterPasswordUnlockData: { + kdf: mockNewKdfConfig.toSdkConfig(), + salt: mockSalt, + masterKeyWrappedUserKey: mockWrappedUserKey.encryptedString, + }, + oldMasterPasswordAuthenticationData: { + kdf: mockOldKdfConfig.toSdkConfig(), + salt: mockSalt, + masterPasswordAuthenticationHash: mockOldHash, + }, + }; + + beforeEach(() => { + mockSdkClient.crypto().make_update_kdf.mockReturnValue(mockUpdateKdfResult); + }); + it("should throw an error if masterPassword is null", async () => { await expect( sut.updateUserKdfParams(null as unknown as string, mockNewKdfConfig, mockUserId), @@ -90,61 +118,31 @@ describe("ChangeKdfService", () => { ).rejects.toThrow("userId"); }); - it("should throw an error if userKey is null", async () => { - keyService.userKey$.mockReturnValueOnce(of(null)); - masterPasswordService.saltForUser$.mockReturnValueOnce(of(mockSalt)); - kdfConfigService.getKdfConfig$.mockReturnValueOnce(of(mockOldKdfConfig)); + it("should throw an error if SDK is not available", async () => { + sdkService.userClient$ = jest.fn().mockReturnValue(of(null)) as any; + await expect( sut.updateUserKdfParams("masterPassword", mockNewKdfConfig, mockUserId), - ).rejects.toThrow(); + ).rejects.toThrow("SDK not available"); }); - it("should throw an error if salt is null", async () => { - keyService.userKey$.mockReturnValueOnce(of(mockUserKey)); - masterPasswordService.saltForUser$.mockReturnValueOnce(of(null)); - kdfConfigService.getKdfConfig$.mockReturnValueOnce(of(mockOldKdfConfig)); - await expect( - sut.updateUserKdfParams("masterPassword", mockNewKdfConfig, mockUserId), - ).rejects.toThrow("Failed to get salt"); - }); + it("should call SDK update_kdf with correct parameters", async () => { + const masterPassword = "masterPassword"; - it("should throw an error if oldKdfConfig is null", async () => { - keyService.userKey$.mockReturnValueOnce(of(mockUserKey)); - masterPasswordService.saltForUser$.mockReturnValueOnce(of(mockSalt)); - kdfConfigService.getKdfConfig$.mockReturnValueOnce(of(null)); - await expect( - sut.updateUserKdfParams("masterPassword", mockNewKdfConfig, mockUserId), - ).rejects.toThrow("Failed to get oldKdfConfig"); - }); + await sut.updateUserKdfParams(masterPassword, mockNewKdfConfig, mockUserId); - it("should call apiService.send with correct parameters", async () => { - keyService.userKey$.mockReturnValueOnce(of(mockUserKey)); - masterPasswordService.saltForUser$.mockReturnValueOnce(of(mockSalt)); - kdfConfigService.getKdfConfig$.mockReturnValueOnce(of(mockOldKdfConfig)); - - masterPasswordService.makeMasterPasswordAuthenticationData - .mockResolvedValueOnce({ - salt: mockSalt, - kdf: mockOldKdfConfig, - masterPasswordAuthenticationHash: mockOldHash, - }) - .mockResolvedValueOnce({ - salt: mockSalt, - kdf: mockNewKdfConfig, - masterPasswordAuthenticationHash: mockNewHash, - }); - - masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValueOnce( - new MasterPasswordUnlockData( - mockSalt, - mockNewKdfConfig, - mockWrappedUserKey as MasterKeyWrappedUserKey, - ), + expect(mockSdkClient.crypto().make_update_kdf).toHaveBeenCalledWith( + masterPassword, + mockNewKdfConfig.toSdkConfig(), ); + }); - await sut.updateUserKdfParams("masterPassword", mockNewKdfConfig, mockUserId); + it("should call changeKdfApiService.updateUserKdfParams with correct request", async () => { + const masterPassword = "masterPassword"; - const expected = new KdfRequest( + await sut.updateUserKdfParams(masterPassword, mockNewKdfConfig, mockUserId); + + const expectedRequest = new KdfRequest( { salt: mockSalt, kdf: mockNewKdfConfig, @@ -153,15 +151,38 @@ describe("ChangeKdfService", () => { new MasterPasswordUnlockData( mockSalt, mockNewKdfConfig, - mockWrappedUserKey as MasterKeyWrappedUserKey, + mockWrappedUserKey.encryptedString as MasterKeyWrappedUserKey, ), - ).authenticateWith({ + ); + expectedRequest.authenticateWith({ salt: mockSalt, kdf: mockOldKdfConfig, masterPasswordAuthenticationHash: mockOldHash, }); - expect(changeKdfApiService.updateUserKdfParams).toHaveBeenCalledWith(expected); + expect(changeKdfApiService.updateUserKdfParams).toHaveBeenCalledWith(expectedRequest); + }); + + it("should properly dispose of SDK resources", async () => { + const masterPassword = "masterPassword"; + jest.spyOn(mockNewKdfConfig, "toSdkConfig").mockReturnValue({} as any); + + await sut.updateUserKdfParams(masterPassword, mockNewKdfConfig, mockUserId); + + expect(mockRef[Symbol.dispose]).toHaveBeenCalled(); + }); + + it("should handle SDK errors properly", async () => { + const masterPassword = "masterPassword"; + const sdkError = new Error("SDK update_kdf failed"); + jest.spyOn(mockNewKdfConfig, "toSdkConfig").mockReturnValue({} as any); + mockSdkClient.crypto().make_update_kdf.mockImplementation(() => { + throw sdkError; + }); + + await expect( + sut.updateUserKdfParams(masterPassword, mockNewKdfConfig, mockUserId), + ).rejects.toThrow("SDK update_kdf failed"); }); }); }); diff --git a/libs/common/src/key-management/kdf/change-kdf-service.ts b/libs/common/src/key-management/kdf/change-kdf-service.ts index d8bc3e21c1a..64fbd1fce05 100644 --- a/libs/common/src/key-management/kdf/change-kdf-service.ts +++ b/libs/common/src/key-management/kdf/change-kdf-service.ts @@ -1,55 +1,56 @@ +import { firstValueFrom, map } from "rxjs"; + import { assertNonNullish } from "@bitwarden/common/auth/utils"; -import { KdfRequest } from "@bitwarden/common/models/request/kdf.request"; import { UserId } from "@bitwarden/common/types/guid"; // eslint-disable-next-line no-restricted-imports -import { KdfConfig, KdfConfigService, KeyService } from "@bitwarden/key-management"; +import { KdfConfig } from "@bitwarden/key-management"; -import { MasterPasswordServiceAbstraction } from "../master-password/abstractions/master-password.service.abstraction"; -import { firstValueFromOrThrow } from "../utils"; +import { KdfRequest } from "../../models/request/kdf.request"; +import { SdkService } from "../../platform/abstractions/sdk/sdk.service"; +import { + fromSdkAuthenticationData, + MasterPasswordAuthenticationData, + MasterPasswordUnlockData, +} from "../master-password/types/master-password.types"; import { ChangeKdfApiService } from "./change-kdf-api.service.abstraction"; import { ChangeKdfService } from "./change-kdf-service.abstraction"; export class DefaultChangeKdfService implements ChangeKdfService { constructor( - private masterPasswordService: MasterPasswordServiceAbstraction, - private keyService: KeyService, - private kdfConfigService: KdfConfigService, private changeKdfApiService: ChangeKdfApiService, + private sdkService: SdkService, ) {} async updateUserKdfParams(masterPassword: string, kdf: KdfConfig, userId: UserId): Promise { assertNonNullish(masterPassword, "masterPassword"); assertNonNullish(kdf, "kdf"); assertNonNullish(userId, "userId"); + const updateKdfResult = await firstValueFrom( + this.sdkService.userClient$(userId).pipe( + map((sdk) => { + if (!sdk) { + throw new Error("SDK not available"); + } - const userKey = await firstValueFromOrThrow(this.keyService.userKey$(userId), "userKey"); - const salt = await firstValueFromOrThrow( - this.masterPasswordService.saltForUser$(userId), - "salt", - ); - const oldKdfConfig = await firstValueFromOrThrow( - this.kdfConfigService.getKdfConfig$(userId), - "oldKdfConfig", + using ref = sdk.take(); + + const updateKdfResponse = ref.value + .crypto() + .make_update_kdf(masterPassword, kdf.toSdkConfig()); + return updateKdfResponse; + }), + ), ); - const oldAuthenticationData = - await this.masterPasswordService.makeMasterPasswordAuthenticationData( - masterPassword, - oldKdfConfig, - salt, - ); - const authenticationData = - await this.masterPasswordService.makeMasterPasswordAuthenticationData( - masterPassword, - kdf, - salt, - ); - const unlockData = await this.masterPasswordService.makeMasterPasswordUnlockData( - masterPassword, - kdf, - salt, - userKey, + const authenticationData: MasterPasswordAuthenticationData = fromSdkAuthenticationData( + updateKdfResult.masterPasswordAuthenticationData, + ); + const unlockData: MasterPasswordUnlockData = MasterPasswordUnlockData.fromSdk( + updateKdfResult.masterPasswordUnlockData, + ); + const oldAuthenticationData: MasterPasswordAuthenticationData = fromSdkAuthenticationData( + updateKdfResult.oldMasterPasswordAuthenticationData, ); const request = new KdfRequest(authenticationData, unlockData); diff --git a/libs/common/src/key-management/master-password/types/master-password.types.ts b/libs/common/src/key-management/master-password/types/master-password.types.ts index 416b296623f..19c8c49c119 100644 --- a/libs/common/src/key-management/master-password/types/master-password.types.ts +++ b/libs/common/src/key-management/master-password/types/master-password.types.ts @@ -1,8 +1,18 @@ import { Jsonify, Opaque } from "type-fest"; // eslint-disable-next-line no-restricted-imports -import { Argon2KdfConfig, KdfConfig, KdfType, PBKDF2KdfConfig } from "@bitwarden/key-management"; -import { EncString } from "@bitwarden/sdk-internal"; +import { + fromSdkKdfConfig, + Argon2KdfConfig, + KdfConfig, + KdfType, + PBKDF2KdfConfig, +} from "@bitwarden/key-management"; +import { + EncString, + MasterPasswordUnlockData as SdkMasterPasswordUnlockData, + MasterPasswordAuthenticationData as SdkMasterPasswordAuthenticationData, +} from "@bitwarden/sdk-internal"; /** * The Base64-encoded master password authentication hash, that is sent to the server for authentication. @@ -24,6 +34,14 @@ export class MasterPasswordUnlockData { readonly masterKeyWrappedUserKey: MasterKeyWrappedUserKey, ) {} + static fromSdk(sdkData: SdkMasterPasswordUnlockData): MasterPasswordUnlockData { + return new MasterPasswordUnlockData( + sdkData.salt as MasterPasswordSalt, + fromSdkKdfConfig(sdkData.kdf), + sdkData.masterKeyWrappedUserKey as MasterKeyWrappedUserKey, + ); + } + toJSON(): any { return { salt: this.salt, @@ -55,3 +73,14 @@ export type MasterPasswordAuthenticationData = { kdf: KdfConfig; masterPasswordAuthenticationHash: MasterPasswordAuthenticationHash; }; + +export function fromSdkAuthenticationData( + sdkData: SdkMasterPasswordAuthenticationData, +): MasterPasswordAuthenticationData { + return { + salt: sdkData.salt as MasterPasswordSalt, + kdf: fromSdkKdfConfig(sdkData.kdf), + masterPasswordAuthenticationHash: + sdkData.masterPasswordAuthenticationHash as MasterPasswordAuthenticationHash, + }; +} diff --git a/libs/key-management/src/index.ts b/libs/key-management/src/index.ts index d21b79540e0..fb551b92cd6 100644 --- a/libs/key-management/src/index.ts +++ b/libs/key-management/src/index.ts @@ -16,6 +16,7 @@ export { Argon2KdfConfig, KdfConfig, DEFAULT_KDF_CONFIG, + fromSdkKdfConfig, } from "./models/kdf-config"; export { KdfConfigService } from "./abstractions/kdf-config.service"; export { DefaultKdfConfigService } from "./kdf-config.service"; diff --git a/libs/key-management/src/models/kdf-config.ts b/libs/key-management/src/models/kdf-config.ts index a2ed8a22505..3296247710e 100644 --- a/libs/key-management/src/models/kdf-config.ts +++ b/libs/key-management/src/models/kdf-config.ts @@ -145,4 +145,18 @@ export class Argon2KdfConfig { } } +export function fromSdkKdfConfig(sdkKdf: Kdf): KdfConfig { + if ("pBKDF2" in sdkKdf) { + return new PBKDF2KdfConfig(sdkKdf.pBKDF2.iterations); + } else if ("argon2id" in sdkKdf) { + return new Argon2KdfConfig( + sdkKdf.argon2id.iterations, + sdkKdf.argon2id.memory, + sdkKdf.argon2id.parallelism, + ); + } else { + throw new Error("Unsupported KDF type"); + } +} + export const DEFAULT_KDF_CONFIG = new PBKDF2KdfConfig(PBKDF2KdfConfig.ITERATIONS.defaultValue); diff --git a/package-lock.json b/package-lock.json index 2c1ca659d99..4e3707a60e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "@angular/platform-browser": "19.2.14", "@angular/platform-browser-dynamic": "19.2.14", "@angular/router": "19.2.14", - "@bitwarden/sdk-internal": "0.2.0-main.311", + "@bitwarden/sdk-internal": "0.2.0-main.315", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", @@ -4690,9 +4690,9 @@ "link": true }, "node_modules/@bitwarden/sdk-internal": { - "version": "0.2.0-main.311", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.311.tgz", - "integrity": "sha512-zJdQykNMFOyivpNaCB9jc85wZ1ci2HM8/E4hI+yS7FgRm0sRigK5rieF3+xRjiq7pEsZSD8AucR+u/XK9ADXiw==", + "version": "0.2.0-main.315", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.315.tgz", + "integrity": "sha512-hdpFRLrDYSJ6+cNXfMyHdTgg/xIePIlEUSn4JWzwru4PvTcEkkFwGJM3L2LoUqTdNMiDQlr0UjDahopT+C2r0g==", "license": "GPL-3.0", "dependencies": { "type-fest": "^4.41.0" diff --git a/package.json b/package.json index 5f2bb4fdfe6..4acf038b65a 100644 --- a/package.json +++ b/package.json @@ -159,7 +159,7 @@ "@angular/platform-browser": "19.2.14", "@angular/platform-browser-dynamic": "19.2.14", "@angular/router": "19.2.14", - "@bitwarden/sdk-internal": "0.2.0-main.311", + "@bitwarden/sdk-internal": "0.2.0-main.315", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", From 433a2801f6ba7a527ce6f0afea4d7c8cac59fbc9 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 20 Oct 2025 13:15:11 +0200 Subject: [PATCH 03/12] [PM-25174] Disable type 0 decryption based on feature flag (#16865) * Disable type 0 decryption based on feature flag * Add tests * Move init to encrypt service --- .../browser/src/background/main.background.ts | 1 + .../src/popup/services/init.service.ts | 5 ++ .../service-container/service-container.ts | 1 + apps/desktop/src/app/services/init.service.ts | 3 + apps/web/src/app/core/init.service.ts | 3 + libs/common/src/enums/feature-flag.enum.ts | 2 + .../crypto/abstractions/encrypt.service.ts | 8 +++ .../encrypt.service.implementation.ts | 46 +++++++++++++++ .../crypto/services/encrypt.service.spec.ts | 56 +++++++++++++++++++ 9 files changed, 125 insertions(+) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 707fa7b670d..7ba55a45892 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1483,6 +1483,7 @@ export default class MainBackground { await this.sdkLoadService.loadAndInit(); // Only the "true" background should run migrations await this.migrationRunner.run(); + this.encryptService.init(this.configService); // This is here instead of in the InitService b/c we don't plan for // side effects to run in the Browser InitService. diff --git a/apps/browser/src/popup/services/init.service.ts b/apps/browser/src/popup/services/init.service.ts index 1930dbd1d4b..1de1731013d 100644 --- a/apps/browser/src/popup/services/init.service.ts +++ b/apps/browser/src/popup/services/init.service.ts @@ -3,6 +3,8 @@ import { inject, Inject, Injectable } from "@angular/core"; import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; +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 { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -29,6 +31,8 @@ export class InitService { private sdkLoadService: SdkLoadService, private viewCacheService: PopupViewCacheService, private readonly migrationRunner: MigrationRunner, + private configService: ConfigService, + private encryptService: EncryptService, @Inject(DOCUMENT) private document: Document, ) {} @@ -40,6 +44,7 @@ export class InitService { this.twoFactorService.init(); await this.viewCacheService.init(); await this.sizeService.init(); + this.encryptService.init(this.configService); const htmlEl = window.document.documentElement; this.themingService.applyThemeChangesTo(this.document); diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index 3453e0cff70..d10849bae0f 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -984,6 +984,7 @@ export class ServiceContainer { this.containerService.attachToGlobal(global); await this.i18nService.init(); this.twoFactorService.init(); + this.encryptService.init(this.configService); // If a user has a BW_SESSION key stored in their env (not process.env.BW_SESSION), // this should set the user key to unlock the vault on init. diff --git a/apps/desktop/src/app/services/init.service.ts b/apps/desktop/src/app/services/init.service.ts index 6b511ff366d..79c93c1390e 100644 --- a/apps/desktop/src/app/services/init.service.ts +++ b/apps/desktop/src/app/services/init.service.ts @@ -9,6 +9,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { DefaultVaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; @@ -52,6 +53,7 @@ export class InitService { private autofillService: DesktopAutofillService, private autotypeService: DesktopAutotypeService, private sdkLoadService: SdkLoadService, + private configService: ConfigService, @Inject(DOCUMENT) private document: Document, private readonly migrationRunner: MigrationRunner, ) {} @@ -62,6 +64,7 @@ export class InitService { await this.sshAgentService.init(); this.nativeMessagingService.init(); await this.migrationRunner.waitForCompletion(); // Desktop will run migrations in the main process + this.encryptService.init(this.configService); const accounts = await firstValueFrom(this.accountService.accounts$); const setUserKeyInMemoryPromises = []; diff --git a/apps/web/src/app/core/init.service.ts b/apps/web/src/app/core/init.service.ts index a3358ff7253..6b03913ef7a 100644 --- a/apps/web/src/app/core/init.service.ts +++ b/apps/web/src/app/core/init.service.ts @@ -9,6 +9,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { DefaultVaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { IpcService } from "@bitwarden/common/platform/ipc"; @@ -40,6 +41,7 @@ export class InitService { private ipcService: IpcService, private sdkLoadService: SdkLoadService, private taskService: TaskService, + private configService: ConfigService, private readonly migrationRunner: MigrationRunner, @Inject(DOCUMENT) private document: Document, ) {} @@ -48,6 +50,7 @@ export class InitService { return async () => { await this.sdkLoadService.loadAndInit(); await this.migrationRunner.run(); + this.encryptService.init(this.configService); const activeAccount = await firstValueFrom(this.accountService.activeAccount$); if (activeAccount) { diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index f6090c01d2b..0ad63b630f4 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -34,6 +34,7 @@ export enum FeatureFlag { PrivateKeyRegeneration = "pm-12241-private-key-regeneration", EnrollAeadOnKeyRotation = "enroll-aead-on-key-rotation", ForceUpdateKDFSettings = "pm-18021-force-update-kdf-settings", + PM25174_DisableType0Decryption = "pm-25174-disable-type-0-decryption", UnlockWithMasterPasswordUnlockData = "pm-23246-unlock-with-master-password-unlock-data", /* Tools */ @@ -113,6 +114,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.PrivateKeyRegeneration]: FALSE, [FeatureFlag.EnrollAeadOnKeyRotation]: FALSE, [FeatureFlag.ForceUpdateKDFSettings]: FALSE, + [FeatureFlag.PM25174_DisableType0Decryption]: FALSE, [FeatureFlag.UnlockWithMasterPasswordUnlockData]: FALSE, /* Platform */ 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 87af3852116..25e5f949b40 100644 --- a/libs/common/src/key-management/crypto/abstractions/encrypt.service.ts +++ b/libs/common/src/key-management/crypto/abstractions/encrypt.service.ts @@ -1,8 +1,16 @@ +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; + import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { EncString } from "../models/enc-string"; export abstract class EncryptService { + /** + * A temporary init method to make the encrypt service listen to feature-flag changes. + * This will be removed once the feature flag has been rolled out. + */ + abstract init(configService: ConfigService): void; + /** * Encrypts a string to an EncString * @param plainValue - The value to encrypt 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 6daede6be67..132bbc306cb 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 @@ -1,7 +1,9 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { EncryptionType } from "@bitwarden/common/platform/enums"; @@ -13,12 +15,28 @@ import { PureCrypto } from "@bitwarden/sdk-internal"; import { EncryptService } from "../abstractions/encrypt.service"; export class EncryptServiceImplementation implements EncryptService { + private disableType0Decryption = false; + constructor( protected cryptoFunctionService: CryptoFunctionService, protected logService: LogService, protected logMacFailures: boolean, ) {} + init(configService: ConfigService): void { + configService.serverConfig$.subscribe((newConfig) => { + if (newConfig != null) { + this.setDisableType0Decryption( + newConfig.featureStates[FeatureFlag.PM25174_DisableType0Decryption] === true, + ); + } + }); + } + + setDisableType0Decryption(disable: boolean): void { + this.disableType0Decryption = disable; + } + async encryptString(plainValue: string, key: SymmetricCryptoKey): Promise { if (plainValue == null) { this.logService.warning( @@ -42,16 +60,25 @@ export class EncryptServiceImplementation implements EncryptService { } async decryptString(encString: EncString, key: SymmetricCryptoKey): Promise { + if (this.disableType0Decryption && encString.encryptionType === EncryptionType.AesCbc256_B64) { + throw new Error("Decryption of AesCbc256_B64 encrypted data is disabled."); + } await SdkLoadService.Ready; return PureCrypto.symmetric_decrypt_string(encString.encryptedString, key.toEncoded()); } async decryptBytes(encString: EncString, key: SymmetricCryptoKey): Promise { + if (this.disableType0Decryption && encString.encryptionType === EncryptionType.AesCbc256_B64) { + throw new Error("Decryption of AesCbc256_B64 encrypted data is disabled."); + } await SdkLoadService.Ready; return PureCrypto.symmetric_decrypt_bytes(encString.encryptedString, key.toEncoded()); } async decryptFileData(encBuffer: EncArrayBuffer, key: SymmetricCryptoKey): Promise { + if (this.disableType0Decryption && encBuffer.encryptionType === EncryptionType.AesCbc256_B64) { + throw new Error("Decryption of AesCbc256_B64 encrypted data is disabled."); + } await SdkLoadService.Ready; return PureCrypto.symmetric_decrypt_filedata(encBuffer.buffer, key.toEncoded()); } @@ -121,6 +148,13 @@ export class EncryptServiceImplementation implements EncryptService { throw new Error("No wrappingKey provided for unwrapping."); } + if ( + this.disableType0Decryption && + wrappedDecapsulationKey.encryptionType === EncryptionType.AesCbc256_B64 + ) { + throw new Error("Decryption of AesCbc256_B64 encrypted data is disabled."); + } + await SdkLoadService.Ready; return PureCrypto.unwrap_decapsulation_key( wrappedDecapsulationKey.encryptedString, @@ -137,6 +171,12 @@ export class EncryptServiceImplementation implements EncryptService { if (wrappingKey == null) { throw new Error("No wrappingKey provided for unwrapping."); } + if ( + this.disableType0Decryption && + wrappedEncapsulationKey.encryptionType === EncryptionType.AesCbc256_B64 + ) { + throw new Error("Decryption of AesCbc256_B64 encrypted data is disabled."); + } await SdkLoadService.Ready; return PureCrypto.unwrap_encapsulation_key( @@ -154,6 +194,12 @@ export class EncryptServiceImplementation implements EncryptService { if (wrappingKey == null) { throw new Error("No wrappingKey provided for unwrapping."); } + if ( + this.disableType0Decryption && + keyToBeUnwrapped.encryptionType === EncryptionType.AesCbc256_B64 + ) { + throw new Error("Decryption of AesCbc256_B64 encrypted data is disabled."); + } await SdkLoadService.Ready; return new SymmetricCryptoKey( diff --git a/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts b/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts index 0cc7824a918..466f59da7c9 100644 --- a/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts +++ b/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts @@ -171,6 +171,15 @@ describe("EncryptService", () => { key.toEncoded(), ); }); + + it("throws if disableType0Decryption is enabled and type is AesCbc256_B64", async () => { + encryptService.setDisableType0Decryption(true); + const key = new SymmetricCryptoKey(makeStaticByteArray(64)); + const encString = new EncString(EncryptionType.AesCbc256_B64, "encrypted_string"); + await expect(encryptService.decryptString(encString, key)).rejects.toThrow( + "Decryption of AesCbc256_B64 encrypted data is disabled.", + ); + }); }); describe("decryptBytes", () => { @@ -184,6 +193,15 @@ describe("EncryptService", () => { key.toEncoded(), ); }); + + it("throws if disableType0Decryption is enabled and type is AesCbc256_B64", async () => { + encryptService.setDisableType0Decryption(true); + const key = new SymmetricCryptoKey(makeStaticByteArray(64)); + const encString = new EncString(EncryptionType.AesCbc256_B64, "encrypted_bytes"); + await expect(encryptService.decryptBytes(encString, key)).rejects.toThrow( + "Decryption of AesCbc256_B64 encrypted data is disabled.", + ); + }); }); describe("decryptFileData", () => { @@ -197,6 +215,20 @@ describe("EncryptService", () => { key.toEncoded(), ); }); + + it("throws if disableType0Decryption is enabled and type is AesCbc256_B64", async () => { + encryptService.setDisableType0Decryption(true); + const key = new SymmetricCryptoKey(makeStaticByteArray(64)); + const encBuffer = EncArrayBuffer.fromParts( + EncryptionType.AesCbc256_B64, + new Uint8Array(16), + new Uint8Array(32), + null, + ); + await expect(encryptService.decryptFileData(encBuffer, key)).rejects.toThrow( + "Decryption of AesCbc256_B64 encrypted data is disabled.", + ); + }); }); describe("unwrapDecapsulationKey", () => { @@ -210,6 +242,14 @@ describe("EncryptService", () => { key.toEncoded(), ); }); + it("throws if disableType0Decryption is enabled and type is AesCbc256_B64", async () => { + encryptService.setDisableType0Decryption(true); + const key = new SymmetricCryptoKey(makeStaticByteArray(64)); + const encString = new EncString(EncryptionType.AesCbc256_B64, "wrapped_decapsulation_key"); + await expect(encryptService.unwrapDecapsulationKey(encString, key)).rejects.toThrow( + "Decryption of AesCbc256_B64 encrypted data is disabled.", + ); + }); it("throws if wrappedDecapsulationKey is null", () => { const key = new SymmetricCryptoKey(makeStaticByteArray(64)); return expect(encryptService.unwrapDecapsulationKey(null, key)).rejects.toThrow( @@ -235,6 +275,14 @@ describe("EncryptService", () => { key.toEncoded(), ); }); + it("throws if disableType0Decryption is enabled and type is AesCbc256_B64", async () => { + encryptService.setDisableType0Decryption(true); + const key = new SymmetricCryptoKey(makeStaticByteArray(64)); + const encString = new EncString(EncryptionType.AesCbc256_B64, "wrapped_encapsulation_key"); + await expect(encryptService.unwrapEncapsulationKey(encString, key)).rejects.toThrow( + "Decryption of AesCbc256_B64 encrypted data is disabled.", + ); + }); it("throws if wrappedEncapsulationKey is null", () => { const key = new SymmetricCryptoKey(makeStaticByteArray(64)); return expect(encryptService.unwrapEncapsulationKey(null, key)).rejects.toThrow( @@ -260,6 +308,14 @@ describe("EncryptService", () => { key.toEncoded(), ); }); + it("throws if disableType0Decryption is enabled and type is AesCbc256_B64", async () => { + encryptService.setDisableType0Decryption(true); + const key = new SymmetricCryptoKey(makeStaticByteArray(64)); + const encString = new EncString(EncryptionType.AesCbc256_B64, "wrapped_symmetric_key"); + await expect(encryptService.unwrapSymmetricKey(encString, key)).rejects.toThrow( + "Decryption of AesCbc256_B64 encrypted data is disabled.", + ); + }); it("throws if keyToBeUnwrapped is null", () => { const key = new SymmetricCryptoKey(makeStaticByteArray(64)); return expect(encryptService.unwrapSymmetricKey(null, key)).rejects.toThrow( From 281c43f087846d2c285632f3cfe74dbecf4904d3 Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 12:07:17 +0000 Subject: [PATCH 04/12] Autosync the updated translations (#16931) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/az/messages.json | 2 +- apps/desktop/src/locales/pl/messages.json | 16 ++++++++-------- apps/desktop/src/locales/zh_CN/messages.json | 2 +- apps/desktop/src/locales/zh_TW/messages.json | 8 ++++---- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index 8c1e1bd23d6..3bcd3f5f92b 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -2550,7 +2550,7 @@ } }, "vaultCustomTimeoutMinimum": { - "message": "Minimum custom timeout is 1 minute." + "message": "Minimum özəl bitmə vaxtı 1 dəqiqədir." }, "inviteAccepted": { "message": "Dəvət qəbul edildi" diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index 6359bbccb00..4abade3d1c8 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -2550,7 +2550,7 @@ } }, "vaultCustomTimeoutMinimum": { - "message": "Minimum custom timeout is 1 minute." + "message": "Minimalny niestandardowy czas to 1 minuta." }, "inviteAccepted": { "message": "Zaproszenie zostało zaakceptowane" @@ -2667,7 +2667,7 @@ } }, "exportingOrganizationVaultFromPasswordManagerWithDataOwnershipDesc": { - "message": "Only the organization vault associated with $ORGANIZATION$ will be exported.", + "message": "Tylko sejf organizacji $ORGANIZATION$ zostanie wyeksportowany.", "placeholders": { "organization": { "content": "$1", @@ -2676,7 +2676,7 @@ } }, "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { - "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. My items collections will not be included.", + "message": "Tylko sejf organizacji $ORGANIZATION$ zostanie wyeksportowany. Twoje kolekcje nie zostaną uwzględnione.", "placeholders": { "organization": { "content": "$1", @@ -4114,13 +4114,13 @@ "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." }, "typeShortcut": { - "message": "Type shortcut" + "message": "Rodzaj skrótu" }, "editAutotypeShortcutDescription": { "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." }, "invalidShortcut": { - "message": "Invalid shortcut" + "message": "Skrót jest nieprawidłowy" }, "moreBreadcrumbs": { "message": "Więcej nawigacji", @@ -4153,7 +4153,7 @@ "description": "Verb" }, "unArchive": { - "message": "Unarchive" + "message": "Usuń z archiwum" }, "itemsInArchive": { "message": "Elementy w archiwum" @@ -4165,10 +4165,10 @@ "message": "Zarchiwizowane elementy pojawią się tutaj i zostaną wykluczone z wyników wyszukiwania i sugestii autouzupełniania." }, "itemWasSentToArchive": { - "message": "Item was sent to archive" + "message": "Element został przeniesiony do archiwum" }, "itemUnarchived": { - "message": "Item was unarchived" + "message": "Element został usunięty z archiwum" }, "archiveItem": { "message": "Archiwizuj element" diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index 2193e69af24..d3ec23a7994 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -2676,7 +2676,7 @@ } }, "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { - "message": "仅会导出与 $ORGANIZATION$ 关联的组织密码库,不包括我的项目集合。", + "message": "仅会导出与 $ORGANIZATION$ 关联的组织密码库。不包括「我的项目」集合。", "placeholders": { "organization": { "content": "$1", diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index 4a081aadb1a..f22651b960b 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -2550,7 +2550,7 @@ } }, "vaultCustomTimeoutMinimum": { - "message": "Minimum custom timeout is 1 minute." + "message": "自訂逾時時間最小為 1 分鐘。" }, "inviteAccepted": { "message": "邀請已接受" @@ -4153,7 +4153,7 @@ "description": "Verb" }, "unArchive": { - "message": "Unarchive" + "message": "取消封存" }, "itemsInArchive": { "message": "封存中的項目" @@ -4165,10 +4165,10 @@ "message": "封存的項目會顯示在此處,且不會出現在一般搜尋結果或自動填入建議中。" }, "itemWasSentToArchive": { - "message": "Item was sent to archive" + "message": "項目已移至封存" }, "itemUnarchived": { - "message": "Item was unarchived" + "message": "項目取消封存" }, "archiveItem": { "message": "封存項目" From 2cc17698985a5992421eb11c45837b93d850d61d Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 12:07:35 +0000 Subject: [PATCH 05/12] Autosync the updated translations (#16930) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/web/src/locales/az/messages.json | 6 +- apps/web/src/locales/bg/messages.json | 2 +- apps/web/src/locales/de/messages.json | 4 +- apps/web/src/locales/id/messages.json | 302 +++++++++++------------ apps/web/src/locales/pt_BR/messages.json | 8 +- apps/web/src/locales/sv/messages.json | 4 +- apps/web/src/locales/zh_CN/messages.json | 6 +- apps/web/src/locales/zh_TW/messages.json | 36 +-- 8 files changed, 184 insertions(+), 184 deletions(-) diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index 83aacc70815..df5d51b12ec 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -11849,12 +11849,12 @@ "message": "Yüksəltmə prosesiniz emal olunarkən xəta ilə üzləşdik. Lütfən yenidən sınayın." }, "bitwardenFreeplanMessage": { - "message": "You have the Bitwarden Free plan" + "message": "Bitwarden Ödənişsiz planınız var" }, "upgradeCompleteSecurity": { - "message": "Upgrade for complete security" + "message": "Tam təhlükəsizlik üçün yüksəldin" }, "viewbusinessplans": { - "message": "View business plans" + "message": "Biznes planlarına bax" } } diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index ef9d72b4b63..8e9df78e164 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -7150,7 +7150,7 @@ } }, "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { - "message": "Ще бъдат изнесени само записите от трезора свързан с $ORGANIZATION$. Моите колекции няма да бъдат включени.", + "message": "Ще бъдат изнесени само записите от трезора свързан с $ORGANIZATION$. Колекциите от вида „Моите записи“ няма да бъдат включени.", "placeholders": { "organization": { "content": "$1", diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index ae47ed19ae6..b422165fc9e 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -11295,10 +11295,10 @@ "description": "Verb" }, "unArchive": { - "message": "Nicht mehr archivieren" + "message": "Archivieren rück­gän­gig ma­chen" }, "itemsInArchive": { - "message": "Einträge im Archiv" + "message": "Archiveinträge" }, "noItemsInArchive": { "message": "Keine Einträge im Archiv" diff --git a/apps/web/src/locales/id/messages.json b/apps/web/src/locales/id/messages.json index e1f4f314012..9369bd3f564 100644 --- a/apps/web/src/locales/id/messages.json +++ b/apps/web/src/locales/id/messages.json @@ -1499,16 +1499,16 @@ "message": "Setelan" }, "accountEmail": { - "message": "Account email" + "message": "Akun email" }, "requestHint": { - "message": "Request hint" + "message": "Minta petunjuk" }, "requestPasswordHint": { - "message": "Request password hint" + "message": "Minta petunjuk sandi" }, "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": { - "message": "Enter your account email address and your password hint will be sent to you" + "message": "Masukkan alamat email akun dan petunjuk sandi Anda akan dikirimkan kepada Anda" }, "getMasterPasswordHint": { "message": "Dapatkan petunjuk sandi utama" @@ -1526,7 +1526,7 @@ "message": "Pengetikan ulang sandi utama diperlukan." }, "masterPasswordMinlength": { - "message": "Master password must be at least $VALUE$ characters long.", + "message": "Sandi utama minimal harus $VALUE$ karakter.", "description": "The Master Password must be at least a specific number of characters long.", "placeholders": { "value": { @@ -1542,10 +1542,10 @@ "message": "Akun baru Anda telah dibuat! Sekarang Anda bisa masuk." }, "newAccountCreated2": { - "message": "Your new account has been created!" + "message": "Akun baru Anda telah dibuat!" }, "youHaveBeenLoggedIn": { - "message": "You have been logged in!" + "message": "Anda telah masuk!" }, "trialAccountCreated": { "message": "Akun berhasil dibuat." @@ -1557,16 +1557,16 @@ "message": "Terjadi kesalahan yang tak diduga." }, "expirationDateError": { - "message": "Please select an expiration date that is in the future." + "message": "Silakan pilih tanggal kedaluwarsa yang akan datang." }, "emailAddress": { "message": "Alamat email" }, "yourVaultIsLockedV2": { - "message": "Your vault is locked" + "message": "Brankas Anda terkunci" }, "yourAccountIsLocked": { - "message": "Your account is locked" + "message": "Akun Anda terkunci" }, "uuid": { "message": "UUID" @@ -1591,7 +1591,7 @@ "message": "Sandi utama tidak valid" }, "invalidMasterPasswordConfirmEmailAndHost": { - "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "message": "Sandi utama tidak valid. Pastikan email benar dan akun Anda dibuat di $HOST$.", "placeholders": { "host": { "content": "$1", @@ -1609,34 +1609,34 @@ "message": "Tidak ada item yang dapat dicantumkan." }, "noItemsInTrash": { - "message": "No items in trash" + "message": "Tidak ada item di sampah" }, "noItemsInTrashDesc": { - "message": "Items you delete will appear here and be permanently deleted after 30 days" + "message": "Item yang Anda hapus akan muncul di sini dan dihapus secara permanen setelah 30 hari" }, "noItemsInVault": { - "message": "No items in the vault" + "message": "Tidak ada item di brankas" }, "emptyVaultDescription": { - "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + "message": "Brankas ini melindungi lebih dari sekadar sandi Anda. Simpan info login, ID, kartu, dan catatan yang aman di sini." }, "emptyFavorites": { - "message": "You haven't favorited any items" + "message": "Anda belum memfavoritkan item apa pun" }, "emptyFavoritesDesc": { - "message": "Add frequently used items to favorites for quick access." + "message": "Tambahkan item yang sering digunakan ke favorit untuk akses cepat." }, "noSearchResults": { - "message": "No search results returned" + "message": "Tidak ada hasil pencarian yang cocok" }, "clearFiltersOrTryAnother": { - "message": "Clear filters or try another search term" + "message": "Hapus filter atau coba istilah pencarian lainnya" }, "noPermissionToViewAllCollectionItems": { - "message": "You do not have permission to view all items in this collection." + "message": "Anda tidak memiliki izin untuk melihat semua item dalam koleksi ini." }, "youDoNotHavePermissions": { - "message": "You do not have permissions to this collection" + "message": "Anda tidak memiliki izin untuk koleksi ini" }, "noCollectionsInList": { "message": "Tidak ada koleksi yang akan ditampilkan." @@ -1648,7 +1648,7 @@ "message": "Tidak ada pengguna untuk dicantumkan." }, "noMembersInList": { - "message": "There are no members to list." + "message": "Tidak ada anggota untuk ditampilkan." }, "noEventsInList": { "message": "Tidak ada acara untuk dicantumkan." @@ -1660,13 +1660,13 @@ "message": "Anda tidak berada dalam organisasi apapun. Organisasi memungkinkan Anda dengan aman berbagi item dengan pengguna lainnya." }, "notificationSentDevice": { - "message": "A notification has been sent to your device." + "message": "Pemberitahuan telah dikirim ke perangkat Anda." }, "notificationSentDevicePart1": { - "message": "Unlock Bitwarden on your device or on the " + "message": "Buka kunci Bitwarden di perangkat Anda atau di " }, "accessAttemptBy": { - "message": "Access attempt by $EMAIL$", + "message": "Upaya akses oleh $EMAIL$", "placeholders": { "email": { "content": "$1", @@ -1675,22 +1675,22 @@ } }, "confirmAccess": { - "message": "Confirm access" + "message": "Konfirmasi akses" }, "denyAccess": { - "message": "Deny access" + "message": "Tolak akses" }, "notificationSentDeviceAnchor": { - "message": "web app" + "message": "apl web" }, "notificationSentDevicePart2": { - "message": "Make sure the Fingerprint phrase matches the one below before approving." + "message": "Pastikan frasa Sidik Jari cocok dengan yang di bawah ini sebelum menyetujui." }, "notificationSentDeviceComplete": { - "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." + "message": "Buka kunci Bitwarden di perangkat Anda. Pastikan frasa Sidik Jari sesuai dengan frasa di bawah ini sebelum menyetujui." }, "aNotificationWasSentToYourDevice": { - "message": "A notification was sent to your device" + "message": "Pemberitahuan telah dikirim ke perangkat Anda" }, "versionNumber": { "message": "Versi $VERSION_NUMBER$", @@ -1711,14 +1711,14 @@ } }, "dontAskAgainOnThisDeviceFor30Days": { - "message": "Don't ask again on this device for 30 days" + "message": "Jangan tanya lagi di perangkat ini selama 30 hari" }, "selectAnotherMethod": { - "message": "Select another method", + "message": "Pilih metode lain", "description": "Select another two-step login method" }, "useYourRecoveryCode": { - "message": "Use your recovery code" + "message": "Gunakan kode pemulihan Anda" }, "insertU2f": { "message": "Masukkan kunci keamanan ke port USB komputer Anda. Jika ada tombolnya, tekanlah." @@ -1736,29 +1736,29 @@ "message": "Opsi login dua-langkah" }, "selectTwoStepLoginMethod": { - "message": "Select two-step login method" + "message": "Pilih metode login dua langkah" }, "recoveryCodeTitle": { "message": "Kode Pemulihan" }, "invalidRecoveryCode": { - "message": "Invalid recovery code" + "message": "Kode pemulihan tidak valid" }, "authenticatorAppTitle": { "message": "Aplikasi Autentikasi" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "Masukkan kode yang dibuat oleh apl autentikator seperti Bitwarden Authenticator.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP security key" + "message": "Kunci keamanan OTP Yubico" }, "yubiKeyDesc": { "message": "Gunakan YubiKey untuk mengakses akun Anda. Bekerja dengan YubiKey 4, 4 Nano, 4C, dan peranti NEO." }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "Masukkan kode yang dibuat oleh Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1781,13 +1781,13 @@ "message": "(Dipindahkan dari FIDO)" }, "openInNewTab": { - "message": "Open in new tab" + "message": "Buka di tab baru" }, "emailTitle": { "message": "Email" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "Masukkan kode yang dikirim ke email Anda." }, "continue": { "message": "Lanjutkan" @@ -1814,7 +1814,7 @@ } }, "deleteSelectedCollectionsDesc": { - "message": "$COUNT$ collection(s) will be permanently deleted.", + "message": "$COUNT$ koleksi akan dihapus secara permanen.", "placeholders": { "count": { "content": "$1", @@ -1823,10 +1823,10 @@ } }, "deleteSelectedConfirmation": { - "message": "Are you sure you want to continue?" + "message": "Apakah Anda yakin ingin melanjutkan?" }, "moveSelectedItemsDesc": { - "message": "Choose a folder that you would like to add the $COUNT$ selected item(s) to.", + "message": "Pilih folder tempat Anda ingin menambahkan $COUNT$ item yang dipilih.", "placeholders": { "count": { "content": "$1", @@ -1841,10 +1841,10 @@ "message": "Salin Kode Verifikasi" }, "copyUuid": { - "message": "Copy UUID" + "message": "Salin UUID" }, "errorRefreshingAccessToken": { - "message": "Access Token Refresh Error" + "message": "Kesalahan Penyegaran Token Akses" }, "errorRefreshingAccessTokenDesc": { "message": "Token penyegaran atau kunci API tidak ditemukan. Silakan coba keluar dan masuk kembali." @@ -1934,13 +1934,13 @@ "message": "Pembuat sandi" }, "minComplexityScore": { - "message": "Skor Kompleksitas Minimum" + "message": "Skor kompleksitas minimum" }, "minNumbers": { - "message": "Angka Minimum" + "message": "Angka minimum" }, "minSpecial": { - "message": "Karakter Khusus Minimum", + "message": "Minimum karakter spesial", "description": "Minimum special characters" }, "ambiguous": { @@ -1973,10 +1973,10 @@ "message": "Karakter Khusus (!@#$%^&*)" }, "numWords": { - "message": "Jumlah Kata" + "message": "Jumlah kata" }, "wordSeparator": { - "message": "Pemisah Kata" + "message": "Pemisah kata" }, "capitalize": { "message": "Gunakan Huruf Besar", @@ -2072,10 +2072,10 @@ "message": "Sandi utama saat Ini" }, "newMasterPass": { - "message": "Kata Sandi Utama Baru" + "message": "Sandi utama baru" }, "confirmNewMasterPass": { - "message": "Konfirmasi Kata Sandi Utama Baru" + "message": "Konfirmasi sandi utama baru" }, "encKeySettings": { "message": "Pengaturan Kunci Enkripsi" @@ -2087,7 +2087,7 @@ "message": "Iterasi KDF" }, "kdfIterationsDesc": { - "message": "Iterasi KDF yang lebih tinggi dapat membantu melindungi kata sandi utama Anda agar tidak dipaksakan secara brutal oleh penyerang. Kami merekomendasikan nilai $VALUE$ atau lebih.", + "message": "Iterasi KDF yang lebih tinggi dapat membantu melindungi sandi utama Anda dari serangan brute force oleh penyerang. Kami merekomendasikan nilai $VALUE$ atau lebih.", "placeholders": { "value": { "content": "$1", @@ -2496,7 +2496,7 @@ "message": "Penyedia proses masuk dua langkah ini diaktifkan di akun Anda." }, "twoStepLoginAuthDesc": { - "message": "Masukkan kata sandi utama Anda untuk mengubah pengaturan login dua langkah." + "message": "Masukkan sandi utama Anda untuk mengubah pengaturan login dua langkah." }, "twoStepAuthenticatorInstructionPrefix": { "message": "Download an authenticator app such as" @@ -2622,7 +2622,7 @@ "message": "Client Id" }, "twoFactorDuoClientSecret": { - "message": "Client Secret" + "message": "Rahasia Klien" }, "twoFactorDuoApiHostname": { "message": "Nama Host API" @@ -2756,13 +2756,13 @@ "message": "Instruksi" }, "exposedPasswordsReport": { - "message": "Kata Sandi yang Terpapar" + "message": "Sandi yang terekspos" }, "exposedPasswordsReportDesc": { - "message": "Kata yang terpapar di sebuah kebocoran data adalah sasaran empuk untuk para peretas. Ubah kata sandi terkait untuk mencegah potensi terjadinya peretasan." + "message": "Sandi yang terekspos saat terjadi pelanggaran data merupakan target mudah bagi penyerang. Ubah sandi ini untuk mencegah potensi pembobolan." }, "exposedPasswordsFound": { - "message": "Kata Sandi Terkena Ditemukan" + "message": "Sandi yang terekspos ditemukan" }, "exposedPasswordsFoundReportDesc": { "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", @@ -2778,10 +2778,10 @@ } }, "noExposedPasswords": { - "message": "Tidak ada item di lemari besi Anda yang memiliki kata sandi yang telah terungkap dalam pelanggaran data yang diketahui." + "message": "Tidak ada item di brankas Anda yang memiliki sandi yang telah terekspos dalam pelanggaran data yang diketahui." }, "checkExposedPasswords": { - "message": "Periksa Kata Sandi yang Terpapar" + "message": "Periksa sandi yang terekspos" }, "timesExposed": { "message": "Times exposed" @@ -2796,16 +2796,16 @@ } }, "weakPasswordsReport": { - "message": "Laporan Kata Sandi Lemah" + "message": "Sandi lemah" }, "weakPasswordsReportDesc": { - "message": "Kata sandi yang lemah dapat dengan mudah ditebak oleh peretas dan alat otomatis yang digunakan untuk memecahkan kata sandi. Pembuat kata sandi Bitwarden dapat membantu Anda membuat kata sandi yang kuat." + "message": "Sandi yang lemah dapat dengan mudah ditebak oleh penyerang. Ubah sandi tersebut menjadi sandi yang kuat menggunakan generator sandi." }, "weakPasswordsFound": { - "message": "Kata Sandi Lemah Ditemukan" + "message": "Ditemukan sandi yang lemah" }, "weakPasswordsFoundReportDesc": { - "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", + "message": "Kami menemukan $COUNT$ item di $VAULT$ Anda dengan sandi lemah. Anda harus memperbaruinya agar menggunakan sandi yang lebih kuat.", "placeholders": { "count": { "content": "$1", @@ -2818,22 +2818,22 @@ } }, "noWeakPasswords": { - "message": "Tidak ada item di lemari besi Anda yang memiliki kata sandi yang lemah." + "message": "Tidak ada item di brankas Anda yang memiliki sandi lemah." }, "weakness": { - "message": "Weakness" + "message": "Kelemahan" }, "reusedPasswordsReport": { - "message": "Laporan Kata Sandi yang Digunakan Kembali" + "message": "Sandi yang digunakan berulang" }, "reusedPasswordsReportDesc": { - "message": "Jika layanan yang Anda gunakan disusupi, menggunakan kembali kata sandi yang sama di tempat lain dapat memungkinkan peretas untuk dengan mudah mendapatkan akses ke lebih banyak akun online Anda. Anda harus menggunakan kata sandi unik untuk setiap akun atau layanan." + "message": "Menggunakan sandi berulang akan memudahkan penyerang membobol banyak akun. Ubah sandi ini agar masing-masing unik." }, "reusedPasswordsFound": { - "message": "Kata Sandi yang Digunakan Kembali Ditemukan" + "message": "Sandi yang digunakan berulang ditemukan" }, "reusedPasswordsFoundReportDesc": { - "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", + "message": "Kami menemukan $COUNT$ sandi yang digunakan kembali di $VAULT$ Anda. Anda harus mengubahnya ke sandi yang unik.", "placeholders": { "count": { "content": "$1", @@ -2849,7 +2849,7 @@ "message": "Tidak ada login di vault Anda yang memiliki sandi yang sedang digunakan kembali." }, "timesReused": { - "message": "Times reused" + "message": "Digunakan berulang" }, "reusedXTimes": { "message": "Digunakan kembali $COUNT$ kali", @@ -2951,7 +2951,7 @@ "message": "Harap pastikan bahwa akun Anda memiliki cukup kredit yang tersedia untuk pembelian ini. Jika akun Anda tidak memiliki cukup kredit yang tersedia, metode pembayaran default Anda yang tercatat akan digunakan untuk selisihnya. Anda dapat menambahkan kredit ke akun Anda dari halaman Penagihan." }, "notEnoughAccountCredit": { - "message": "You do not have enough account credit for this purchase. You can add credit to your account from the Billing page." + "message": "Anda tidak memiliki cukup kredit akun untuk pembelian ini. Anda dapat menambahkan kredit ke akun Anda dari halaman Penagihan." }, "creditAppliedDesc": { "message": "Saldo akun Anda dapat digunakan untuk melakukan pembelian. Semua saldo yang tersedia akan secara otomatis ditagihkan ke faktur yang dibuat untuk akun ini." @@ -2970,13 +2970,13 @@ "message": "Penyimpanan terenkripsi 1 GB untuk lampiran file." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Opsi login dua langkah eksklusif seperti YubiKey dan Duo." }, "premiumSignUpEmergency": { "message": "Akses darurat" }, "premiumSignUpReports": { - "message": "Kebersihan kata sandi, kesehatan akun, dan laporan pelanggaran data untuk menjaga brankas Anda tetap aman." + "message": "Kebersihan sandi, kesehatan akun, dan laporan pelanggaran data untuk menjaga brankas Anda tetap aman." }, "premiumSignUpTotp": { "message": "Generator kode verifikasi TOTP (2FA) untuk login di lemari besi Anda." @@ -2997,7 +2997,7 @@ } }, "premiumPriceWithFamilyPlan": { - "message": "Go premium for just $PRICE$ /year, or get premium accounts for $FAMILYPLANUSERCOUNT$ users and unlimited family sharing with a ", + "message": "Dapatkan premium hanya $PRICE$ /tahun, atau dapatkan akun premium untuk pengguna $FAMILYPLANUSERCOUNT$ dan berbagi keluarga tanpa batas dengan ", "placeholders": { "price": { "content": "$1", @@ -3010,7 +3010,7 @@ } }, "bitwardenFamiliesPlan": { - "message": "Bitwarden Families plan." + "message": "Paket keluarga Bitwarden." }, "addons": { "message": "Pengaya" @@ -3055,10 +3055,10 @@ } }, "showPricingSummary": { - "message": "Show pricing summary" + "message": "Tampilkan ringkasan harga" }, "hidePricingSummary": { - "message": "Hide pricing summary" + "message": "Sembunyikan ringkasan harga" }, "summary": { "message": "Ringkasan" @@ -3092,7 +3092,7 @@ } }, "paymentChargedWithUnpaidSubscription": { - "message": "Your payment method will be charged for any unpaid subscriptions." + "message": "Metode pembayaran Anda akan dikenai biaya untuk setiap langganan yang belum dibayar." }, "paymentChargedWithTrial": { "message": "Paket Anda dilengkapi dengan uji coba gratis selama 7 hari. Metode pembayaran Anda tidak akan ditagih hingga uji coba berakhir. Penagihan akan dilakukan secara berulang setiap $INTERVAL$. Anda dapat membatalkannya kapan saja." @@ -3116,7 +3116,7 @@ "message": "Batalkan Langganan" }, "subscriptionExpiration": { - "message": "Subscription expiration" + "message": "Kedaluwarsa langganan" }, "subscriptionCanceled": { "message": "Langganan telah dibatalkan." @@ -3158,7 +3158,7 @@ "message": "Unduh Lisensi" }, "viewBillingToken": { - "message": "View Billing Token" + "message": "Lihat Token Penagihan" }, "updateLicense": { "message": "Perbarui Lisensi" @@ -3167,7 +3167,7 @@ "message": "Kelola Langganan" }, "launchCloudSubscription": { - "message": "Launch Cloud Subscription" + "message": "Luncurkan Langganan Cloud" }, "storage": { "message": "Penyimpanan" @@ -3207,10 +3207,10 @@ "message": "Faktur" }, "noUnpaidInvoices": { - "message": "No unpaid invoices." + "message": "Tidak ada faktur yang belum dibayar." }, "noPaidInvoices": { - "message": "No paid invoices." + "message": "Tidak ada faktur yang dibayar." }, "paid": { "message": "Dibayar", @@ -3269,7 +3269,7 @@ "message": "Hubungi Dukungan Pelanggan" }, "contactSupportShort": { - "message": "Contact Support" + "message": "Hubungi Dukungan" }, "updatedPaymentMethod": { "message": "Metode pembayaran yang diperbarui." @@ -3487,7 +3487,7 @@ } }, "trialSecretsManagerThankYou": { - "message": "Thanks for signing up for Bitwarden Secrets Manager for $PLAN$!", + "message": "Terima kasih telah mendaftar di Bitwarden Secrets Manager untuk $PLAN$!", "placeholders": { "plan": { "content": "$1", @@ -3580,7 +3580,7 @@ "message": "Apakah Anda yakin ingin menghapus grup ini?" }, "deleteMultipleGroupsConfirmation": { - "message": "Are you sure you want to delete the following $QUANTITY$ group(s)?", + "message": "Apakah Anda yakin ingin menghapus $QUANTITY$ grup berikut?", "placeholders": { "quantity": { "content": "$1", @@ -3607,10 +3607,10 @@ "message": "Id eksternal dapat digunakan sebagai referensi atau untuk menautkan sumber daya ini ke sistem eksternal seperti direktori pengguna." }, "ssoExternalId": { - "message": "SSO External ID" + "message": "Eksternal SSO ID" }, "ssoExternalIdDesc": { - "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + "message": "ID Eksternal SSO adalah referensi tidak terenkripsi antara Bitwarden dan penyedia SSO yang Anda konfigurasikan." }, "nestCollectionUnder": { "message": "Nest collection under" @@ -3751,13 +3751,13 @@ "message": "Bitwarden Web vault" }, "bitSecretsManager": { - "message": "Bitwarden Secrets Manager" + "message": "Manajer Rahasia Bitwarden" }, "loggedIn": { "message": "Sudah masuk." }, "changedPassword": { - "message": "Kata sandi akun diubah." + "message": "Sandi akun telah diubah" }, "enabledUpdated2fa": { "message": "Proses masuk dua langkah diaktifkan / diperbarui." @@ -3848,7 +3848,7 @@ } }, "viewedPasswordItemId": { - "message": "Melihat kata sandi untuk item $ID$.", + "message": "Melihat sandi untuk item $ID$.", "placeholders": { "id": { "content": "$1", @@ -4371,13 +4371,13 @@ "message": "You have a pending login request from another device." }, "reviewLoginRequest": { - "message": "Review login request" + "message": "Tinjau permintaan masuk" }, "loginRequest": { - "message": "Login request" + "message": "Permintaan masuk" }, "freeTrialEndPromptCount": { - "message": "Your free trial ends in $COUNT$ days.", + "message": "Uji coba gratis Anda berakhir dalam $COUNT$ hari.", "placeholders": { "count": { "content": "$1", @@ -4386,7 +4386,7 @@ } }, "freeTrialEndPromptMultipleDays": { - "message": "$ORGANIZATION$, your free trial ends in $COUNT$ days.", + "message": "$ORGANIZATION$, uji coba gratis Anda berakhir dalam $COUNT$ hari.", "placeholders": { "count": { "content": "$2", @@ -4399,7 +4399,7 @@ } }, "freeTrialEndPromptTomorrow": { - "message": "$ORGANIZATION$, your free trial ends tomorrow.", + "message": "$ORGANIZATION$, uji coba gratis Anda berakhir besok.", "placeholders": { "organization": { "content": "$1", @@ -4408,10 +4408,10 @@ } }, "freeTrialEndPromptTomorrowNoOrgName": { - "message": "Your free trial ends tomorrow." + "message": "Uji coba gratis Anda berakhir besok." }, "freeTrialEndPromptToday": { - "message": "$ORGANIZATION$, your free trial ends today.", + "message": "$ORGANIZATION$, uji coba gratis Anda berakhir hari ini.", "placeholders": { "organization": { "content": "$1", @@ -4420,16 +4420,16 @@ } }, "freeTrialEndingTodayWithoutOrgName": { - "message": "Your free trial ends today." + "message": "Uji coba gratis Anda berakhir hari ini." }, "clickHereToAddPaymentMethod": { - "message": "Click here to add a payment method." + "message": "Klik di sini untuk menambahkan metode pembayaran." }, "joinOrganization": { "message": "Bergabunglah dengan Organisasi" }, "joinOrganizationName": { - "message": "Join $ORGANIZATIONNAME$", + "message": "Gabung $ORGANIZATIONNAME$", "placeholders": { "organizationName": { "content": "$1", @@ -4441,7 +4441,7 @@ "message": "Anda telah diundang untuk bergabung dengan organisasi yang tercantum di atas. Untuk menerima undangan, Anda harus masuk atau membuat akun Bitwarden baru." }, "finishJoiningThisOrganizationBySettingAMasterPassword": { - "message": "Finish joining this organization by setting a master password." + "message": "Selesaikan bergabung dengan organisasi ini dengan menetapkan sandi utama." }, "inviteAccepted": { "message": "Undangan Diterima" @@ -4450,7 +4450,7 @@ "message": "Anda dapat mengakses organisasi ini setelah administrator mengonfirmasi keanggotaan Anda. Kami akan mengirimi Anda email jika itu terjadi." }, "inviteInitAcceptedDesc": { - "message": "You can now access this organization." + "message": "Anda sekarang dapat mengakses organisasi ini." }, "inviteAcceptFailed": { "message": "Tidak dapat menerima undangan. Minta admin organisasi untuk mengirim undangan baru." @@ -4471,7 +4471,7 @@ "message": "Jika Anda tidak dapat mengakses akun Anda melalui metode masuk dua langkah biasa, Anda dapat menggunakan kode pemulihan masuk dua langkah untuk menonaktifkan semua penyedia dua langkah di akun Anda." }, "logInBelowUsingYourSingleUseRecoveryCode": { - "message": "Log in below using your single-use recovery code. This will turn off all two-step providers on your account." + "message": "Masuk di bawah ini menggunakan kode pemulihan sekali pakai. Ini akan menonaktifkan semua penyedia dua langkah di akun Anda." }, "recoverAccountTwoStep": { "message": "Pulihkan Akun Dua Langkah Masuk" @@ -4492,7 +4492,7 @@ "message": "Anda telah meminta untuk menghapus akun Bitwarden Anda. Klik tombol di bawah untuk konfirmasi." }, "deleteRecoverOrgConfirmDesc": { - "message": "You have requested to delete your Bitwarden organization." + "message": "Anda telah meminta untuk menghapus organisasi Bitwarden Anda." }, "myOrganization": { "message": "Organisasi Saya" @@ -4504,7 +4504,7 @@ "message": "Hapus Organisasi" }, "deletingOrganizationContentWarning": { - "message": "Masukan kata sandi utama untuk mengonfirmasi penghapusan $ORGANIZATION$ serta semua data yang terkait. Data brankas di $ORGANIZATION$ termasuk:", + "message": "Masukkan sandi utama untuk mengonfirmasi penghapusan $ORGANIZATION$ serta semua data yang terkait. Data brankas di $ORGANIZATION$ termasuk:", "placeholders": { "organization": { "content": "$1", @@ -4621,7 +4621,7 @@ "message": "Tetapkan jumlah kuota untuk langganan Anda. Begitu batas ini tercapai, Anda tidak akan bisa mengundang pengguna baru." }, "limitSmSubscriptionDesc": { - "message": "Set a seat limit for your Secrets Manager subscription. Once this limit is reached, you will not be able to invite new members." + "message": "Setel batas kuota untuk langganan Manajer Rahasia Anda. Setelah batas ini tercapai, Anda tidak akan dapat mengundang anggota baru." }, "maxSeatLimit": { "message": "Batas Kuota (opsional)", @@ -4660,7 +4660,7 @@ "message": "Langganan diperbarui" }, "subscribedToSecretsManager": { - "message": "Subscription updated. You now have access to Secrets Manager." + "message": "Langganan diperbarui. Anda sekarang memiliki akses ke Manajer Rahasia." }, "additionalOptions": { "message": "Opsi Tambahan" @@ -4672,7 +4672,7 @@ "message": "Penyesuaian pada paket langganan Anda akan mengakibatkan perubahan prorata pada total tagihan Anda. Jika anggota yang baru diundang melampaui kuota langganan Anda, Anda akan langsung dikenakan biaya prorata untuk pengguna tambahan tersebut." }, "smStandaloneTrialSeatCountUpdateMessageFragment1": { - "message": "If you want to add additional" + "message": "Jika Anda ingin menambahkan tambahan" }, "smStandaloneTrialSeatCountUpdateMessageFragment2": { "message": "seats without the bundled offer, please contact" @@ -4924,7 +4924,7 @@ "description": "ex. Date this item was created" }, "datePasswordUpdated": { - "message": "Kata Sandi Diperbarui", + "message": "Sandi diperbarui", "description": "ex. Date this password was updated" }, "organizationIsDisabled": { @@ -4940,7 +4940,7 @@ "message": "Suspended organizations cannot be accessed. Please contact your organization owner for assistance." }, "secretsCannotCreate": { - "message": "Secrets cannot be created in suspended organizations. Please contact your organization owner for assistance." + "message": "Rahasia tidak dapat dibuat dalam organisasi yang ditangguhkan. Silakan hubungi pemilik organisasi Anda untuk mendapatkan bantuan." }, "projectsCannotCreate": { "message": "Projects cannot be created in suspended organizations. Please contact your organization owner for assistance." @@ -4983,10 +4983,10 @@ "description": "ex. A very weak password. Scale: Very Weak -> Weak -> Good -> Strong" }, "weakMasterPassword": { - "message": "Kata Sandi Utama Lemah" + "message": "Sandi utama lemah" }, "weakMasterPasswordDesc": { - "message": "Kata sandi utama yang Anda pilih lemah. Anda harus menggunakan kata sandi utama yang kuat (atau frasa sandi) untuk melindungi akun Bitwarden Anda dengan benar. Apakah Anda yakin ingin menggunakan kata sandi utama ini?" + "message": "Sandi lemah teridentifikasi. Gunakan sandi yang kuat untuk melindungi akun Anda. Apakah Anda yakin ingin menggunakan sandi yang lemah?" }, "rotateAccountEncKey": { "message": "Juga rotasikan kunci enkripsi akun saya" @@ -5070,16 +5070,16 @@ "message": "Kami tidak dapat menagih kartu Anda. Harap lihat dan bayar faktur yang belum dibayar yang tercantum di bawah." }, "minLength": { - "message": "Panjang Minimum" + "message": "Panjang minimum" }, "clone": { "message": "Klon" }, "masterPassPolicyTitle": { - "message": "Kata sandi utama diperlukan" + "message": "Persyaratan sandi utama" }, "masterPassPolicyDesc": { - "message": "Tetapkan persyaratan minimum untuk kekuatan kata sandi utama." + "message": "Tetapkan persyaratan untuk kekuatan sandi utama." }, "passwordStrengthScore": { "message": "Password strength score $SCORE$", @@ -5103,10 +5103,10 @@ "message": "Anda adalah anggota organisasi yang memerlukan login dua langkah untuk diaktifkan di akun pengguna Anda. Jika Anda menonaktifkan semua penyedia proses masuk dua langkah, Anda akan secara otomatis dihapus dari organisasi ini." }, "passwordGeneratorPolicyDesc": { - "message": "Tetapkan persyaratan minimum untuk konfigurasi pembuat kata sandi." + "message": "Tetapkan persyaratan untuk pembuat sandi." }, "masterPasswordPolicyInEffect": { - "message": "Satu atau lebih kebijakan organisasi memerlukan kata sandi utama Anda untuk memenuhi persyaratan berikut:" + "message": "Satu atau lebih kebijakan organisasi memerlukan sandi utama Anda untuk memenuhi persyaratan berikut:" }, "policyInEffectMinComplexity": { "message": "Skor kompleksitas minimum $SCORE$", @@ -5145,10 +5145,10 @@ } }, "masterPasswordPolicyRequirementsNotMet": { - "message": "Kata sandi utama Anda yang baru tidak memenuhi persyaratan kebijakan." + "message": "Sandi utama Anda yang baru tidak memenuhi persyaratan kebijakan." }, "minimumNumberOfWords": { - "message": "Jumlah Kata Minimum" + "message": "Jumlah kata minimum" }, "overridePasswordTypePolicy": { "message": "Password Type", @@ -5161,7 +5161,7 @@ "message": "Tindakan ketika Batas Waktu Brankas Habis" }, "vaultTimeoutActionLockDesc": { - "message": "Kubah yang terkunci mengharuskan Anda memasukkan kembali kata sandi utama Anda untuk mengaksesnya lagi." + "message": "Sandi utama atau metode pembukaan kunci lainnya diperlukan untuk mengakses brankas Anda lagi." }, "vaultTimeoutActionLogOutDesc": { "message": "Vault keluar mengharuskan Anda mengautentikasi ulang untuk mengaksesnya lagi." @@ -5241,7 +5241,7 @@ "message": "Konfirmasi Tindakan Timeout" }, "hidePasswords": { - "message": "Sembunyikan Kata Sandi" + "message": "Sembunyikan sandi" }, "countryPostalCodeRequiredDesc": { "message": "Kami memerlukan informasi ini untuk menghitung pajak penjualan dan pelaporan keuangan saja." @@ -5256,7 +5256,7 @@ "message": "Informasi pajak diperbarui." }, "setMasterPassword": { - "message": "Atur Kata Sandi Utama" + "message": "Atur sandi utama" }, "identifier": { "message": "Pengenal" @@ -5468,13 +5468,13 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "removePassword": { - "message": "Hapus Kata Sandi" + "message": "Hapus sandi" }, "removedPassword": { - "message": "Kata Sandi yang Dihapus" + "message": "Sandi dihapus" }, "removePasswordConfirmation": { - "message": "Anda yakin ingin menghapus kata sandi?" + "message": "Anda yakin ingin menghapus sandi?" }, "allSends": { "message": "Semua Dikirim" @@ -5497,11 +5497,11 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendProtectedPassword": { - "message": "Pengiriman ini dilindungi dengan kata sandi. Silakan ketikkan kata sandi di bawah ini untuk melanjutkan.", + "message": "Kirim ini dilindungi dengan sandi. Silakan ketik sandi di bawah ini untuk melanjutkan.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendProtectedPasswordDontKnow": { - "message": "Tidak tahu kata sandinya? Minta sandi yang dibutuhkan Pengirim untuk mengakses Kirim ini.", + "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": { @@ -5577,7 +5577,7 @@ "message": "Pengambilalihan" }, "takeoverDesc": { - "message": "Dapat mengatur ulang akun Anda dengan kata sandi utama baru." + "message": "Dapat mengatur ulang akun Anda dengan sandi utama baru." }, "waitTime": { "message": "Waktu Tunggu" @@ -5668,7 +5668,7 @@ "message": "Grantor details not found" }, "passwordResetFor": { - "message": "Setel ulang sandi untuk $USER$. Sekarang Anda dapat masuk menggunakan kata sandi baru.", + "message": "Atur ulang sandi untuk $USER$. Sekarang Anda dapat masuk menggunakan sandi baru.", "placeholders": { "user": { "content": "$1", @@ -5893,10 +5893,10 @@ "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more, see how it works, **or** try it now.'" }, "developmentDevOpsAndITTeamsChooseBWSecret": { - "message": "Development, DevOps, and IT teams choose Bitwarden Secrets Manager to securely manage and deploy their infrastructure and machine secrets." + "message": "Tim pengembangan, DevOps, dan TI memilih Bitwarden Manajer Rahasia untuk mengelola dan menerapkan infrastruktur dan rahasia mesin mereka secara aman." }, "centralizeSecretsManagement": { - "message": "Centralize secrets management." + "message": "Sentralisasikan pengelolaan rahasia." }, "centralizeSecretsManagementDescription": { "message": "Securely store and manage secrets in one location to prevent secret sprawl across your organization." @@ -6034,7 +6034,7 @@ "message": "Your new password cannot be the same as your current password." }, "hintEqualsPassword": { - "message": "Petunjuk kata sandi Anda tidak boleh sama dengan kata sandi Anda." + "message": "Petunjuk sandi Anda tidak boleh sama dengan sandi Anda." }, "enrollAccountRecovery": { "message": "Enroll in account recovery" @@ -6070,7 +6070,7 @@ } }, "eventAdminPasswordReset": { - "message": "Kata sandi utama sudah diubah untuk pengguna $ID$.", + "message": "Sandi utama diatur ulang untuk pengguna $ID$.", "placeholders": { "id": { "content": "$1", @@ -6097,7 +6097,7 @@ } }, "resetPassword": { - "message": "Atur Ulang Kata Sandi" + "message": "Atur ulang sandi" }, "resetPasswordLoggedOutWarning": { "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", @@ -6121,16 +6121,16 @@ "message": "pengguna ini" }, "resetPasswordMasterPasswordPolicyInEffect": { - "message": "Satu atau lebih kebijakan organisasi memerlukan kata sandi utama Anda untuk memenuhi persyaratan berikut:" + "message": "Satu atau lebih kebijakan organisasi memerlukan sandi utama Anda untuk memenuhi persyaratan berikut:" }, "changePasswordDelegationMasterPasswordPolicyInEffect": { "message": "One or more organization policies require the master password to meet the following requirements:" }, "resetPasswordSuccess": { - "message": "Ubah kata kunci berhasil!" + "message": "Atur ulang kata sandi berhasil!" }, "resetPasswordEnrollmentWarning": { - "message": "Dengan mengikuti fitur ini mengijinkan admin organisasi untuk merubah kata sandi utama anda. Anda yakin mau masuk fitur ini?" + "message": "Pendaftaran akan memungkinkan administrator organisasi untuk mengubah sandi utama Anda" }, "accountRecoveryPolicy": { "message": "Account recovery administration" @@ -6395,10 +6395,10 @@ "message": "Master password successfully set" }, "updatedMasterPassword": { - "message": "Kata Sandi Telah Diperbarui" + "message": "Sandi utama disimpan" }, "updateMasterPassword": { - "message": "Perbarui Kata Sandi Utama" + "message": "Perbarui sandi utama" }, "accountRecoveryUpdateMasterPasswordSubtitle": { "message": "Change your master password to complete account recovery." @@ -6504,7 +6504,7 @@ "message": "Your vault timeout exceeds the restriction set by your organization." }, "vaultCustomTimeoutMinimum": { - "message": "Minimum custom timeout is 1 minute." + "message": "Batas waktu kustom minimum adalah 1 menit." }, "vaultTimeoutRangeError": { "message": "Vault timeout is not within allowed range." @@ -6609,7 +6609,7 @@ "message": "Signing behavior" }, "spMinIncomingSigningAlgorithm": { - "message": "Minimum incoming signing algorithm" + "message": "Algoritma persetujuan masuk minimum" }, "spWantAssertionsSigned": { "message": "Expect signed assertions" @@ -6840,7 +6840,7 @@ "message": "Tinggalkan Organisasi" }, "removeMasterPassword": { - "message": "Hapus Kata Sandi Utama" + "message": "Hapus sandi utama" }, "removedMasterPassword": { "message": "Sandi utama dihapus" @@ -7162,7 +7162,7 @@ "message": "Akses ditolak. Anda tidak mempunyai izin untuk melihat halaman ini." }, "masterPassword": { - "message": "Kata Sandi Utama" + "message": "Sandi utama" }, "security": { "message": "Keamanan" @@ -7206,7 +7206,7 @@ "message": "Generate email" }, "generatePassword": { - "message": "Buat Kata Sandi" + "message": "Buat sandi" }, "generatePassphrase": { "message": "Buat frasa sandi" @@ -7224,7 +7224,7 @@ "message": "Email generated" }, "spinboxBoundariesHint": { - "message": "Value must be between $MIN$ and $MAX$.", + "message": "Nilai harus ada di antara $MIN$ dan $MAX$.", "description": "Explains spin box minimum and maximum values to the user", "placeholders": { "min": { @@ -8566,7 +8566,7 @@ "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login, if any. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, "secretsManager": { - "message": "Secrets Manager" + "message": "Manajer Rahasia" }, "secretsManagerAccessDescription": { "message": "Activate user access to Secrets Manager." @@ -8898,7 +8898,7 @@ "message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?" }, "characterMinimum": { - "message": "$LENGTH$ character minimum", + "message": "Minimum karakter $LENGTH$", "placeholders": { "length": { "content": "$1", @@ -8907,7 +8907,7 @@ } }, "masterPasswordMinimumlength": { - "message": "Master password must be at least $LENGTH$ characters long.", + "message": "Sandi utama minimal harus $LENGTH$ karakter.", "placeholders": { "length": { "content": "$1", diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index ba9ad6fa8cf..e87403000ae 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -91,7 +91,7 @@ "message": "Atribuir tarefas a membros para monitorar progresso" }, "onceYouReviewApps": { - "message": "Once you review applications and mark them as critical, you can assign tasks to members to resolve at-risk items and monitor progress here" + "message": "Ao revisar aplicativos e marcá-los como críticos, você pode atribuir traferas a membros para resolver itens em risco e monitorar o progresso aqui" }, "sendReminders": { "message": "Enviar lembretes" @@ -5953,7 +5953,7 @@ "message": "visualize e selecione os membros que você deseja dar acesso ao Gerenciador de Segredos." }, "openYourOrganizations": { - "message": "Abrir o da sua organização" + "message": "Abra o da sua organização" }, "usingTheMenuSelect": { "message": "Usando o menu, selecione" @@ -5970,7 +5970,7 @@ "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send **or** sign up to try it today.'" }, "sendAccessTaglineSignUp": { - "message": "registre-se", + "message": "cadastre-se", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or **sign up** to try it today.'" }, "sendAccessTaglineTryToday": { @@ -5987,7 +5987,7 @@ } }, "viewSend": { - "message": "Visualizar Send", + "message": "Ver Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "viewSendHiddenEmailWarning": { diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index 71fd1245b9d..f94aa1dfc66 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -11852,9 +11852,9 @@ "message": "You have the Bitwarden Free plan" }, "upgradeCompleteSecurity": { - "message": "Upgrade for complete security" + "message": "Uppgradera för fullständig säkerhet" }, "viewbusinessplans": { - "message": "View business plans" + "message": "Visa prismodeller" } } diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index 14a668922ec..c4e94246fb2 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -6528,7 +6528,7 @@ "message": "为所有现有成员和新成员激活浏览器扩展上的页面加载时的自动填充设置。" }, "experimentalFeature": { - "message": "被入侵或不受信任的网站可以利用页面加载时的自动填充功能。" + "message": "被入侵或不受信任的网站可能恶意利用页面加载时的自动填充功能。" }, "learnMoreAboutAutofill": { "message": "进一步了解自动填充" @@ -6711,7 +6711,7 @@ "message": "链接已失效。请让赞助方重新发送邀请。" }, "reclaimedFreePlan": { - "message": "收回了免费方案" + "message": "重新申领了免费方案" }, "redeem": { "message": "兑换" @@ -7150,7 +7150,7 @@ } }, "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { - "message": "仅会导出与 $ORGANIZATION$ 关联的组织密码库,不包括我的项目集合。", + "message": "仅会导出与 $ORGANIZATION$ 关联的组织密码库。不包括「我的项目」集合。", "placeholders": { "organization": { "content": "$1", diff --git a/apps/web/src/locales/zh_TW/messages.json b/apps/web/src/locales/zh_TW/messages.json index 16d5a3c4b67..c5348401a24 100644 --- a/apps/web/src/locales/zh_TW/messages.json +++ b/apps/web/src/locales/zh_TW/messages.json @@ -11295,45 +11295,45 @@ "description": "Verb" }, "unArchive": { - "message": "Unarchive" + "message": "取消封存" }, "itemsInArchive": { - "message": "Items in archive" + "message": "封存中的項目" }, "noItemsInArchive": { "message": "封存中沒有項目" }, "noItemsInArchiveDesc": { - "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + "message": "封存的項目會顯示在此處,且不會出現在一般搜尋結果或自動填入建議中。" }, "itemWasSentToArchive": { - "message": "Item was sent to archive" + "message": "項目已移至封存" }, "itemsWereSentToArchive": { - "message": "Items were sent to archive" + "message": "項目已移至封存" }, "itemUnarchived": { - "message": "Item was unarchived" + "message": "項目取消封存" }, "bulkArchiveItems": { - "message": "Items archived" + "message": "項目已封存" }, "bulkUnarchiveItems": { - "message": "Items unarchived" + "message": "項目已取消封存" }, "archiveItem": { - "message": "Archive item", + "message": "封存項目", "description": "Verb" }, "archiveItemConfirmDesc": { - "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + "message": "封存的項目將不會出現在一般搜尋結果或自動填入建議中。確定要封存此項目嗎?" }, "archiveBulkItems": { - "message": "Archive items", + "message": "封存項目", "description": "Verb" }, "archiveBulkItemsConfirmDesc": { - "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive these items?" + "message": "封存的項目將不會出現在一般搜尋結果或自動填入建議中。確定要封存此項目嗎?" }, "businessUnit": { "message": "營業單位" @@ -11837,24 +11837,24 @@ "message": "不更新繼續" }, "upgradeYourPlan": { - "message": "Upgrade your plan" + "message": "升級你的方案" }, "upgradeNow": { - "message": "Upgrade now" + "message": "立即升級" }, "formWillCreateNewFamiliesOrgMessage": { - "message": "Completing this form will create a new Families organization. You can upgrade your Free organization from the Admin Console." + "message": "填寫此表單將建立新的家庭組織。你可以從管理主控台升級你的免費組織。" }, "upgradeErrorMessage": { "message": "處理你的升級時發生錯誤。請再試一次。" }, "bitwardenFreeplanMessage": { - "message": "You have the Bitwarden Free plan" + "message": "你目前使用 Bitwarden 免費方案" }, "upgradeCompleteSecurity": { - "message": "Upgrade for complete security" + "message": "升級以獲得完整的安全防護" }, "viewbusinessplans": { - "message": "View business plans" + "message": "檢視商業方案" } } From 97906fffd97eb36f3ce5eec0cbfe25f3a83cc4dc Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 12:08:11 +0000 Subject: [PATCH 06/12] Autosync the updated translations (#16929) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/src/_locales/ar/messages.json | 4 +++ apps/browser/src/_locales/az/messages.json | 4 +++ apps/browser/src/_locales/be/messages.json | 4 +++ apps/browser/src/_locales/bg/messages.json | 4 +++ apps/browser/src/_locales/bn/messages.json | 4 +++ apps/browser/src/_locales/bs/messages.json | 4 +++ apps/browser/src/_locales/ca/messages.json | 4 +++ apps/browser/src/_locales/cs/messages.json | 4 +++ apps/browser/src/_locales/cy/messages.json | 4 +++ apps/browser/src/_locales/da/messages.json | 4 +++ apps/browser/src/_locales/de/messages.json | 20 +++++++------ apps/browser/src/_locales/el/messages.json | 4 +++ apps/browser/src/_locales/en_GB/messages.json | 4 +++ apps/browser/src/_locales/en_IN/messages.json | 4 +++ apps/browser/src/_locales/es/messages.json | 4 +++ apps/browser/src/_locales/et/messages.json | 4 +++ apps/browser/src/_locales/eu/messages.json | 4 +++ apps/browser/src/_locales/fa/messages.json | 4 +++ apps/browser/src/_locales/fi/messages.json | 4 +++ apps/browser/src/_locales/fil/messages.json | 4 +++ apps/browser/src/_locales/fr/messages.json | 4 +++ apps/browser/src/_locales/gl/messages.json | 4 +++ apps/browser/src/_locales/he/messages.json | 4 +++ apps/browser/src/_locales/hi/messages.json | 4 +++ apps/browser/src/_locales/hr/messages.json | 4 +++ apps/browser/src/_locales/hu/messages.json | 4 +++ apps/browser/src/_locales/id/messages.json | 4 +++ apps/browser/src/_locales/it/messages.json | 4 +++ apps/browser/src/_locales/ja/messages.json | 4 +++ apps/browser/src/_locales/ka/messages.json | 4 +++ apps/browser/src/_locales/km/messages.json | 4 +++ apps/browser/src/_locales/kn/messages.json | 4 +++ apps/browser/src/_locales/ko/messages.json | 4 +++ apps/browser/src/_locales/lt/messages.json | 4 +++ apps/browser/src/_locales/lv/messages.json | 4 +++ apps/browser/src/_locales/ml/messages.json | 4 +++ apps/browser/src/_locales/mr/messages.json | 4 +++ apps/browser/src/_locales/my/messages.json | 4 +++ apps/browser/src/_locales/nb/messages.json | 4 +++ apps/browser/src/_locales/ne/messages.json | 4 +++ apps/browser/src/_locales/nl/messages.json | 4 +++ apps/browser/src/_locales/nn/messages.json | 4 +++ apps/browser/src/_locales/or/messages.json | 4 +++ apps/browser/src/_locales/pl/messages.json | 4 +++ apps/browser/src/_locales/pt_BR/messages.json | 4 +++ apps/browser/src/_locales/pt_PT/messages.json | 4 +++ apps/browser/src/_locales/ro/messages.json | 4 +++ apps/browser/src/_locales/ru/messages.json | 4 +++ apps/browser/src/_locales/si/messages.json | 4 +++ apps/browser/src/_locales/sk/messages.json | 4 +++ apps/browser/src/_locales/sl/messages.json | 4 +++ apps/browser/src/_locales/sr/messages.json | 4 +++ apps/browser/src/_locales/sv/messages.json | 4 +++ apps/browser/src/_locales/ta/messages.json | 4 +++ apps/browser/src/_locales/te/messages.json | 4 +++ apps/browser/src/_locales/th/messages.json | 4 +++ apps/browser/src/_locales/tr/messages.json | 4 +++ apps/browser/src/_locales/uk/messages.json | 4 +++ apps/browser/src/_locales/vi/messages.json | 4 +++ apps/browser/src/_locales/zh_CN/messages.json | 6 +++- apps/browser/src/_locales/zh_TW/messages.json | 28 +++++++++++-------- 61 files changed, 265 insertions(+), 21 deletions(-) diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 82639f19974..10443fcf449 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 1d5ca88f40e..ad44440a343 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Key Connector domenini təsdiqlə" + }, + "settingDisabledByPolicy": { + "message": "Bu ayar, təşkilatınızın siyasəti tərəfindən sıradan çıxarılıb.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 60fa6e22481..e9ec6a06b8c 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index fdeb336d94f..942a2f489a0 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Потвърждаване на домейна на конектора за ключове" + }, + "settingDisabledByPolicy": { + "message": "Тази настройка е изключена съгласно политиката на организацията Ви.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index 9baa0547ffb..8b8b89d45a2 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index 402c278f1f9..914b8700d13 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index 255d2bcb1b2..8f3a0ca386d 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index 9e08cf87e7a..db522f3aa4e 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Potvrdit doménu Key Connectoru" + }, + "settingDisabledByPolicy": { + "message": "Toto nastavení je zakázáno zásadami Vaší organizace.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 70f0163d349..eacbb06fd53 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index d65e8382434..ddc6f33599f 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index e41187db557..8878f4b698e 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -5580,30 +5580,30 @@ "message": "Willkommen in deinem Tresor!" }, "phishingPageTitleV2": { - "message": "Phishing attempt detected" + "message": "Phishing-Versuch erkannt" }, "phishingPageSummary": { - "message": "The site you are attempting to visit is a known malicious site and a security risk." + "message": "Die Website, die du versuchst zu öffnen, ist eine bekannte böswillige Website und ein Sicherheitsrisiko." }, "phishingPageCloseTabV2": { - "message": "Close this tab" + "message": "Diesen Tab schließen" }, "phishingPageContinueV2": { - "message": "Continue to this site (not recommended)" + "message": "Weiter zu dieser Seite (nicht empfohlen)" }, "phishingPageExplanation1": { - "message": "This site was found in ", + "message": "Diese Seite wurde in ", "description": "This is in multiple parts to allow for bold text in the middle of the sentence. A proper name follows this." }, "phishingPageExplanation2": { - "message": ", an open-source list of known phishing sites used for stealing personal and sensitive information.", + "message": " gefunden, einer Open-Source-Liste bekannter Phishing-Seiten, die zum Diebstahl persönlicher und vertraulicher Informationen verwendet werden.", "description": "This is in multiple parts to allow for bold text in the middle of the sentence. A proper name precedes this." }, "phishingPageLearnMore": { - "message": "Learn more about phishing detection" + "message": "Erfahre mehr über die Phishing-Erkennung" }, "protectedBy": { - "message": "Protected by $PRODUCT$", + "message": "Geschützt durch $PRODUCT$", "placeholders": { "product": { "content": "$1", @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Key Connector-Domain bestätigen" + }, + "settingDisabledByPolicy": { + "message": "Diese Einstellung ist durch die Richtlinien deiner Organisation deaktiviert.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index 5fbf848c299..7f519130df0 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index c3cb86b41e9..1b78e39ecf8 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organisation's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index b8c2905f8da..e7c3a197c75 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organisation's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index c63ba159700..cc3242fd4a9 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index 9182a97ace9..96adaeba324 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index 1432a4a163e..ab1d3f8ef8e 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index e7366161956..8d76ec3f428 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index f810ea1c725..b782c7e11af 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index 03b405805e6..6b85bdf8f43 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index 0eb29428ee9..a4518b54afc 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirmez le domaine de Key Connector" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index b03f83272a0..66f459d97b7 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index 953f7ae1150..5243fa03283 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "אשר דומיין של Key Connector" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index d041cbd23ed..e887f573ba9 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index 4b5bd0f8612..a4441a9a142 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Potvrdi domenu kontektora ključa" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index c6726c5619d..2f3d46f78d8 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "A Key Connector tartomány megerősítése" + }, + "settingDisabledByPolicy": { + "message": "Ezt a beállítást a szervezet házirendje letiltotta.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index 9a3830111c5..ccd332b9c1b 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 6e2899ddc38..10b1c678826 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Conferma dominio Key Connector" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 1f0711dd9d5..7c9d9e80ed4 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index fa40f16db71..3b3189acd6d 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index db205f4740b..026e24dbd3a 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index 5cff99b20db..42a5c4f1b05 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index edb56940dc2..b17055c72d0 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index 7e99a52a5e1..4811d7585ed 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index 6bd32684f57..ce2ffa00c40 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Apstiprināt Key Connector domēnu" + }, + "settingDisabledByPolicy": { + "message": "Šis iestatījums ir atspējots apvienības pamatnostādnēs.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index 8c53bac93db..4641fc0416b 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index d40a7ee8ee7..40370d4b980 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index db205f4740b..026e24dbd3a 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 04c76c63d1f..7091c084082 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index db205f4740b..026e24dbd3a 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index 9d6d54bfe70..7d8760a8710 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Key Connector-domein bevestigen" + }, + "settingDisabledByPolicy": { + "message": "Deze instelling is uitgeschakeld door het beleid van uw organisatie.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index db205f4740b..026e24dbd3a 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index db205f4740b..026e24dbd3a 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index 3de2aa27d90..13d00bc6f88 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Potwierdź domenę Key Connector" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index b9069525242..b1a6bc73f63 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirmar domínio do Conector de Chave" + }, + "settingDisabledByPolicy": { + "message": "Essa configuração está desativada pela política da sua organização.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index a1d403b54da..54eff3eb2ed 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirmar o domínio do Key Connector" + }, + "settingDisabledByPolicy": { + "message": "Esta configuração está desativada pela política da sua organização.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index b9a501ce461..9c1e2bcd79a 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index 2abae0ffde6..1100c4b382c 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Подтвердите домен соединителя ключей" + }, + "settingDisabledByPolicy": { + "message": "Этот параметр отключен политикой вашей организации.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index 6291fc0a40c..76d4464489b 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index 130d4db6503..0d10ec1dd6b 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Potvrdiť doménu Key Connectora" + }, + "settingDisabledByPolicy": { + "message": "Politika organizácie vypla toto nastavenie.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index 253e4fb3550..923fd2ce058 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index db482e74175..0cd98548b0f 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Потврдите домен конектора кључа" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 979750a4d56..e5de8bb5edf 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Bekräfta Key Connector-domän" + }, + "settingDisabledByPolicy": { + "message": "Denna inställning är inaktiverad enligt din organisations policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/ta/messages.json b/apps/browser/src/_locales/ta/messages.json index fea818db9f4..68ae29a7a93 100644 --- a/apps/browser/src/_locales/ta/messages.json +++ b/apps/browser/src/_locales/ta/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Key Connector டொமைனை உறுதிப்படுத்து" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index db205f4740b..026e24dbd3a 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index 9a8c074fb0c..cfb23d95a02 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 3969bb8095c..206c0da5b88 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Key Connector alan adını doğrulayın" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index b2988e6e9cb..1adbf19496b 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Підтвердити домен Key Connector" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index c94ba40c9a6..885fe83f667 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "Xác nhận tên miền Key Connector" + }, + "settingDisabledByPolicy": { + "message": "This setting is disabled by your organization's policy.", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index c0c48881394..738e2c13ecb 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -3226,7 +3226,7 @@ } }, "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc": { - "message": "仅会导出与 $ORGANIZATION$ 关联的组织密码库,不包括我的项目集合。", + "message": "仅会导出与 $ORGANIZATION$ 关联的组织密码库。不包括「我的项目」集合。", "placeholders": { "organization": { "content": "$1", @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "确认 Key Connector 域名" + }, + "settingDisabledByPolicy": { + "message": "此设置被您组织的策略禁用了。", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index eb3e9d98eb6..b83e78a3b02 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -6,7 +6,7 @@ "message": "Bitwarden logo" }, "extName": { - "message": "Bitwarden - 免費密碼管理工具", + "message": "Bitwarden - 密碼管理工具", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { @@ -559,7 +559,7 @@ "description": "Verb" }, "unArchive": { - "message": "Unarchive" + "message": "取消封存" }, "itemsInArchive": { "message": "封存中的項目" @@ -571,10 +571,10 @@ "message": "封存的項目會顯示在此處,且不會出現在一般搜尋結果或自動填入建議中。" }, "itemWasSentToArchive": { - "message": "Item was sent to archive" + "message": "項目已移至封存" }, "itemUnarchived": { - "message": "Item was unarchived" + "message": "項目取消封存" }, "archiveItem": { "message": "封存項目" @@ -5580,30 +5580,30 @@ "message": "歡迎來到你的密碼庫!" }, "phishingPageTitleV2": { - "message": "Phishing attempt detected" + "message": "偵測到網路釣魚嘗試" }, "phishingPageSummary": { - "message": "The site you are attempting to visit is a known malicious site and a security risk." + "message": "你正要造訪的網站為已知惡意網站,存在安全風險。" }, "phishingPageCloseTabV2": { - "message": "Close this tab" + "message": "關閉此分頁" }, "phishingPageContinueV2": { - "message": "Continue to this site (not recommended)" + "message": "繼續前往此網站(不建議)" }, "phishingPageExplanation1": { - "message": "This site was found in ", + "message": "此網站被列於", "description": "This is in multiple parts to allow for bold text in the middle of the sentence. A proper name follows this." }, "phishingPageExplanation2": { - "message": ", an open-source list of known phishing sites used for stealing personal and sensitive information.", + "message": ",這是一份開源的已知網路釣魚網站清單,用於竊取個人與敏感資訊。", "description": "This is in multiple parts to allow for bold text in the middle of the sentence. A proper name precedes this." }, "phishingPageLearnMore": { - "message": "Learn more about phishing detection" + "message": "進一步了解網路釣魚偵測" }, "protectedBy": { - "message": "Protected by $PRODUCT$", + "message": "由 $PRODUCT$ 保護", "placeholders": { "product": { "content": "$1", @@ -5714,5 +5714,9 @@ }, "confirmKeyConnectorDomain": { "message": "確認 Key Connector 網域" + }, + "settingDisabledByPolicy": { + "message": "此設定已被你的組織原則停用。", + "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } From 7c972906aa8b7752d729eb1f82e888a4eed2fb20 Mon Sep 17 00:00:00 2001 From: SmithThe4th Date: Mon, 20 Oct 2025 09:41:50 -0400 Subject: [PATCH 07/12] [PM-10128] [BEEEP] Add Right-Click Menu Options to Vault (#10954) * Added right click functionality on cipher row * Updated menu directive to position menu option on mouse event location * Updated menu directive to reopen menu option on new mouse event location and close previously opened menu-option * removed preventdefault call * Added new events for favorite and edit cipher * Added new menu options favorite, edit cipher Added new copy options for the other cipher types Simplified the copy by using the copy cipher field directive * Listen to new events * Refactored parameter to be MouseEvent * Added locales * Remove the backdrop from `MenuTriggerForDirective` * Handle the Angular overlay's outside pointer events * Cleaned up cipher row component as copy functions and disable menu functions would not be needed anymore * Fixed bug with right clicking on a row * Add right click to collections * Disable backdrop on right click * Fixed bug where dvivided didn't show for secure notes * Added comments to enable to disable context menu * Removed conditionals * Removed preferences setting to enable to disable setting * Removed setting from right click listener * improve context menu positioning to prevent viewport clipping * Keep icon consisten when favorite or not * fixed prettier issues * removed duplicate translation keys * Fix favorite status not persisting by toggling in handleFavoriteEvent * Addressed claude comments * Added comment to variable --------- Co-authored-by: Addison Beck --- .../vault-cipher-row.component.html | 57 +++++- .../vault-items/vault-cipher-row.component.ts | 131 ++++++++++---- .../vault-collection-row.component.ts | 16 +- .../vault-items/vault-item-event.ts | 4 +- .../vault-items/vault-items.module.ts | 2 + .../vault/individual-vault/vault.component.ts | 27 +++ apps/web/src/locales/en/messages.json | 9 + .../src/menu/menu-trigger-for.directive.ts | 163 +++++++++++++++--- 8 files changed, 339 insertions(+), 70 deletions(-) diff --git a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html index a897c8c4c2c..43ce8530d55 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html @@ -93,7 +93,7 @@ - @if (!decryptionFailure && !hideMenu) { + @if (!decryptionFailure) { - - - - @@ -130,6 +130,53 @@ + + + + + + + + + + + + + + + + + + + + -
+
+ + +
  • + + + +
  • diff --git a/apps/desktop/src/vault/app/vault/vault-items-v2.component.ts b/apps/desktop/src/vault/app/vault/vault-items-v2.component.ts index 06654fb1a5c..290a38ac08c 100644 --- a/apps/desktop/src/vault/app/vault/vault-items-v2.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-items-v2.component.ts @@ -7,6 +7,7 @@ import { distinctUntilChanged, debounceTime } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { VaultItemsComponent as BaseVaultItemsComponent } from "@bitwarden/angular/vault/components/vault-items.component"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { uuidAsString } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; @@ -33,8 +34,9 @@ export class VaultItemsV2Component extends BaseVaultIt cipherService: CipherService, accountService: AccountService, restrictedItemTypesService: RestrictedItemTypesService, + configService: ConfigService, ) { - super(searchService, cipherService, accountService, restrictedItemTypesService); + super(searchService, cipherService, accountService, restrictedItemTypesService, configService); this.searchBarService.searchText$ .pipe(debounceTime(SearchTextDebounceInterval), distinctUntilChanged(), takeUntilDestroyed()) diff --git a/apps/desktop/src/vault/app/vault/vault-v2.component.html b/apps/desktop/src/vault/app/vault/vault-v2.component.html index a3f55f0ec63..2696dd0d452 100644 --- a/apps/desktop/src/vault/app/vault/vault-v2.component.html +++ b/apps/desktop/src/vault/app/vault/vault-v2.component.html @@ -19,6 +19,7 @@ (onClone)="cloneCipher($event)" (onDelete)="deleteCipher()" (onCancel)="cancelCipher($event)" + (onArchiveToggle)="refreshCurrentCipher()" [masterPasswordAlreadyPrompted]="cipherRepromptId === cipherId" >
    diff --git a/apps/desktop/src/vault/app/vault/vault-v2.component.ts b/apps/desktop/src/vault/app/vault/vault-v2.component.ts index 6dda97807be..b7b0bf2e1b2 100644 --- a/apps/desktop/src/vault/app/vault/vault-v2.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-v2.component.ts @@ -20,6 +20,8 @@ import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; 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 { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; @@ -33,6 +35,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { getByIds } from "@bitwarden/common/platform/misc"; import { SyncService } from "@bitwarden/common/platform/sync"; import { CipherId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; @@ -74,6 +77,7 @@ import { DefaultCipherFormConfigService, PasswordRepromptService, CipherFormComponent, + ArchiveCipherUtilitiesService, } from "@bitwarden/vault"; import { NavComponent } from "../../../app/layout/nav.component"; @@ -211,6 +215,9 @@ export class VaultV2Component private folderService: FolderService, private configService: ConfigService, private authRequestService: AuthRequestServiceAbstraction, + private cipherArchiveService: CipherArchiveService, + private policyService: PolicyService, + private archiveCipherUtilitiesService: ArchiveCipherUtilitiesService, ) {} async ngOnInit() { @@ -490,6 +497,12 @@ export class VaultV2Component async viewCipherMenu(c: CipherViewLike) { const cipher = await this.cipherService.getFullCipherView(c); + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + const userCanArchive = await firstValueFrom(this.cipherArchiveService.userCanArchive$(userId)); + const orgOwnershipPolicy = await firstValueFrom( + this.policyService.policyAppliesToUser$(PolicyType.OrganizationDataOwnership, userId), + ); + const menu: RendererMenuItem[] = [ { label: this.i18nService.t("view"), @@ -514,7 +527,11 @@ export class VaultV2Component }); }, }); - if (!cipher.organizationId) { + + const archivedWithOrgOwnership = cipher.isArchived && orgOwnershipPolicy; + const canCloneArchived = !cipher.isArchived || userCanArchive; + + if (!cipher.organizationId && !archivedWithOrgOwnership && canCloneArchived) { menu.push({ label: this.i18nService.t("clone"), click: () => { @@ -538,6 +555,26 @@ export class VaultV2Component } } + if (userCanArchive && !cipher.organizationId && !cipher.isDeleted && !cipher.isArchived) { + menu.push({ + label: this.i18nService.t("archiveVerb"), + click: async () => { + await this.archiveCipherUtilitiesService.archiveCipher(cipher); + await this.refreshCurrentCipher(); + }, + }); + } + + if (cipher.isArchived) { + menu.push({ + label: this.i18nService.t("unArchive"), + click: async () => { + await this.archiveCipherUtilitiesService.unarchiveCipher(cipher); + await this.refreshCurrentCipher(); + }, + }); + } + switch (cipher.type) { case CipherType.Login: if ( @@ -723,8 +760,6 @@ export class VaultV2Component this.cipherId = cipher.id; this.cipher = cipher; - - await this.vaultItemsComponent?.load(this.activeFilter.buildFilter()).catch(() => {}); await this.go().catch(() => {}); await this.vaultItemsComponent?.refresh().catch(() => {}); } @@ -757,7 +792,11 @@ export class VaultV2Component ); this.activeFilter = vaultFilter; await this.vaultItemsComponent - ?.reload(this.activeFilter.buildFilter(), vaultFilter.status === "trash") + ?.reload( + this.activeFilter.buildFilter(), + vaultFilter.status === "trash", + vaultFilter.status === "archive", + ) .catch(() => {}); await this.go().catch(() => {}); } @@ -831,6 +870,20 @@ export class VaultV2Component } } + /** Refresh the current cipher object */ + protected async refreshCurrentCipher() { + 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), + ), + ); + } + private dirtyInput(): boolean { return ( (this.action === "add" || this.action === "edit" || this.action === "clone") && diff --git a/libs/angular/src/vault/components/vault-items.component.ts b/libs/angular/src/vault/components/vault-items.component.ts index 8ae4acfb687..414ec1509ed 100644 --- a/libs/angular/src/vault/components/vault-items.component.ts +++ b/libs/angular/src/vault/components/vault-items.component.ts @@ -11,11 +11,14 @@ import { of, shareReplay, switchMap, + take, } from "rxjs"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; 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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -37,6 +40,7 @@ export class VaultItemsComponent implements OnDestroy loaded = false; ciphers: C[] = []; deleted = false; + archived = false; organization: Organization; CipherType = CipherType; @@ -73,13 +77,24 @@ export class VaultItemsComponent implements OnDestroy this._filter$.next(value); } + private archiveFeatureEnabled = false; + constructor( protected searchService: SearchService, protected cipherService: CipherService, protected accountService: AccountService, protected restrictedItemTypesService: RestrictedItemTypesService, + private configService: ConfigService, ) { this.subscribeToCiphers(); + + // Check if archive feature flag is enabled + this.configService + .getFeatureFlag$(FeatureFlag.PM19148_InnovationArchive) + .pipe(takeUntilDestroyed(), take(1)) + .subscribe((isEnabled) => { + this.archiveFeatureEnabled = isEnabled; + }); } ngOnDestroy(): void { @@ -87,19 +102,20 @@ export class VaultItemsComponent implements OnDestroy this.destroy$.complete(); } - async load(filter: (cipher: C) => boolean = null, deleted = false) { + async load(filter: (cipher: C) => boolean = null, deleted = false, archived = false) { this.deleted = deleted ?? false; + this.archived = archived; await this.applyFilter(filter); this.loaded = true; } - async reload(filter: (cipher: C) => boolean = null, deleted = false) { + async reload(filter: (cipher: C) => boolean = null, deleted = false, archived = false) { this.loaded = false; - await this.load(filter, deleted); + await this.load(filter, deleted, archived); } async refresh() { - await this.reload(this.filter, this.deleted); + await this.reload(this.filter, this.deleted, this.archived); } async applyFilter(filter: (cipher: C) => boolean = null) { @@ -125,6 +141,16 @@ export class VaultItemsComponent implements OnDestroy protected deletedFilter: (cipher: C) => boolean = (c) => CipherViewLikeUtils.isDeleted(c) === this.deleted; + protected archivedFilter: (cipher: C) => boolean = (c) => { + // When the archive feature is not enabled, + // always return true to avoid filtering out any items. + if (!this.archiveFeatureEnabled) { + return true; + } + + return CipherViewLikeUtils.isArchived(c) === this.archived; + }; + /** * Creates stream of dependencies that results in the list of ciphers to display * within the vault list. @@ -158,7 +184,7 @@ export class VaultItemsComponent implements OnDestroy return this.searchService.searchCiphers( userId, searchText, - [filter, this.deletedFilter, restrictedTypeFilter], + [filter, this.deletedFilter, this.archivedFilter, restrictedTypeFilter], allCiphers, ); }), diff --git a/libs/angular/src/vault/services/custom-nudges-services/new-item-nudge.service.ts b/libs/angular/src/vault/services/custom-nudges-services/new-item-nudge.service.ts index e89b877562a..4afc356a095 100644 --- a/libs/angular/src/vault/services/custom-nudges-services/new-item-nudge.service.ts +++ b/libs/angular/src/vault/services/custom-nudges-services/new-item-nudge.service.ts @@ -1,5 +1,5 @@ import { inject, Injectable } from "@angular/core"; -import { combineLatest, Observable, switchMap } from "rxjs"; +import { combineLatest, filter, Observable, switchMap } from "rxjs"; import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -20,7 +20,7 @@ export class NewItemNudgeService extends DefaultSingleNudgeService { nudgeStatus$(nudgeType: NudgeType, userId: UserId): Observable { return combineLatest([ this.getNudgeStatus$(nudgeType, userId), - this.cipherService.cipherViews$(userId), + this.cipherService.cipherViews$(userId).pipe(filter((ciphers) => ciphers != null)), ]).pipe( switchMap(async ([nudgeStatus, ciphers]) => { if (nudgeStatus.hasSpotlightDismissed) { diff --git a/libs/angular/src/vault/vault-filter/components/status-filter.component.ts b/libs/angular/src/vault/vault-filter/components/status-filter.component.ts index ba3842a6e11..dc6a90f928d 100644 --- a/libs/angular/src/vault/vault-filter/components/status-filter.component.ts +++ b/libs/angular/src/vault/vault-filter/components/status-filter.component.ts @@ -9,11 +9,12 @@ import { VaultFilter } from "../models/vault-filter.model"; export class StatusFilterComponent { @Input() hideFavorites = false; @Input() hideTrash = false; + @Input() hideArchive = false; @Output() onFilterChange: EventEmitter = new EventEmitter(); @Input() activeFilter: VaultFilter; get show() { - return !(this.hideFavorites && this.hideTrash); + return !(this.hideFavorites && this.hideTrash && this.hideArchive); } applyFilter(cipherStatus: CipherStatus) { diff --git a/libs/angular/src/vault/vault-filter/components/vault-filter.component.ts b/libs/angular/src/vault/vault-filter/components/vault-filter.component.ts index 0b0cb14bbb8..9199c53bfcb 100644 --- a/libs/angular/src/vault/vault-filter/components/vault-filter.component.ts +++ b/libs/angular/src/vault/vault-filter/components/vault-filter.component.ts @@ -10,6 +10,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { UserId } from "@bitwarden/common/types/guid"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; import { ITreeNodeObject } from "@bitwarden/common/vault/models/domain/tree-node"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; @@ -42,9 +43,12 @@ export class VaultFilterComponent implements OnInit { collections: DynamicTreeNode; folders$: Observable>; + protected showArchiveVaultFilter = false; + constructor( protected vaultFilterService: DeprecatedVaultFilterService, protected accountService: AccountService, + protected cipherArchiveService: CipherArchiveService, ) {} get displayCollections() { @@ -65,6 +69,15 @@ export class VaultFilterComponent implements OnInit { } this.folders$ = await this.vaultFilterService.buildNestedFolders(); this.collections = await this.initCollections(); + + const userCanArchive = await firstValueFrom( + this.cipherArchiveService.userCanArchive$(this.activeUserId), + ); + const showArchiveVault = await firstValueFrom( + this.cipherArchiveService.showArchiveVault$(this.activeUserId), + ); + + this.showArchiveVaultFilter = userCanArchive || showArchiveVault; this.isLoaded = true; } diff --git a/libs/angular/src/vault/vault-filter/models/cipher-status.model.ts b/libs/angular/src/vault/vault-filter/models/cipher-status.model.ts index f93cd8b2107..c05484a55fc 100644 --- a/libs/angular/src/vault/vault-filter/models/cipher-status.model.ts +++ b/libs/angular/src/vault/vault-filter/models/cipher-status.model.ts @@ -1 +1 @@ -export type CipherStatus = "all" | "favorites" | "trash"; +export type CipherStatus = "all" | "favorites" | "trash" | "archive"; 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 ea5e9eb9b24..a2f8aa7a352 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 @@ -56,6 +56,34 @@ describe("VaultFilter", () => { }); }); + describe("given a archived cipher", () => { + const cipher = createCipher({ archivedDate: new Date() }); + + it("should return true when filtering for archive", () => { + const filterFunction = createFilterFunction({ status: "archive" }); + + const result = filterFunction(cipher); + + expect(result).toBe(true); + }); + + it("should return false when filtering for favorites", () => { + const filterFunction = createFilterFunction({ status: "favorites" }); + + const result = filterFunction(cipher); + + expect(result).toBe(false); + }); + + it("should return false when filtering for trash", () => { + const filterFunction = createFilterFunction({ status: "trash" }); + + const result = filterFunction(cipher); + + expect(result).toBe(false); + }); + }); + describe("given a cipher with type", () => { it("should return true when filter matches cipher type", () => { const cipher = createCipher({ type: CipherType.Identity }); @@ -103,12 +131,12 @@ describe("VaultFilter", () => { }); describe("given a cipher without folder", () => { - const cipher = createCipher({ folderId: null }); + const cipher = createCipher({ folderId: undefined }); it("should return true when filtering on unassigned folder", () => { const filterFunction = createFilterFunction({ selectedFolder: true, - selectedFolderId: null, + selectedFolderId: undefined, }); const result = filterFunction(cipher); @@ -175,7 +203,7 @@ describe("VaultFilter", () => { it("should return true when filtering for unassigned collection", () => { const filterFunction = createFilterFunction({ selectedCollection: true, - selectedCollectionId: null, + selectedCollectionId: undefined, }); const result = filterFunction(cipher); @@ -195,12 +223,12 @@ describe("VaultFilter", () => { }); describe("given an individual cipher (without organization or collection)", () => { - const cipher = createCipher({ organizationId: null, collectionIds: [] }); + const cipher = createCipher({ organizationId: undefined, collectionIds: [] }); it("should return false when filtering for unassigned collection", () => { const filterFunction = createFilterFunction({ selectedCollection: true, - selectedCollectionId: null, + selectedCollectionId: undefined, }); const result = filterFunction(cipher); @@ -209,7 +237,7 @@ describe("VaultFilter", () => { }); it("should return true when filtering for my vault only", () => { - const cipher = createCipher({ organizationId: null }); + const cipher = createCipher({ organizationId: undefined }); const filterFunction = createFilterFunction({ myVaultOnly: true, }); @@ -230,11 +258,12 @@ function createCipher(options: Partial = {}) { const cipher = new CipherView(); cipher.favorite = options.favorite ?? false; - cipher.deletedDate = options.deletedDate; - cipher.type = options.type; - cipher.folderId = options.folderId; - cipher.collectionIds = options.collectionIds; - cipher.organizationId = options.organizationId; + cipher.deletedDate = options.deletedDate ?? null; + cipher.archivedDate = options.archivedDate ?? null; + cipher.type = options.type ?? CipherType.Login; + cipher.folderId = options.folderId ?? undefined; + cipher.collectionIds = options.collectionIds ?? []; + cipher.organizationId = options.organizationId ?? undefined; return cipher; } 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 bade2244ff0..87536036644 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 @@ -50,6 +50,9 @@ export class VaultFilter { if (this.status === "trash" && cipherPassesFilter) { cipherPassesFilter = CipherViewLikeUtils.isDeleted(cipher); } + if (this.status === "archive" && cipherPassesFilter) { + cipherPassesFilter = CipherViewLikeUtils.isArchived(cipher); + } if (this.cipherType != null && cipherPassesFilter) { cipherPassesFilter = CipherViewLikeUtils.getType(cipher) === this.cipherType; } diff --git a/libs/common/src/vault/models/view/cipher.view.ts b/libs/common/src/vault/models/view/cipher.view.ts index c586297d6a5..7412c68d695 100644 --- a/libs/common/src/vault/models/view/cipher.view.ts +++ b/libs/common/src/vault/models/view/cipher.view.ts @@ -160,6 +160,10 @@ export class CipherView implements View, InitializerMetadata { } get canAssignToCollections(): boolean { + if (this.isArchived) { + return false; + } + if (this.organizationId == null) { return true; } diff --git a/libs/vault/src/cipher-form/components/cipher-form.component.spec.ts b/libs/vault/src/cipher-form/components/cipher-form.component.spec.ts index 08afee33ae3..a002956b54a 100644 --- a/libs/vault/src/cipher-form/components/cipher-form.component.spec.ts +++ b/libs/vault/src/cipher-form/components/cipher-form.component.spec.ts @@ -2,7 +2,7 @@ import { ChangeDetectorRef } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { ReactiveFormsModule } from "@angular/forms"; import { mock } from "jest-mock-extended"; -import { of } from "rxjs"; +import { Observable, of } from "rxjs"; import { ViewCacheService } from "@bitwarden/angular/platform/view-cache"; import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -13,7 +13,6 @@ import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { Fido2CredentialView } from "@bitwarden/common/vault/models/view/fido2-credential.view"; import { ToastService } from "@bitwarden/components"; -import { UserId } from "@bitwarden/user-core"; import { CipherFormConfig } from "../abstractions/cipher-form-config.service"; import { CipherFormService } from "../abstractions/cipher-form.service"; @@ -72,7 +71,7 @@ describe("CipherFormComponent", () => { }); it("should remove archivedDate when user cannot archive and cipher is archived", async () => { - mockAccountService.activeAccount$ = of({ id: "user-id" as UserId } as Account); + mockAccountService.activeAccount$ = of({ id: "user-id" }) as Observable; mockCipherArchiveService.userCanArchive$.mockReturnValue(of(false)); mockAddEditFormService.saveCipher = jest.fn().mockResolvedValue(new CipherView()); @@ -154,6 +153,15 @@ describe("CipherFormComponent", () => { expect(component["updatedCipherView"]?.login.fido2Credentials).toBeNull(); }); + + it("clears archiveDate on updatedCipherView", async () => { + cipherView.archivedDate = new Date(); + decryptCipher.mockResolvedValue(cipherView); + + await component.ngOnInit(); + + expect(component["updatedCipherView"]?.archivedDate).toBeNull(); + }); }); describe("enableFormFields", () => { diff --git a/libs/vault/src/cipher-form/components/cipher-form.component.ts b/libs/vault/src/cipher-form/components/cipher-form.component.ts index 117dd98ba43..f7676818edf 100644 --- a/libs/vault/src/cipher-form/components/cipher-form.component.ts +++ b/libs/vault/src/cipher-form/components/cipher-form.component.ts @@ -263,6 +263,7 @@ export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, Ci if (this.config.mode === "clone") { this.updatedCipherView.id = null; + this.updatedCipherView.archivedDate = null; if (this.updatedCipherView.login) { this.updatedCipherView.login.fido2Credentials = null; diff --git a/libs/vault/src/index.ts b/libs/vault/src/index.ts index efaefc77ade..3f05c753da4 100644 --- a/libs/vault/src/index.ts +++ b/libs/vault/src/index.ts @@ -27,3 +27,4 @@ export { SshImportPromptService } from "./services/ssh-import-prompt.service"; export * from "./abstractions/change-login-password.service"; export * from "./services/default-change-login-password.service"; +export * from "./services/archive-cipher-utilities.service"; diff --git a/libs/vault/src/services/archive-cipher-utilities.service.spec.ts b/libs/vault/src/services/archive-cipher-utilities.service.spec.ts new file mode 100644 index 00000000000..76a4073325e --- /dev/null +++ b/libs/vault/src/services/archive-cipher-utilities.service.spec.ts @@ -0,0 +1,122 @@ +import { MockProxy, mock } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { CipherId } from "@bitwarden/common/types/guid"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { DialogService, ToastService } from "@bitwarden/components"; + +import { ArchiveCipherUtilitiesService } from "./archive-cipher-utilities.service"; +import { PasswordRepromptService } from "./password-reprompt.service"; + +describe("ArchiveCipherUtilitiesService", () => { + let service: ArchiveCipherUtilitiesService; + + let cipherArchiveService: MockProxy; + let dialogService: MockProxy; + let passwordRepromptService: MockProxy; + let toastService: MockProxy; + let i18nService: MockProxy; + let accountService: MockProxy; + + const mockCipher = new CipherView(); + mockCipher.id = "cipher-id" as CipherId; + const mockUserId = "user-id"; + + beforeEach(() => { + cipherArchiveService = mock(); + dialogService = mock(); + passwordRepromptService = mock(); + toastService = mock(); + i18nService = mock(); + accountService = mock(); + + accountService.activeAccount$ = new BehaviorSubject({ id: mockUserId } as any).asObservable(); + + dialogService.openSimpleDialog.mockResolvedValue(true); + passwordRepromptService.passwordRepromptCheck.mockResolvedValue(true); + cipherArchiveService.archiveWithServer.mockResolvedValue(undefined); + cipherArchiveService.unarchiveWithServer.mockResolvedValue(undefined); + i18nService.t.mockImplementation((key) => key); + + service = new ArchiveCipherUtilitiesService( + cipherArchiveService, + dialogService, + passwordRepromptService, + toastService, + i18nService, + accountService, + ); + }); + + describe("archiveCipher()", () => { + it("returns early when confirmation dialog is cancelled", async () => { + dialogService.openSimpleDialog.mockResolvedValue(false); + + await service.archiveCipher(mockCipher); + + expect(passwordRepromptService.passwordRepromptCheck).toHaveBeenCalled(); + expect(cipherArchiveService.archiveWithServer).not.toHaveBeenCalled(); + }); + + it("returns early when password reprompt fails", async () => { + passwordRepromptService.passwordRepromptCheck.mockResolvedValue(false); + + await service.archiveCipher(mockCipher); + + expect(cipherArchiveService.archiveWithServer).not.toHaveBeenCalled(); + }); + + it("archives cipher and shows success toast when successful", async () => { + await service.archiveCipher(mockCipher); + + expect(cipherArchiveService.archiveWithServer).toHaveBeenCalledWith( + mockCipher.id, + mockUserId, + ); + expect(toastService.showToast).toHaveBeenCalledWith({ + variant: "success", + message: "itemWasSentToArchive", + }); + }); + + it("shows error toast when archiving fails", async () => { + cipherArchiveService.archiveWithServer.mockRejectedValue(new Error("test error")); + + await service.archiveCipher(mockCipher); + + expect(toastService.showToast).toHaveBeenCalledWith({ + variant: "error", + message: "errorOccurred", + }); + }); + }); + + describe("unarchiveCipher()", () => { + it("unarchives cipher and shows success toast when successful", async () => { + await service.unarchiveCipher(mockCipher); + + expect(cipherArchiveService.unarchiveWithServer).toHaveBeenCalledWith( + mockCipher.id, + mockUserId, + ); + expect(toastService.showToast).toHaveBeenCalledWith({ + variant: "success", + message: "itemWasUnarchived", + }); + }); + + it("shows error toast when unarchiving fails", async () => { + cipherArchiveService.unarchiveWithServer.mockRejectedValue(new Error("test error")); + + await service.unarchiveCipher(mockCipher); + + expect(toastService.showToast).toHaveBeenCalledWith({ + variant: "error", + message: "errorOccurred", + }); + }); + }); +}); diff --git a/libs/vault/src/services/archive-cipher-utilities.service.ts b/libs/vault/src/services/archive-cipher-utilities.service.ts new file mode 100644 index 00000000000..bbe7dba6715 --- /dev/null +++ b/libs/vault/src/services/archive-cipher-utilities.service.ts @@ -0,0 +1,80 @@ +import { Injectable } 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { CipherId } from "@bitwarden/common/types/guid"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { DialogService, ToastService } from "@bitwarden/components"; + +import { PasswordRepromptService } from "./password-reprompt.service"; + +/** + * Wrapper around {@link CipherArchiveService} to provide UI enhancements for archiving/unarchiving ciphers. + */ +@Injectable({ providedIn: "root" }) +export class ArchiveCipherUtilitiesService { + constructor( + private cipherArchiveService: CipherArchiveService, + private dialogService: DialogService, + private passwordRepromptService: PasswordRepromptService, + private toastService: ToastService, + private i18nService: I18nService, + private accountService: AccountService, + ) {} + + /** Archive a cipher, with confirmation dialog and password reprompt checks. */ + async archiveCipher(cipher: CipherView) { + const repromptPassed = await this.passwordRepromptService.passwordRepromptCheck(cipher); + if (!repromptPassed) { + return; + } + + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "archiveItem" }, + content: { key: "archiveItemConfirmDesc" }, + type: "info", + }); + + if (!confirmed) { + return; + } + + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + await this.cipherArchiveService + .archiveWithServer(cipher.id as CipherId, userId) + .then(() => { + this.toastService.showToast({ + variant: "success", + message: this.i18nService.t("itemWasSentToArchive"), + }); + }) + .catch(() => { + this.toastService.showToast({ + variant: "error", + message: this.i18nService.t("errorOccurred"), + }); + }); + } + + /** Unarchives a cipher */ + async unarchiveCipher(cipher: CipherView) { + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + await this.cipherArchiveService + .unarchiveWithServer(cipher.id as CipherId, userId) + .then(() => { + this.toastService.showToast({ + variant: "success", + message: this.i18nService.t("itemWasUnarchived"), + }); + }) + .catch(() => { + this.toastService.showToast({ + variant: "error", + message: this.i18nService.t("errorOccurred"), + }); + }); + } +}