From d5f503a0d6b855442ce55b9c042357393d7394bf Mon Sep 17 00:00:00 2001 From: Will Martin Date: Thu, 18 Apr 2024 13:23:35 -0400 Subject: [PATCH 001/110] [CL-18] toast component and service (#6490) Update toast styles and new service to CL. --- apps/browser/src/_locales/en/messages.json | 3 + .../foreground-platform-utils.service.ts | 23 +--- apps/browser/src/popup/app.component.ts | 17 ++- apps/browser/src/popup/app.module.ts | 5 +- apps/browser/src/popup/scss/plugins.scss | 98 -------------- apps/browser/src/popup/scss/popup.scss | 1 - .../src/popup/services/services.module.ts | 16 +-- apps/desktop/src/app/app.component.ts | 38 +----- apps/desktop/src/locales/en/messages.json | 3 + apps/desktop/src/scss/plugins.scss | 95 -------------- apps/desktop/src/scss/styles.scss | 1 - apps/web/src/app/app.component.ts | 39 +----- apps/web/src/app/shared/shared.module.ts | 3 - apps/web/src/locales/en/messages.json | 3 + apps/web/src/scss/styles.scss | 3 - apps/web/src/scss/toasts.scss | 117 ----------------- .../src/components/toastr.component.ts | 98 -------------- libs/angular/src/jslib.module.ts | 7 +- .../abstractions/platform-utils.service.ts | 5 + libs/components/src/index.ts | 1 + libs/components/src/toast/index.ts | 2 + .../components/src/toast/toast.component.html | 24 ++++ libs/components/src/toast/toast.component.ts | 66 ++++++++++ libs/components/src/toast/toast.module.ts | 39 ++++++ libs/components/src/toast/toast.service.ts | 57 ++++++++ libs/components/src/toast/toast.spec.ts | 16 +++ libs/components/src/toast/toast.stories.ts | 124 ++++++++++++++++++ libs/components/src/toast/toast.tokens.css | 4 + libs/components/src/toast/toastr.component.ts | 26 ++++ libs/components/src/toast/toastr.css | 23 ++++ libs/components/src/toast/utils.ts | 14 ++ libs/components/src/tw-theme.css | 3 + 32 files changed, 440 insertions(+), 534 deletions(-) delete mode 100644 apps/browser/src/popup/scss/plugins.scss delete mode 100644 apps/desktop/src/scss/plugins.scss delete mode 100644 apps/web/src/scss/toasts.scss delete mode 100644 libs/angular/src/components/toastr.component.ts create mode 100644 libs/components/src/toast/index.ts create mode 100644 libs/components/src/toast/toast.component.html create mode 100644 libs/components/src/toast/toast.component.ts create mode 100644 libs/components/src/toast/toast.module.ts create mode 100644 libs/components/src/toast/toast.service.ts create mode 100644 libs/components/src/toast/toast.spec.ts create mode 100644 libs/components/src/toast/toast.stories.ts create mode 100644 libs/components/src/toast/toast.tokens.css create mode 100644 libs/components/src/toast/toastr.component.ts create mode 100644 libs/components/src/toast/toastr.css create mode 100644 libs/components/src/toast/utils.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 36e3ce65a87..8c81088fc50 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -3000,6 +3000,9 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, diff --git a/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts b/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts index 8cf1a8d3e4d..24aa45d5c32 100644 --- a/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts +++ b/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts @@ -1,13 +1,10 @@ -import { SecurityContext } from "@angular/core"; -import { DomSanitizer } from "@angular/platform-browser"; -import { ToastrService } from "ngx-toastr"; +import { ToastService } from "@bitwarden/components"; import { BrowserPlatformUtilsService } from "./browser-platform-utils.service"; export class ForegroundPlatformUtilsService extends BrowserPlatformUtilsService { constructor( - private sanitizer: DomSanitizer, - private toastrService: ToastrService, + private toastService: ToastService, clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void, biometricCallback: () => Promise, win: Window & typeof globalThis, @@ -21,20 +18,6 @@ export class ForegroundPlatformUtilsService extends BrowserPlatformUtilsService text: string | string[], options?: any, ): void { - if (typeof text === "string") { - // Already in the correct format - } else if (text.length === 1) { - text = text[0]; - } else { - let message = ""; - text.forEach( - (t: string) => - (message += "

" + this.sanitizer.sanitize(SecurityContext.HTML, t) + "

"), - ); - text = message; - options.enableHtml = true; - } - this.toastrService.show(text, title, options, "toast-" + type); - // noop + this.toastService._showToast({ type, title, text, options }); } } diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index c224e652f6b..2aba93ac951 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -1,19 +1,18 @@ import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import { NavigationEnd, Router, RouterOutlet } from "@angular/router"; -import { ToastrService } from "ngx-toastr"; import { filter, concatMap, Subject, takeUntil, firstValueFrom, map } from "rxjs"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { DialogService, SimpleDialogOptions } from "@bitwarden/components"; +import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/components"; import { BrowserApi } from "../platform/browser/browser-api"; import { ZonedMessageListenerService } from "../platform/browser/zoned-message-listener.service"; import { BrowserStateService } from "../platform/services/abstractions/browser-state.service"; -import { ForegroundPlatformUtilsService } from "../platform/services/platform-utils/foreground-platform-utils.service"; import { BrowserSendStateService } from "../tools/popup/services/browser-send-state.service"; import { VaultBrowserStateService } from "../vault/services/vault-browser-state.service"; @@ -35,7 +34,6 @@ export class AppComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); constructor( - private toastrService: ToastrService, private broadcasterService: BroadcasterService, private authService: AuthService, private i18nService: I18nService, @@ -46,9 +44,10 @@ export class AppComponent implements OnInit, OnDestroy { private cipherService: CipherService, private changeDetectorRef: ChangeDetectorRef, private ngZone: NgZone, - private platformUtilsService: ForegroundPlatformUtilsService, + private platformUtilsService: PlatformUtilsService, private dialogService: DialogService, private browserMessagingApi: ZonedMessageListenerService, + private toastService: ToastService, ) {} async ngOnInit() { @@ -83,10 +82,10 @@ export class AppComponent implements OnInit, OnDestroy { if (msg.command === "doneLoggingOut") { this.authService.logOut(async () => { if (msg.expired) { - this.showToast({ - type: "warning", + this.toastService.showToast({ + variant: "warning", title: this.i18nService.t("loggedOut"), - text: this.i18nService.t("loginExpired"), + message: this.i18nService.t("loginExpired"), }); } @@ -116,7 +115,7 @@ export class AppComponent implements OnInit, OnDestroy { // eslint-disable-next-line @typescript-eslint/no-floating-promises this.showNativeMessagingFingerprintDialog(msg); } else if (msg.command === "showToast") { - this.showToast(msg); + this.toastService._showToast(msg); } else if (msg.command === "reloadProcess") { const forceWindowReload = this.platformUtilsService.isSafari() || diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index d179868448b..5718542b016 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -11,11 +11,10 @@ import { BrowserModule } from "@angular/platform-browser"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component"; -import { BitwardenToastModule } from "@bitwarden/angular/components/toastr.component"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { ColorPasswordCountPipe } from "@bitwarden/angular/pipes/color-password-count.pipe"; import { ColorPasswordPipe } from "@bitwarden/angular/pipes/color-password.pipe"; -import { AvatarModule, ButtonModule } from "@bitwarden/components"; +import { AvatarModule, ButtonModule, ToastModule } from "@bitwarden/components"; import { ExportScopeCalloutComponent } from "@bitwarden/vault-export-ui"; import { AccountSwitcherComponent } from "../auth/popup/account-switching/account-switcher.component"; @@ -87,7 +86,7 @@ import "../platform/popup/locales"; imports: [ A11yModule, AppRoutingModule, - BitwardenToastModule.forRoot({ + ToastModule.forRoot({ maxOpened: 2, autoDismiss: true, closeButton: true, diff --git a/apps/browser/src/popup/scss/plugins.scss b/apps/browser/src/popup/scss/plugins.scss deleted file mode 100644 index e1e386d62d4..00000000000 --- a/apps/browser/src/popup/scss/plugins.scss +++ /dev/null @@ -1,98 +0,0 @@ -@import "~ngx-toastr/toastr"; - -@import "variables.scss"; -@import "buttons.scss"; - -// Toaster - -.toast-container { - .toast-close-button { - @include themify($themes) { - color: themed("toastTextColor"); - } - font-size: 18px; - margin-right: 4px; - } - - .ngx-toastr { - @include themify($themes) { - color: themed("toastTextColor"); - } - align-items: center; - background-image: none !important; - border-radius: $border-radius; - box-shadow: 0 0 8px rgba(0, 0, 0, 0.35); - display: flex; - padding: 15px; - - .toast-close-button { - position: absolute; - right: 5px; - top: 0; - } - - &:hover { - box-shadow: 0 0 10px rgba(0, 0, 0, 0.6); - } - - .icon i::before { - float: left; - font-style: normal; - font-family: $icomoon-font-family; - font-size: 25px; - line-height: 20px; - padding-right: 15px; - } - - .toast-message { - p { - margin-bottom: 0.5rem; - - &:last-child { - margin-bottom: 0; - } - } - } - - &.toast-danger, - &.toast-error { - @include themify($themes) { - background-color: themed("dangerColor"); - } - - .icon i::before { - content: map_get($icons, "error"); - } - } - - &.toast-warning { - @include themify($themes) { - background-color: themed("warningColor"); - } - - .icon i::before { - content: map_get($icons, "exclamation-triangle"); - } - } - - &.toast-info { - @include themify($themes) { - background-color: themed("infoColor"); - } - - .icon i:before { - content: map_get($icons, "info-circle"); - } - } - - &.toast-success { - @include themify($themes) { - background-color: themed("successColor"); - } - - .icon i:before { - content: map_get($icons, "check"); - } - } - } -} diff --git a/apps/browser/src/popup/scss/popup.scss b/apps/browser/src/popup/scss/popup.scss index 0d7e4281386..850ef96c64e 100644 --- a/apps/browser/src/popup/scss/popup.scss +++ b/apps/browser/src/popup/scss/popup.scss @@ -8,7 +8,6 @@ @import "buttons.scss"; @import "misc.scss"; @import "modal.scss"; -@import "plugins.scss"; @import "environment.scss"; @import "pages.scss"; @import "@angular/cdk/overlay-prebuilt.css"; diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 4906198047a..f3be8490c12 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -1,7 +1,5 @@ import { APP_INITIALIZER, NgModule, NgZone } from "@angular/core"; -import { DomSanitizer } from "@angular/platform-browser"; import { Router } from "@angular/router"; -import { ToastrService } from "ngx-toastr"; import { UnauthGuard as BaseUnauthGuardService } from "@bitwarden/angular/auth/guards"; import { AngularThemingService } from "@bitwarden/angular/platform/services/theming/angular-theming.service"; @@ -83,7 +81,7 @@ import { FolderService as FolderServiceAbstraction } from "@bitwarden/common/vau import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service"; import { TotpService } from "@bitwarden/common/vault/services/totp.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { UnauthGuardService } from "../../auth/popup/services"; import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service"; @@ -259,15 +257,9 @@ const safeProviders: SafeProvider[] = [ }), safeProvider({ provide: PlatformUtilsService, - useExisting: ForegroundPlatformUtilsService, - }), - safeProvider({ - provide: ForegroundPlatformUtilsService, - useClass: ForegroundPlatformUtilsService, - useFactory: (sanitizer: DomSanitizer, toastrService: ToastrService) => { + useFactory: (toastService: ToastService) => { return new ForegroundPlatformUtilsService( - sanitizer, - toastrService, + toastService, (clipboardValue: string, clearMs: number) => { void BrowserApi.sendMessage("clearClipboard", { clipboardValue, clearMs }); }, @@ -284,7 +276,7 @@ const safeProviders: SafeProvider[] = [ window, ); }, - deps: [DomSanitizer, ToastrService], + deps: [ToastService], }), safeProvider({ provide: PasswordGenerationServiceAbstraction, diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index b2b44e6b217..ad99a3a4474 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -3,14 +3,11 @@ import { NgZone, OnDestroy, OnInit, - SecurityContext, Type, ViewChild, ViewContainerRef, } from "@angular/core"; -import { DomSanitizer } from "@angular/platform-browser"; import { Router } from "@angular/router"; -import { IndividualConfig, ToastrService } from "ngx-toastr"; import { firstValueFrom, Subject, takeUntil } from "rxjs"; import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; @@ -49,7 +46,7 @@ import { CollectionService } from "@bitwarden/common/vault/abstractions/collecti import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { DeleteAccountComponent } from "../auth/delete-account.component"; import { LoginApprovalComponent } from "../auth/login/login-approval.component"; @@ -129,9 +126,8 @@ export class AppComponent implements OnInit, OnDestroy { private cipherService: CipherService, private authService: AuthService, private router: Router, - private toastrService: ToastrService, + private toastService: ToastService, private i18nService: I18nService, - private sanitizer: DomSanitizer, private ngZone: NgZone, private vaultTimeoutService: VaultTimeoutService, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, @@ -294,7 +290,7 @@ export class AppComponent implements OnInit, OnDestroy { ); break; case "showToast": - this.showToast(message); + this.toastService._showToast(message); break; case "copiedToClipboard": if (!message.clearing) { @@ -674,34 +670,6 @@ export class AppComponent implements OnInit, OnDestroy { }); } - private showToast(msg: any) { - let message = ""; - - const options: Partial = {}; - - if (typeof msg.text === "string") { - message = msg.text; - } else if (msg.text.length === 1) { - message = msg.text[0]; - } else { - msg.text.forEach( - (t: string) => - (message += "

" + this.sanitizer.sanitize(SecurityContext.HTML, t) + "

"), - ); - options.enableHtml = true; - } - if (msg.options != null) { - if (msg.options.trustedHtml === true) { - options.enableHtml = true; - } - if (msg.options.timeout != null && msg.options.timeout > 0) { - options.timeOut = msg.options.timeout; - } - } - - this.toastrService.show(message, msg.title, options, "toast-" + msg.type); - } - private routeToVault(action: string, cipherType: CipherType) { if (!this.router.url.includes("vault")) { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 3d2b40ac625..ff9cbc97ccb 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/scss/plugins.scss b/apps/desktop/src/scss/plugins.scss deleted file mode 100644 index c156456809c..00000000000 --- a/apps/desktop/src/scss/plugins.scss +++ /dev/null @@ -1,95 +0,0 @@ -@import "~ngx-toastr/toastr"; - -@import "variables.scss"; - -.toast-container { - .toast-close-button { - @include themify($themes) { - color: themed("toastTextColor"); - } - font-size: 18px; - margin-right: 4px; - } - - .ngx-toastr { - @include themify($themes) { - color: themed("toastTextColor"); - } - align-items: center; - background-image: none !important; - border-radius: $border-radius; - box-shadow: 0 0 8px rgba(0, 0, 0, 0.35); - display: flex; - padding: 15px; - - .toast-close-button { - position: absolute; - right: 5px; - top: 0; - } - - &:hover { - box-shadow: 0 0 10px rgba(0, 0, 0, 0.6); - } - - .icon i::before { - float: left; - font-style: normal; - font-family: $icomoon-font-family; - font-size: 25px; - line-height: 20px; - padding-right: 15px; - } - - .toast-message { - p { - margin-bottom: 0.5rem; - - &:last-child { - margin-bottom: 0; - } - } - } - - &.toast-danger, - &.toast-error { - @include themify($themes) { - background-color: themed("dangerColor"); - } - - .icon i::before { - content: map_get($icons, "error"); - } - } - - &.toast-warning { - @include themify($themes) { - background-color: themed("warningColor"); - } - - .icon i::before { - content: map_get($icons, "exclamation-triangle"); - } - } - - &.toast-info { - @include themify($themes) { - background-color: themed("infoColor"); - } - - .icon i:before { - content: map_get($icons, "info-circle"); - } - } - - &.toast-success { - @include themify($themes) { - background-color: themed("successColor"); - } - - .icon i:before { - content: map_get($icons, "check"); - } - } - } -} diff --git a/apps/desktop/src/scss/styles.scss b/apps/desktop/src/scss/styles.scss index 033a0f8b674..54c1385dcf0 100644 --- a/apps/desktop/src/scss/styles.scss +++ b/apps/desktop/src/scss/styles.scss @@ -11,7 +11,6 @@ @import "buttons.scss"; @import "misc.scss"; @import "modal.scss"; -@import "plugins.scss"; @import "environment.scss"; @import "header.scss"; @import "left-nav.scss"; diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index 7a3b34969a5..1da2d94c15b 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -1,9 +1,7 @@ import { DOCUMENT } from "@angular/common"; -import { Component, Inject, NgZone, OnDestroy, OnInit, SecurityContext } from "@angular/core"; -import { DomSanitizer } from "@angular/platform-browser"; +import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core"; import { NavigationEnd, Router } from "@angular/router"; import * as jq from "jquery"; -import { IndividualConfig, ToastrService } from "ngx-toastr"; import { Subject, switchMap, takeUntil, timer } from "rxjs"; import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service"; @@ -29,7 +27,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { PolicyListService } from "./admin-console/core/policy-list.service"; import { @@ -68,14 +66,13 @@ export class AppComponent implements OnDestroy, OnInit { private cipherService: CipherService, private authService: AuthService, private router: Router, - private toastrService: ToastrService, + private toastService: ToastService, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private ngZone: NgZone, private vaultTimeoutService: VaultTimeoutService, private cryptoService: CryptoService, private collectionService: CollectionService, - private sanitizer: DomSanitizer, private searchService: SearchService, private notificationsService: NotificationsService, private stateService: StateService, @@ -209,7 +206,7 @@ export class AppComponent implements OnDestroy, OnInit { break; } case "showToast": - this.showToast(message); + this.toastService._showToast(message); break; case "convertAccountToKeyConnector": // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. @@ -327,34 +324,6 @@ export class AppComponent implements OnDestroy, OnInit { }, IdleTimeout); } - private showToast(msg: any) { - let message = ""; - - const options: Partial = {}; - - if (typeof msg.text === "string") { - message = msg.text; - } else if (msg.text.length === 1) { - message = msg.text[0]; - } else { - msg.text.forEach( - (t: string) => - (message += "

" + this.sanitizer.sanitize(SecurityContext.HTML, t) + "

"), - ); - options.enableHtml = true; - } - if (msg.options != null) { - if (msg.options.trustedHtml === true) { - options.enableHtml = true; - } - if (msg.options.timeout != null && msg.options.timeout > 0) { - options.timeOut = msg.options.timeout; - } - } - - this.toastrService.show(message, msg.title, options, "toast-" + msg.type); - } - private idleStateChanged() { if (this.isIdle) { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. diff --git a/apps/web/src/app/shared/shared.module.ts b/apps/web/src/app/shared/shared.module.ts index bc775f07e2d..1b04583a395 100644 --- a/apps/web/src/app/shared/shared.module.ts +++ b/apps/web/src/app/shared/shared.module.ts @@ -4,7 +4,6 @@ import { NgModule } from "@angular/core"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { RouterModule } from "@angular/router"; import { InfiniteScrollModule } from "ngx-infinite-scroll"; -import { ToastrModule } from "ngx-toastr"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { @@ -52,7 +51,6 @@ import "./locales"; ReactiveFormsModule, InfiniteScrollModule, RouterModule, - ToastrModule, JslibModule, // Component library modules @@ -90,7 +88,6 @@ import "./locales"; ReactiveFormsModule, InfiniteScrollModule, RouterModule, - ToastrModule, JslibModule, // Component library diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 7632392c237..f14574508c7 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, diff --git a/apps/web/src/scss/styles.scss b/apps/web/src/scss/styles.scss index 98b3512ba59..8fbea200a96 100644 --- a/apps/web/src/scss/styles.scss +++ b/apps/web/src/scss/styles.scss @@ -43,8 +43,6 @@ @import "~bootstrap/scss/_utilities"; @import "~bootstrap/scss/_print"; -@import "~ngx-toastr/toastr"; - @import "./base"; @import "./buttons"; @import "./callouts"; @@ -54,5 +52,4 @@ @import "./pages"; @import "./plugins"; @import "./tables"; -@import "./toasts"; @import "./vault-filters"; diff --git a/apps/web/src/scss/toasts.scss b/apps/web/src/scss/toasts.scss deleted file mode 100644 index 6685de6449d..00000000000 --- a/apps/web/src/scss/toasts.scss +++ /dev/null @@ -1,117 +0,0 @@ -.toast-container { - .toast-close-button { - font-size: 18px; - margin-right: 4px; - } - - .ngx-toastr { - align-items: center; - background-image: none !important; - border-radius: $border-radius; - box-shadow: 0 0 8px rgba(0, 0, 0, 0.35); - display: flex; - padding: 15px; - - .toast-close-button { - position: absolute; - right: 5px; - top: 0; - } - - &:hover { - box-shadow: 0 0 10px rgba(0, 0, 0, 0.6); - } - - .icon i::before { - float: left; - font-style: normal; - font-family: $icomoon-font-family; - font-size: 25px; - line-height: 20px; - padding-right: 15px; - } - - .toast-message { - p { - margin-bottom: 0.5rem; - - &:last-child { - margin-bottom: 0; - } - } - } - - &.toast-danger, - &.toast-error { - @include themify($themes) { - background-color: themed("danger"); - } - - &, - &:before, - & .toast-close-button { - @include themify($themes) { - color: themed("textDangerColor") !important; - } - } - - .icon i::before { - content: map_get($icons, "error"); - } - } - - &.toast-warning { - @include themify($themes) { - background-color: themed("warning"); - } - - &, - &:before, - & .toast-close-button { - @include themify($themes) { - color: themed("textWarningColor") !important; - } - } - - .icon i::before { - content: map_get($icons, "exclamation-triangle"); - } - } - - &.toast-info { - @include themify($themes) { - background-color: themed("info"); - } - - &, - &:before, - & .toast-close-button { - @include themify($themes) { - color: themed("textInfoColor") !important; - } - } - - .icon i:before { - content: map_get($icons, "info-circle"); - } - } - - &.toast-success { - @include themify($themes) { - background-color: themed("success"); - } - - &, - &:before, - & .toast-close-button { - @include themify($themes) { - color: themed("textSuccessColor") !important; - } - } - - .icon i:before { - content: map_get($icons, "check"); - } - } - } -} diff --git a/libs/angular/src/components/toastr.component.ts b/libs/angular/src/components/toastr.component.ts deleted file mode 100644 index bfe20ed8666..00000000000 --- a/libs/angular/src/components/toastr.component.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { animate, state, style, transition, trigger } from "@angular/animations"; -import { CommonModule } from "@angular/common"; -import { Component, ModuleWithProviders, NgModule } from "@angular/core"; -import { - DefaultNoComponentGlobalConfig, - GlobalConfig, - Toast as BaseToast, - ToastPackage, - ToastrService, - TOAST_CONFIG, -} from "ngx-toastr"; - -@Component({ - selector: "[toast-component2]", - template: ` - -
- -
-
-
- {{ title }} [{{ duplicatesCount + 1 }}] -
-
-
- {{ message }} -
-
-
-
-
- `, - animations: [ - trigger("flyInOut", [ - state("inactive", style({ opacity: 0 })), - state("active", style({ opacity: 1 })), - state("removed", style({ opacity: 0 })), - transition("inactive => active", animate("{{ easeTime }}ms {{ easing }}")), - transition("active => removed", animate("{{ easeTime }}ms {{ easing }}")), - ]), - ], - preserveWhitespaces: false, -}) -export class BitwardenToast extends BaseToast { - constructor( - protected toastrService: ToastrService, - public toastPackage: ToastPackage, - ) { - super(toastrService, toastPackage); - } -} - -export const BitwardenToastGlobalConfig: GlobalConfig = { - ...DefaultNoComponentGlobalConfig, - toastComponent: BitwardenToast, -}; - -@NgModule({ - imports: [CommonModule], - declarations: [BitwardenToast], - exports: [BitwardenToast], -}) -export class BitwardenToastModule { - static forRoot(config: Partial = {}): ModuleWithProviders { - return { - ngModule: BitwardenToastModule, - providers: [ - { - provide: TOAST_CONFIG, - useValue: { - default: BitwardenToastGlobalConfig, - config: config, - }, - }, - ], - }; - } -} diff --git a/libs/angular/src/jslib.module.ts b/libs/angular/src/jslib.module.ts index 64fb44e3b8b..5f1bf796aa9 100644 --- a/libs/angular/src/jslib.module.ts +++ b/libs/angular/src/jslib.module.ts @@ -2,10 +2,9 @@ import { CommonModule, DatePipe } from "@angular/common"; import { NgModule } from "@angular/core"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; -import { AutofocusDirective } from "@bitwarden/components"; +import { AutofocusDirective, ToastModule } from "@bitwarden/components"; import { CalloutComponent } from "./components/callout.component"; -import { BitwardenToastModule } from "./components/toastr.component"; import { A11yInvalidDirective } from "./directives/a11y-invalid.directive"; import { A11yTitleDirective } from "./directives/a11y-title.directive"; import { ApiActionDirective } from "./directives/api-action.directive"; @@ -34,7 +33,7 @@ import { IconComponent } from "./vault/components/icon.component"; @NgModule({ imports: [ - BitwardenToastModule.forRoot({ + ToastModule.forRoot({ maxOpened: 5, autoDismiss: true, closeButton: true, @@ -77,7 +76,7 @@ import { IconComponent } from "./vault/components/icon.component"; A11yTitleDirective, ApiActionDirective, AutofocusDirective, - BitwardenToastModule, + ToastModule, BoxRowDirective, CalloutComponent, CopyTextDirective, diff --git a/libs/common/src/platform/abstractions/platform-utils.service.ts b/libs/common/src/platform/abstractions/platform-utils.service.ts index d518a17f7b4..f2dff46c780 100644 --- a/libs/common/src/platform/abstractions/platform-utils.service.ts +++ b/libs/common/src/platform/abstractions/platform-utils.service.ts @@ -28,6 +28,11 @@ export abstract class PlatformUtilsService { abstract getApplicationVersionNumber(): Promise; abstract supportsWebAuthn(win: Window): boolean; abstract supportsDuo(): boolean; + /** + * @deprecated use `@bitwarden/components/ToastService.showToast` instead + * + * Jira: [CL-213](https://bitwarden.atlassian.net/browse/CL-213) + */ abstract showToast( type: "error" | "success" | "warning" | "info", title: string, diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index 139e69ebb66..527d5f36153 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -29,6 +29,7 @@ export * from "./section"; export * from "./select"; export * from "./table"; export * from "./tabs"; +export * from "./toast"; export * from "./toggle-group"; export * from "./typography"; export * from "./utils/i18n-mock.service"; diff --git a/libs/components/src/toast/index.ts b/libs/components/src/toast/index.ts new file mode 100644 index 00000000000..f0b55402194 --- /dev/null +++ b/libs/components/src/toast/index.ts @@ -0,0 +1,2 @@ +export * from "./toast.module"; +export * from "./toast.service"; diff --git a/libs/components/src/toast/toast.component.html b/libs/components/src/toast/toast.component.html new file mode 100644 index 00000000000..f301995d0a6 --- /dev/null +++ b/libs/components/src/toast/toast.component.html @@ -0,0 +1,24 @@ +
+
+ +
+ {{ variant | i18n }} +

{{ title }}

+

+ {{ m }} +

+
+ +
+
+
diff --git a/libs/components/src/toast/toast.component.ts b/libs/components/src/toast/toast.component.ts new file mode 100644 index 00000000000..4a31e005868 --- /dev/null +++ b/libs/components/src/toast/toast.component.ts @@ -0,0 +1,66 @@ +import { Component, EventEmitter, Input, Output } from "@angular/core"; + +import { IconButtonModule } from "../icon-button"; +import { SharedModule } from "../shared"; + +export type ToastVariant = "success" | "error" | "info" | "warning"; + +const variants: Record = { + success: { + icon: "bwi-check", + bgColor: "tw-bg-success-600", + }, + error: { + icon: "bwi-error", + bgColor: "tw-bg-danger-600", + }, + info: { + icon: "bwi-info-circle", + bgColor: "tw-bg-info-600", + }, + warning: { + icon: "bwi-exclamation-triangle", + bgColor: "tw-bg-warning-600", + }, +}; + +@Component({ + selector: "bit-toast", + templateUrl: "toast.component.html", + standalone: true, + imports: [SharedModule, IconButtonModule], +}) +export class ToastComponent { + @Input() variant: ToastVariant = "info"; + + /** + * The message to display + * + * Pass an array to render multiple paragraphs. + **/ + @Input({ required: true }) + message: string | string[]; + + /** An optional title to display over the message. */ + @Input() title: string; + + /** + * The percent width of the progress bar, from 0-100 + **/ + @Input() progressWidth = 0; + + /** Emits when the user presses the close button */ + @Output() onClose = new EventEmitter(); + + protected get iconClass(): string { + return variants[this.variant].icon; + } + + protected get bgColor(): string { + return variants[this.variant].bgColor; + } + + protected get messageArray(): string[] { + return Array.isArray(this.message) ? this.message : [this.message]; + } +} diff --git a/libs/components/src/toast/toast.module.ts b/libs/components/src/toast/toast.module.ts new file mode 100644 index 00000000000..bf39a0be9ad --- /dev/null +++ b/libs/components/src/toast/toast.module.ts @@ -0,0 +1,39 @@ +import { CommonModule } from "@angular/common"; +import { ModuleWithProviders, NgModule } from "@angular/core"; +import { DefaultNoComponentGlobalConfig, GlobalConfig, TOAST_CONFIG } from "ngx-toastr"; + +import { ToastComponent } from "./toast.component"; +import { BitwardenToastrComponent } from "./toastr.component"; + +@NgModule({ + imports: [CommonModule, ToastComponent], + declarations: [BitwardenToastrComponent], + exports: [BitwardenToastrComponent], +}) +export class ToastModule { + static forRoot(config: Partial = {}): ModuleWithProviders { + return { + ngModule: ToastModule, + providers: [ + { + provide: TOAST_CONFIG, + useValue: { + default: BitwardenToastrGlobalConfig, + config: config, + }, + }, + ], + }; + } +} + +export const BitwardenToastrGlobalConfig: GlobalConfig = { + ...DefaultNoComponentGlobalConfig, + toastComponent: BitwardenToastrComponent, + tapToDismiss: false, + timeOut: 5000, + extendedTimeOut: 2000, + maxOpened: 5, + autoDismiss: true, + progressBar: true, +}; diff --git a/libs/components/src/toast/toast.service.ts b/libs/components/src/toast/toast.service.ts new file mode 100644 index 00000000000..8bbff02c418 --- /dev/null +++ b/libs/components/src/toast/toast.service.ts @@ -0,0 +1,57 @@ +import { Injectable } from "@angular/core"; +import { IndividualConfig, ToastrService } from "ngx-toastr"; + +import type { ToastComponent } from "./toast.component"; +import { calculateToastTimeout } from "./utils"; + +export type ToastOptions = { + /** + * The duration the toast will persist in milliseconds + **/ + timeout?: number; +} & Pick; + +/** + * Presents toast notifications + **/ +@Injectable({ providedIn: "root" }) +export class ToastService { + constructor(private toastrService: ToastrService) {} + + showToast(options: ToastOptions) { + const toastrConfig: Partial = { + payload: { + message: options.message, + variant: options.variant, + title: options.title, + }, + timeOut: + options.timeout != null && options.timeout > 0 + ? options.timeout + : calculateToastTimeout(options.message), + }; + + this.toastrService.show(null, options.title, toastrConfig); + } + + /** + * @deprecated use `showToast` instead + * + * Converts options object from PlatformUtilsService + **/ + _showToast(options: { + type: "error" | "success" | "warning" | "info"; + title: string; + text: string | string[]; + options?: { + timeout?: number; + }; + }) { + this.showToast({ + message: options.text, + variant: options.type, + title: options.title, + timeout: options.options?.timeout, + }); + } +} diff --git a/libs/components/src/toast/toast.spec.ts b/libs/components/src/toast/toast.spec.ts new file mode 100644 index 00000000000..92d8071dc53 --- /dev/null +++ b/libs/components/src/toast/toast.spec.ts @@ -0,0 +1,16 @@ +import { calculateToastTimeout } from "./utils"; + +describe("Toast default timer", () => { + it("should have a minimum of 5000ms", () => { + expect(calculateToastTimeout("")).toBe(5000); + expect(calculateToastTimeout([""])).toBe(5000); + expect(calculateToastTimeout(" ")).toBe(5000); + }); + + it("should return an extra second for each 120 words", () => { + expect(calculateToastTimeout("foo ".repeat(119))).toBe(5000); + expect(calculateToastTimeout("foo ".repeat(120))).toBe(6000); + expect(calculateToastTimeout("foo ".repeat(240))).toBe(7000); + expect(calculateToastTimeout(["foo ".repeat(120), " \n foo ".repeat(120)])).toBe(7000); + }); +}); diff --git a/libs/components/src/toast/toast.stories.ts b/libs/components/src/toast/toast.stories.ts new file mode 100644 index 00000000000..d209453d85c --- /dev/null +++ b/libs/components/src/toast/toast.stories.ts @@ -0,0 +1,124 @@ +import { CommonModule } from "@angular/common"; +import { Component, Input } from "@angular/core"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { action } from "@storybook/addon-actions"; +import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + +import { ButtonModule } from "../button"; +import { I18nMockService } from "../utils/i18n-mock.service"; + +import { ToastComponent } from "./toast.component"; +import { BitwardenToastrGlobalConfig, ToastModule } from "./toast.module"; +import { ToastOptions, ToastService } from "./toast.service"; + +const toastServiceExampleTemplate = ` + +`; +@Component({ + selector: "toast-service-example", + template: toastServiceExampleTemplate, +}) +export class ToastServiceExampleComponent { + @Input() + toastOptions: ToastOptions; + + constructor(protected toastService: ToastService) {} +} + +export default { + title: "Component Library/Toast", + component: ToastComponent, + + decorators: [ + moduleMetadata({ + imports: [CommonModule, BrowserAnimationsModule, ButtonModule], + declarations: [ToastServiceExampleComponent], + }), + applicationConfig({ + providers: [ + ToastModule.forRoot().providers, + { + provide: I18nService, + useFactory: () => { + return new I18nMockService({ + close: "Close", + success: "Success", + error: "Error", + warning: "Warning", + }); + }, + }, + ], + }), + ], + args: { + onClose: action("emit onClose"), + variant: "info", + progressWidth: 50, + title: "", + message: "Hello Bitwarden!", + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library", + }, + }, +} as Meta; + +type Story = StoryObj; + +export const Default: Story = { + render: (args) => ({ + props: args, + template: ` +
+ + + + +
+ `, + }), +}; + +/** + * Avoid using long messages in toasts. + */ +export const LongContent: Story = { + ...Default, + args: { + title: "Foo", + message: [ + "Lorem ipsum dolor sit amet, consectetur adipisci", + "Lorem ipsum dolor sit amet, consectetur adipisci", + ], + }, +}; + +export const Service: Story = { + render: (args) => ({ + props: { + toastOptions: args, + }, + template: ` + + `, + }), + args: { + title: "", + message: "Hello Bitwarden!", + variant: "error", + timeout: BitwardenToastrGlobalConfig.timeOut, + } as ToastOptions, + parameters: { + chromatic: { disableSnapshot: true }, + docs: { + source: { + code: toastServiceExampleTemplate, + }, + }, + }, +}; diff --git a/libs/components/src/toast/toast.tokens.css b/libs/components/src/toast/toast.tokens.css new file mode 100644 index 00000000000..2ff9e99ae50 --- /dev/null +++ b/libs/components/src/toast/toast.tokens.css @@ -0,0 +1,4 @@ +:root { + --bit-toast-width: 19rem; + --bit-toast-width-full: 96%; +} diff --git a/libs/components/src/toast/toastr.component.ts b/libs/components/src/toast/toastr.component.ts new file mode 100644 index 00000000000..70085dfc474 --- /dev/null +++ b/libs/components/src/toast/toastr.component.ts @@ -0,0 +1,26 @@ +import { animate, state, style, transition, trigger } from "@angular/animations"; +import { Component } from "@angular/core"; +import { Toast as BaseToastrComponent } from "ngx-toastr"; + +@Component({ + template: ` + + `, + animations: [ + trigger("flyInOut", [ + state("inactive", style({ opacity: 0 })), + state("active", style({ opacity: 1 })), + state("removed", style({ opacity: 0 })), + transition("inactive => active", animate("{{ easeTime }}ms {{ easing }}")), + transition("active => removed", animate("{{ easeTime }}ms {{ easing }}")), + ]), + ], + preserveWhitespaces: false, +}) +export class BitwardenToastrComponent extends BaseToastrComponent {} diff --git a/libs/components/src/toast/toastr.css b/libs/components/src/toast/toastr.css new file mode 100644 index 00000000000..fabf8caf10c --- /dev/null +++ b/libs/components/src/toast/toastr.css @@ -0,0 +1,23 @@ +@import "~ngx-toastr/toastr"; +@import "./toast.tokens.css"; + +/* Override all default styles from `ngx-toaster` */ +.toast-container .ngx-toastr { + all: unset; + display: block; + width: var(--bit-toast-width); + + /* Needed to make hover states work in Electron, since the toast appears in the draggable region. */ + -webkit-app-region: no-drag; +} + +/* Disable hover styles */ +.toast-container .ngx-toastr:hover { + box-shadow: none; +} + +.toast-container.toast-bottom-full-width .ngx-toastr { + width: var(--bit-toast-width-full); + margin-left: auto; + margin-right: auto; +} diff --git a/libs/components/src/toast/utils.ts b/libs/components/src/toast/utils.ts new file mode 100644 index 00000000000..4c8323f3967 --- /dev/null +++ b/libs/components/src/toast/utils.ts @@ -0,0 +1,14 @@ +/** + * Given a toast message, calculate the ideal timeout length following: + * a minimum of 5 seconds + 1 extra second per 120 additional words + * + * @param message the toast message to be displayed + * @returns the timeout length in milliseconds + */ +export const calculateToastTimeout = (message: string | string[]): number => { + const paragraphs = Array.isArray(message) ? message : [message]; + const numWords = paragraphs + .map((paragraph) => paragraph.split(/\s+/).filter((word) => word !== "")) + .flat().length; + return 5000 + Math.floor(numWords / 120) * 1000; +}; diff --git a/libs/components/src/tw-theme.css b/libs/components/src/tw-theme.css index 0087af28ae8..72e8e1e5e85 100644 --- a/libs/components/src/tw-theme.css +++ b/libs/components/src/tw-theme.css @@ -171,6 +171,9 @@ @import "./popover/popover.component.css"; @import "./search/search.component.css"; +@import "./toast/toast.tokens.css"; +@import "./toast/toastr.css"; + /** * tw-break-words does not work with table cells: * https://github.com/tailwindlabs/tailwindcss/issues/835 From ce75f7b5659aeafe036edf4e27ee8ba1c7608913 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Thu, 18 Apr 2024 14:06:31 -0400 Subject: [PATCH 002/110] Vault/pm-7580/resolve-cipher-update-race (#8806) * Resolve updated values from updates Uses the now returned updated values from cipher service to guarantee-return the updated cipher for CLI edits * Use updated cipher for creation * Use updated cipher for editing collections * Await async methods Cipher data more closely approximates server responses. TODO: this should really use actual response types --- apps/cli/src/commands/edit.command.ts | 6 +-- apps/cli/src/vault/create.command.ts | 6 +-- .../src/vault/abstractions/cipher.service.ts | 37 ++++++++++++++-- .../src/vault/services/cipher.service.spec.ts | 43 +++++++------------ .../src/vault/services/cipher.service.ts | 31 ++++++++----- 5 files changed, 73 insertions(+), 50 deletions(-) diff --git a/apps/cli/src/commands/edit.command.ts b/apps/cli/src/commands/edit.command.ts index 3d4f9529adb..e64ff8b5512 100644 --- a/apps/cli/src/commands/edit.command.ts +++ b/apps/cli/src/commands/edit.command.ts @@ -86,8 +86,7 @@ export class EditCommand { cipherView = CipherExport.toView(req, cipherView); const encCipher = await this.cipherService.encrypt(cipherView); try { - await this.cipherService.updateWithServer(encCipher); - const updatedCipher = await this.cipherService.get(cipher.id); + const updatedCipher = await this.cipherService.updateWithServer(encCipher); const decCipher = await updatedCipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher), ); @@ -111,8 +110,7 @@ export class EditCommand { cipher.collectionIds = req; try { - await this.cipherService.saveCollectionsWithServer(cipher); - const updatedCipher = await this.cipherService.get(cipher.id); + const updatedCipher = await this.cipherService.saveCollectionsWithServer(cipher); const decCipher = await updatedCipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher), ); diff --git a/apps/cli/src/vault/create.command.ts b/apps/cli/src/vault/create.command.ts index b813227109f..78ee04e73c0 100644 --- a/apps/cli/src/vault/create.command.ts +++ b/apps/cli/src/vault/create.command.ts @@ -80,8 +80,7 @@ export class CreateCommand { private async createCipher(req: CipherExport) { const cipher = await this.cipherService.encrypt(CipherExport.toView(req)); try { - await this.cipherService.createWithServer(cipher); - const newCipher = await this.cipherService.get(cipher.id); + const newCipher = await this.cipherService.createWithServer(cipher); const decCipher = await newCipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(newCipher), ); @@ -142,12 +141,11 @@ export class CreateCommand { } try { - await this.cipherService.saveAttachmentRawWithServer( + const updatedCipher = await this.cipherService.saveAttachmentRawWithServer( cipher, fileName, new Uint8Array(fileBuf).buffer, ); - const updatedCipher = await this.cipherService.get(cipher.id); const decCipher = await updatedCipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher), ); diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index 501fd876652..22e2c54a59a 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -47,8 +47,24 @@ export abstract class CipherService { updateLastUsedDate: (id: string) => Promise; updateLastLaunchedDate: (id: string) => Promise; saveNeverDomain: (domain: string) => Promise; - createWithServer: (cipher: Cipher, orgAdmin?: boolean) => Promise; - updateWithServer: (cipher: Cipher, orgAdmin?: boolean, isNotClone?: boolean) => Promise; + /** + * Create a cipher with the server + * + * @param cipher The cipher to create + * @param orgAdmin If true, the request is submitted as an organization admin request + * + * @returns A promise that resolves to the created cipher + */ + createWithServer: (cipher: Cipher, orgAdmin?: boolean) => Promise; + /** + * Update a cipher with the server + * @param cipher The cipher to update + * @param orgAdmin If true, the request is submitted as an organization admin request + * @param isNotClone If true, the cipher is not a clone and should be treated as a new cipher + * + * @returns A promise that resolves to the updated cipher + */ + updateWithServer: (cipher: Cipher, orgAdmin?: boolean, isNotClone?: boolean) => Promise; shareWithServer: ( cipher: CipherView, organizationId: string, @@ -70,7 +86,14 @@ export abstract class CipherService { data: ArrayBuffer, admin?: boolean, ) => Promise; - saveCollectionsWithServer: (cipher: Cipher) => Promise; + /** + * Save the collections for a cipher with the server + * + * @param cipher The cipher to save collections for + * + * @returns A promise that resolves when the collections have been saved + */ + saveCollectionsWithServer: (cipher: Cipher) => Promise; /** * Bulk update collections for many ciphers with the server * @param orgId @@ -84,7 +107,13 @@ export abstract class CipherService { collectionIds: CollectionId[], removeCollections: boolean, ) => Promise; - upsert: (cipher: CipherData | CipherData[]) => Promise; + /** + * Update the local store of CipherData with the provided data. Values are upserted into the existing store. + * + * @param cipher The cipher data to upsert. Can be a single CipherData object or an array of CipherData objects. + * @returns A promise that resolves to a record of updated cipher store, keyed by their cipher ID. Returns all ciphers, not just those updated + */ + upsert: (cipher: CipherData | CipherData[]) => Promise>; replace: (ciphers: { [id: string]: CipherData }) => Promise; clear: (userId: string) => Promise; moveManyWithServer: (ids: string[], folderId: string) => Promise; diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index 28c4bfc653f..9b037531187 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -174,23 +174,20 @@ describe("Cipher Service", () => { it("should call apiService.postCipherAdmin when orgAdmin param is true and the cipher orgId != null", async () => { const spy = jest .spyOn(apiService, "postCipherAdmin") - .mockImplementation(() => Promise.resolve(cipherObj)); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - cipherService.createWithServer(cipherObj, true); + .mockImplementation(() => Promise.resolve(cipherObj.toCipherData())); + await cipherService.createWithServer(cipherObj, true); const expectedObj = new CipherCreateRequest(cipherObj); expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalledWith(expectedObj); }); + it("should call apiService.postCipher when orgAdmin param is true and the cipher orgId is null", async () => { cipherObj.organizationId = null; const spy = jest .spyOn(apiService, "postCipher") - .mockImplementation(() => Promise.resolve(cipherObj)); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - cipherService.createWithServer(cipherObj, true); + .mockImplementation(() => Promise.resolve(cipherObj.toCipherData())); + await cipherService.createWithServer(cipherObj, true); const expectedObj = new CipherRequest(cipherObj); expect(spy).toHaveBeenCalled(); @@ -201,10 +198,8 @@ describe("Cipher Service", () => { cipherObj.collectionIds = ["123"]; const spy = jest .spyOn(apiService, "postCipherCreate") - .mockImplementation(() => Promise.resolve(cipherObj)); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - cipherService.createWithServer(cipherObj); + .mockImplementation(() => Promise.resolve(cipherObj.toCipherData())); + await cipherService.createWithServer(cipherObj); const expectedObj = new CipherCreateRequest(cipherObj); expect(spy).toHaveBeenCalled(); @@ -214,10 +209,8 @@ describe("Cipher Service", () => { it("should call apiService.postCipher when orgAdmin and collectionIds logic is false", async () => { const spy = jest .spyOn(apiService, "postCipher") - .mockImplementation(() => Promise.resolve(cipherObj)); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - cipherService.createWithServer(cipherObj); + .mockImplementation(() => Promise.resolve(cipherObj.toCipherData())); + await cipherService.createWithServer(cipherObj); const expectedObj = new CipherRequest(cipherObj); expect(spy).toHaveBeenCalled(); @@ -229,10 +222,8 @@ describe("Cipher Service", () => { it("should call apiService.putCipherAdmin when orgAdmin and isNotClone params are true", async () => { const spy = jest .spyOn(apiService, "putCipherAdmin") - .mockImplementation(() => Promise.resolve(cipherObj)); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - cipherService.updateWithServer(cipherObj, true, true); + .mockImplementation(() => Promise.resolve(cipherObj.toCipherData())); + await cipherService.updateWithServer(cipherObj, true, true); const expectedObj = new CipherRequest(cipherObj); expect(spy).toHaveBeenCalled(); @@ -243,10 +234,8 @@ describe("Cipher Service", () => { cipherObj.edit = true; const spy = jest .spyOn(apiService, "putCipher") - .mockImplementation(() => Promise.resolve(cipherObj)); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - cipherService.updateWithServer(cipherObj); + .mockImplementation(() => Promise.resolve(cipherObj.toCipherData())); + await cipherService.updateWithServer(cipherObj); const expectedObj = new CipherRequest(cipherObj); expect(spy).toHaveBeenCalled(); @@ -257,10 +246,8 @@ describe("Cipher Service", () => { cipherObj.edit = false; const spy = jest .spyOn(apiService, "putPartialCipher") - .mockImplementation(() => Promise.resolve(cipherObj)); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - cipherService.updateWithServer(cipherObj); + .mockImplementation(() => Promise.resolve(cipherObj.toCipherData())); + await cipherService.updateWithServer(cipherObj); const expectedObj = new CipherPartialRequest(cipherObj); expect(spy).toHaveBeenCalled(); diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 7d06b3185f8..4a13196c9c3 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -573,7 +573,7 @@ export class CipherService implements CipherServiceAbstraction { await this.domainSettingsService.setNeverDomains(domains); } - async createWithServer(cipher: Cipher, orgAdmin?: boolean): Promise { + async createWithServer(cipher: Cipher, orgAdmin?: boolean): Promise { let response: CipherResponse; if (orgAdmin && cipher.organizationId != null) { const request = new CipherCreateRequest(cipher); @@ -588,10 +588,16 @@ export class CipherService implements CipherServiceAbstraction { cipher.id = response.id; const data = new CipherData(response, cipher.collectionIds); - await this.upsert(data); + const updated = await this.upsert(data); + // No local data for new ciphers + return new Cipher(updated[cipher.id as CipherId]); } - async updateWithServer(cipher: Cipher, orgAdmin?: boolean, isNotClone?: boolean): Promise { + async updateWithServer( + cipher: Cipher, + orgAdmin?: boolean, + isNotClone?: boolean, + ): Promise { let response: CipherResponse; if (orgAdmin && isNotClone) { const request = new CipherRequest(cipher); @@ -605,7 +611,9 @@ export class CipherService implements CipherServiceAbstraction { } const data = new CipherData(response, cipher.collectionIds); - await this.upsert(data); + const updated = await this.upsert(data); + // updating with server does not change local data + return new Cipher(updated[cipher.id as CipherId], cipher.localData); } async shareWithServer( @@ -732,11 +740,13 @@ export class CipherService implements CipherServiceAbstraction { return new Cipher(cData); } - async saveCollectionsWithServer(cipher: Cipher): Promise { + async saveCollectionsWithServer(cipher: Cipher): Promise { const request = new CipherCollectionsRequest(cipher.collectionIds); const response = await this.apiService.putCipherCollections(cipher.id, request); const data = new CipherData(response); - await this.upsert(data); + const updated = await this.upsert(data); + // Collection updates don't change local data + return new Cipher(updated[cipher.id as CipherId], cipher.localData); } /** @@ -782,9 +792,9 @@ export class CipherService implements CipherServiceAbstraction { await this.encryptedCiphersState.update(() => ciphers); } - async upsert(cipher: CipherData | CipherData[]): Promise { + async upsert(cipher: CipherData | CipherData[]): Promise> { const ciphers = cipher instanceof CipherData ? [cipher] : cipher; - await this.updateEncryptedCipherState((current) => { + return await this.updateEncryptedCipherState((current) => { ciphers.forEach((c) => (current[c.id as CipherId] = c)); return current; }); @@ -796,12 +806,13 @@ export class CipherService implements CipherServiceAbstraction { private async updateEncryptedCipherState( update: (current: Record) => Record, - ) { + ): Promise> { await this.clearDecryptedCiphersState(); - await this.encryptedCiphersState.update((current) => { + const [, updatedCiphers] = await this.encryptedCiphersState.update((current) => { const result = update(current ?? {}); return result; }); + return updatedCiphers; } async clear(userId?: string): Promise { From 40ba15c07e1a5f719727c62bad2dbc5a4b272101 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Thu, 18 Apr 2024 14:24:16 -0500 Subject: [PATCH 003/110] [SM-956] Secret Manager: Integrations Page (#8701) * add navigation item for integrations and SDKs page * Initial routing to Integrations & SDKs page * Initial add of integrations component * Initial add of SDKs component * add secret manage integration images * remove integration & sdk components in favor of a single component * add integration & integration grid components * add integrations & sdks * rename page & components to integration after design discussion * add external rel attribute for SDK links * remove ts extension * refactor: use pseudo element to cover as a link * refactor: change secondaryText to linkText to align with usage * update icon for integrations * add new badge option for integration cards * hardcode integration/sdk names * add dark mode images for integrations and sdks * update integration/sdk card with dark mode image when applicable * refactor integration types to be an enum * fix enum typings in integration grid test --------- Co-authored-by: Robyn MacCallum --- .../secrets-manager/integrations/ansible.svg | 4 + .../integrations/github-white.svg | 10 + .../secrets-manager/integrations/github.svg | 3 + .../integrations/gitlab-white.svg | 3 + .../secrets-manager/integrations/gitlab.svg | 6 + .../secrets-manager/sdks/c-plus-plus.png | Bin 0 -> 12445 bytes .../images/secrets-manager/sdks/c-sharp.svg | 7 + .../src/images/secrets-manager/sdks/go.svg | 7 + .../secrets-manager/sdks/java-white.svg | 15 ++ .../src/images/secrets-manager/sdks/java.svg | 15 ++ .../src/images/secrets-manager/sdks/php.svg | 4 + .../images/secrets-manager/sdks/python.svg | 19 ++ .../src/images/secrets-manager/sdks/ruby.png | Bin 0 -> 19253 bytes .../src/images/secrets-manager/sdks/wasm.svg | 11 ++ apps/web/src/locales/en/messages.json | 52 ++++++ .../integration-card.component.html | 29 +++ .../integration-card.component.spec.ts | 174 ++++++++++++++++++ .../integration-card.component.ts | 93 ++++++++++ .../integration-grid.component.html | 15 ++ .../integration-grid.component.spec.ts | 81 ++++++++ .../integration-grid.component.ts | 15 ++ .../integrations-routing.module.ts | 17 ++ .../integrations/integrations.component.html | 16 ++ .../integrations.component.spec.ts | 77 ++++++++ .../integrations/integrations.component.ts | 113 ++++++++++++ .../integrations/integrations.module.ts | 15 ++ .../integrations/models/integration.ts | 21 +++ .../layout/navigation.component.html | 6 + .../app/secrets-manager/sm-routing.module.ts | 8 + libs/common/src/enums/index.ts | 1 + .../common/src/enums/integration-type.enum.ts | 4 + 31 files changed, 841 insertions(+) create mode 100644 apps/web/src/images/secrets-manager/integrations/ansible.svg create mode 100644 apps/web/src/images/secrets-manager/integrations/github-white.svg create mode 100644 apps/web/src/images/secrets-manager/integrations/github.svg create mode 100644 apps/web/src/images/secrets-manager/integrations/gitlab-white.svg create mode 100644 apps/web/src/images/secrets-manager/integrations/gitlab.svg create mode 100644 apps/web/src/images/secrets-manager/sdks/c-plus-plus.png create mode 100644 apps/web/src/images/secrets-manager/sdks/c-sharp.svg create mode 100644 apps/web/src/images/secrets-manager/sdks/go.svg create mode 100644 apps/web/src/images/secrets-manager/sdks/java-white.svg create mode 100644 apps/web/src/images/secrets-manager/sdks/java.svg create mode 100644 apps/web/src/images/secrets-manager/sdks/php.svg create mode 100644 apps/web/src/images/secrets-manager/sdks/python.svg create mode 100644 apps/web/src/images/secrets-manager/sdks/ruby.png create mode 100644 apps/web/src/images/secrets-manager/sdks/wasm.svg create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.html create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.spec.ts create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.ts create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.html create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.spec.ts create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.ts create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations-routing.module.ts create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.html create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.ts create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.module.ts create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/integrations/models/integration.ts create mode 100644 libs/common/src/enums/integration-type.enum.ts diff --git a/apps/web/src/images/secrets-manager/integrations/ansible.svg b/apps/web/src/images/secrets-manager/integrations/ansible.svg new file mode 100644 index 00000000000..7a32617ab2f --- /dev/null +++ b/apps/web/src/images/secrets-manager/integrations/ansible.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/web/src/images/secrets-manager/integrations/github-white.svg b/apps/web/src/images/secrets-manager/integrations/github-white.svg new file mode 100644 index 00000000000..030c7c67235 --- /dev/null +++ b/apps/web/src/images/secrets-manager/integrations/github-white.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/apps/web/src/images/secrets-manager/integrations/github.svg b/apps/web/src/images/secrets-manager/integrations/github.svg new file mode 100644 index 00000000000..99e3ffbbe2e --- /dev/null +++ b/apps/web/src/images/secrets-manager/integrations/github.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/images/secrets-manager/integrations/gitlab-white.svg b/apps/web/src/images/secrets-manager/integrations/gitlab-white.svg new file mode 100644 index 00000000000..7f7bf006ee2 --- /dev/null +++ b/apps/web/src/images/secrets-manager/integrations/gitlab-white.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/images/secrets-manager/integrations/gitlab.svg b/apps/web/src/images/secrets-manager/integrations/gitlab.svg new file mode 100644 index 00000000000..9bbc28c37a8 --- /dev/null +++ b/apps/web/src/images/secrets-manager/integrations/gitlab.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/web/src/images/secrets-manager/sdks/c-plus-plus.png b/apps/web/src/images/secrets-manager/sdks/c-plus-plus.png new file mode 100644 index 0000000000000000000000000000000000000000..bac17e2ae270435654cf2c206938ce51ae4b97a9 GIT binary patch literal 12445 zcmV;OFk;V%P)4Tx04R}lkijd1aTv!xV_I0DmMCclDK%QNgX@em?XYHRxai%x88!3k-6S{p z1Dqr`H@P_PB5~lX6mmiiJ5#%`UeCK>8+q#K`{~>BJiq7nK#NmTDr*PUas?wgGwGgR zSadr|*lEX!i+)Ydt3V_YmJ+_TIm)-#EWH`EX2nkW#&@osjo{_$%i&Ou`di-=9jSC) z7yXd*>&hsA%_|()EGUb&gGrpCt>Cu@9Kt%*g0!q>vx`ihnkeiZg38~jWI zuVpv%uN|!Io#|YBPP9*QfG{(-iHpXF5+Ff{DW?DF@mG6-tfSb%V3iCj6l6j`97UG2 zaEKf_V|avpcqRSTt$*LJ?x|jwIFP5F>fX{l6bGSex9$y>f|_W$e28)3;J5|*4%Ks-bgNuq~}3g?|aImy98j0b|kk~uM) z{0+c@qH>~#hi3xGNRGQ~1U9od$H}4l|7u~*Wtmwf^vrbkRPAR!GgDphTlMWquij&T zIzc1AaU3&mTgq^jvpobHGlT2)a_V zaKKOugo(hkjd88zYWaMb45lLYqTMMa!!dab!+mUHnC)9<^)GQJE)DA+1Ss+RgR{%I z#BiLfAU1gcj%+d5IL%#-=pZ1+K{3c=t81F2ZrU|vip@a~>J=aaDDeUWyrXPqZ%!Bn zjBIoLbkb1(5B(Tm(rqmJ(zdw+fAug48jWWJDDij()RW}qd=}S-wJBEMy6qWH;=39L z3HS}mm{)F{HTbftacDT-AV7)d8(dvV_2tjwo9ir!zXNa!F~W__5|^TS6G+Q3@R7ZJ+RroRcj_ zF@-)p%Em{@GS_h3_gd`ie*f`N20@=JC7utuz}&3~{a6+@;s(YjA0+R$Byr2-m|Vsz z_;gnPM89PkBpFfS@z~JQx8XXDRjve%o55qU0Z$4$$!9XPdfBGG_ACf^VI)!jN<1DK zvR=$^qoDFE5i4 zM|2J{@pi-;v9tSsFOC8Wvy_y09=6MzO(}g?nQa3`f7HVy#2t-+Ju`oAdX`J7az3sMC7z1_ zlMAV{W0R+0XWM|FwQw<7Pz1xn_dFRVTeXRbpSj%Llz1*cHGNysIJuQwiEEo-E(8ze zAZaYiEu`0vZr=qc@tVNs#jUBG%&hgF7~~23r&u+C)&CwbC=|!RE=I*H+44gF%>Vh* zFE=4TJJU^YT>mk9^Mz(0Q@?`K%U97S!a)$~bsGdYhN+Nq%*R$4^WnCceJgJhJm?-% z;x%Y;BIZ@tA?11C*kyQoz%4x|4U(Z>Z$=;+Ckw0b_~Emgy@UG~CEktppS2}Mdj0R932ggPH0z=0KaK)1=1w)LB) z-JRpZoCKeA&=T+UFN?#f4JGqsAbShn#F2t;DM=NOt3mtbF>ub@2Pay-?g>o`<CNSeSd1`moV!@3Q<|%9=$<#a4O~{^q%Kd`50n~Y@@|y`E>g{X`KIwo_NoH zxi3s(Z2KpMk*&eq%64wGCk-qr1hBK_a3I@o=d8Y)Vi<-MRpA>IR@B6s8=Lfyjl=6Y zz&y~XUi7Dg5x`EH1e|>SmN|V-h_#xcB;Ndw&$Y9tG$>ceJdIn>GO>bB%k2#UWW-~? zRWq>kqgew{5L;YMRK%OU?L>sa+H5geyID*LC$5g>!_w+yzUfC^I)DvFvlcx9oF3oVE4R$*{|EWuE!0SfLT8Z81ZIr zO`L;PQr9C0&4Z%3X(fe*Kq1bs)-aqUcFUY07JnDc@5IAtpf%c{qRw3ZuQk~~XR<B zgT?QYA6S|EbyU8a!EdsTQ}a3TUj970xz48jI~r}AkECnnxER6dOSscKR0$nhg+M3# z@6b{McQn^Pvrq+Sl>CUGnyf68RGXpJXo1XPJ!F;Yp}5)vRXV@Vu7uyn9Ms~8@kZ1- zw;A<+=KHe1J|o_OJ?Uy=q47D!#w|r@f@oh>LQH8#;t1CFt_bMaJ`B3F358Y>N>IxA zA(^W)>jk4zxHrQujXua?i=Ej&)>r3$J|W&*RJ&l~tZ1~6 zx#xdX3F0r!a8$d$XB6~Cvh{2qE;uBtI91Im(Zkt{YDmeegGxFYUT-4~5^w}ui1MGm z@!e>=5^uqd^hl%L_yMlrrt@7;v85NPlEHvZkua=V6!h&NJhC~%ldKmn7i!_u)hbBH zsRaxE4t6JFm`zHVa@9vOdf`qeKaMxz&5XU+o8gqZu~@-j3)~6*?V73Jf!-})1deba zDsdl8l67EwP7NGPEQfM!aIXnbz$3@P$shZCE1@4fpM-ya30EQ;?0ar=z@2FkK-+ZJDq1E z-6x$|hrp9V+TwIFIP@#UW!J(F7mI_LfMtwEK4i<(zPaA?YOjcg8k)ncN;VEhx}Xzk zZWW<|iG$n1echV}<(;LIX;tvkg_0oMh~W|~%E)2cr*%Q?XP!BQXVI?5vZ`$B@%%QZ zNJrLf69>0}2Ya;urNY0*0N%6}-*NBn5e37#HiLb*{yUIVj)(L%zOxW;ddvWYxfXBH zz(P+8>G6n%qF=pnzKhzPf=9Ty_nqM|?ZFNxNuu-=Z%M`@(lR?v6hcaVoh0k$s8VK& z&2}NL_zx-TC!BZm{C0jGk9aZ;HsD6HxVP^WK^3eePP-RQ z=nTiNRKo6a#VD#K>Cul(&Mz^=Wfz2)EY{W7(BnO6Y>!5~8K0iNUnWMsG z^Os2A@~oJ6Xj;FPn=q&~YN7Dx{p4B3B9BEzzgHgZ0>8wU!JhcidI4^cR|#qiM*YdO zYpR0E8Wq-s{M#MQ__%5K!)@ZZ-J@RqJifWkB1=K0x3}jmwa;*k9A-V-5y+a3I?oYE zDbT_fzvV-d4zq3cq{M{xVcFS<7@oxOU*6{Sb)WVmuPQr&j zd7n+i7h5FWuHTOXS2j%zwEj&0F&fLousO+2Zc-A%lw zZT;@$t(z7bHM%8F-(wX0WMu@`qd1MSj^%Z(Sa^Ho}-+1o{^Yg5t&6?U6=4dROa zD>*74;$PJH7lBDARBl0)6bDh%ED)~RVA3DEoXM8dXalxw9kj$7*}DZy8cL^^f#~=o z&nfra0o8g7T*$2PTM-Oqi}gZwK3pj*3-w#3PBIEg;tl8&0ngnp*<(m2O)Wg{S))3l z4y;^=%l0w(IAAYbD=;PIm4uk^Y)%L+=!i$o>1JYs6E%9&1r`C)1k)bs0OcrVmRoM{ zNnr}AYK=!PXEJ(|CB!Egc$lc$=Xf6GBZz2WYB?+z--S2Td4i~v$P1yyt5x&HbmaZU zN44H)`86@kxaZOp4U*2=%@f{H4jST-5pKcwPS6t7=}{*v1jr%#OL)$wQg}ZLG25)R z<5#l{yU!;vc@@>}pUL?NnxL6#P8!-4qTAYq%Kd~c&BkQ}h%(;^cejBb&X>3xr(r;= zm1WaP%8VDX3p99LM^MwshUK(>4+`S-?-+rKh^UQAd!q~W^RYo~pahjEok7J)?zp1r zI`({KuDPUE7x3LmSIaGV;x$Kg=+8gU!BsqHxBw&YBp&gkACm zT_xQkhoUl`d%IEddt&RA zJL<%Vz_Y{esJG*wGhf%=QcFkhPMSoz7@sk6p>*cArnS;hIkH#{=$S zElJ`%g;Pwiii8nMuEa`DGU}*IL%Vf#M$cpmLs3en{|!5QL!z6DN?PaJ)7hp)JI|}G zmDiCOnIu(6N{)EM{@egOw-cK?hi6wrEqYp!ju*{bM6)w7G(!HrJXQdG&{WvSUM929Xr;`k+>;Xf$su21l&!>^xOi zzi#tqdCPFk_%Bqqeju>|o*2>whT^t;pd8W7`oZ2*%c}3=l+`+Opo$TYd?iJ^$54%f z&|OKj3BE?cT}GXgfpn#{CWt+pha+wU%y{@NXpNgP{&Fl5?$=8N)`ef4wPh4nOMY7E z2v^AvuNw+LcW)=`(N2iA1+q%@u;Irn7=zQ#as694>La}RCFE9F7wtR;`x5d=-;+%L za~CAZRQ$m{ZYz3n7b}m$=(x}xR~8>TuHS)C^k-9X=i@WbtNYsh9Po&8l~`raEQ68_kYR8&g>ht z2EKrltUWjUoP}n$y5M2FcRV?mRA_zu>$8f4Yn8qzs;JWxUT^gP&fz_TH_ap?-FqS< zogI22CG*d{S*Qifv%~wcS|dAU;}O%?b$co4?4UOK6g~0oY@va>@br7Tq?p8Vlr_lq^6K|LRul?*e zUH}=z`bJQT?`L|k4!%OE7gyYl;~CcOulyU9eHX{zaY4{;p4<*I(Xa6aq6n0b+~Jf8 zIDS<+Yns#P)zG132#j@d)R3HCZJqY%Z_I`4a?$rCfA%9$gVjDtgSOX%GtDeTsSvU) zFE#g{v&AqF$@aQ5J$9`T~v2Fi<>Ol{~=3QKL5a*9+)K1w(rbTrWtuWf@JHXq>Pa+2{`H8|RNCgI_7 zCjJVD!&*Ugo#kvsuDL?1r~2-Fh`VTs7u{AE%Z)>LZYKp-YcLwmX6C}RvT8N%%hJsg zA2+IKiPr(o@B-}$DvhENfOdqJ84WeY z6T&{KWND37cQQREq`Xcq%J%mWEPhQ^l)$$^O<93tXT)sqL>k-KU_cICA?^T~{B9Ioen zzfvIN0tZ>yL`l4G+|>-UtJVoqL`-KgThR`N zoNt&t*!vDx`OG~I<5ug<@cy3UTb}*jh^|hLC+_LuT#X@F#w>ek5K8YjzGbrc@Kq?N z^!}wA_#S)}$0iTCF}bcILnzROXRCjG*ZcuSrbAHlsDUBOH)3q0}A47V#q^=X(&t=@P#{hGS4x=xhs z=u>b6o;YnV4;zTw$_|t>n=S0c)GPYb{1T202~&eC@S9D6!a}V1ijsJ&0MskU8UK&E zM9k|-_BHeQ#MDre#d7^#lu6G*NVpOIiBth)ph%b~O5zy>u(6?1`d6N~pt!_*`ce{G zRja*u9cN=@psfu7gPw!12qUONoNo_`q*De%F-1u{1FA+3vSl@N|~<2=q{jD#wDGigFu2o1O4`p>robrC>fEojKnADo81^6t~nv%O;@ znVSw>hPQXcIgRV+!NM=kIP$LdoAYo_*fev5!{5fY0tsiH*oSDPLr(Ok}!RVPVGsnrnfQP(Bw7V4^X#q``Ajsz}GE#~!$`MZ|fHO3r zOqXw0SyQ7uayCv;SzW_V;vA=jI=s51Lou6(W+qUng`ud1Z{S3jo+yc@6@ZQtVk|`| zXHdbb_s0sjj$_XyrP|VS@_a)K)~bY>3MFp5kP%Rh(@+0z$%>F1zAUmRiC3Wwyqsh+ zG~C!0RSf2%AxBRgsy??|Ou1}Lyqcl3*;wCC2TcYgnDDKo2{S`@gc=x`f462m1xkYP zL`giN>qH2S*c0%Sc-i@dx)T=?G)A+@-}mDbVALtVf*UZQNI(s4@qrtgH$nhj0%Z^- z@hWl2LTnT$0{p`6cUL6*5?|(4H>j+sHJ^%4v=x<>d$w`nR&GrMwpr!4{=@S>oe?6U zl%P=3!}liO;`@;(iDyI7mDie}U4ShGb#5I}Z?3q!?)Fn(z0qhqmzc_B=H@F|mJPh_ z3VTnoS~XCGFAsG{9fU=OgB%yG0dNnm*$Xnpan`j4elH#GJe>ua5 zc%`1ggcB|Cu9fNQ4J8UevF#H>+QNw|RbVum;X=|C zQ%Xi=U^hYNsHI8>FgD1K(f>8Ao@0x{3TSN#H*j%cTvEN8$+vGAraZX(aZTEL&oGYOVo-DA!L$aQ(;ZSu|ex@0Uhd zyR?p=yPr)ozUYZZ>h@cz@XR3(jBbmg#q**hU#rLv_L* z_g8ph@*f0QLL%fnZ{lD$^v?6RDdbqwm$wBU)TyQX?CQtmo2Cypb#Cq0*15NNh$(*4 z+f@!~GWhX%FQ9j42af{kMK)Llb&iAx0hsV|1VpG>31bIGLyrzE;bM9r)aXo(g8ALA zMMMaE`S-`+jVZ%Hqw=ljE+P1k=+=rU_jR#hUd-9_5*8)@r10|3f#e^*aHsL4Q0e;m z60=zclbkC|bq5q*hW*RK{UCnRY0U) znkj>s^be-Mq#?m6ekK{>k)CrgyB3P7g`tn7sh%7a9s=*q84FiFn-6~;)z_~CBxIjD zb^x5-I1M)b?GcCw4cx7zj;+GwyI&ovIJ{(nwOiZBpxS_u%v6(fbYeQojQ@i=MK;y% z1@o*0ZreZoKyM(UAaQx6wV>r$&4InLQD|{>;iO^k+3OQw#)P|}O>@`J=j>_t>J!ze z%jS*g#k2@hv6rq@fX;;X0mbFqo@%;X!CA41M}UKego+D!qc55m4V~NEY}-SqA!JCyqFp?vVTXOQ}=Ijo!d39pB6gG)yA?wY-7q%DYV;f zPe%y~%&*5C&17#s-dBC_bP6ipZKjf%@lDBWu2`)$)0s<2mhbi+v}NSvhWz!NA5ay8 zbl2;|95qpUAw|-WcYwuegJ~On0x%g>P*Gw7ZFL=RHpv{oi=TM-+Rw_%uig2}J_VY! zQd(`c`ca%r(^l*_Cb0K7Esm=Qto!C9WEEA}gQN8$eFRjMGxZjbK>X?_L`}Sk>RR@v zBgf5$PG5*Ln#{NC9K65l3|vSnv^O686bS-p1(mS!D+e!4n@t9F)k>%;F@nYH*ixlP z>L^O$8H`55(Q_A#yZ0YqipwjV?tYnXOjq>!ju1HZAmSwT3`-MHpJZ zH32ICyWAdtD=OmQI*z@Va)teN??E-1w8G17+-;XQ$vLI49;YqTi35RU+mAx(HCH5% zu_ltr1Sa(K^k`Kq8(6CD-6}|9(OPaMzr8EZQ|SCKw&u#`g5$7Fr0oW z&;*$YY$!h{P$xLz+0b3d$}yr=X6xmw99{(qGYYFd5FM@ zuYQAXj;44R`!>;7Rt9=)7#_l-^h+cMDve;KpMoG>ola*vej&kJP*Um_v&3WpPqb$( zcv5=GXK&LL1@)Mm)x~^y%=cxoSyfPj)|?bdEks6X(E3rp`+YzQfMP+W3rQ(lN_w`M z1I{m`v$yg~AMb-OjS8keGRW>lKmCZnd*7Xb6^;(ceOU&pB^)YCIkfw3!V!=`hsklU0c~!MvM>t1dlB+dS z|M3G5o7Z&YbA%B8e((PKG`zL_NImBKr|w3I#14HOs{hw(jS!AUOe(eK`w9%+P<7L} zO+1uIu5~ky`rulv-g+)6)smT4fEB#G#EB<)coK)j3e9})C#W-+>ThU!k_L_A8Um|! z9LF`?Z-E-=92e4N(?AU>L{@365P=&oG7qfZa^SY1Mz@KFq{##~E~8+_HeXCmWl}P- z))^vUb$2=((4f#qNP1}lYpTf}TsK!u4|zSTik81_@V zcjdaf>Uz-YAw3|T>=cskw)wC&)>=?jW;k{(PN_GVJRinyn-hNl$YP%yw7)lJ91tBP z)On9UHO}N-{$w9~aUjwA#C)DYz4)05j0O(E!g2i%4Jz`X@%%Cmwvu40gflP zCj$S;qOzsBYP1Es1daYa0!H@W>*xvg}+BZy{gzO)Y**|^5fOlFJuY+|xL zE$3P!$GY&QXq#_CX5+au4}(ypXCWt*8vandq$rvakrmwh@@&hEvC_8g=JqHg?S(D0UZjANW$4HWcrDEL~x0v zP+9T(?|0s?%-A)>|%TAvA);^&eTLB|y*odmtvpZCOx{h(}O;`rT9G zPhO1wxwx#t?RBCUTZQPPT>6(0@YZu9aKDR=ehu5uV6wu7U8i6*-g_iB4T8eSiTGLA zv#{%>Up%R=$HW7Ok--?|Hyrgwc#^SL<8-?-3bwuWIEY;>^K%h79F0%N^`-&%QE0I|k<_MtwoZpP@K-T6$*uY}jP9Dturr-`)* zy(xDASd}B-=$kp7CgL6ONUmd7jYEAG`#ddx*yGKcIHdm2y-o9`cAUjt%iqhNUtSAq z{&fPj?ui4d0Il%;UQ!=@V;B?eu{?sK-BE9H@Sb=i_i?MA1D4(4O+myQm#9&n`^O=$ z?QMKi znLpx@(u|Ag1+4tPNWh+a6i&=(4C!$P%zpG9cxGg86hA}JKM}dI8a?d#EfvU}xdz&t zMN|X6i^`y6iunaNxGJ8nl=&qdDb?i3sO!Gp8z5s|$6t8gBDftfZ~WM>ZuKMK!-G0Q zWT;@Y58Tu+22}d{?_;U(Esku*l5+9zn<@cDfQV+SWe}bJA#B;gDpg8NZSKZksx$*wsfQ~R}L@yZ7r40=1+OGa0uCwsI^d|-|ujW_4*~ zWV@JNM9KC$;6^mh@GZ!=S7Bca>Wtvu`Hpz@vXbd1C*O=apCj#F^z%9bo!T~o-W^-P zxWUmdAi8b6J-beAnxRAzzs-UQxPk0OUd}CtxQrq=ol*c7(+i=vQUY?6ymuz%DE8GQ zaA?_S-ji;SAipi#AmLlSj$8RGE>72CrMlemjC#}~KsIMOwP}t!qv6mlD!ksnWFI7E zbF`Trf>YKAoTh42a{O1I^vw09BdQ|_u?%KQ{fI@Z$kw2oX;yI!q~VB`Q(Oi4Wpz+g zr3EwIw5QG)0!$_f<}OA33jcwlF+Sh6^&jy_eIJTxg?APfqLIM`IQ?wq3_K)f`biP+0|FO8E}IYg-a6-pyc}i`SmMML&ZO0fs^Si?AGTgYOnw?Zr85%R;lR-3D(5 zPK`#hnZ5|RG_QS&tvC6vyhOBX1?a+O!m-6k{>zrXoWwceHLU-mt2=>(dlRK!W}~(j7q^m`)&7;-0W2gnUeVc*)KMh`{xQ#u`W^qK)6V`Gtk_ll|oXklGLfPs@}Hedf8T85PTqU4z$;xHY)o?jmgHY3W=3WD1u3hcsDSg z5EBVj`9fTHzlj745k*VTLN-Exsli*|o58{^$J+zOMh`{*Q<}uHH_LVLvv-kvlkHyg zQw#{mz6H*@5)Q?rilMZglu_Emvm-Ko)flh=k^iLEkL;KT@)NarvC0K-c(EkbaZN!S z9K^FDFlNOxl;2y4Q&4Fry|mX${GVvKl6ecy<31OERa~oqIM4B1tJ|%ivW{MYjt|D- z+7t9oIWL9 zee3*N4x_KUh;lFF`cX?EH<)P4#sP}=!CRmYkA>5(5)I8k?I`gYHvb=2-w7rTuOD%L z#Su`lrllKJ*$>}qaZUX#Ft)|M(#q_II`EzaC7vTl$FJ)KtYs6DZ?vPw)X!xEjsPoL z42J`!&CBJI`Z*vaUQ>XNTlEZzpRGdzc55maeRc~1sNaM`J95i^#3xkS?k(`3Rv`y| z)XR=Q=8;rrKWqmmRXSY%4McA$erCtU;U^LELN)!N>K9<|QZ#KvoihaJi02HM#{Oet z+T$TF9=;pfCm+)#Pa|&n2g5{Rad8@^!yTB!F2W{3Gv5aaAf1&K&{MQioY)YuK^{V zH<&;9L37a7qV{WMDJG-vCMglc)gs|O1%qZ4oLofC=BV=k0ZKd%K)WT%MAUw5VAdl6 z|9s0c$#{^>N!dL-YVeGfj96+l{PC|373*uVUM z0LmhXB2eP--;4~5UNsv#Dp~)b9S9LS2JpHLtU@*Dn{hO!=W#qXB_$q@VBVC}=(R0T zOp2`i=Hi-2Z0CQ3WbZ*ZyGAJ1b#B5|0zWA6eBFqnV|t)=toJaRKl(Z=Ueafn!(g$^ z351nYUdk_$L@4q6)lj3?K8YjXDjfa#`72Ae@?qd23J0teqgH0O>M4-Glz9GZhzT3j zV6FKZ%74C!*`g$jAY3(ydBxx;w+%-*JSw2h9|%z5`2*#~c|45TuVvh8=sh1tzyQh1 zGAMt_egmvxDI8i_(zuB9w{H=k#Pcn_O{YG7eP3WrpWqa4WYYn8e|iMZ0&j99=Oi)oQNfJ7OuBUgc*@i@Zm!3~=q;qdBAM}erH bFcA3v??;y!V7JTB00000NkvXXu0mjfP1!2= literal 0 HcmV?d00001 diff --git a/apps/web/src/images/secrets-manager/sdks/c-sharp.svg b/apps/web/src/images/secrets-manager/sdks/c-sharp.svg new file mode 100644 index 00000000000..f0da2234f1b --- /dev/null +++ b/apps/web/src/images/secrets-manager/sdks/c-sharp.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/apps/web/src/images/secrets-manager/sdks/go.svg b/apps/web/src/images/secrets-manager/sdks/go.svg new file mode 100644 index 00000000000..d335346e71e --- /dev/null +++ b/apps/web/src/images/secrets-manager/sdks/go.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/apps/web/src/images/secrets-manager/sdks/java-white.svg b/apps/web/src/images/secrets-manager/sdks/java-white.svg new file mode 100644 index 00000000000..082897c081a --- /dev/null +++ b/apps/web/src/images/secrets-manager/sdks/java-white.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/apps/web/src/images/secrets-manager/sdks/java.svg b/apps/web/src/images/secrets-manager/sdks/java.svg new file mode 100644 index 00000000000..085cedd07c8 --- /dev/null +++ b/apps/web/src/images/secrets-manager/sdks/java.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/apps/web/src/images/secrets-manager/sdks/php.svg b/apps/web/src/images/secrets-manager/sdks/php.svg new file mode 100644 index 00000000000..5e36980ca0f --- /dev/null +++ b/apps/web/src/images/secrets-manager/sdks/php.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/web/src/images/secrets-manager/sdks/python.svg b/apps/web/src/images/secrets-manager/sdks/python.svg new file mode 100644 index 00000000000..36f5cd7f05f --- /dev/null +++ b/apps/web/src/images/secrets-manager/sdks/python.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/src/images/secrets-manager/sdks/ruby.png b/apps/web/src/images/secrets-manager/sdks/ruby.png new file mode 100644 index 0000000000000000000000000000000000000000..2dac1c8da81848acf1994ce7d9446f8f3ac5a70a GIT binary patch literal 19253 zcmZ^J1#}#}vgVj6X0~HyW@ct)rWnRBX687tV`k==)<5fDe6E|5bxNKm zs4Pkzo;KbKa_6l|EEjPU0S$1i@f6sM(EIr40^qG*Ux@-VScj1%`s3<0!{O z*G)iv88{ps8K5*(Axepng3;B)H`=ny*oog{HK_bN?1=zbA!e-8iz0RZM-7yyumAp9?^4ng}L9Y_Em3!bWMtt10Fcl?O+dPft%~PYz~B7i$(aK0ZDcR(2M4 zcBa1?Os?J_H#09LkSoQ%jr?Cbl9sL(EDej_z(kwPn>i#=C!GEmqs{*|&?R6!A4wfL-e>m*y?1C)+tK@%W>ivJ2|3mU0rXb5d{q#Tj z?cam)uiC$(B8(`=@?Rq?jMy}ko&x}g0^}scG`%3!I}x(1bo34b&;3(Z!0kR4ZZw^R zxERfO2zpuHV8!qu5u?P=o+4v{@~&qf5D;x3t8?|o~go zcl7rRyYYusoPao}vZQ(Oa9_7wpaxcuYm7RVJQI%*gx1&RybAOVRa=&|m$Tz@5#t;` z>^Kpv^sMfErpbjGig(8>hE7;C=bXjT`j6sh2#?+9GVlQboT;8A!lw9OK-LLY^>42( zhGd2FUt~N2K0g;goGWro8QR)az+|0D7X-EFGCH2X$2_~3*f#2OQupcr+gYx7$--GK zr=?C#eZho3D@aI-0%%-N^4#7~qzb19#h$BcKiw#wS(vyOIUw?mkYiA3G+QG|rJurQn z1GoOT)O~D=Dm1Tnf3Cb%s-OilDzeT#p|=KDtKNegzMK! zoxMe4I;|`3{?%Lp7PHi7_=g<^c?g*wb}$Mx)wY79{eYAUTeNxs`1uEq!7+#j;| z?Q8QqUx~Q6h5?~6>im@Xl)`vOQ-kL7@S#(lTeynC8DHg(FPP7cXg=RH9R@M5c!DNq zgpWoK9X&Z7Hcv8K8;YgVYt#0m!V?{EMVs@1Jn@av{S$1&%yA`(gw+YG5Q)LiztX{Y zD7wd?K_wWFKV5~2II|sEE*N++Yg#?*&sBMXHe0rsig1P>&W*bKZVpma5AAHjP`muj zbHL9iZ@90xg@r1>>WpiFad0%6n3Bqnym^oN>mFHLFap=ndxTZUtSXzu}l!md1PTJ z;emjgyaNncIgYJb4m5lM2B3br>-j3MsOzLN>;MHubtgHSA>&U5JbrpTb=_~9)ttP0+8wvppx>xx?8QZ0#;LdW?IQ&;>+x{s zo+T*4+d5D%TLU_?MV|}#UAsLEy6QfsXLDQvJD|zqlb^J~8=1waO$RgqxGuwcQi#x# zn>5lG#n-0#(bBsmVwWmux}QqDWYmmeJ3joQ%QCxCv|$bPjRUJ)8!VWCi4o`Y(MnoO4Fz_MF4(MshD|BkDki_T4<*MpX}p^cED(dL^p1&-GxWp#lJ zQS6A~twJK`WYC5r7*AR;Wo-v4o{?!bbgBKo2o)Tx^U+!X;!Q=6n@g6_Ztb!*ndkM| zdF-26?b4Rxc5uIgu}VrRN}k3xE!{Id{^DB;os+~$&y};cOcl;<=Y{I)<)w4k@>L!hAW1`S zdcnN5L}v}-J}RJW|)?Y{TDNv+WTdbO-FU{N7sF_xH19=<8@cpBk8}jNaG~lXBvDz1L^OK{hRaW? zZBV8g)OmnBI4&qoHBYMd*Ril?;g5x~Hi^x-Gd2hsF1h#x*4e{u;k)I6Gq`|{mQ@*% zYuuvIV=*24&mL8E{z-aHkFeU1+zhX_20rq{gD?snk*sPojTu#=*fbRSO_U!Aum#&G z@^>Qzgp=AD3UXn+oqmTW~X5e|1mfQr+# zL$%kJiT+ZB^^`}wU=zJtx$}F~trE_%u?IJ3`G~{#?)W`l093R7I$B}i3WuUop2^+1 zNT=D}3^V>iqC^djF96S3!0vfQzw`1Nj#3Wehg-x;oL;K znOX#Cj}*lAeX9LKvZFTWGBl4~y?=vjBuC;A*xA&umea=S5%g;p41(Kg#8Q2D-JIx$ z3*1TJc~f+Kb>%4`SY4Z^#TcgNll@L#6d^6}fJR_5f9WDm#8f0+wUnSrKmUhR0F$KH zZCv;5anHva7VVlXbL3MZ{ZjQ5`2ID^uC9e|Ugy~R>ZQ7Bv(82DtgibpGbhKLK@Vg* zHvU}VB2a*y%#1D4qO_f1l3grGSuRA!!BtwzHPBx=_O8{-04SmA5m8+Ceo+9m-y(WI z8nw0iC)&kcP~c(NFa}duc)9)dHvNJ8%T_^7A9e76up>=(T+ztZus3`ugy|sIn3GIN zfXSdgc_{~q?4JA}zDop$_Mq&qGK4Ond)H#K&@FGpp+CX-wrINt;=r z>25J)R47_ayWOSdrwAPL5Fs*TY+uUCtgiP=H-Q&GM_TK;v{L9aVZ`Z`gd-Y^v zy0n^@RtGlJ@MJH5fG$XSOy4(DKI}f~%+})|oQ)U>Nk3T)I*zU@i@h_m znKeY$sp+NbfC^dbh$>b_43;=Rj!I<#d0Co54_vw8dsTfnF5>mkwKzVKZtImpVx=q-u3V{Z*Os4H@Q41nQ;9iG3A=lC{%^A#cjH!n|h<_dY7Wi z$G+(5t`D+9*rhtY=x^>pFi!f~v{4nBMl0c;@!_N=^S85oBqo zk34;&br3>ttAWQIgHhj2+Yw7(s8X)6Rd1B@>AP1Rxv8u^U{b*ZC65w~kIwP*uuZw3 z-pQ+c@`|DxKFSo~hcYQttpb$Hi4 zgU<@XpZO`31Ikp9hM6)IR2J2Ujv{PlHHhh_E^>j*IQlWayCP-~458Je;stKtTACMJ zrf1%X#3)xt_eR=RTwcqd== zZ)b4|Spd|cTB7^;jP8jJ-aFz`clJbzCofj|cSlz0GoO{Nh)CVP54M7~e*L^+q4vg7 zRU+GJ1Oy6UJY3Zn`IYU`IW^Q<`j>Hy^kvSL*Djtoxiu1Pv*TQ3D}c4y<_e>aZoZ*; zK$H&g&awYo7Z@nME+q7xIPtz82w4t%hg;h6%FE;(gf0SMCfJWllz*bdhR0+aGv>Mq zaEuhU7O02{F?dcK>V3NJ8~HAYkro?L0-g} z@kN5Cb4w5OE(titdI@nx#;vc5qLZ!zsEqihH0~5Bn{z*mXMd;bdihOat?#4LdcRR{ z?j0*Uem;`q`{!q*(ev6w<%#l)PpLQ>-PbRmj7|H|Lf0(YqbO;V>+zj%&GfA~{#jW9 zl}kMF!RdOKQFztfbAa-;y+eEBjIGEe(S?ILyd2>e3e@?^N5`_pyv=0yN_)?yOxd< zo$sYOHWiH9$Y;LukYO>Q)yt+Bq>?N(#&EPS;fiMs3C?_D&EB5JlkK;Q&#q=@qR;eo zV~=n^Vklv8jAOmlgU%X_oDMc#jSi5pMLy&c$Zg-(_`^IjCGp zl9tP(o6QFud$S+l!1MC=`Q4hYoL|z!Yey@g`+;x%-N!w-f036zh!_< zU31xmma?qHK+xJMxr&~Bk}SIg5;SUg$rHmkyHSgOAkfOU3ys$GK?@Ju@(wYI$y%C4 z{K!u7AcsNY!+|;28l+ajU*`_FzPl<-!xvsXcMbwP4$&m1s>%7nDNp2{05TkVwT`0e zWcJUO)|~Q@o3JtFUcF3!&XxHFPK+ONL!ux8qB6UHmpnqBCseQ&i&uQ+>w!P$*|nwJ z?NHi7wfK`DlvJCvp18t8oRU1pbb+r&G=3-jhoNF+~G>H1F)j5MXNd%tS)uT znkqcCN+}t2Mc*|KQWRur!Vk7H@g%Ycd|}zXYi8-b(a9JVe)3)dTe!}0ZV8^~>W*o( znBsh|5MyH#Tp6sr7^srQQsL4&U=F?ev=TCNv+=H6nufgQj*E%=XUyk*M88@AM%q2S zYzj|BCEk|P9p8az!v6tMoluIegwRa{<52Qd7&K-*JMB z)h9>pE`)3qm(JH5)c4eb-H*H2*^O+2gvF6gF)iWfebG30Y*yZhA-nG=kIZM(>Umk= zL!umz@JJYGw>2JjG7n&E$49xA^Y1KHLOQMmxmpWp^scu0$dw912FH-s%iVn9je~?$ z*9cb%#aeEr0jCV&Q;3qvEb`bpnwUGe?OI;8ixdfhte~Lz_Tt-`ve-oLk|b#>M&m~6{E}4nSRn~^7G~jqGI5Qtf%ZK;oWfutdvI3H)>fu zd~2QPzKpE&8v|X7#oWHVS!b`dZ*bwhg5igz(h|d(nx_;HV2XbJT+I@jk|ISe6(#h3 z;`C^_%_+{Vv}Ms8&B6=lm@m*M^^hOeprbP29CwzlS{y0iJWxY37>*|4tH<98aM;-v zI*%do)mmu?KDxU_U~3LllP0k6v-GprZh)lM9kS_gtphWh;R925#GW~#gX_ptLCpBN z7ss(gPKU20CN34f)#C93w=(%7w#$?0kz8}Ov3nEBIp~jrD4r{~v125zq%qgHr5ZU0 zz7I%a3KBI=vthbRRQXuK`8vNS{GI+aN~T8e)u~oCNbRm^K%sUm-j6L+S(w^XJzleP zhg**d%2w4{2y@_|HK^-ve-uGr9R^jfzz1lpA>LuF+qQU=!Hs0JP`DkscBrBn-RT}a z;(p+RILW7uUaP~snOLAO96dv<^B5syaY{7bNd({|uHtK^!oxqsY-m^b1^705*RHJ! zrfbKE7KS~^dyA>u=0JL}wov?8o{|ykzGGPGo+5hLS(1NSopP-Thk`Y<*Tp7v-Gx6{ zEZO)Ki~wVl%sknxnbC9}c)3Z@`m$qbXXW~$dXs(16}xYYtn zOjPHHT0N(z8}voyfTyaUj_eMWLzZsj?8Z%k4L9{JSNp-FRfm|`);b!l1GQROcb0VD zo#};Ku#XOy{d9DKE+*>+cj_xVqa~VHe|jdGUR2=hIY*(tx}fYP8|*c!l(pu-eR}tM zW>)`~K5?kAz}v{&7JgaFhde$P}dT?#KTEM;^f&`(aedx6~9d*lJ~e>n3=_` zMPz$!67DjMss-Mak*!U7T!&;KR|aDKcP2$=l@+2iMyVRs^t4#}bd;62mZryFuq{FJ zMqd4J>!XKndBKIlnhh$SJ$7^7WKyrozh*0;^FQ;9g;3?RyWK=@;Ggfps6OoGye2g^ z28*&0tU=D8tok$$iJaFCyzU)tgTVTZN8@|Ym?;e*a3ZtIkKPmS2B5C%^M?;(JEzrR z^$^!&MWOJo1um*&wP1)wFA^r};-Mr->CfUWH@P!JKc-||r2`mIQ`o@Jtz0#}O{-T! zAAfENAB@;JkPD`&|G6{Wp^*E$n+s;zA{< z-OCWrUL=P+YUv$pC)68$+?QYHj|Oxu_TK%wonP)QnnLocjt~-g@9@eL?|Mr4b3X;) zo0prhmS;JJHU!a;|Co~Q=FKVov%R%~cd^mC^K75yyQE7N%cTm_j5f=)^`{Kg#B3RcFPKy zk+fBT)WT@a;DI@;S&wg?SK&|isEv|f)Jv~=*O^q|i_m7!)I62jkn?^_wh)Dm;cO)_ z-+|AlP_fIrs82I-@J(~iwVAgeF}0O~QNEWjN5K`d3VFL&?fUu=h%5ZtM-_B6HHYstFx{By1a$_{7j~8r@lS<;zm8t6mS)sB5eJ}p=updsjc9aEyK+180At%^I$ z;LjHQTIa#mq3ba?botXjzOx0SZaw2QjNQFwpJ)tRHC;Xw@>s3vc6qq#-hH0bOXOA| zy0IWC<6=YSozXtjvV<;{SB>&*PsGDkEkb78H#$XuR+uei` zAgaEUA${liK6?`Q8l($RtJPuMBVgd*&nS9P&@gi0N)?S&a9qY{Nts;dw}Crmz^L@; z2G}tLPUcqGJc^m$8PFYWACENJ5-fkD>ZMp4T}sP*S`Z5K-G%qB9~qSeul-KQBmCS{ zPR-2j{Cipn6N%s#e=zAZtjlZ|Btgxem}W@aHmKbgCjx;pgMEFOw7g4Twt~p5b+z>! zfq>{dug7$EM9~3mPkUS+jL&aTl`{6r4r}g`i*+M;oy!EdcRkkYg#zMHgs*DHnmOj3 zny@71F;t+Rq*Ap>&8w0DIjOmp9^dP5+n~K*%6x|?TGnohU*5Y34KG5QfUd`_a_Jbr zERAh&uUhbqWT61_vjv%Xz68+#w}Et8rU8!nQmhZ14veMmMP*}IcHHI{V6eJn${Oz~YWdI^B&F4+i&-{i6 z(1=}b77Swis@_nGW68HapdD6tJ`$IAC=DTcxcj)dV;r=Uc=tgwQ+T8UT#oo`2xO(1HN$+9fk;_Pb=ora09 z@}6@BMN3VEr&O+W=GnR}HIyn4=O#~&FKh=^`}g?uPHf<~ z!}CK;t%j)v&s(S6=k<#OdQ5bs_Ck6p2YOSd69jQ}y0&0>0~VTw5Z;ggO57P!w}*ny zC|~yvSLKU!y{paDW#hh&rCiZPHp^$f>@&>%s$y-@NcXEy)HL_YTOjQyT#0hK;PaVU zM|iQG2=3}qcRl!`4R;c$YI%&VtRnTjoTrRajXyd6>lgkt=p2L|@=lduA(+F3#1L*_u>7J&kF!1E~V0@S6 z5jsUDYLo)%&ZqPF&{ z73i}Him2$8!Y8^|uLHhGr|05Z5Qh#( z{f=gwh)5T+zn-!0%7#@gi-NGk7SeVVV3HSf0@@1VO;DYvaQGpM_ob{mN}`VTBL&Hg zW|$MLQRz8C!-X3aq9-sbV>3HHwzW6!CyJ6HG@yh-4^Ss0w|+ved`kR0 z&6=wP|`kTC^*?I-);-uzJNjmo@1KVoJD$b+v>_M7#-4P`C9CasE77e` zyp&RCMjk|zK-oONgn*&Oww}N_5Lr@wT(PuI?IZ^PW;ODUYro_fc)jMm1dF_2dTvj3 z4o(D|1bFVCZua(N(!rF^PF#D*WKqa37Nc`wXGBe)6@Q)8dnvcK0xe;p;C@6n_~E=*ydbC7n#w+wi~!nGqFZU>cN;_J(kd?6Ikd4rNTgpc*(9SZvp zipdVW3VaW{Cnf2rxY_UE#zvQ|`yqv(S-p-?*zOrb05m1uO{xx|9uT4e0MX`*u2wz z1WgU?R~=Dc`bjDXq{$x{^n$^!Fk6tm1*rrM&}erV>2SLE2OWEng4k;!xf86|*wGG> znEuz_Gya%!9+bIqW^`W>ibU_i4HLI^A9cN}mOb>{7>U#W)b8{Gis84Std=cd5BvxAvqX_;@P#4Jg=3kr=%L64LQDj z6}a~mIZ4RBq2&m2p5@y$6`k>7bG_+TLbm;0|2fL=sUj8i(*sRnnAg{z#zlf`?Y^nW z122QcfY1`Ua`lfmtw>*C!RcO_S$b6m?W%%iKGVG$*4fho*V{4Zox6mP&X^*Zk);UR zgYB@>BB8EpD0xldk(;50NW~L<-q4)FEdCyK$Ubuc_hG3xoS8VrA-Y^u-3r-*(7Z-J z3Ysnyy{Xl^{mk_t2G6#c!1qD{A0ZZr>dvG=G^fj)R_9-${R|fGHgu_Y(kKwJ@k#iN z&p!0@AsyW3;VUJVJ!vM;3IJMQ*{anEx>HU7a8 zq0_1+URu|c>xpo;F81N?uORJ4mu6A8N`Nu=4bxVvlhj1SXIaY=5#HwMxJ{8UuBo0tkYM&v?i1EGT7MExs z=lAB0Uz{4T^rF88Vx3vJu>4zf(pxPB+pXyAa7KVlHK{ddQ#AL8@P3lOdF|%43%*B4 z83HTJos6O@PMmMh@hM&uWG2~PQ|7UOf|dceJj_tBHSd;NLV#3;%qD`|g*3PQg;OZ` zyXBa674(SHbORJJLUv=(@5lCCEV2-c3DRYPgtAgwTH<1Pe`k}my_Nai#9JO6huP=# z4*@;NkETbvA7`HHSDqci$u&MGJ#1`jTQ*ow!2?8j_zZ!OQaTRe#6Ks33jwyO;yxAF zJRDjUxg5&+re=mXP&C?C5kf3C73z8&K^!#;iTghcv<-9{xo@$4uLoX?T^(4nsyVEj zGg#l!iNumfFGuIj?P5HK*+*T|8w-!8iavM~D6?q5_k4oy11}UPJ>|z&&A8E(HS*fb zlZ}o|?f=*i8+)1UeoX^kmIMq57hFyIZXPMWPX@a0k1@Ns3vUn=QLlvNX3TyZUhM%n zafm6TaMpAW`laot{a;^$nNk@wod?vp;|CR(FxTrB;H5;WeohOH`-bE~gd^(Mh$z@4 zDQujAC){j(BVtzUWiCUb`M~GLGiUi`_we5Edj>(y15h@MN*#hXe;Vsq!I_664YODf zYKE}I@-jhngE6yOg|V}X2kVzfs40`A4h(@#VK4cF!pDmyJ}{!u;IM!wq9gHqq9HgEiXjf zr_{n%p4Hct4%Nb==PHa^w05wC$&>On-m)3~#vW7Vkum3Z3oV~){Ps2%(q;43x8k92 z*Y(<&x6FTIIdYogt+6?1<@S?lU(h2(MWw1eXJ&+7>cV6?upfAjSR53T#0XlRD~-OA ze(UlwwIj4c*c4EdfinGqJ8Gu1v`wA*REwhd1$r8BkO_)zAl*c9q+$ZsVs4wOdT5A! z4znc^)y?E_x270|0d(IJ@G!yCJ(K{(oG+W#{h7*rg)Mo0u%n2(z<@47 z+H(4^7LD}HDMW6=Q4I?!79iXVBe8K`meUXU_^4*?C9>)P<&xr3Rs#l@Mxpa=Wb;Yx zzFVGefO>K3*xR66}kcIWGjy%mzElodTzw{XwdW5Dg}?u(r7tkB{5 zg(k1aD^sV@nio z#>)4WDj;^KLAG)8&Rr+!y{yVP&;^4xt^Q^0upCJuIY8QyM?~PT+`xm>UvWz1b4YMv1hUb1$ z_ja>!)0oX2XC~(O`EnHx*w?f37rpL^3rd(}de-CNs`R0OoYHqP!v3C@dfy)tuL^vh zDpXY>b<&Xl==~s5A&)W$oV^VlUcFK`-?5FH4XlP=&coJ2%>hEj#b{QGT z8oV82Bh6`qd(ca7+$y*9w6jfey<-h9BP0#1R8HM*WE-rc z3<9qg&eKKSI6yDexb5`aQxv8!#mh7B_}$x1m4%j+q?ITy{Hx5H~~ zEKh1J_j*&4R&kL1h&0Bwr`(ZnqWnHDXV^mI2Vr7H;SC&}dj-8hIkB2W78V0T6A*dB zcBdFY(s-GaUY!fhQ?!t(8CQb{67kcc3V`lho0SlCI5hs?NbeVRA0o;Y%_8O0r%Q6{ zs}XrK4rQ+JCgjT9)-V%T1jTxPC1<+S<2wl9<}tVL8>QB6C)};+nQ^TjhfLKSF3AP~ z3z6%m8;;=LVGoy?8fx4b8etzEk2 z97^ZB8R{4bCnTp#1*-ZZGW6)e4Lv~LmYD~j|0a4GFChxUF?7h-Eb7n67i`cJ7wUpD zAhq4n)+=D+Dt?|5wE~(`LS^Ic?;C9xII*}SEY3R(&1=vquFk#=us}H9PW7_APyf14 zoiYHewlvner%*WEr<|ijf`bcRP03n#5%`iJ)JB?zk_rv&IN{2?Mp9D+a-GZlpOtz1 zH`lW+0_hU8JfIX!pQkprk54HUhov0?)kNY}chJ@Q&Sk=D8d9HM3{i9IF7!GxoRurY z>i5eeT|HLp1sD)apR-AWt(pUJ0R(S27t7~DpzGqyBUT1pGC@adHJC4rVnPc0jXi1< zh@wNJ;2nHF7C~-`2F?t+u)W1RVW%T70Ut+>+Cg&vJoh#~Hg!&X!>peEDCYX${?}2K z3G~uhf#@PFRVN#qnv3|AM2#_p<2lOdzuRSz1%)%r>AWJ#V7z>?_8OXEjSDnY{GUGX zyi&4vbm43tq?QswQNfK*ecq9%Hmk!8Pvq^{^4thxVu=I$0GB)j{x|*WI)RJZO<^;O z4!?-p`eN1NigGYLX#vJbxf<~sWFDm|vVL#Xd0!35<02RTW*+taBNUS1#t)USVC$9< zi6jColxWQ)O)#oj1i91o3p;%mkD{u1+Nn+ZzUpnW66{WtLyDMY<}xfXQF23M+jg&m zohowA?q}@WYK0GB&w|Y7ahdf#gAI_KFa^!xd{#gi6fk}O-loBMl@B*tGDRMlekKH~ z_u7^n@=#2ZKOlkt5WDH!52f$nO8bYC2J$4r5ZY!4$&r><*3+QcM?3m*W)I) zINE!NFSrkOwB4^!0gkf?`z|0meXo}!9W=&F(%tC5WQ=$IkyK=O%W*mGuNIEN)n5qiNjp9#2!vJ9zp*=K!s6)03`;v#(wFza zYZhY&P%4(!V+umyO>0#iSrmX??|l{T2mZmXl9nnGhZ6K=ab#~M2vn8|%RhdUpS7`jWPuX;ynNm== zcGFa>k*x6NJgBhU_xRld7G`C*A)6B83hlDTZIHKpm45tgx8~pd=vwW$PS4XCuVf)K z2ekoeN(~--T%gl(wrBX7qEO@xnEg{YxGWLd?k&uwX56pV#a9KpzV5fLwEsX2H#x<4)0#D;VbwEOh!1y> z_uak?jY@?p*@-M*ZozglNqF$eptf6iJ6hXSbzvJJ=xRYJS!m?2WEh_q_4Q4pHM{0; zsp&S7NIe{1R!X%!n0{y(fhKc{6>bulR$;?xE7Qk!^Wt z5+`-}&kfolm3XKT8gvBAIQH_)LK)FxjO-}&oQWUO8UaRr%k#V$&=|~3_C3GH)wBGB z_C}S+QnzlkiH&JGc|&m3!y-qIe~|2MORm_IT3d%YRyM;nm00=4LRUmIL`e+OSg-X& zn0f96`i)olo+~ePXT*lP3=LMu%ju!W!%EBGaRyl&CyYe>QDWyxZmEkWRy20y;Np3& z3K8r*Y+{^{Z20L7x6nhT>PkVdxzwK>^b^|S=7(r`Fc1q}cK$amED=9fY zQ)b-gN}qjv8o5LN;ns(MxTVWaRuTOSGA+2RN@F)b`-wab==&wWxlkfYeXKz*)Z=$BDdWl4i(4g z^a;){Q5}LxtHKD)@~4ZujKVMMVRFBSiK7+g4RM0)<#sQtp&xQT5&ul-7$?W8Rh|nE zdA8#n_)DXeb_f(lo63`y#ZPqWT{c1t24V5u9-@#6Iojzg6|_5fjHOObdA%iwKJ67< zwmthQntY0E9Av#sz&7zs$5OdV+>=vMF)Zo|BU@gs)3ibTQ&FWd*ymsuG3eM@GC(#HIDxajJlZ{h$*a!6T0KSmY5k4XR5>&n=;2Da&#{5l> zF8!s;khl~Nq{TXjwXfv1@rBdn@!9HCQVdP{wPUPfyBKXj4;H9+`zGRG#`*#~pbd`o zh}V=?GH*PwsRmRZK~2mzS?h7?rPud}%R1qRv(qQTIoGOa-9e=-QQN?~(USLF@s63) z>R-Eb&YU{hG}HA`+)`S@p>rDmZiN2s=ki@&3obYxIG0%qB~mo>^6sC74YCh5Yx|Nl}+ss#OQSu8tkMjJ2CI^s>+2FtcL< zh|D1%|GO>wi(F47I%#HgF8}RJF!UExP?xlVFYu)r-jMAywoVi9E( z^rK7=0@5Fv`dr)#LBo7jEHPN85JnIiWM#fML6m)9V8V%TJF1e3?^-J6UtlY-%up^n zZ5jFAh{uGT9yWr_;*3pLqaP2cQ6Wniobis-6EI3&)`9kCOYJEYA!j5V$~)EopCzSRIv<{$ekoeqbYffUWs0Eo{(fd9KlZ`XqPd6(=EAM2I&UMsK4;E$;+A9fU` z$-6`AoOhT+14Qg?-ZDaa)(NR&{%aaqPO9cY$HFP4Q=#)>o=@Oju~2)DR|+ruG+0Gq z;$5l*bk3RbknwAmb4HVBMPGb}PXZ=%U|$-|x=+HQMzY5o9;_5UT4H8jdVX0Q6-Of! zbfyK*GH5nOv?h4>(l=Gy;vqBG6`r^3OGB{EN<;|k5i@@GR^C`R8VlznOxN(m4}^-@ zuXfPR+&iOZo%sT^iGUo8-pFbyiLh@nt(>aM6Xx%YK`KuqrVWn(jHK{KsautWB!!2; zEw7fp4sfzG#W{!L-aCB4R$_Cl=qfsePd@%RZnXnV;T3{yL7@aa)8e&tLH2pS`XG;I zBO@PIG=?NxsHQxAl;IE^?3ZS9O8;Vc#KGjr%QE)alI=g0U4FKrS4IlnsbdsdUkA!Q zC160IYxN*Oo));gQ*TQM#T}b9*0da=UqG_}lVYQ;9e#2*Z4jEL0OxqQb>)lHKi`m- zF{p5RiGBa-UO~=A$NgjNS$<<}uS0-O_+*hq#vM^`2FAm>QOQa?2*G!<-n90G43t`m@DD-yJwOm!=T9w9pp0Bh5TQ0@9z3aB zy1f$RPm|aZKVFn~xYGIi#wDhb-wrL`_M{S1**vj#wQMs+bM)v*4A?2=U2y5F z_F!(N>B$h2`6g~mu1Q3-G^RDlP0y%GDV*$0ZMDn&AcMxH8jnJQ;rDA0Yz>kQ)kl>8?z%-vY8SB-R)29#=K3 z#^uX|Y=&rH4N3)t8wHg6iwNYl*_b_F$|VD%a9cnHyk$GdZrnz;EKq}t!8;3IV9Dd9 z|NhP~If{ZcW}EN;obFFw75Jt10a|>RwIEI4lJ&FACYzDm_okq~-1e=5o5s&rAukZ_ zn9b{-!^M)47;Dq$ppl2_uG-oSpn~qX(eQofA~EpNp+!C&$%3RWj50G}8x;LeJf=7r z#Gl{i@o7yy1vflt>ZO|-y2D?&_oZck2q_v;VgkC#ukT8)zTt+iD!=kz8x5Iw>zMYyK41AxP zqpflEEzgo5@_3j~M*PGXx`}wfm@W@zQcZv<662b_Ck7n*hL)@a1Cu6*M;wfUrMKaR zPem6A!p^Y>0U^ri4>>ZeWQuZrE&4XCqV7ikQDIfK8l{1O*SGx}cDNZ~vAjr%_H@NZ^pn*Rd;Bs=VO)r~uqTWmyo<5^pCYpg&w>tn2WMHz$Xn6# zg#kaTgtSb?iiT!lVhQwiOJXvuacA>c{hw6K4p2BV`WT{%B7u?$1W2g2x(7PK$2Jvw zb3Xf*h^O?i-f6Az#dD974wwRzocnPq`tnNGpx;duA-vfHx!&L2?`nNWkkMB9AJjK1 z{7u6M^#ur&gROp)~b}MZT%5hS!ItfRYu>i)0Ui#`-@2s0mm0^^G8Q0#pQ%F%-Jk z5d^%sNpApRFXy20m7FBs*gN3nXe058CaW8csb-U{72-<%)Yz^_6zufvI`){fzv6!u z$vOv(w?t2dhsK2bl3(j0)_rk7j)eIIy9ZF;a%mE$XM$3hO+7lt6lE+cY$#Bka|k$% z<UX--5dteFx1#lD|8{C6cpdk)2Yw!q=(I~COEn!<+ zOrws^$Kg1{eC5b;_Y7Vzo#mmlmq1@D-NBr*M2%z~Nv;OTbE{W(G?#X8q&xo()%jMO zns{Nnt!?!1jmDl}+`iv@GzK)dThy6nwYK|^G=FgHDk+EgxTmo;kMGwGSF}#d%u;lI=A7j5*DX0-`hQPIFn)Ou2#j2%}jd?e^;+jvkoX8$Auyy_9O5Gsu9C~v!&*`(geQa>Q4MX!HR#g9cAhoZX6 z)FAW`&}e6173gC}SY&t5z)SvkQP9XUMdCEDHYwAyMwYo3pSMvJ(6<2y=D0t?z)8;j zv;Y7G-$_J4RIIcyJTef^ulNdwdzo|Zrd&Hm4GSM>i9`*P1=AEpUd_5zM|v(PXe zTQw3T5SWn_m90^T8U-we?;=O3v-KSB@~QGZg;M#HS^B^y!WQEr`=XtvE{2jq5o#bB zgx@o^H;SZk)Ma$`MW4>ln4rBvjqjSE$t@t&X;nOxF>W*qNQGHL7vhaB^TRN6lec^! zoq-+v9;_TRYYWPnXhtM@+yZdt@Q~~4+2X#mZ!cd|WO~J5J=u55YWCrP5 z`17Roj(R)!B9A{fE^m)o;jE_QhKXuV12hDEOAN&$f~Gf(QhKWjqX~-NOY*sB%RnyGLMH@dWQ&6uT#aiWV@qm1 zs3k99u813BgEEGK*DgF zYQ`^BjlW$*`=|qW^~w5@w=VU3ly?)&CPHBIdn$?B&@-64@`9c1&sPql`^Nb#f$e>I zytl{{>nOL?;UlXC{;EW79lRpDJ@^OtKzm-c2K6W<ZRREcD$ zgmp{#bb<`q`$6Bv9vdSFl@N*^1IYF{;WA~_k7a9S*~dDM58v<`eROsXVD(}5Fw^WI zte11)qa!6J4PZ?yDUkLG8D$jToT}EheOE+CeMvrXKqxD0*I_&yn zOgBH}PQkF`ig)j%HW3&f>7zxix(HXVX@f_SMTE2+#!vCR+b5Lt%Ge2LIM( z&1#N#yDk7I{~xbe_I}=0f1h>?7VM0>ifhX`lO&&7^}-M(Th@vbGwteJowwA`yZt=c zl#Zap@vkUb>a!hwqap-m)*+}>lU~~@sK$_$&sTYS6Dt;Naf(o0N{sK4Zfh!Q~uA@G`&P> zQIFzi0P2FrkMnB8peQtP;VcJR2E)frmD;6pJMH635vYzLK;Es+L~GC<_J>0-Di5B8 zwgp&IA1d`y7RxEyuB8GLX5$hh;%VZ-T{(FW3;>l^FAMQaniLh$o}d^LNP^ZqPgK*=I5h{D=2TB_d48i?Hh1}Mcf!9TV^N1!|cg!O!?$&6;B$2N|qx8fcm_GfjLnlYv)^y zg7HX%8ET0EVa=h*q@w-9ejU;kKmF-vqSf>58+w^E>Q!F5XhEr7l#zI56Ah)5F%WQu znrK3))c`+x$1YO4Jw?iKX37&_KTe(#H2iow0T}y=LTj4&=oCNWd#W#Oj}Mfq<9x(- z(hZC+tUl!GFZ7T0l+DvM2EcAg&+^L&1Tg{-D?EO)e=FD)>2tDh?|f?a0*|E=hGj%1 z+BclL?2=}?{Bpp%oZ>y3RO{kKECME3vQiG6nsv)K@09lGe4kX0w#f7n1^b++C{3R@ z?AzouUbeN9Sx6?4P=1Bjt03yma4SHP3Ebm+vkPeu5a~F3C`botsnwY6sg~vjT79$g zqoeaQ8b@G#BQ0H + + + + + + + + + + diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index f14574508c7..845816562bf 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -7906,6 +7906,9 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, "unassignedItemsBannerNotice": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." }, @@ -7962,6 +7965,55 @@ "errorAssigningTargetFolder": { "message": "Error assigning target folder." }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations":{ + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, "createNewClientToManageAsProvider": { "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." }, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.html new file mode 100644 index 00000000000..15b2519daee --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.html @@ -0,0 +1,29 @@ + diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.spec.ts new file mode 100644 index 00000000000..94cec5f627f --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.spec.ts @@ -0,0 +1,174 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { BehaviorSubject } from "rxjs"; + +import { SYSTEM_THEME_OBSERVABLE } from "../../../../../../../libs/angular/src/services/injection-tokens"; +import { ThemeType } from "../../../../../../../libs/common/src/platform/enums"; +import { ThemeStateService } from "../../../../../../../libs/common/src/platform/theming/theme-state.service"; + +import { IntegrationCardComponent } from "./integration-card.component"; + +describe("IntegrationCardComponent", () => { + let component: IntegrationCardComponent; + let fixture: ComponentFixture; + + const systemTheme$ = new BehaviorSubject(ThemeType.Light); + const usersPreferenceTheme$ = new BehaviorSubject(ThemeType.Light); + + beforeEach(async () => { + // reset system theme + systemTheme$.next(ThemeType.Light); + + await TestBed.configureTestingModule({ + declarations: [IntegrationCardComponent], + providers: [ + { + provide: ThemeStateService, + useValue: { selectedTheme$: usersPreferenceTheme$ }, + }, + { + provide: SYSTEM_THEME_OBSERVABLE, + useValue: systemTheme$, + }, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(IntegrationCardComponent); + component = fixture.componentInstance; + + component.name = "Integration Name"; + component.image = "test-image.png"; + component.linkText = "Get started with integration"; + component.linkURL = "https://example.com/"; + + fixture.detectChanges(); + }); + + it("assigns link href", () => { + const link = fixture.nativeElement.querySelector("a"); + + expect(link.href).toBe("https://example.com/"); + }); + + it("renders card body", () => { + const name = fixture.nativeElement.querySelector("h3"); + const link = fixture.nativeElement.querySelector("a"); + + expect(name.textContent).toBe("Integration Name"); + expect(link.textContent.trim()).toBe("Get started with integration"); + }); + + it("assigns external rel attribute", () => { + component.externalURL = true; + fixture.detectChanges(); + + const link = fixture.nativeElement.querySelector("a"); + + expect(link.rel).toBe("noopener noreferrer"); + }); + + describe("new badge", () => { + beforeEach(() => { + jest.useFakeTimers(); + jest.setSystemTime(new Date("2023-09-01")); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it("shows when expiration is in the future", () => { + component.newBadgeExpiration = "2023-09-02"; + expect(component.showNewBadge()).toBe(true); + }); + + it("does not show when expiration is not set", () => { + expect(component.showNewBadge()).toBe(false); + }); + + it("does not show when expiration is in the past", () => { + component.newBadgeExpiration = "2023-08-31"; + expect(component.showNewBadge()).toBe(false); + }); + + it("does not show when expiration is today", () => { + component.newBadgeExpiration = "2023-09-01"; + expect(component.showNewBadge()).toBe(false); + }); + + it("does not show when expiration is invalid", () => { + component.newBadgeExpiration = "not-a-date"; + expect(component.showNewBadge()).toBe(false); + }); + }); + + describe("imageDarkMode", () => { + it("ignores theme changes when darkModeImage is not set", () => { + systemTheme$.next(ThemeType.Dark); + usersPreferenceTheme$.next(ThemeType.Dark); + + fixture.detectChanges(); + + expect(component.imageEle.nativeElement.src).toContain("test-image.png"); + }); + + describe("user prefers the system theme", () => { + beforeEach(() => { + component.imageDarkMode = "test-image-dark.png"; + }); + + it("sets image src to imageDarkMode", () => { + usersPreferenceTheme$.next(ThemeType.System); + systemTheme$.next(ThemeType.Dark); + + fixture.detectChanges(); + + expect(component.imageEle.nativeElement.src).toContain("test-image-dark.png"); + }); + + it("sets image src to light mode image", () => { + component.imageEle.nativeElement.src = "test-image-dark.png"; + + usersPreferenceTheme$.next(ThemeType.System); + systemTheme$.next(ThemeType.Light); + + fixture.detectChanges(); + + expect(component.imageEle.nativeElement.src).toContain("test-image.png"); + }); + }); + + describe("user prefers dark mode", () => { + beforeEach(() => { + component.imageDarkMode = "test-image-dark.png"; + }); + + it("updates image to dark mode", () => { + systemTheme$.next(ThemeType.Light); // system theme shouldn't matter + usersPreferenceTheme$.next(ThemeType.Dark); + + fixture.detectChanges(); + + expect(component.imageEle.nativeElement.src).toContain("test-image-dark.png"); + }); + }); + + describe("user prefers light mode", () => { + beforeEach(() => { + component.imageDarkMode = "test-image-dark.png"; + }); + + it("updates image to light mode", () => { + component.imageEle.nativeElement.src = "test-image-dark.png"; + + systemTheme$.next(ThemeType.Dark); // system theme shouldn't matter + usersPreferenceTheme$.next(ThemeType.Light); + + fixture.detectChanges(); + + expect(component.imageEle.nativeElement.src).toContain("test-image.png"); + }); + }); + }); +}); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.ts new file mode 100644 index 00000000000..bf5f5bd3112 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.ts @@ -0,0 +1,93 @@ +import { + AfterViewInit, + Component, + ElementRef, + Inject, + Input, + OnDestroy, + ViewChild, +} from "@angular/core"; +import { Observable, Subject, combineLatest, takeUntil } from "rxjs"; + +import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens"; +import { ThemeType } from "@bitwarden/common/platform/enums"; +import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; + +@Component({ + selector: "sm-integration-card", + templateUrl: "./integration-card.component.html", +}) +export class IntegrationCardComponent implements AfterViewInit, OnDestroy { + private destroyed$: Subject = new Subject(); + @ViewChild("imageEle") imageEle: ElementRef; + + @Input() name: string; + @Input() image: string; + @Input() imageDarkMode?: string; + @Input() linkText: string; + @Input() linkURL: string; + + /** Adds relevant `rel` attribute to external links */ + @Input() externalURL?: boolean; + + /** + * Date of when the new badge should be hidden. + * When omitted, the new badge is never shown. + * + * @example "2024-12-31" + */ + @Input() newBadgeExpiration?: string; + + constructor( + private themeStateService: ThemeStateService, + @Inject(SYSTEM_THEME_OBSERVABLE) + private systemTheme$: Observable, + ) {} + + ngAfterViewInit() { + combineLatest([this.themeStateService.selectedTheme$, this.systemTheme$]) + .pipe(takeUntil(this.destroyed$)) + .subscribe(([theme, systemTheme]) => { + // When the card doesn't have a dark mode image, exit early + if (!this.imageDarkMode) { + return; + } + + if (theme === ThemeType.System) { + // When the user's preference is the system theme, + // use the system theme to determine the image + const prefersDarkMode = + systemTheme === ThemeType.Dark || systemTheme === ThemeType.SolarizedDark; + + this.imageEle.nativeElement.src = prefersDarkMode ? this.imageDarkMode : this.image; + } else if (theme === ThemeType.Dark || theme === ThemeType.SolarizedDark) { + // When the user's preference is dark mode, use the dark mode image + this.imageEle.nativeElement.src = this.imageDarkMode; + } else { + // Otherwise use the light mode image + this.imageEle.nativeElement.src = this.image; + } + }); + } + + ngOnDestroy(): void { + this.destroyed$.next(); + this.destroyed$.complete(); + } + + /** Show the "new" badge when expiration is in the future */ + showNewBadge() { + if (!this.newBadgeExpiration) { + return false; + } + + const expirationDate = new Date(this.newBadgeExpiration); + + // Do not show the new badge for invalid dates + if (isNaN(expirationDate.getTime())) { + return false; + } + + return expirationDate > new Date(); + } +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.html new file mode 100644 index 00000000000..a0c82d2f342 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.html @@ -0,0 +1,15 @@ +
    +
  • + +
  • +
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.spec.ts new file mode 100644 index 00000000000..e74e057e069 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.spec.ts @@ -0,0 +1,81 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; +import { mock } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { SYSTEM_THEME_OBSERVABLE } from "../../../../../../../libs/angular/src/services/injection-tokens"; +import { IntegrationType } from "../../../../../../../libs/common/src/enums"; +import { ThemeType } from "../../../../../../../libs/common/src/platform/enums"; +import { ThemeStateService } from "../../../../../../../libs/common/src/platform/theming/theme-state.service"; +import { IntegrationCardComponent } from "../integration-card/integration-card.component"; +import { Integration } from "../models/integration"; + +import { IntegrationGridComponent } from "./integration-grid.component"; + +describe("IntegrationGridComponent", () => { + let component: IntegrationGridComponent; + let fixture: ComponentFixture; + const integrations: Integration[] = [ + { + name: "Integration 1", + image: "test-image1.png", + linkText: "Get started with integration 1", + linkURL: "https://example.com/1", + type: IntegrationType.Integration, + }, + { + name: "SDK 2", + image: "test-image2.png", + linkText: "View SDK 2", + linkURL: "https://example.com/2", + type: IntegrationType.SDK, + }, + ]; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [IntegrationGridComponent, IntegrationCardComponent], + providers: [ + { + provide: ThemeStateService, + useValue: mock(), + }, + { + provide: SYSTEM_THEME_OBSERVABLE, + useValue: of(ThemeType.Light), + }, + ], + }); + + fixture = TestBed.createComponent(IntegrationGridComponent); + component = fixture.componentInstance; + component.integrations = integrations; + fixture.detectChanges(); + }); + + it("lists all integrations", () => { + expect(component.integrations).toEqual(integrations); + + const cards = fixture.debugElement.queryAll(By.directive(IntegrationCardComponent)); + + expect(cards.length).toBe(integrations.length); + }); + + it("assigns the correct attributes to IntegrationCardComponent", () => { + expect(component.integrations).toEqual(integrations); + + const card = fixture.debugElement.queryAll(By.directive(IntegrationCardComponent))[1]; + + expect(card.componentInstance.name).toBe("SDK 2"); + expect(card.componentInstance.image).toBe("test-image2.png"); + expect(card.componentInstance.linkText).toBe("View SDK 2"); + expect(card.componentInstance.linkURL).toBe("https://example.com/2"); + }); + + it("assigns `externalURL` for SDKs", () => { + const card = fixture.debugElement.queryAll(By.directive(IntegrationCardComponent)); + + expect(card[0].componentInstance.externalURL).toBe(false); + expect(card[1].componentInstance.externalURL).toBe(true); + }); +}); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.ts new file mode 100644 index 00000000000..058d59d702c --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.ts @@ -0,0 +1,15 @@ +import { Component, Input } from "@angular/core"; + +import { IntegrationType } from "@bitwarden/common/enums"; + +import { Integration } from "../models/integration"; + +@Component({ + selector: "sm-integration-grid", + templateUrl: "./integration-grid.component.html", +}) +export class IntegrationGridComponent { + @Input() integrations: Integration[]; + + protected IntegrationType = IntegrationType; +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations-routing.module.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations-routing.module.ts new file mode 100644 index 00000000000..91402113a95 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations-routing.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from "@angular/core"; +import { RouterModule, Routes } from "@angular/router"; + +import { IntegrationsComponent } from "./integrations.component"; + +const routes: Routes = [ + { + path: "", + component: IntegrationsComponent, + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class IntegrationsRoutingModule {} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.html new file mode 100644 index 00000000000..a2f21888613 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.html @@ -0,0 +1,16 @@ + + + + +
+

{{ "integrationsDesc" | i18n }}

+ +
+ +
+

+ {{ "sdks" | i18n }} +

+

{{ "sdksDesc" | i18n }}

+ +
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts new file mode 100644 index 00000000000..10fbaa1f3fb --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts @@ -0,0 +1,77 @@ +import { Component } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; +import { mock } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { SYSTEM_THEME_OBSERVABLE } from "../../../../../../libs/angular/src/services/injection-tokens"; +import { I18nService } from "../../../../../../libs/common/src/platform/abstractions/i18n.service"; +import { ThemeType } from "../../../../../../libs/common/src/platform/enums"; +import { ThemeStateService } from "../../../../../../libs/common/src/platform/theming/theme-state.service"; +import { I18nPipe } from "../../../../../../libs/components/src/shared/i18n.pipe"; + +import { IntegrationCardComponent } from "./integration-card/integration-card.component"; +import { IntegrationGridComponent } from "./integration-grid/integration-grid.component"; +import { IntegrationsComponent } from "./integrations.component"; + +@Component({ + selector: "app-header", + template: "
", +}) +class MockHeaderComponent {} + +@Component({ + selector: "sm-new-menu", + template: "
", +}) +class MockNewMenuComponent {} + +describe("IntegrationsComponent", () => { + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ + IntegrationsComponent, + IntegrationGridComponent, + IntegrationCardComponent, + MockHeaderComponent, + MockNewMenuComponent, + I18nPipe, + ], + providers: [ + { + provide: I18nService, + useValue: mock({ t: (key) => key }), + }, + { + provide: ThemeStateService, + useValue: mock(), + }, + { + provide: SYSTEM_THEME_OBSERVABLE, + useValue: of(ThemeType.Light), + }, + ], + }).compileComponents(); + fixture = TestBed.createComponent(IntegrationsComponent); + fixture.detectChanges(); + }); + + it("divides Integrations & SDKS", () => { + const [integrationList, sdkList] = fixture.debugElement.queryAll( + By.directive(IntegrationGridComponent), + ); + + // Validate only expected names, as the data is constant + expect( + (integrationList.componentInstance as IntegrationGridComponent).integrations.map( + (i) => i.name, + ), + ).toEqual(["GitHub Actions", "GitLab CI/CD", "Ansible"]); + + expect( + (sdkList.componentInstance as IntegrationGridComponent).integrations.map((i) => i.name), + ).toEqual(["C#", "C++", "Go", "Java", "JS WebAssembly", "php", "Python", "Ruby"]); + }); +}); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.ts new file mode 100644 index 00000000000..f11048b6a30 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.ts @@ -0,0 +1,113 @@ +import { Component } from "@angular/core"; + +import { IntegrationType } from "@bitwarden/common/enums"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + +import { Integration } from "./models/integration"; + +@Component({ + selector: "sm-integrations", + templateUrl: "./integrations.component.html", +}) +export class IntegrationsComponent { + private integrationsAndSdks: Integration[] = []; + + constructor(i18nService: I18nService) { + this.integrationsAndSdks = [ + { + name: "GitHub Actions", + linkText: i18nService.t("setUpGithubActions"), + linkURL: "https://bitwarden.com/help/github-actions-integration/", + image: "../../../../../../../images/secrets-manager/integrations/github.svg", + imageDarkMode: "../../../../../../../images/secrets-manager/integrations/github-white.svg", + type: IntegrationType.Integration, + }, + { + name: "GitLab CI/CD", + linkText: i18nService.t("setUpGitlabCICD"), + linkURL: "https://bitwarden.com/help/gitlab-integration/", + image: "../../../../../../../images/secrets-manager/integrations/gitlab.svg", + imageDarkMode: "../../../../../../../images/secrets-manager/integrations/gitlab-white.svg", + type: IntegrationType.Integration, + }, + { + name: "Ansible", + linkText: i18nService.t("setUpAnsible"), + linkURL: "https://bitwarden.com/help/ansible-integration/", + image: "../../../../../../../images/secrets-manager/integrations/ansible.svg", + type: IntegrationType.Integration, + }, + { + name: "C#", + linkText: i18nService.t("cSharpSDKRepo"), + linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/csharp", + image: "../../../../../../../images/secrets-manager/sdks/c-sharp.svg", + type: IntegrationType.SDK, + }, + { + name: "C++", + linkText: i18nService.t("cPlusPlusSDKRepo"), + linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/cpp", + image: "../../../../../../../images/secrets-manager/sdks/c-plus-plus.png", + type: IntegrationType.SDK, + }, + { + name: "Go", + linkText: i18nService.t("goSDKRepo"), + linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/go", + image: "../../../../../../../images/secrets-manager/sdks/go.svg", + type: IntegrationType.SDK, + }, + { + name: "Java", + linkText: i18nService.t("javaSDKRepo"), + linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/java", + image: "../../../../../../../images/secrets-manager/sdks/java.svg", + imageDarkMode: "../../../../../../../images/secrets-manager/sdks/java-white.svg", + type: IntegrationType.SDK, + }, + { + name: "JS WebAssembly", + linkText: i18nService.t("jsWebAssemblySDKRepo"), + linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/js", + image: "../../../../../../../images/secrets-manager/sdks/wasm.svg", + type: IntegrationType.SDK, + }, + { + name: "php", + linkText: i18nService.t("phpSDKRepo"), + linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/php", + image: "../../../../../../../images/secrets-manager/sdks/php.svg", + type: IntegrationType.SDK, + }, + { + name: "Python", + linkText: i18nService.t("pythonSDKRepo"), + linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/python", + image: "../../../../../../../images/secrets-manager/sdks/python.svg", + type: IntegrationType.SDK, + }, + { + name: "Ruby", + linkText: i18nService.t("rubySDKRepo"), + linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/ruby", + image: "../../../../../../../images/secrets-manager/sdks/ruby.png", + type: IntegrationType.SDK, + }, + ]; + } + + /** Filter out content for the integrations sections */ + get integrations(): Integration[] { + return this.integrationsAndSdks.filter( + (integration) => integration.type === IntegrationType.Integration, + ); + } + + /** Filter out content for the SDKs section */ + get sdks(): Integration[] { + return this.integrationsAndSdks.filter( + (integration) => integration.type === IntegrationType.SDK, + ); + } +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.module.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.module.ts new file mode 100644 index 00000000000..0d26b626f16 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.module.ts @@ -0,0 +1,15 @@ +import { NgModule } from "@angular/core"; + +import { SecretsManagerSharedModule } from "../shared/sm-shared.module"; + +import { IntegrationCardComponent } from "./integration-card/integration-card.component"; +import { IntegrationGridComponent } from "./integration-grid/integration-grid.component"; +import { IntegrationsRoutingModule } from "./integrations-routing.module"; +import { IntegrationsComponent } from "./integrations.component"; + +@NgModule({ + imports: [SecretsManagerSharedModule, IntegrationsRoutingModule], + declarations: [IntegrationsComponent, IntegrationGridComponent, IntegrationCardComponent], + providers: [], +}) +export class IntegrationsModule {} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/models/integration.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/models/integration.ts new file mode 100644 index 00000000000..51ca79b30f7 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/models/integration.ts @@ -0,0 +1,21 @@ +import { IntegrationType } from "@bitwarden/common/enums"; + +/** Integration or SDK */ +export type Integration = { + name: string; + image: string; + /** + * Optional image shown in dark mode. + */ + imageDarkMode?: string; + linkURL: string; + linkText: string; + type: IntegrationType; + /** + * Shows the "New" badge until the defined date. + * When omitted, the badge is never shown. + * + * @example "2024-12-31" + */ + newBadgeExpiration?: string; +}; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.html index c6c7bc6efb2..e382fbd9a98 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.html @@ -22,6 +22,12 @@ route="machine-accounts" [relativeTo]="route.parent" > + IntegrationsModule, + data: { + titleId: "integrations", + }, + }, { path: "trash", loadChildren: () => TrashModule, diff --git a/libs/common/src/enums/index.ts b/libs/common/src/enums/index.ts index 378af213e67..9ca806899a1 100644 --- a/libs/common/src/enums/index.ts +++ b/libs/common/src/enums/index.ts @@ -3,6 +3,7 @@ export * from "./device-type.enum"; export * from "./event-system-user.enum"; export * from "./event-type.enum"; export * from "./http-status-code.enum"; +export * from "./integration-type.enum"; export * from "./native-messaging-version.enum"; export * from "./notification-type.enum"; export * from "./product-type.enum"; diff --git a/libs/common/src/enums/integration-type.enum.ts b/libs/common/src/enums/integration-type.enum.ts new file mode 100644 index 00000000000..acb95106976 --- /dev/null +++ b/libs/common/src/enums/integration-type.enum.ts @@ -0,0 +1,4 @@ +export enum IntegrationType { + Integration = "integration", + SDK = "sdk", +} From b26c9df056e0a0d665ac65271008d709b65933f1 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Fri, 19 Apr 2024 05:41:46 -0400 Subject: [PATCH 004/110] Fix migrated state service data (#8815) State service held data in an encrypted pair, with potentially both encrypted and decrypted values. We want the encrypted form for these disk migrations (decrypted would always be empty on disk anyways). --- ...e-cipher-service-to-state-provider.spec.ts | 24 +++++++++++-------- ...7-move-cipher-service-to-state-provider.ts | 9 ++++--- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/libs/common/src/state-migrations/migrations/57-move-cipher-service-to-state-provider.spec.ts b/libs/common/src/state-migrations/migrations/57-move-cipher-service-to-state-provider.spec.ts index 499cff1c89a..f51699bc791 100644 --- a/libs/common/src/state-migrations/migrations/57-move-cipher-service-to-state-provider.spec.ts +++ b/libs/common/src/state-migrations/migrations/57-move-cipher-service-to-state-provider.spec.ts @@ -26,11 +26,13 @@ function exampleJSON() { }, }, ciphers: { - "cipher-id-10": { - id: "cipher-id-10", - }, - "cipher-id-11": { - id: "cipher-id-11", + encrypted: { + "cipher-id-10": { + id: "cipher-id-10", + }, + "cipher-id-11": { + id: "cipher-id-11", + }, }, }, }, @@ -150,11 +152,13 @@ describe("CipherServiceMigrator", () => { }, }, ciphers: { - "cipher-id-10": { - id: "cipher-id-10", - }, - "cipher-id-11": { - id: "cipher-id-11", + encrypted: { + "cipher-id-10": { + id: "cipher-id-10", + }, + "cipher-id-11": { + id: "cipher-id-11", + }, }, }, }, diff --git a/libs/common/src/state-migrations/migrations/57-move-cipher-service-to-state-provider.ts b/libs/common/src/state-migrations/migrations/57-move-cipher-service-to-state-provider.ts index e71d889bb79..80c776e1b69 100644 --- a/libs/common/src/state-migrations/migrations/57-move-cipher-service-to-state-provider.ts +++ b/libs/common/src/state-migrations/migrations/57-move-cipher-service-to-state-provider.ts @@ -4,7 +4,9 @@ import { Migrator } from "../migrator"; type ExpectedAccountType = { data: { localData?: unknown; - ciphers?: unknown; + ciphers?: { + encrypted: unknown; + }; }; }; @@ -37,7 +39,7 @@ export class CipherServiceMigrator extends Migrator<56, 57> { } //Migrate ciphers - const ciphers = account?.data?.ciphers; + const ciphers = account?.data?.ciphers?.encrypted; if (ciphers != null) { await helper.setToUser(userId, CIPHERS_DISK, ciphers); delete account.data.ciphers; @@ -68,7 +70,8 @@ export class CipherServiceMigrator extends Migrator<56, 57> { const ciphers = await helper.getFromUser(userId, CIPHERS_DISK); if (account.data && ciphers != null) { - account.data.ciphers = ciphers; + account.data.ciphers ||= { encrypted: null }; + account.data.ciphers.encrypted = ciphers; await helper.set(userId, account); } await helper.setToUser(userId, CIPHERS_DISK, null); From ec1973b334814f7b64be39a3072bcd857d014a17 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 19 Apr 2024 10:39:43 +0000 Subject: [PATCH 005/110] Autosync the updated translations (#8826) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/src/_locales/ar/messages.json | 40 ++++++++++---- apps/browser/src/_locales/az/messages.json | 54 +++++++++++++------ apps/browser/src/_locales/be/messages.json | 40 ++++++++++---- apps/browser/src/_locales/bg/messages.json | 40 ++++++++++---- apps/browser/src/_locales/bn/messages.json | 40 ++++++++++---- apps/browser/src/_locales/bs/messages.json | 40 ++++++++++---- apps/browser/src/_locales/ca/messages.json | 40 ++++++++++---- apps/browser/src/_locales/cs/messages.json | 40 ++++++++++---- apps/browser/src/_locales/cy/messages.json | 40 ++++++++++---- apps/browser/src/_locales/da/messages.json | 40 ++++++++++---- apps/browser/src/_locales/de/messages.json | 40 ++++++++++---- apps/browser/src/_locales/el/messages.json | 40 ++++++++++---- apps/browser/src/_locales/en_GB/messages.json | 40 ++++++++++---- apps/browser/src/_locales/en_IN/messages.json | 40 ++++++++++---- apps/browser/src/_locales/es/messages.json | 44 ++++++++++----- apps/browser/src/_locales/et/messages.json | 40 ++++++++++---- apps/browser/src/_locales/eu/messages.json | 40 ++++++++++---- apps/browser/src/_locales/fa/messages.json | 40 ++++++++++---- apps/browser/src/_locales/fi/messages.json | 40 ++++++++++---- apps/browser/src/_locales/fil/messages.json | 40 ++++++++++---- apps/browser/src/_locales/fr/messages.json | 48 ++++++++++++----- apps/browser/src/_locales/gl/messages.json | 52 ++++++++++++------ apps/browser/src/_locales/he/messages.json | 40 ++++++++++---- apps/browser/src/_locales/hi/messages.json | 40 ++++++++++---- apps/browser/src/_locales/hr/messages.json | 40 ++++++++++---- apps/browser/src/_locales/hu/messages.json | 40 ++++++++++---- apps/browser/src/_locales/id/messages.json | 40 ++++++++++---- apps/browser/src/_locales/it/messages.json | 40 ++++++++++---- apps/browser/src/_locales/ja/messages.json | 40 ++++++++++---- apps/browser/src/_locales/ka/messages.json | 40 ++++++++++---- apps/browser/src/_locales/km/messages.json | 40 ++++++++++---- apps/browser/src/_locales/kn/messages.json | 40 ++++++++++---- apps/browser/src/_locales/ko/messages.json | 40 ++++++++++---- apps/browser/src/_locales/lt/messages.json | 40 ++++++++++---- apps/browser/src/_locales/lv/messages.json | 40 ++++++++++---- apps/browser/src/_locales/ml/messages.json | 40 ++++++++++---- apps/browser/src/_locales/mr/messages.json | 40 ++++++++++---- apps/browser/src/_locales/my/messages.json | 40 ++++++++++---- apps/browser/src/_locales/nb/messages.json | 40 ++++++++++---- apps/browser/src/_locales/ne/messages.json | 40 ++++++++++---- apps/browser/src/_locales/nl/messages.json | 40 ++++++++++---- apps/browser/src/_locales/nn/messages.json | 40 ++++++++++---- apps/browser/src/_locales/or/messages.json | 40 ++++++++++---- apps/browser/src/_locales/pl/messages.json | 44 ++++++++++----- apps/browser/src/_locales/pt_BR/messages.json | 40 ++++++++++---- apps/browser/src/_locales/pt_PT/messages.json | 40 ++++++++++---- apps/browser/src/_locales/ro/messages.json | 40 ++++++++++---- apps/browser/src/_locales/ru/messages.json | 40 ++++++++++---- apps/browser/src/_locales/si/messages.json | 40 ++++++++++---- apps/browser/src/_locales/sk/messages.json | 40 ++++++++++---- apps/browser/src/_locales/sl/messages.json | 40 ++++++++++---- apps/browser/src/_locales/sr/messages.json | 40 ++++++++++---- apps/browser/src/_locales/sv/messages.json | 40 ++++++++++---- apps/browser/src/_locales/te/messages.json | 40 ++++++++++---- apps/browser/src/_locales/th/messages.json | 40 ++++++++++---- apps/browser/src/_locales/tr/messages.json | 40 ++++++++++---- apps/browser/src/_locales/uk/messages.json | 40 ++++++++++---- apps/browser/src/_locales/vi/messages.json | 54 +++++++++++++------ apps/browser/src/_locales/zh_CN/messages.json | 40 ++++++++++---- apps/browser/src/_locales/zh_TW/messages.json | 40 ++++++++++---- apps/browser/store/locales/gl/copy.resx | 2 +- 61 files changed, 1829 insertions(+), 629 deletions(-) diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 1f7c5bbe98c..b7e26f33620 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "تغيير كلمة المرور الرئيسية" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "عبارة بصمة الإصبع", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "أُضيف المجلد" }, - "changeMasterPass": { - "message": "تغيير كلمة المرور الرئيسية" - }, - "changeMasterPasswordConfirmation": { - "message": "يمكنك تغيير كلمة المرور الرئيسية من خزنة الويب في bitwarden.com. هل تريد زيارة الموقع الآن؟" - }, "twoStepLoginConfirmation": { "message": "تسجيل الدخول بخطوتين يجعل حسابك أكثر أمنا من خلال مطالبتك بالتحقق من تسجيل الدخول باستخدام جهاز آخر مثل مفتاح الأمان، تطبيق المصادقة، الرسائل القصيرة، المكالمة الهاتفية، أو البريد الإلكتروني. يمكن تمكين تسجيل الدخول بخطوتين على خزنة الويب bitwarden.com. هل تريد زيارة الموقع الآن؟" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 2111ea6704e..20834af27ff 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Ana parolu dəyişdir" }, + "continueToWebApp": { + "message": "Veb tətbiqlə davam edilsin?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Ana parolunuzu Bitwarden veb tətbiqində dəyişdirə bilərsiniz." + }, "fingerprintPhrase": { "message": "Barmaq izi ifadəsi", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Qovluq əlavə edildi" }, - "changeMasterPass": { - "message": "Ana parolu dəyişdir" - }, - "changeMasterPasswordConfirmation": { - "message": "Ana parolunuzu bitwarden.com veb anbarında dəyişdirə bilərsiniz. İndi saytı ziyarət etmək istəyirsiniz?" - }, "twoStepLoginConfirmation": { "message": "İki addımlı giriş, güvənlik açarı, kimlik doğrulayıcı tətbiq, SMS, telefon zəngi və ya e-poçt kimi digər cihazlarla girişinizi doğrulamanızı tələb edərək hesabınızı daha da güvənli edir. İki addımlı giriş, bitwarden.com veb anbarında qurula bilər. Veb saytı indi ziyarət etmək istəyirsiniz?" }, @@ -1045,7 +1045,7 @@ "message": "Bildiriş server URL-si" }, "iconsUrl": { - "message": "Nişan server URL-si" + "message": "İkon server URL-si" }, "environmentSaved": { "message": "Mühit URL-ləri saxlanıldı." @@ -1072,7 +1072,7 @@ "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { - "message": "Avto-doldurma nişanı seçiləndə", + "message": "Avto-doldurma ikonu seçiləndə", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, "enableAutoFillOnPageLoad": { @@ -1109,7 +1109,7 @@ "message": "Anbarı açılan pəncərədə aç" }, "commandOpenSidebar": { - "message": "Anbar yan sətirdə aç" + "message": "Anbarı yan çubuqda aç" }, "commandAutofillDesc": { "message": "Hazırkı veb sayt üçün son istifadə edilən giriş məlumatlarını avto-doldur" @@ -1162,7 +1162,7 @@ "message": "Bu brauzer bu açılan pəncərədə U2F tələblərini emal edə bilmir. U2F istifadə edərək giriş etmək üçün bu açılan pəncərəni yeni bir pəncərədə açmaq istəyirsiniz?" }, "enableFavicon": { - "message": "Veb sayt nişanlarını göstər" + "message": "Veb sayt ikonlarını göstər" }, "faviconDesc": { "message": "Hər girişin yanında tanına bilən təsvir göstər." @@ -1724,7 +1724,7 @@ "message": "İcazə tələb xətası" }, "nativeMessaginPermissionSidebarDesc": { - "message": "Bu əməliyyatı kənar çubuqda icra edilə bilməz. Lütfən açılan pəncərədə yenidən sınayın." + "message": "Bu əməliyyat yan çubuqda icra edilə bilməz. Lütfən açılan pəncərədə yenidən sınayın." }, "personalOwnershipSubmitError": { "message": "Müəssisə Siyasətinə görə, elementləri şəxsi anbarınızda saxlamağınız məhdudlaşdırılıb. Sahiblik seçimini təşkilat olaraq dəyişdirin və mövcud kolleksiyalar arasından seçim edin." @@ -1924,10 +1924,10 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendLinuxChromiumFileWarning": { - "message": "Bir fayl seçmək üçün (mümkünsə) kənar çubuqdakı uzantını açın və ya bu bannerə klikləyərək yeni bir pəncərədə açın." + "message": "Bir fayl seçmək üçün (mümkünsə) yan çubuqdakı uzantını açın və ya bu bannerə klikləyərək yeni bir pəncərədə açın." }, "sendFirefoxFileWarning": { - "message": "Firefox istifadə edərək bir fayl seçmək üçün kənar çubuqdakı uzantını açın və ya bu bannerə klikləyərək yeni bir pəncərədə açın." + "message": "Firefox istifadə edərək bir fayl seçmək üçün yan çubuqdakı uzantını açın və ya bu bannerə klikləyərək yeni bir pəncərədə açın." }, "sendSafariFileWarning": { "message": "Safari istifadə edərək bir fayl seçmək üçün bu bannerə klikləyərək yeni bir pəncərədə açın." @@ -3000,16 +3000,36 @@ "message": "Kimlik məlumatlarını saxlama xətası. Detallar üçün konsolu yoxlayın.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Uğurlu" + }, "removePasskey": { "message": "Parolu sil" }, "passkeyRemoved": { "message": "Parol silindi" }, - "unassignedItemsBanner": { - "message": "Bildiriş: Təyin edilməmiş təşkilat elementləri artıq Bütün Anbarlar görünüşündə görünməyəndir və yalnız Admin Konsolu vasitəsilə əlçatandır. Bu elementləri görünən etmək üçün Admin Konsolundan bir kolleksiyaya təyin edin." + "unassignedItemsBannerNotice": { + "message": "Bildiriş: Təyin edilməyən təşkilat elementləri artıq Bütün Anbarlar görünüşündə görünən olmayacaq və yalnız Admin Konsolu vasitəsilə əlçatan olacaq." }, - "unassignedItemsBannerSelfHost": { - "message": "Bildiriş: 2 May 2024-cü ildən etibarən təyin edilməmiş təşkilat elementləri artıq Bütün Anbarlar görünüşündə görünməyən və yalnız Admin Konsolu vasitəsilə əlçatan olacaq. Bu elementləri görünən etmək üçün Admin Konsolundan bir kolleksiyaya təyin edin." + "unassignedItemsBannerSelfHostNotice": { + "message": "Bildiriş: 16 May 2024-cü il tarixindən etibarən, təyin edilməyən təşkilat elementləri Bütün Anbarlar görünüşündə görünən olmayacaq və yalnız Admin Konsolu vasitəsilə əlçatan olacaq." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Bu elementləri görünən etmək üçün", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "bir kolleksiyaya təyin edin.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Konsolu" + }, + "errorAssigningTargetCollection": { + "message": "Hədəf kolleksiyaya təyin etmə xətası." + }, + "errorAssigningTargetFolder": { + "message": "Hədəf qovluğa təyin etmə xətası." } } diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 08cb351abb9..630ad48ee63 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Змяніць асноўны пароль" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Фраза адбітка пальца", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Папка дададзена" }, - "changeMasterPass": { - "message": "Змяніць асноўны пароль" - }, - "changeMasterPasswordConfirmation": { - "message": "Вы можаце змяніць свой асноўны пароль у вэб-сховішчы на bitwarden.com. Перайсці на вэб-сайт зараз?" - }, "twoStepLoginConfirmation": { "message": "Двухэтапны ўваход робіць ваш уліковы запіс больш бяспечным, патрабуючы пацвярджэнне ўваходу на іншай прыладзе з выкарыстаннем ключа бяспекі, праграмы аўтэнтыфікацыі, SMS, тэлефоннага званка або электроннай пошты. Двухэтапны ўваход уключаецца на bitwarden.com. Перайсці на вэб-сайт, каб зрабіць гэта?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 87dfc8d3beb..edfcd8a9b40 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Промяна на главната парола" }, + "continueToWebApp": { + "message": "Продължаване към уеб приложението?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Може да промените главната си парола в уеб приложението на Битуорден." + }, "fingerprintPhrase": { "message": "Уникална фраза", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Добавена папка" }, - "changeMasterPass": { - "message": "Промяна на главната парола" - }, - "changeMasterPasswordConfirmation": { - "message": "Главната парола на трезор може да се промени чрез сайта bitwarden.com. Искате ли да го посетите?" - }, "twoStepLoginConfirmation": { "message": "Двустепенното вписване защитава регистрацията ви, като ви кара да потвърдите влизането си чрез устройство-ключ, приложение за удостоверение, мобилно съобщение, телефонно обаждане или електронна поща. Двустепенното вписване може да се включи чрез сайта bitwarden.com. Искате ли да го посетите?" }, @@ -3000,16 +3000,36 @@ "message": "Грешка при запазването на идентификационните данни. Вижте конзолата за подробности.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Премахване на секретния ключ" }, "passkeyRemoved": { "message": "Секретният ключ е премахнат" }, - "unassignedItemsBanner": { - "message": "Известие: неразпределените елементи на организацията вече не се виждат в изгледа с „Всички трезори“, а са достъпни само през Административната конзола. Добавете тези елементи към някоя колекция в Административната конзола, за да станат видими." + "unassignedItemsBannerNotice": { + "message": "Известие: неразпределените елементи в организацията вече няма да се виждат в изгледа с всички трезори, а са достъпни само през Административната конзола." }, - "unassignedItemsBannerSelfHost": { - "message": "Известие: от 2 май 2024г. неразпределените елементи на организациите вече няма се виждат в изгледа с „Всички трезори“, а ще бъдат достъпни само през Административната конзола. Добавете тези елементи към някоя колекция в Административната конзола, за да станат видими." + "unassignedItemsBannerSelfHostNotice": { + "message": "Известие: след 16 май 2024, неразпределените елементи в организацията вече няма да се виждат в изгледа с всички трезори, а ще бъдат достъпни само през Административната конзола." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Добавете тези елементи към колекция в", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "за да ги направите видими.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Административна конзола" + }, + "errorAssigningTargetCollection": { + "message": "Грешка при задаването на целева колекция." + }, + "errorAssigningTargetFolder": { + "message": "Грешка при задаването на целева папка." } } diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index 1bdaeef7c6a..2ab44bf7e14 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "মূল পাসওয়ার্ড পরিবর্তন" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "ফিঙ্গারপ্রিন্ট ফ্রেজ", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "ফোল্ডার জোড়া হয়েছে" }, - "changeMasterPass": { - "message": "মূল পাসওয়ার্ড পরিবর্তন" - }, - "changeMasterPasswordConfirmation": { - "message": "আপনি bitwarden.com ওয়েব ভল্ট থেকে মূল পাসওয়ার্ডটি পরিবর্তন করতে পারেন। আপনি কি এখনই ওয়েবসাইটটি দেখতে চান?" - }, "twoStepLoginConfirmation": { "message": "দ্বি-পদক্ষেপ লগইন অন্য ডিভাইসে আপনার লগইনটি যাচাই করার জন্য সিকিউরিটি কী, প্রমাণীকরণকারী অ্যাপ্লিকেশন, এসএমএস, ফোন কল বা ই-মেইল ব্যাবহারের মাধ্যমে আপনার অ্যাকাউন্টকে আরও সুরক্ষিত করে। bitwarden.com ওয়েব ভল্টে দ্বি-পদক্ষেপের লগইন সক্ষম করা যাবে। আপনি কি এখনই ওয়েবসাইটটি দেখতে চান?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index 92a667afeba..8d2a25db9c6 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change master password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Folder added" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index fc67602b600..30d4c4e636f 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Canvia la contrasenya mestra" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Frase d'empremta digital", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Carpeta afegida" }, - "changeMasterPass": { - "message": "Canvia la contrasenya mestra" - }, - "changeMasterPasswordConfirmation": { - "message": "Podeu canviar la contrasenya mestra a la caixa forta web de bitwarden.com. Voleu visitar el lloc web ara?" - }, "twoStepLoginConfirmation": { "message": "L'inici de sessió en dues passes fa que el vostre compte siga més segur, ja que obliga a verificar el vostre inici de sessió amb un altre dispositiu, com ara una clau de seguretat, una aplicació autenticadora, un SMS, una trucada telefònica o un correu electrònic. Es pot habilitar l'inici de sessió en dues passes a la caixa forta web de bitwarden.com. Voleu visitar el lloc web ara?" }, @@ -3000,16 +3000,36 @@ "message": "S'ha produït un error en guardar les credencials. Consulteu la consola per obtenir més informació.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Suprimeix la clau de pas" }, "passkeyRemoved": { "message": "Clau de pas suprimida" }, - "unassignedItemsBanner": { - "message": "Nota: els elements de l'organització sense assignar ja no es veuran a la vista \"Totes les caixes fortes\" i només es veuran des de la consola d'administració. Assigneu-los-hi una col·lecció des de la consola per fer-los visibles." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index d989d25bf27..42a4f9edb1a 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Změnit hlavní heslo" }, + "continueToWebApp": { + "message": "Pokračovat do webové aplikace?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Hlavní heslo můžete změnit ve webové aplikaci Bitwardenu." + }, "fingerprintPhrase": { "message": "Fráze otisku prstu", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Složka byla přidána" }, - "changeMasterPass": { - "message": "Změnit hlavní heslo" - }, - "changeMasterPasswordConfirmation": { - "message": "Hlavní heslo si můžete změnit na webové stránce bitwarden.com. Chcete tuto stránku nyní otevřít?" - }, "twoStepLoginConfirmation": { "message": "Dvoufázové přihlášení činí Váš účet mnohem bezpečnějším díky nutnosti po každém úspěšném přihlášení zadat ověřovací kód získaný z bezpečnostního klíče, aplikace, SMS, telefonního hovoru nebo e-mailu. Dvoufázové přihlášení lze aktivovat na webové stránce bitwarden.com. Chcete tuto stránku nyní otevřít?" }, @@ -3000,16 +3000,36 @@ "message": "Chyba při ukládání přihlašovacích údajů. Podrobnosti naleznete v konzoli.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Úspěch" + }, "removePasskey": { "message": "Odebrat přístupový klíč" }, "passkeyRemoved": { "message": "Přístupový klíč byl odebrán" }, - "unassignedItemsBanner": { - "message": "Upozornění: Nepřiřazené položky organizace již nejsou viditelné ve Vašem zobrazení všech trezorů a jsou nyní přístupné jen v konzoli správce. Přiřaďte tyto položky do kolekce z konzole pro správce, aby byly viditelné." + "unassignedItemsBannerNotice": { + "message": "Upozornění: Nepřiřazené položky organizace již nejsou viditelné ve vašem zobrazení všech trezorů a jsou nyní přístupné pouze v konzoli správce." }, - "unassignedItemsBannerSelfHost": { - "message": "Upozornění: Dne 2. května 2024 již nebudou nepřiřazené položky organizace viditelné v zobrazení Všechny trezory a budou přístupné jen prostřednictvím konzoly správce. Přiřaďte tyto položky do kolekce z konzoly pro správce, aby byly viditelné." + "unassignedItemsBannerSelfHostNotice": { + "message": "Upozornění: 16. květba 2024 již nebudou nepřiřazené položky organizace viditelné ve vašem zobrazení všech trezorů a budou přístupné pouze v konzoli správce." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Přiřadit tyto položky ke kolekci z", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "aby byly viditelné.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Konzole správce" + }, + "errorAssigningTargetCollection": { + "message": "Chyba při přiřazování cílové kolekce." + }, + "errorAssigningTargetFolder": { + "message": "Chyba při přiřazování cílové složky." } } diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 79178bc9d5d..52d3cf7d56a 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Newid y prif gyfrinair" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Ymadrodd unigryw", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Ffolder wedi'i hychwanegu" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index d808d974124..08aebe98e14 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Skift hovedadgangskode" }, + "continueToWebApp": { + "message": "Fortsæt til web-app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Hovedadgangskoden kan ændres via Bitwarden web-appen." + }, "fingerprintPhrase": { "message": "Fingeraftrykssætning", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Mappe tilføjet" }, - "changeMasterPass": { - "message": "Skift hovedadgangskode" - }, - "changeMasterPasswordConfirmation": { - "message": "Du kan ændre din hovedadgangskode i bitwarden.com web-boksen. Vil du besøge hjemmesiden nu?" - }, "twoStepLoginConfirmation": { "message": "To-trins login gør din konto mere sikker ved at kræve, at du verificerer dit login med en anden enhed, såsom en sikkerhedsnøgle, autentificeringsapp, SMS, telefonopkald eller e-mail. To-trins login kan aktiveres i bitwarden.com web-boksen. Vil du besøge hjemmesiden nu?" }, @@ -3000,16 +3000,36 @@ "message": "Fejl under import. Tjek konsollen for detaljer.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Gennemført" + }, "removePasskey": { "message": "Fjern adgangsnøgle" }, "passkeyRemoved": { "message": "Adgangsnøgle fjernet" }, - "unassignedItemsBanner": { - "message": "Bemærk: Utildelte organisationsemner er ikke længere synlige i Alle Bokse-visningen og er kun tilgængelige via Adminkonsollen. Føj disse emner til en samling fra Adminkonsollen for at gøre dem synlige." + "unassignedItemsBannerNotice": { + "message": "Bemærk: Utildelte organisationsemner er ikke længere synlige i Alle Bokse-visningen, men er kun tilgængelige via Admin-konsol." }, - "unassignedItemsBannerSelfHost": { - "message": "Bemærk: Pr. 2. maj 2024 vil utildelte organisationsemner ikke længere være synlige i Alle Bokse-visningen og vil kun være tilgængelige via Admin-konsollen. Tildel disse emner til en samling via Admin-konsollen for at gøre dem synlige." + "unassignedItemsBannerSelfHostNotice": { + "message": "Bemærk: Pr. 16. maj 2024 er utildelte organisationsemner er ikke længere synlige i Alle Bokse-visningen, men er kun tilgængelige via Admin-konsol." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Tildel disse emner til en samling via", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "for at gøre dem synlige.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin-konsol" + }, + "errorAssigningTargetCollection": { + "message": "Fejl ved tildeling af målsamling." + }, + "errorAssigningTargetFolder": { + "message": "Fejl ved tildeling af målmappe." } } diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index deb92e992db..4edca5557cf 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Master-Passwort ändern" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerabdruck-Phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Ordner hinzugefügt" }, - "changeMasterPass": { - "message": "Master-Passwort ändern" - }, - "changeMasterPasswordConfirmation": { - "message": "Du kannst dein Master-Passwort im Bitwarden.com Web-Tresor ändern. Möchtest du die Seite jetzt öffnen?" - }, "twoStepLoginConfirmation": { "message": "Mit der Zwei-Faktor-Authentifizierung wird dein Konto zusätzlich abgesichert, da jede Anmeldung mit einem anderen Gerät wie einem Sicherheitsschlüssel, einer Authentifizierungs-App, einer SMS, einem Anruf oder einer E-Mail verifiziert werden muss. Die Zwei-Faktor-Authentifizierung kann im bitwarden.com Web-Tresor aktiviert werden. Möchtest du die Website jetzt öffnen?" }, @@ -3000,16 +3000,36 @@ "message": "Fehler beim Speichern der Zugangsdaten. Details in der Konsole.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Passkey entfernen" }, "passkeyRemoved": { "message": "Passkey entfernt" }, - "unassignedItemsBanner": { - "message": "Hinweis: Nicht zugeordnete Organisationseinträge sind nicht mehr in der Ansicht aller Tresore sichtbar und nur über die Administrator-Konsole zugänglich. Weise diese Einträge einer Sammlung aus der Administrator-Konsole zu, um sie sichtbar zu machen." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Hinweis: Ab dem 2. Mai 2024 werden nicht zugewiesene Organisationseinträge nicht mehr in der Ansicht aller Tresore sichtbar sein und sind nur über die Administrator-Konsole zugänglich. Weise diese Elemente einer Sammlung aus der Administrator-Konsole zu, um sie sichtbar zu machen." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Fehler beim Zuweisen der Ziel-Sammlung." + }, + "errorAssigningTargetFolder": { + "message": "Fehler beim Zuweisen des Ziel-Ordners." } } diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index 36b14e447fc..75cd5ef2fa8 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Αλλαγή Κύριου Κωδικού" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Φράση Δακτυλικών Αποτυπωμάτων", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Προστέθηκε φάκελος" }, - "changeMasterPass": { - "message": "Αλλαγή Κύριου Κωδικού" - }, - "changeMasterPasswordConfirmation": { - "message": "Μπορείτε να αλλάξετε τον κύριο κωδικό στο bitwarden.com. Θέλετε να επισκεφθείτε την ιστοσελίδα τώρα;" - }, "twoStepLoginConfirmation": { "message": "Η σύνδεση σε δύο βήματα καθιστά πιο ασφαλή τον λογαριασμό σας, απαιτώντας να επαληθεύσετε τα στοιχεία σας με μια άλλη συσκευή, όπως κλειδί ασφαλείας, εφαρμογή επαλήθευσης, μήνυμα SMS, τηλεφωνική κλήση ή email. Μπορείτε να ενεργοποιήσετε τη σύνδεση σε δύο βήματα στο bitwarden.com. Θέλετε να επισκεφθείτε την ιστοσελίδα τώρα;" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index 1ac55feb429..b4b7b314db4 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change master password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Folder added" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organisation items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organisation items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organisation items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organisation items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index cbe214f0b33..6dd78dc292e 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change master password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Added folder" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be enabled on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index ee5666f3ccc..20b9a91814f 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Cambiar contraseña maestra" }, + "continueToWebApp": { + "message": "¿Continuar a la aplicación web?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Puedes cambiar la contraseña maestra en la aplicación web de Bitwarden." + }, "fingerprintPhrase": { "message": "Frase de huella digital", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Carpeta añadida" }, - "changeMasterPass": { - "message": "Cambiar contraseña maestra" - }, - "changeMasterPasswordConfirmation": { - "message": "Puedes cambiar tu contraseña maestra en la caja fuerte web de bitwarden.com. ¿Quieres visitar ahora el sitio web?" - }, "twoStepLoginConfirmation": { "message": "La autenticación en dos pasos hace que tu cuenta sea mucho más segura, requiriendo que introduzcas un código de seguridad de una aplicación de autenticación cada vez que accedes. La autenticación en dos pasos puede ser habilitada en la caja fuerte web de bitwarden.com. ¿Quieres visitar ahora el sitio web?" }, @@ -3000,16 +3000,36 @@ "message": "Se produjo un error al guardar las credenciales. Revise la consola para obtener detalles.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { - "message": "Remove passkey" + "message": "Eliminar passkey" }, "passkeyRemoved": { - "message": "Passkey removed" + "message": "Passkey eliminada" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index ea1758468e4..c8325288479 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Muuda ülemparooli" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Sõrmejälje fraas", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Kaust on lisatud" }, - "changeMasterPass": { - "message": "Muuda ülemparooli" - }, - "changeMasterPasswordConfirmation": { - "message": "Saad oma ülemparooli muuta bitwarden.com veebihoidlas. Soovid seda kohe teha?" - }, "twoStepLoginConfirmation": { "message": "Kaheastmeline kinnitamine aitab konto turvalisust tõsta. Lisaks paroolile pead kontole ligipääsemiseks kinnitama sisselogimise päringu SMS-ga, telefonikõnega, autentimise rakendusega või e-postiga. Kaheastmelist kinnitust saab sisse lülitada bitwarden.com veebihoidlas. Soovid seda kohe avada?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Eemalda pääsuvõti" }, "passkeyRemoved": { "message": "Pääsuvõti on eemaldatud" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index 529a1e81270..9504f06c65b 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Aldatu pasahitz nagusia" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Hatz-marka digitalaren esaldia", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Karpeta gehituta" }, - "changeMasterPass": { - "message": "Aldatu pasahitz nagusia" - }, - "changeMasterPasswordConfirmation": { - "message": "Zure pasahitz nagusia alda dezakezu bitwarden.com webgunean. Orain joan nahi duzu webgunera?" - }, "twoStepLoginConfirmation": { "message": "Bi urratseko saio hasiera dela eta, zure kontua seguruagoa da, beste aplikazio/gailu batekin saioa hastea eskatzen baitizu; adibidez, segurtasun-gako, autentifikazio-aplikazio, SMS, telefono dei edo email bidez. Bi urratseko saio hasiera bitwarden.com webgunean aktibatu daiteke. Orain joan nahi duzu webgunera?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index 669eb151f4b..c96c5c35cf0 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "تغییر کلمه عبور اصلی" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "عبارت اثر انگشت", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "پوشه اضافه شد" }, - "changeMasterPass": { - "message": "تغییر کلمه عبور اصلی" - }, - "changeMasterPasswordConfirmation": { - "message": "شما می‌توانید کلمه عبور اصلی خود را در bitwarden.com تغییر دهید. آیا می‌خواهید از سایت بازدید کنید؟" - }, "twoStepLoginConfirmation": { "message": "ورود دو مرحله ای باعث می‌شود که حساب کاربری شما با استفاده از یک دستگاه دیگر مانند کلید امنیتی، برنامه احراز هویت، پیامک، تماس تلفنی و یا ایمیل، اعتبار خود را با ایمنی بیشتر اثبات کند. ورود دو مرحله ای می تواند در bitwarden.com فعال شود. آیا می‌خواهید از سایت بازدید کنید؟" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 17aea532ba3..b3602afd82d 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Vaihda pääsalasana" }, + "continueToWebApp": { + "message": "Avataanko verkkosovellus?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Voit vaihtaa pääsalasanasi Bitwardenin verkkosovelluksessa." + }, "fingerprintPhrase": { "message": "Tunnistelauseke", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Kansio lisätty" }, - "changeMasterPass": { - "message": "Vaihda pääsalasana" - }, - "changeMasterPasswordConfirmation": { - "message": "Voit vaihtaa pääsalasanasi bitwarden.com-verkkoholvissa. Haluatko käydä sivustolla nyt?" - }, "twoStepLoginConfirmation": { "message": "Kaksivaiheinen kirjautuminen parantaa tilisi suojausta vaatimalla kirjautumisen vahvistuksen salasanan lisäksi todennuslaitteen, ‑sovelluksen, tekstiviestin, puhelun tai sähköpostin avulla. Voit ottaa kaksivaiheisen kirjautumisen käyttöön bitwarden.com‑verkkoholvissa. Haluatko avata sen nyt?" }, @@ -3000,16 +3000,36 @@ "message": "Virhe tallennettaessa käyttäjätietoja. Näet isätietoja hallinnasta.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Poista suojausavain" }, "passkeyRemoved": { "message": "Suojausavain poistettiin" }, - "unassignedItemsBanner": { - "message": "Huomautus: Organisaatioiden kokoelmiin määrittämättömät kohteet eivät enää näy laitteiden \"Kaikki holvit\" -näkymissä, vaan ne ovat nähtävissä vain Hallintapaneelista. Määritä kohteet kokoelmiin Hallintapaneelista, jotta ne ovat jatkossakin käytettävissä kaikilta laitteilta." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Huomautus: 2.5.2024 alkaen kokoelmiin määrittämättömät organisaatioiden kohteet eivät enää näy laitteiden \"Kaikki holvit\" -näkymissä, vaan ne ovat nähtävissä vain Hallintapaneelista. Määritä kohteet kokoelmiin Hallintapaneelista, jotta ne ovat jatkossakin käytettävissä kaikilta laitteilta." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Määritä nämä kohteet kokoelmaan", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": ", jotta ne näkyvät.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Hallintapaneelista" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index 42d5060e285..28418d984d1 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Baguhin ang Master Password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Hulmabig ng Hilik ng Dako", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Idinagdag na folder" }, - "changeMasterPass": { - "message": "Palitan ang master password" - }, - "changeMasterPasswordConfirmation": { - "message": "Maaari mong palitan ang iyong master password sa bitwarden.com web vault. Gusto mo bang bisitahin ang website ngayon?" - }, "twoStepLoginConfirmation": { "message": "Ang two-step login ay nagpapagaan sa iyong account sa pamamagitan ng pag-verify sa iyong login sa isa pang device tulad ng security key, authenticator app, SMS, tawag sa telepono o email. Ang two-step login ay maaaring magawa sa bitwarden.com web vault. Gusto mo bang bisitahin ang website ngayon?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index 6cced1cb0d1..206f07800d8 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Changer le mot de passe principal" }, + "continueToWebApp": { + "message": "Poursuivre vers l'application web ?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Vous pouvez modifier votre mot de passe principal sur l'application web de Bitwarden." + }, "fingerprintPhrase": { "message": "Phrase d'empreinte", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -525,13 +531,13 @@ "message": "Impossible de scanner le QR code à partir de la page web actuelle" }, "totpCaptureSuccess": { - "message": "Clé de l'Authentificateur ajoutée" + "message": "Clé Authenticator ajoutée" }, "totpCapture": { "message": "Scanner le QR code de l'authentificateur à partir de la page web actuelle" }, "copyTOTP": { - "message": "Copier la clé de l'Authentificateur (TOTP)" + "message": "Copier la clé Authenticator (TOTP)" }, "loggedOut": { "message": "Déconnecté" @@ -557,12 +563,6 @@ "addedFolder": { "message": "Dossier ajouté" }, - "changeMasterPass": { - "message": "Changer le mot de passe principal" - }, - "changeMasterPasswordConfirmation": { - "message": "Vous pouvez changer votre mot de passe principal depuis le coffre web de bitwarden.com. Voulez-vous visiter le site web maintenant ?" - }, "twoStepLoginConfirmation": { "message": "L'authentification à deux facteurs rend votre compte plus sûr en vous demandant de vérifier votre connexion avec un autre dispositif tel qu'une clé de sécurité, une application d'authentification, un SMS, un appel téléphonique ou un courriel. L'authentification à deux facteurs peut être configurée dans le coffre web de bitwarden.com. Voulez-vous visiter le site web maintenant ?" }, @@ -659,7 +659,7 @@ "message": "Liste les éléments des cartes de paiement sur la Page d'onglet pour faciliter la saisie automatique." }, "showIdentitiesCurrentTab": { - "message": "Afficher les identités sur la page d'onglet" + "message": "Afficher les identités sur la Page d'onglet" }, "showIdentitiesCurrentTabDesc": { "message": "Liste les éléments d'identité sur la Page d'onglet pour faciliter la saisie automatique." @@ -802,7 +802,7 @@ "message": "En savoir plus" }, "authenticatorKeyTotp": { - "message": "Clé de l'Authentificateur (TOTP)" + "message": "Clé Authenticator (TOTP)" }, "verificationCodeTotp": { "message": "Code de vérification (TOTP)" @@ -3000,16 +3000,36 @@ "message": "Erreur lors de l'enregistrement des identifiants. Consultez la console pour plus de détails.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Retirer la clé d'identification (passkey)" }, "passkeyRemoved": { "message": "Clé d'identification (passkey) retirée" }, - "unassignedItemsBanner": { - "message": "Notice : les éléments d'organisation non assignés ne sont plus visibles dans la vue Tous les coffres et sont uniquement accessibles via la console d'administration. Assignez ces éléments à une collection à partir de la console d'administration pour les rendre visibles." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Remarque : au 2 mai 2024, les éléments d'organisation non assignés ne sont plus visibles dans votre vue Tous les coffres sur tous les appareils et sont uniquement accessibles via la Console d'administration. Assignez ces éléments à une collection à partir de la Console d'administration pour les rendre visibles." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index 023e03b834a..95b880d1a54 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -14,28 +14,28 @@ "message": "Log in or create a new account to access your secure vault." }, "createAccount": { - "message": "Create account" + "message": "Crea unha conta" }, "login": { - "message": "Log in" + "message": "Iniciar sesión" }, "enterpriseSingleSignOn": { "message": "Enterprise single sign-on" }, "cancel": { - "message": "Cancel" + "message": "Cancelar" }, "close": { - "message": "Close" + "message": "Pechar" }, "submit": { "message": "Submit" }, "emailAddress": { - "message": "Email address" + "message": "Enderezo de correo electrónico" }, "masterPass": { - "message": "Master password" + "message": "Contrasinal mestre" }, "masterPassDesc": { "message": "The master password is the password you use to access your vault. It is very important that you do not forget your master password. There is no way to recover the password in the event that you forget it." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change master password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Folder added" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index 1e633f5eb93..efb82e64ec1 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "החלף סיסמה ראשית" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "סיסמת טביעת אצבע", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "נוספה תיקייה" }, - "changeMasterPass": { - "message": "החלף סיסמה ראשית" - }, - "changeMasterPasswordConfirmation": { - "message": "באפשרותך לשנות את הסיסמה הראשית שלך דרך הכספת באתר bitwarden.com. האם ברצונך לפתוח את האתר כעת?" - }, "twoStepLoginConfirmation": { "message": "התחברות בשני-שלבים הופכת את החשבון שלך למאובטח יותר בכך שאתה נדרש לוודא בכל כניסה בעזרת מכשיר אחר כדוגמת מפתח אבטחה, תוכנת אימות, SMS, שיחת טלפון, או אימייל. ניתן להפעיל את \"התחברות בשני-שלבים\" בכספת שבאתר bitwarden.com. האם ברצונך לפתוח את האתר כעת?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 44f645bc471..9c13fa6efa3 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change Master Password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint Phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "जोड़ा गया फ़ोल्डर" }, - "changeMasterPass": { - "message": "Change Master Password" - }, - "changeMasterPasswordConfirmation": { - "message": "आप वेब वॉल्ट bitwarden.com पर अपना मास्टर पासवर्ड बदल सकते हैं।क्या आप अब वेबसाइट पर जाना चाहते हैं?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to enter a security code from an authenticator app whenever you log in. Two-step login can be enabled on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index e74a72bc4fa..ee4c4e7859e 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Promjeni glavnu lozinku" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Jedinstvena fraza", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Mapa dodana" }, - "changeMasterPass": { - "message": "Promjeni glavnu lozinku" - }, - "changeMasterPasswordConfirmation": { - "message": "Svoju glavnu lozinku možeš promijeniti na web trezoru. Želiš li sada posjetiti bitwarden.com?" - }, "twoStepLoginConfirmation": { "message": "Prijava dvostrukom autentifikacijom čini tvoj račun još sigurnijim tako što će zahtijevati da potvrdiš prijavu putem drugog uređaja pomoću sigurnosnog koda, autentifikatorske aplikacije, SMS-om, pozivom ili e-poštom. Prijavu dvostrukom autentifikacijom možeš omogućiti na web trezoru. Želiš li sada posjetiti bitwarden.com?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index cadd72a4750..7d6a8a208be 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Mesterjelszó módosítása" }, + "continueToWebApp": { + "message": "Tovább a webes alkalmazáshoz?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "A mesterjelszó a Bitwarden webalkalmazásban módosítható." + }, "fingerprintPhrase": { "message": "Ujjlenyomat kifejezés", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "A mappa hozzáadásra került." }, - "changeMasterPass": { - "message": "Mesterjelszó módosítása" - }, - "changeMasterPasswordConfirmation": { - "message": "Mesterjelszavadat a bitwarden.com webes széfén tudod megváltoztatni. Szeretnéd meglátogatni a most a weboldalt?" - }, "twoStepLoginConfirmation": { "message": "A kétlépcsős bejelentkezés biztonságosabbá teszi a fiókot azáltal, hogy ellenőrizni kell a bejelentkezést egy másik olyan eszközzel mint például biztonsági kulcs, hitelesítő alkalmazás, SMS, telefon hívás vagy email. A kétlépcsős bejelentkezést a bitwarden.com webes széfben lehet engedélyezni. Felkeressük a webhelyet most?" }, @@ -3000,16 +3000,36 @@ "message": "Hiba történt a hitelesítések mentésekor. A részletekért ellenőrizzük a konzolt.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Jelszó eltávolítása" }, "passkeyRemoved": { "message": "A jelszó eltávolításra került." }, - "unassignedItemsBanner": { - "message": "Megjegyzés: A nem hozzá nem rendelt szervezeti elemek már nem láthatók az Összes széf nézetben és csak az Adminisztrátori konzolon keresztül érhetők el. Rendeljük ezeket az elemeket egy gyűjteményhez az Adminisztrátor konzolból, hogy láthatóvá tegyük azokat." + "unassignedItemsBannerNotice": { + "message": "Megjegyzés: A nem hozzárendelt szervezeti elemek már nem láthatók az Összes széf nézetben a különböző eszközökön és csak az Adminisztrátori konzolon keresztül érhetők el." }, - "unassignedItemsBannerSelfHost": { - "message": "Figyelmeztetés: 2024. május 2-án a nem hozzá rendelt szervezeti elemek többé nem lesznek láthatók az Összes széf nézetben a különböző eszközökön és csak a Felügyeleti konzolon keresztül lesznek elérhetők. Rendeljük ezeket az elemeket egy gyűjteményhez az Adminisztrátori konzolból, hogy láthatóvá tegyük azokat." + "unassignedItemsBannerSelfHostNotice": { + "message": "Megjegyzés: 2024. május 16-tól a nem hozzárendelt szervezeti elemek többé nem lesznek láthatók az Összes széf nézetben a különböző eszközökön és csak az Adminisztrátori konzolon keresztül lesznek elérhetők." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Rendeljük hozzá ezeket az elemeket a gyűjteményhez", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "a láthatósághoz.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Adminisztrátori konzol" + }, + "errorAssigningTargetCollection": { + "message": "Hiba történt a célgyűjtemény hozzárendelése során." + }, + "errorAssigningTargetFolder": { + "message": "Hiba történt a célmappa hozzárendelése során." } } diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index 9907a7520c3..92b60324ad5 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Ubah Kata Sandi Utama" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Frasa Sidik Jari", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Tambah Folder" }, - "changeMasterPass": { - "message": "Ubah Kata Sandi Utama" - }, - "changeMasterPasswordConfirmation": { - "message": "Anda dapat mengubah kata sandi utama Anda di brankas web bitwarden.com. Anda ingin mengunjungi situs web sekarang?" - }, "twoStepLoginConfirmation": { "message": "Info masuk dua langkah membuat akun Anda lebih aman dengan mengharuskan Anda memverifikasi info masuk Anda dengan peranti lain seperti kode keamanan, aplikasi autentikasi, SMK, panggilan telepon, atau email. Info masuk dua langkah dapat diaktifkan di brankas web bitwarden.com. Anda ingin mengunjungi situs web sekarang?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 65a5a1ad045..6887b134dfb 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Cambia password principale" }, + "continueToWebApp": { + "message": "Passa al sito web?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Puoi modificare la tua password principale sul sito web di Bitwarden." + }, "fingerprintPhrase": { "message": "Frase impronta", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Cartella aggiunta" }, - "changeMasterPass": { - "message": "Cambia password principale" - }, - "changeMasterPasswordConfirmation": { - "message": "Puoi cambiare la tua password principale sulla cassaforte online di bitwarden.com. Vuoi visitare ora il sito?" - }, "twoStepLoginConfirmation": { "message": "La verifica in due passaggi rende il tuo account più sicuro richiedendoti di verificare il tuo login usando un altro dispositivo come una chiave di sicurezza, app di autenticazione, SMS, telefonata, o email. Può essere abilitata nella cassaforte web su bitwarden.com. Vuoi visitare il sito?" }, @@ -3000,16 +3000,36 @@ "message": "Errore durante il salvataggio delle credenziali. Controlla la console per più dettagli.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Rimuovi passkey" }, "passkeyRemoved": { "message": "Passkey rimossa" }, - "unassignedItemsBanner": { - "message": "Avviso: gli elementi dell'organizzazione non assegnati non sono più visibili nella visualizzazione Tutte le Cassaforti su tutti i dispositivi e sono ora accessibili solo tramite la Console di amministrazione. Assegna questi elementi ad una raccolta dalla Console di amministrazione per renderli visibili." + "unassignedItemsBannerNotice": { + "message": "Avviso: gli elementi dell'organizzazione non assegnati non sono più visibili nella visualizzazione Tutte le Cassaforti su tutti i dispositivi e sono ora accessibili solo tramite la Console di amministrazione." }, - "unassignedItemsBannerSelfHost": { - "message": "Avviso: dal 2 maggio 2024, gli elementi dell'organizzazione non assegnati non saranno più visibili nella visualizzazione Tutte le Cassaforti su tutti i dispositivi e saranno accessibili solo tramite la Console di amministrazione. Assegna questi elementi ad una raccolta dalla Console di amministrazione per renderli visibili." + "unassignedItemsBannerSelfHostNotice": { + "message": "Avviso: dal 16 maggio 2024, gli elementi dell'organizzazione non assegnati non saranno più visibili nella visualizzazione Tutte le Cassaforti su tutti i dispositivi e saranno accessibili solo tramite la Console di amministrazione." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assegna questi elementi ad una raccolta dalla", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "per renderli visibili.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Console di amministrazione" + }, + "errorAssigningTargetCollection": { + "message": "Errore nell'assegnazione della raccolta di destinazione." + }, + "errorAssigningTargetFolder": { + "message": "Errore nell'assegnazione della cartella di destinazione." } } diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 05fb7fe5de6..744c76a5098 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "マスターパスワードの変更" }, + "continueToWebApp": { + "message": "ウェブアプリに進みますか?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Bitwarden ウェブアプリでマスターパスワードを変更できます。" + }, "fingerprintPhrase": { "message": "パスフレーズ", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "フォルダを追加しました" }, - "changeMasterPass": { - "message": "マスターパスワードの変更" - }, - "changeMasterPasswordConfirmation": { - "message": "マスターパスワードは bitwarden.com ウェブ保管庫で変更できます。ウェブサイトを開きますか?" - }, "twoStepLoginConfirmation": { "message": "2段階認証を使うと、ログイン時にセキュリティキーや認証アプリ、SMS、電話やメールでの認証を必要にすることでアカウントをさらに安全に出来ます。2段階認証は bitwarden.com ウェブ保管庫で有効化できます。ウェブサイトを開きますか?" }, @@ -3000,16 +3000,36 @@ "message": "資格情報の保存中にエラーが発生しました。詳細はコンソールを確認してください。", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "成功" + }, "removePasskey": { "message": "パスキーを削除" }, "passkeyRemoved": { "message": "パスキーを削除しました" }, - "unassignedItemsBanner": { - "message": "注意: 割り当てられていない組織項目は、すべての保管庫のビューでは表示されなくなり、管理コンソールからのみアクセスできます。 管理コンソールからコレクションにこれらのアイテムを割り当てると、表示するようにできます。" + "unassignedItemsBannerNotice": { + "message": "注意: 割り当てられていない組織アイテムは、すべての保管庫ビューでは表示されなくなり、管理コンソールからのみアクセスできるようになります。" }, - "unassignedItemsBannerSelfHost": { - "message": "お知らせ:2024年5月2日に、 割り当てられていない組織アイテムはデバイス間のすべての保管庫ビューに表示されなくなり、管理コンソールからのみアクセス可能になります。 管理コンソールからコレクションにこれらのアイテムを割り当てると、表示できるようになります。" + "unassignedItemsBannerSelfHostNotice": { + "message": "お知らせ:2024年5月16日に、 割り当てられていない組織アイテムは、すべての保管庫ビューに表示されなくなり、管理コンソールからのみアクセス可能になります。" + }, + "unassignedItemsBannerCTAPartOne": { + "message": "これらのアイテムのコレクションへの割り当てを", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "で実行すると表示できるようになります。", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "管理コンソール" + }, + "errorAssigningTargetCollection": { + "message": "ターゲットコレクションの割り当てに失敗しました。" + }, + "errorAssigningTargetFolder": { + "message": "ターゲットフォルダーの割り当てに失敗しました。" } } diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index d67b88ba9c4..ab7b84d22ea 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change master password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Folder added" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index 023e03b834a..67e1f247871 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change master password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Folder added" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index 61cfadc7626..9b363cba1f9 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "ಮಾಸ್ಟರ್ ಪಾಸ್ವರ್ಡ್ ಬದಲಾಯಿಸಿ" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "ಫಿಂಗರ್ಪ್ರಿಂಟ್ ಫ್ರೇಸ್", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "ಫೋಲ್ಡರ್ ಸೇರಿಸಿ" }, - "changeMasterPass": { - "message": "ಮಾಸ್ಟರ್ ಪಾಸ್ವರ್ಡ್ ಬದಲಾಯಿಸಿ" - }, - "changeMasterPasswordConfirmation": { - "message": "ನಿಮ್ಮ ಮಾಸ್ಟರ್ ಪಾಸ್‌ವರ್ಡ್ ಅನ್ನು ನೀವು bitwarden.com ವೆಬ್ ವಾಲ್ಟ್‌ನಲ್ಲಿ ಬದಲಾಯಿಸಬಹುದು. ನೀವು ಈಗ ವೆಬ್‌ಸೈಟ್‌ಗೆ ಭೇಟಿ ನೀಡಲು ಬಯಸುವಿರಾ?" - }, "twoStepLoginConfirmation": { "message": "ಭದ್ರತಾ ಕೀ, ದೃಢೀಕರಣ ಅಪ್ಲಿಕೇಶನ್, ಎಸ್‌ಎಂಎಸ್, ಫೋನ್ ಕರೆ ಅಥವಾ ಇಮೇಲ್‌ನಂತಹ ಮತ್ತೊಂದು ಸಾಧನದೊಂದಿಗೆ ನಿಮ್ಮ ಲಾಗಿನ್ ಅನ್ನು ಪರಿಶೀಲಿಸುವ ಅಗತ್ಯವಿರುವ ಎರಡು ಹಂತದ ಲಾಗಿನ್ ನಿಮ್ಮ ಖಾತೆಯನ್ನು ಹೆಚ್ಚು ಸುರಕ್ಷಿತಗೊಳಿಸುತ್ತದೆ. ಬಿಟ್ವಾರ್ಡೆನ್.ಕಾಮ್ ವೆಬ್ ವಾಲ್ಟ್ನಲ್ಲಿ ಎರಡು-ಹಂತದ ಲಾಗಿನ್ ಅನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಬಹುದು. ನೀವು ಈಗ ವೆಬ್‌ಸೈಟ್‌ಗೆ ಭೇಟಿ ನೀಡಲು ಬಯಸುವಿರಾ?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index c71fbdf7a81..3e4f5769c08 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "마스터 비밀번호 변경" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "지문 구절", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "폴더 추가함" }, - "changeMasterPass": { - "message": "마스터 비밀번호 변경" - }, - "changeMasterPasswordConfirmation": { - "message": "bitwarden.com 웹 보관함에서 마스터 비밀번호를 바꿀 수 있습니다. 지금 웹 사이트를 방문하시겠습니까?" - }, "twoStepLoginConfirmation": { "message": "2단계 인증은 보안 키, 인증 앱, SMS, 전화 통화 등의 다른 기기로 사용자의 로그인 시도를 검증하여 사용자의 계정을 더욱 안전하게 만듭니다. 2단계 인증은 bitwarden.com 웹 보관함에서 활성화할 수 있습니다. 지금 웹 사이트를 방문하시겠습니까?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index 0fc146c2501..c690c2727b4 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Keisti pagrindinį slaptažodį" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Pirštų atspaudų frazė", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Katalogas pridėtas" }, - "changeMasterPass": { - "message": "Keisti pagrindinį slaptažodį" - }, - "changeMasterPasswordConfirmation": { - "message": "Pagrindinį slaptažodį galite pakeisti „bitwarden.com“ žiniatinklio saugykloje. Ar norite dabar apsilankyti svetainėje?" - }, "twoStepLoginConfirmation": { "message": "Prisijungus dviem veiksmais, jūsų paskyra tampa saugesnė, reikalaujant patvirtinti prisijungimą naudojant kitą įrenginį, pvz., saugos raktą, autentifikavimo programėlę, SMS, telefono skambutį ar el. paštą. Dviejų žingsnių prisijungimą galima įjungti „bitwarden.com“ interneto saugykloje. Ar norite dabar apsilankyti svetainėje?" }, @@ -3000,16 +3000,36 @@ "message": "Klaida išsaugant kredencialus. Išsamesnės informacijos patikrink konsolėje.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Pašalinti slaptaraktį" }, "passkeyRemoved": { "message": "Pašalintas slaptaraktis" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index efac417556f..d26b10a30ce 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Mainīt galveno paroli" }, + "continueToWebApp": { + "message": "Pāriet uz tīmekļa lietotni?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Savu galveno paroli var mainīt Bitwarden tīmekļa lietotnē." + }, "fingerprintPhrase": { "message": "Atpazīšanas vārdkopa", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Pievienoja mapi" }, - "changeMasterPass": { - "message": "Mainīt galveno paroli" - }, - "changeMasterPasswordConfirmation": { - "message": "Galveno paroli ir iespējams mainīt bitwarden.com tīmekļa glabātavā. Vai tagad apmeklēt tīmekļvietni?" - }, "twoStepLoginConfirmation": { "message": "Divpakāpju pieteikšanās padara kontu krietni drošāku, pieprasot apstiprināt pieteikšanos ar tādu citu ierīču vai pakalpojumu starpniecību kā drošības atslēga, autentificētāja lietotne, īsziņa, tālruņa zvans vai e-pasts. Divpakāpju pieteikšanos var iespējot bitwarden.com tīmekļa glabātavā. Vai tagad apmeklēt tīmekļvietni?" }, @@ -3000,16 +3000,36 @@ "message": "Kļūda piekļuves informācijas saglabāšanā. Jāpārbauda, vai konsolē ir izvērstāka informācija.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Noņemt piekļuves atslēgu" }, "passkeyRemoved": { "message": "Piekļuves atslēga noņemta" }, - "unassignedItemsBanner": { - "message": "Jāņem vērā: nepiešķirti apvienības vienumi vairs nav redzami skatā \"Visas glabātavas\" un ir sasniedzami tikai no pārvaldības konsoles, kur šie vienumi jāpiešķir krājumam, lai padarītu tos redzamus." + "unassignedItemsBannerNotice": { + "message": "Jāņem vērā: nepiešķirti apvienības vienumi vairs nav redzami skatā \"Visas glabātavas\" un ir pieejami tikai pārvaldības konsolē." }, - "unassignedItemsBannerSelfHost": { - "message": "Jāņem vērā: 2024. gada 2. maijā nepiešķirti apvienības vienumi vairs nebūs redzami skatā \"Visas glabātavas\" un būs sasniedzami tikai no pārvaldības konsoles, kur šie vienumi jāpiešķir krājumam, lai padarītu tos redzamus." + "unassignedItemsBannerSelfHostNotice": { + "message": "Jāņem vērā: no 2024. gada 16. maija nepiešķirti apvienības vienumi vairs nebūs redzami skatā \"Visas glabātavas\" un būs pieejami tikai pārvaldības konsolē." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Piešķirt šos vienumus krājumam", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "lai padarītu tos redzamus.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "pārvaldības konsolē," + }, + "errorAssigningTargetCollection": { + "message": "Kļūda mērķa krājuma piešķiršanā." + }, + "errorAssigningTargetFolder": { + "message": "Kļūda mērķa mapes piešķiršanā." } } diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index 9b66e6f0d69..1db5f6458b3 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "പ്രാഥമിക പാസ്‌വേഡ് മാറ്റുക" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "ഫിംഗർപ്രിന്റ് ഫ്രേസ്‌", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "ചേർക്കപ്പെട്ട ഫോൾഡർ" }, - "changeMasterPass": { - "message": "പ്രാഥമിക പാസ്‌വേഡ് മാറ്റുക" - }, - "changeMasterPasswordConfirmation": { - "message": "തങ്ങൾക്കു ബിറ്റ് വാർഡൻ വെബ് വാൾട്ടിൽ പ്രാഥമിക പാസ്‌വേഡ് മാറ്റാൻ സാധിക്കും.വെബ്സൈറ്റ് ഇപ്പോൾ സന്ദർശിക്കാൻ ആഗ്രഹിക്കുന്നുവോ?" - }, "twoStepLoginConfirmation": { "message": "സുരക്ഷാ കീ, ഓതന്റിക്കേറ്റർ അപ്ലിക്കേഷൻ, SMS, ഫോൺ കോൾ അല്ലെങ്കിൽ ഇമെയിൽ പോലുള്ള മറ്റൊരു ഉപകരണം ഉപയോഗിച്ച് തങ്ങളുടെ ലോഗിൻ സ്ഥിരീകരിക്കാൻ ആവശ്യപ്പെടുന്നതിലൂടെ രണ്ട്-ഘട്ട ലോഗിൻ തങ്ങളുടെ അക്കൗണ്ടിനെ കൂടുതൽ സുരക്ഷിതമാക്കുന്നു. bitwarden.com വെബ് വാൾട്ടിൽ രണ്ട്-ഘട്ട ലോഗിൻ പ്രവർത്തനക്ഷമമാക്കാനാകും.തങ്ങള്ക്കു ഇപ്പോൾ വെബ്സൈറ്റ് സന്ദർശിക്കാൻ ആഗ്രഹമുണ്ടോ?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index f9f37b2511b..06cf84efff3 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "मुख्य पासवर्ड बदला" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "अंगुलिमुद्रा वाक्यांश", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Folder added" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index 023e03b834a..67e1f247871 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change master password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Folder added" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 82d847ff0f0..220fe95e233 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Endre hovedpassordet" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingeravtrykksfrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "La til en mappe" }, - "changeMasterPass": { - "message": "Endre hovedpassordet" - }, - "changeMasterPasswordConfirmation": { - "message": "Du kan endre superpassordet ditt på bitwarden.net-netthvelvet. Vil du besøke det nettstedet nå?" - }, "twoStepLoginConfirmation": { "message": "2-trinnsinnlogging gjør kontoen din mer sikker, ved å kreve at du verifiserer din innlogging med en annen enhet, f.eks. en autentiseringsapp, SMS, e-post, telefonsamtale, eller sikkerhetsnøkkel. 2-trinnsinnlogging kan aktiveres i netthvelvet ditt på bitwarden.com. Vil du besøke bitwarden.com nå?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index 023e03b834a..67e1f247871 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change master password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Folder added" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index 13d59c4546f..808e599e70a 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Hoofdwachtwoord wijzigen" }, + "continueToWebApp": { + "message": "Doorgaan naar web-app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Je kunt je hoofdwachtwoord wijzigen in de Bitwarden-webapp." + }, "fingerprintPhrase": { "message": "Vingerafdrukzin", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Map is toegevoegd" }, - "changeMasterPass": { - "message": "Hoofdwachtwoord wijzigen" - }, - "changeMasterPasswordConfirmation": { - "message": "Je kunt je hoofdwachtwoord wijzigen in de kluis op bitwarden.com. Wil je de website nu bezoeken?" - }, "twoStepLoginConfirmation": { "message": "Tweestapsaanmelding beschermt je account door je inlogpoging te bevestigen met een ander apparaat zoals een beveiligingscode, authenticatie-app, SMS, spraakoproep of e-mail. Je kunt Tweestapsaanmelding inschakelen in de webkluis op bitwarden.com. Wil je de website nu bezoeken?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Succes" + }, "removePasskey": { "message": "Passkey verwijderen" }, "passkeyRemoved": { "message": "Passkey verwijderd" }, - "unassignedItemsBanner": { - "message": "Let op: Niet-toegewezen organisatie-items zijn niet langer zichtbaar in de weergave van alle kluisjes en zijn alleen toegankelijk via de Admin Console. Om deze items zichtbaar te maken, moet je ze toewijzen aan een collectie via de Admin Console." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Kennisgeving: Vanaf 2 mei 2024 zijn niet-toegewezen organisatie-items op geen enkel apparaat meer zichtbaar in de weergave van alle kluisjes en alleen toegankelijk via de Admin Console. Je kunt deze items in het Admin Console aan een collectie toewijzen om ze zichtbaar te maken." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Fout bij toewijzen doelverzameling." + }, + "errorAssigningTargetFolder": { + "message": "Fout bij toewijzen doelmap." } } diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index 023e03b834a..67e1f247871 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change master password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Folder added" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index 023e03b834a..67e1f247871 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change master password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Folder added" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index e4b97ec9568..1ef79bac42a 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Zmień hasło główne" }, + "continueToWebApp": { + "message": "Kontynuować do aplikacji internetowej?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Możesz zmienić swoje hasło główne w aplikacji internetowej Bitwarden." + }, "fingerprintPhrase": { "message": "Unikalny identyfikator konta", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Folder został dodany" }, - "changeMasterPass": { - "message": "Zmień hasło główne" - }, - "changeMasterPasswordConfirmation": { - "message": "Hasło główne możesz zmienić na stronie sejfu bitwarden.com. Czy chcesz przejść do tej strony?" - }, "twoStepLoginConfirmation": { "message": "Logowanie dwustopniowe sprawia, że konto jest bardziej bezpieczne poprzez wymuszenie potwierdzenia logowania z innego urządzenia, takiego jak z klucza bezpieczeństwa, aplikacji uwierzytelniającej, wiadomości SMS, telefonu lub adresu e-mail. Logowanie dwustopniowe możesz włączyć w sejfie internetowym bitwarden.com. Czy chcesz przejść do tej strony?" }, @@ -2709,7 +2709,7 @@ "message": "Otwórz rozszerzenie w nowym oknie, aby dokończyć logowanie." }, "popoutExtension": { - "message": "Popout extension" + "message": "Otwórz rozszerzenie w nowym oknie" }, "launchDuo": { "message": "Uruchom DUO" @@ -2822,7 +2822,7 @@ "message": "Wybierz dane logowania do których przypisać passkey" }, "passkeyItem": { - "message": "Passkey Item" + "message": "Element Passkey" }, "overwritePasskey": { "message": "Zastąpić passkey?" @@ -3000,16 +3000,36 @@ "message": "Błąd podczas zapisywania danych logowania. Sprawdź konsolę, aby uzyskać szczegóły.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Sukces" + }, "removePasskey": { "message": "Usuń passkey" }, "passkeyRemoved": { "message": "Passkey został usunięty" }, - "unassignedItemsBanner": { - "message": "Uwaga: Nieprzypisane elementy w organizacji nie są już widoczne w widoku Wszystkie sejfy i są dostępne tylko przez Konsolę Administracyjną. Przypisz te elementy do kolekcji z konsoli administracyjnej, aby były one widoczne." + "unassignedItemsBannerNotice": { + "message": "Uwaga: Nieprzypisane elementy organizacji nie są już widoczne w widoku Wszystkie sejfy i są teraz dostępne tylko przez Konsolę Administracyjną." }, - "unassignedItemsBannerSelfHost": { - "message": "Uwaga: 2 maja 2024 r. nieprzypisane elementy w organizacji nie będą już widoczne w widoku Wszystkie sejfy i będą dostępne tylko przez Konsolę Administracyjną. Przypisz te elementy do kolekcji z konsoli administracyjnej, aby były one widoczne." + "unassignedItemsBannerSelfHostNotice": { + "message": "Uwaga: 16 maja 2024 r. nieprzypisana elementy w organizacji nie będą już widoczne w widoku Wszystkie sejfy i będą dostępne tylko przez Konsolę Administracyjną." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Przypisz te elementy do kolekcji z", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": ", aby były widoczne.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Konsola Administracyjna" + }, + "errorAssigningTargetCollection": { + "message": "Wystąpił błąd podczas przypisywania kolekcji." + }, + "errorAssigningTargetFolder": { + "message": "Wystąpił błąd podczas przypisywania folderu." } } diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index a4e0688e3d4..9322e680d22 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Alterar Senha Mestra" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Frase Biométrica", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Pasta adicionada" }, - "changeMasterPass": { - "message": "Alterar Senha Mestra" - }, - "changeMasterPasswordConfirmation": { - "message": "Você pode alterar a sua senha mestra no cofre web em bitwarden.com. Você deseja visitar o site agora?" - }, "twoStepLoginConfirmation": { "message": "O login de duas etapas torna a sua conta mais segura ao exigir que digite um código de segurança de um aplicativo de autenticação quando for iniciar a sessão. O login de duas etapas pode ser ativado no cofre web bitwarden.com. Deseja visitar o site agora?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index c35531e4452..d4fdc1be816 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Alterar palavra-passe mestra" }, + "continueToWebApp": { + "message": "Continuar para a aplicação Web?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Pode alterar a sua palavra-passe mestra na aplicação Web Bitwarden." + }, "fingerprintPhrase": { "message": "Frase de impressão digital", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Pasta adicionada" }, - "changeMasterPass": { - "message": "Alterar palavra-passe mestra" - }, - "changeMasterPasswordConfirmation": { - "message": "Pode alterar o seu endereço de e-mail no cofre do site bitwarden.com. Deseja visitar o site agora?" - }, "twoStepLoginConfirmation": { "message": "A verificação de dois passos torna a sua conta mais segura, exigindo que verifique o seu início de sessão com outro dispositivo, como uma chave de segurança, aplicação de autenticação, SMS, chamada telefónica ou e-mail. A verificação de dois passos pode ser configurada em bitwarden.com. Pretende visitar o site agora?" }, @@ -3000,16 +3000,36 @@ "message": "Erro ao guardar as credenciais. Verifique a consola para obter detalhes.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Com sucesso" + }, "removePasskey": { "message": "Remover chave de acesso" }, "passkeyRemoved": { "message": "Chave de acesso removida" }, - "unassignedItemsBanner": { - "message": "Aviso: Os itens da organização não atribuídos já não são visíveis na vista Todos os cofres e só são acessíveis através da consola de administração. Atribua estes itens a uma coleção a partir da Consola de administração para os tornar visíveis." + "unassignedItemsBannerNotice": { + "message": "Aviso: Os itens da organização não atribuídos já não são visíveis na vista Todos os cofres e só são acessíveis através da Consola de administração." }, - "unassignedItemsBannerSelfHost": { - "message": "Aviso: A 2 de maio de 2024, os itens da organização não atribuídos deixarão de ser visíveis na vista Todos os cofres e só estarão acessíveis através da Consola de administração. Atribua estes itens a uma coleção a partir da Consola de administração para os tornar visíveis." + "unassignedItemsBannerSelfHostNotice": { + "message": "Aviso: A 16 de maio de 2024, os itens da organização não atribuídos deixarão de estar visíveis na vista Todos os cofres e só estarão acessíveis através da consola de administração." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Atribua estes itens a uma coleção a partir da", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "para os tornar visíveis.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Consola de administração" + }, + "errorAssigningTargetCollection": { + "message": "Erro ao atribuir a coleção de destino." + }, + "errorAssigningTargetFolder": { + "message": "Erro ao atribuir a pasta de destino." } } diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 885d70ca933..5e8c82e70b6 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Schimbare parolă principală" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fraza amprentă", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Dosar adăugat" }, - "changeMasterPass": { - "message": "Schimbare parolă principală" - }, - "changeMasterPasswordConfirmation": { - "message": "Puteți modifica parola principală în seiful web bitwarden.com. Doriți să vizitați saitul acum?" - }, "twoStepLoginConfirmation": { "message": "Autentificarea în două etape vă face contul mai sigur, prin solicitarea unei verificări de autentificare cu un alt dispozitiv, cum ar fi o cheie de securitate, o aplicație de autentificare, un SMS, un apel telefonic sau un e-mail. Autentificarea în două etape poate fi configurată în seiful web bitwarden.com. Doriți să vizitați site-ul web acum?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index 69d9ca200fc..046fe2b9310 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Изменить мастер-пароль" }, + "continueToWebApp": { + "message": "Перейти к веб-приложению?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Изменить мастер-пароль можно в веб-приложении Bitwarden." + }, "fingerprintPhrase": { "message": "Фраза отпечатка", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Папка добавлена" }, - "changeMasterPass": { - "message": "Изменить мастер-пароль" - }, - "changeMasterPasswordConfirmation": { - "message": "Вы можете изменить свой мастер-пароль на bitwarden.com. Перейти на сайт сейчас?" - }, "twoStepLoginConfirmation": { "message": "Двухэтапная аутентификация делает аккаунт более защищенным, поскольку требуется подтверждение входа при помощи другого устройства, например, ключа безопасности, приложения-аутентификатора, SMS, телефонного звонка или электронной почты. Двухэтапная аутентификация включается на bitwarden.com. Перейти на сайт сейчас?" }, @@ -3000,16 +3000,36 @@ "message": "Ошибка сохранения учетных данных. Проверьте консоль для получения подробной информации.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Успешно" + }, "removePasskey": { "message": "Удалить passkey" }, "passkeyRemoved": { "message": "Passkey удален" }, - "unassignedItemsBanner": { - "message": "Обратите внимание: неприсвоенные элементы организации больше не видны в представлении \"Все хранилища\" и доступны только через консоль администратора. Назначьте эти элементы коллекции в консоли администратора, чтобы сделать их видимыми." + "unassignedItemsBannerNotice": { + "message": "Уведомление: Неприсвоенные элементы организации больше не видны в представлении \"Все хранилища\" и доступны только через консоль администратора." }, - "unassignedItemsBannerSelfHost": { - "message": "Уведомление: 2 мая 2024 года неприсвоенные элементы организации больше не будут видны в представлении \"Все хранилища\" и будут доступны только через консоль администратора. Назначьте эти элементы коллекции в консоли администратора, чтобы сделать их видимыми." + "unassignedItemsBannerSelfHostNotice": { + "message": "Уведомление: с 16 мая 2024 года не назначенные элементы организации больше не будут видны в представлении \"Все хранилища\" и будут доступны только через консоль администратора." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Назначьте эти элементы в коллекцию из", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "чтобы сделать их видимыми.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "консоли администратора" + }, + "errorAssigningTargetCollection": { + "message": "Ошибка при назначении целевой коллекции." + }, + "errorAssigningTargetFolder": { + "message": "Ошибка при назначении целевой папки." } } diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index fb026226bb9..e576a50dd73 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "ප්රධාන මුරපදය වෙනස්" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "ඇඟිලි සලකුණු වාක්ය ඛණ්ඩය", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "එකතු කරන ලද ෆෝල්ඩරය" }, - "changeMasterPass": { - "message": "ප්රධාන මුරපදය වෙනස්" - }, - "changeMasterPasswordConfirmation": { - "message": "bitwarden.com වෙබ් සුරක්ෂිතාගාරයේ ඔබේ ප්රධාන මුරපදය වෙනස් කළ හැකිය. ඔබට දැන් වෙබ් අඩවියට පිවිසීමට අවශ්යද?" - }, "twoStepLoginConfirmation": { "message": "ආරක්ෂක යතුරක්, සත්යාපන යෙදුම, කෙටි පණිවුඩ, දුරකථන ඇමතුමක් හෝ විද්යුත් තැපෑල වැනි වෙනත් උපාංගයක් සමඟ ඔබේ පිවිසුම සත්යාපනය කිරීමට ඔබට අවශ්ය වීමෙන් ද්වි-පියවර පිවිසුම ඔබගේ ගිණුම වඩාත් සුරක්ෂිත කරයි. බිට්වොන්.com වෙබ් සුරක්ෂිතාගාරයේ ද්වි-පියවර පිවිසුම සක්රීය කළ හැකිය. ඔබට දැන් වෙබ් අඩවියට පිවිසීමට අවශ්යද?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index a7948d78f37..e34fa525be0 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Zmeniť hlavné heslo" }, + "continueToWebApp": { + "message": "Pokračovať vo webovej aplikácii?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Hlavné heslo si môžete zmeniť vo webovej aplikácii Bitwarden." + }, "fingerprintPhrase": { "message": "Fráza odtlačku", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Pridaný priečinok" }, - "changeMasterPass": { - "message": "Zmeniť hlavné heslo" - }, - "changeMasterPasswordConfirmation": { - "message": "Teraz si môžete zmeniť svoje hlavné heslo vo webovom trezore bitwarden.com. Chcete navštíviť túto stránku teraz?" - }, "twoStepLoginConfirmation": { "message": "Dvojstupňové prihlasovanie robí váš účet bezpečnejším vďaka vyžadovaniu bezpečnostného kódu z overovacej aplikácie vždy, keď sa prihlásite. Dvojstupňové prihlasovanie môžete povoliť vo webovom trezore bitwarden.com. Chcete navštíviť túto stránku teraz?" }, @@ -3000,16 +3000,36 @@ "message": "Chyba pri ukladaní prihlasovacích údajov. Viac informácii nájdete v konzole.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Úspech" + }, "removePasskey": { "message": "Odstrániť prístupový kľúč" }, "passkeyRemoved": { "message": "Prístupový kľúč bol odstránený" }, - "unassignedItemsBanner": { - "message": "Upozornenie: Nepriradené položky organizácie už nie sú viditeľné v zobrazení Všetky Trezory a sú prístupné len cez administrátorskú konzolu. Aby boli viditeľné, priraďte tieto položky do kolekcie z konzoly administrátora." + "unassignedItemsBannerNotice": { + "message": "Upozornenie: Nepriradené položky organizácie už nie sú viditeľné v zobrazení Všetky trezory a sú prístupné iba cez Správcovskú konzolu." }, - "unassignedItemsBannerSelfHost": { - "message": "Upozornenie: 2. mája nepriradené položky organizácie už nebudú viditeľné v zobrazení Všetky Trezory a budú prístupné len cez administrátorskú konzolu. Aby boli viditeľné, priraďte tieto položky do kolekcie z konzoly administrátora." + "unassignedItemsBannerSelfHostNotice": { + "message": "Upozornenie: 16. mája 2024 nepriradené položky organizácie už nebudú viditeľné v zobrazení Všetky trezory a budú prístupné iba cez Správcovskú konzolu." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Priradiť tieto položky do zbierky zo", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": ", aby boli viditeľné.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Správcovská konzola" + }, + "errorAssigningTargetCollection": { + "message": "Chyba pri priraďovaní cieľovej kolekcie." + }, + "errorAssigningTargetFolder": { + "message": "Chyba pri priraďovaní cieľového priečinka." } } diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index 2fac491c9cc..c7f8ed04816 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Spremeni glavno geslo" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Identifikacijsko geslo", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Mapa dodana" }, - "changeMasterPass": { - "message": "Spremeni glavno geslo" - }, - "changeMasterPasswordConfirmation": { - "message": "Svoje glavno geslo lahko spremenite v Bitwardnovem spletnem trezorju. Želite zdaj obiskati Bitwardnovo spletno stran?" - }, "twoStepLoginConfirmation": { "message": "Avtentikacija v dveh korakih dodatno varuje vaš račun, saj zahteva, da vsakokratno prijavo potrdite z drugo napravo, kot je varnostni ključ, aplikacija za preverjanje pristnosti, SMS, telefonski klic ali e-pošta. Avtentikacijo v dveh korakih lahko omogočite v spletnem trezorju bitwarden.com. Ali želite spletno stran obiskati sedaj?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index 6ec1b6181b3..8d1ee8264fc 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Промени главну лозинку" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Сигурносна Фраза Сефа", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Фасцикла додата" }, - "changeMasterPass": { - "message": "Промени главну лозинку" - }, - "changeMasterPasswordConfirmation": { - "message": "Можете променити главну лозинку у Вашем сефу на bitwarden.com. Да ли желите да посетите веб страницу сада?" - }, "twoStepLoginConfirmation": { "message": "Пријава у два корака чини ваш налог сигурнијим захтевом да верификујете своје податке помоћу другог уређаја, као што су безбедносни кључ, апликација, СМС-а, телефонски позив или имејл. Пријављивање у два корака може се омогућити на веб сефу. Да ли желите да посетите веб страницу сада?" }, @@ -3000,16 +3000,36 @@ "message": "Грешка при чувању акредитива. Проверите конзолу за детаље.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Уклонити приступачни кључ" }, "passkeyRemoved": { "message": "Приступачни кључ је уклоњен" }, - "unassignedItemsBanner": { - "message": "Напомена: Недодељене ставке организације више нису видљиве у приказу Сви сефови и доступне су само преко Админ конзоле. Доделите ове ставке колекцији са Админ конзолом да бисте их учинили видљивим." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Обавештење: 2. маја 2024. недодељене ставке организације више неће бити видљиве у приказу Сви сефови и биће доступне само преко Админ конзоле. Доделите ове ставке колекцији са Админ конзолом да бисте их учинили видљивим." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index d798b98ea01..ebe5e6d2812 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Ändra huvudlösenord" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingeravtrycksfras", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Lade till mapp" }, - "changeMasterPass": { - "message": "Ändra huvudlösenord" - }, - "changeMasterPasswordConfirmation": { - "message": "Du kan ändra ditt huvudlösenord på bitwardens webbvalv. Vill du besöka webbplatsen nu?" - }, "twoStepLoginConfirmation": { "message": "Tvåstegsverifiering gör ditt konto säkrare genom att kräva att du verifierar din inloggning med en annan enhet, t.ex. en säkerhetsnyckel, autentiseringsapp, SMS, telefonsamtal eller e-post. Tvåstegsverifiering kan aktiveras i Bitwardens webbvalv. Vill du besöka webbplatsen nu?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index 023e03b834a..67e1f247871 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change master password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Folder added" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index 827ca72854b..718440438e5 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change Master Password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint Phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "เพิ่มโฟลเดอร์แล้ว" }, - "changeMasterPass": { - "message": "Change Master Password" - }, - "changeMasterPasswordConfirmation": { - "message": "คุณสามารถเปลี่ยนรหัสผ่านหลักได้ที่เว็บตู้เซฟ bitwarden.com คุณต้องการเปิดเว็บไซต์เลยหรือไม่?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to enter a security code from an authenticator app whenever you log in. Two-step login can be enabled on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index cd0e12e6b00..7633b472589 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Ana parolayı değiştir" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Parmak izi ifadesi", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Klasör eklendi" }, - "changeMasterPass": { - "message": "Ana parolayı değiştir" - }, - "changeMasterPasswordConfirmation": { - "message": "Ana parolanızı bitwarden.com web kasası üzerinden değiştirebilirsiniz. Siteye gitmek ister misiniz?" - }, "twoStepLoginConfirmation": { "message": "İki aşamalı giriş, hesabınıza girererken işlemi bir güvenlik anahtarı, şifrematik uygulaması, SMS, telefon araması veya e-posta gibi ek bir yöntemle doğrulamanızı isteyerek hesabınızın güvenliğini artırır. İki aşamalı giriş özelliğini bitwarden.com web kasası üzerinden etkinleştirebilirsiniz. Şimdi siteye gitmek ister misiniz?" }, @@ -3000,16 +3000,36 @@ "message": "Kimlik bilgileri kaydedilirken hata oluştu. Ayrıntılar için konsolu kontrol edin.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index 4820860de28..b09d9661425 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Змінити головний пароль" }, + "continueToWebApp": { + "message": "Продовжити у вебпрограмі?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Ви можете змінити головний пароль у вебпрограмі Bitwarden." + }, "fingerprintPhrase": { "message": "Фраза відбитка", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Теку додано" }, - "changeMasterPass": { - "message": "Змінити головний пароль" - }, - "changeMasterPasswordConfirmation": { - "message": "Ви можете змінити головний пароль в сховищі на bitwarden.com. Хочете перейти на вебсайт зараз?" - }, "twoStepLoginConfirmation": { "message": "Двоетапна перевірка дає змогу надійніше захистити ваш обліковий запис, вимагаючи підтвердження входу з використанням іншого пристрою, наприклад, за допомогою ключа безпеки, програми автентифікації, SMS, телефонного виклику, або е-пошти. Ви можете налаштувати двоетапну перевірку в сховищі на bitwarden.com. Хочете перейти на вебсайт зараз?" }, @@ -3000,16 +3000,36 @@ "message": "Помилка збереження облікових даних. Перегляньте подробиці в консолі.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Успішно" + }, "removePasskey": { "message": "Вилучити ключ доступу" }, "passkeyRemoved": { "message": "Ключ доступу вилучено" }, - "unassignedItemsBanner": { - "message": "Увага: непризначені елементи організації більше не видимі у поданні \"Усі сховища\" і доступні лише в консолі адміністратора. Щоб зробити їх видимими, призначте ці елементи збірці в консолі адміністратора." + "unassignedItemsBannerNotice": { + "message": "Примітка: непризначені елементи організації більше не видимі у поданні \"Усі сховища\" і доступні лише в консолі адміністратора." }, - "unassignedItemsBannerSelfHost": { - "message": "Сповіщення: 2 травня 2024 року, непризначені елементи організації більше не будуть видимі в поданні \"Усі сховища\", і будуть доступні лише через консоль адміністратора. Щоб зробити їх видимими, призначте ці елементи збірці в консолі адміністратора." + "unassignedItemsBannerSelfHostNotice": { + "message": "Примітка: 16 травня 2024 року непризначені елементи організації більше не будуть видимі у поданні \"Усі сховища\" і будуть доступні лише через консоль адміністратора." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Призначте ці елементи збірці в", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "щоб зробити їх видимими.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "консолі адміністратора," + }, + "errorAssigningTargetCollection": { + "message": "Помилка призначення цільової збірки." + }, + "errorAssigningTargetFolder": { + "message": "Помилка призначення цільової теки." } } diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index 234e60e756e..af518878b91 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Thay đổi mật khẩu chính" }, + "continueToWebApp": { + "message": "Tiếp tục tới ứng dụng web?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Bạn có thể thay đổi mật khẩu chính của mình trên Bitwarden bản web." + }, "fingerprintPhrase": { "message": "Fingerprint Phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -415,7 +421,7 @@ "message": "Khóa ngay" }, "lockAll": { - "message": "Lock all" + "message": "Khóa tất cả" }, "immediately": { "message": "Ngay lập tức" @@ -494,10 +500,10 @@ "message": "Tài khoản mới của bạn đã được tạo! Bạn có thể đăng nhập từ bây giờ." }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "Bạn đã đăng nhập thành công" }, "youMayCloseThisWindow": { - "message": "You may close this window" + "message": "Bạn có thể đóng cửa sổ này" }, "masterPassSent": { "message": "Chúng tôi đã gửi cho bạn email có chứa gợi ý mật khẩu chính của bạn." @@ -522,16 +528,16 @@ "message": "Không thể tự động điền mục đã chọn trên trang này. Hãy thực hiện sao chép và dán thông tin một cách thủ công." }, "totpCaptureError": { - "message": "Unable to scan QR code from the current webpage" + "message": "Không thể quét mã QR từ trang web hiện tại" }, "totpCaptureSuccess": { - "message": "Authenticator key added" + "message": "Đã thêm khóa xác thực" }, "totpCapture": { - "message": "Scan authenticator QR code from current webpage" + "message": "Quét mã QR xác thực từ trang web hiện tại" }, "copyTOTP": { - "message": "Copy Authenticator key (TOTP)" + "message": "Sao chép khóa Authenticator (TOTP)" }, "loggedOut": { "message": "Đã đăng xuất" @@ -557,12 +563,6 @@ "addedFolder": { "message": "Đã thêm thư mục" }, - "changeMasterPass": { - "message": "Thay đổi mật khẩu chính" - }, - "changeMasterPasswordConfirmation": { - "message": "Bạn có thể thay đổi mật khẩu chính trong trang web kho lưu trữ của Bitwarden. Bạn có muốn truy cập trang web ngay bây giờ không?" - }, "twoStepLoginConfirmation": { "message": "Xác thực hai lớp giúp cho tài khoản của bạn an toàn hơn bằng cách yêu cầu bạn xác minh thông tin đăng nhập của bạn bằng một thiết bị khác như khóa bảo mật, ứng dụng xác thực, SMS, cuộc gọi điện thoại hoặc email. Bạn có thể bật xác thực hai lớp trong kho bitwarden nền web. Bạn có muốn ghé thăm trang web bây giờ?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Lưu ý: Các mục tổ chức chưa được chỉ định sẽ không còn hiển thị trong chế độ xem Tất cả Vault và chỉ có thể truy cập được qua Bảng điều khiển dành cho quản trị viên." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Lưu ý: Vào ngày 16 tháng 5 năm 2024, các mục tổ chức chưa được chỉ định sẽ không còn hiển thị trong chế độ xem Tất cả Vault và sẽ chỉ có thể truy cập được qua Bảng điều khiển dành cho quản trị viên." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Gán các mục này vào một bộ sưu tập từ", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "để làm cho chúng hiển thị.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Bảng điều khiển dành cho quản trị viên" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index 519313df812..5842d4f4c12 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "更改主密码" }, + "continueToWebApp": { + "message": "前往网页 App 吗?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "指纹短语", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "文件夹已添加" }, - "changeMasterPass": { - "message": "修改主密码" - }, - "changeMasterPasswordConfirmation": { - "message": "您可以在 bitwarden.com 网页版密码库修改主密码。您现在要访问这个网站吗?" - }, "twoStepLoginConfirmation": { "message": "两步登录要求您从其他设备(例如安全钥匙、验证器 App、短信、电话或者电子邮件)来验证您的登录,这能使您的账户更加安全。两步登录需要在 bitwarden.com 网页版密码库中设置。现在访问此网站吗?" }, @@ -3000,16 +3000,36 @@ "message": "保存凭据时出错。检查控制台以获取详细信息。", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "移除通行密钥" }, "passkeyRemoved": { "message": "通行密钥已移除" }, - "unassignedItemsBanner": { - "message": "注意:未分配的组织项目在「所有密码库」视图中不再可见,只能通过管理控制台访问。通过管理控制台将这些项目分配给集合以使其可见。" + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "注意:从 2024 年 5 月 2 日起,未分配的组织项目在「所有密码库」视图中将不再可见,只能通过管理控制台访问。通过管理控制台将这些项目分配给集合以使其可见。" + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index b6f1ff574a1..a8163dab98d 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "變更主密碼" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "指紋短語", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "資料夾已新增" }, - "changeMasterPass": { - "message": "變更主密碼" - }, - "changeMasterPasswordConfirmation": { - "message": "您可以在 bitwarden.com 網頁版密碼庫變更主密碼。現在要前往嗎?" - }, "twoStepLoginConfirmation": { "message": "兩步驟登入需要您從其他裝置(例如安全鑰匙、驗證器程式、SMS、手機或電子郵件)來驗證您的登入,這使您的帳戶更加安全。兩步驟登入可以在 bitwarden.com 網頁版密碼庫啟用。現在要前往嗎?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/store/locales/gl/copy.resx b/apps/browser/store/locales/gl/copy.resx index d812256fb73..04e84f6677c 100644 --- a/apps/browser/store/locales/gl/copy.resx +++ b/apps/browser/store/locales/gl/copy.resx @@ -158,7 +158,7 @@ Protexa e comparta datos confidenciais dentro da súa Caixa Forte de Bitwarden d Sincroniza e accede á túa caixa forte desde múltiples dispositivos - Xestiona todos os teus usuarios e contrasinais desde unha caixa forte segura + Xestiona todos os teus inicios de sesión e contrasinais desde unha caixa forte segura Autocompleta rapidamente os teus datos de acceso en calquera páxina web que visites From f6dee29a5fc0e48b7464181bd7f3c03280838fe6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 19 Apr 2024 10:41:39 +0000 Subject: [PATCH 006/110] Autosync the updated translations (#8824) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/web/src/locales/af/messages.json | 139 +++++++++- apps/web/src/locales/ar/messages.json | 139 +++++++++- apps/web/src/locales/az/messages.json | 141 +++++++++- apps/web/src/locales/be/messages.json | 139 +++++++++- apps/web/src/locales/bg/messages.json | 141 +++++++++- apps/web/src/locales/bn/messages.json | 139 +++++++++- apps/web/src/locales/bs/messages.json | 139 +++++++++- apps/web/src/locales/ca/messages.json | 151 ++++++++++- apps/web/src/locales/cs/messages.json | 141 +++++++++- apps/web/src/locales/cy/messages.json | 139 +++++++++- apps/web/src/locales/da/messages.json | 139 +++++++++- apps/web/src/locales/de/messages.json | 141 +++++++++- apps/web/src/locales/el/messages.json | 139 +++++++++- apps/web/src/locales/en_GB/messages.json | 139 +++++++++- apps/web/src/locales/en_IN/messages.json | 139 +++++++++- apps/web/src/locales/eo/messages.json | 139 +++++++++- apps/web/src/locales/es/messages.json | 139 +++++++++- apps/web/src/locales/et/messages.json | 139 +++++++++- apps/web/src/locales/eu/messages.json | 139 +++++++++- apps/web/src/locales/fa/messages.json | 139 +++++++++- apps/web/src/locales/fi/messages.json | 141 +++++++++- apps/web/src/locales/fil/messages.json | 139 +++++++++- apps/web/src/locales/fr/messages.json | 141 +++++++++- apps/web/src/locales/gl/messages.json | 139 +++++++++- apps/web/src/locales/he/messages.json | 139 +++++++++- apps/web/src/locales/hi/messages.json | 139 +++++++++- apps/web/src/locales/hr/messages.json | 139 +++++++++- apps/web/src/locales/hu/messages.json | 141 +++++++++- apps/web/src/locales/id/messages.json | 139 +++++++++- apps/web/src/locales/it/messages.json | 141 +++++++++- apps/web/src/locales/ja/messages.json | 143 +++++++++- apps/web/src/locales/ka/messages.json | 139 +++++++++- apps/web/src/locales/km/messages.json | 139 +++++++++- apps/web/src/locales/kn/messages.json | 139 +++++++++- apps/web/src/locales/ko/messages.json | 139 +++++++++- apps/web/src/locales/lv/messages.json | 153 ++++++++++- apps/web/src/locales/ml/messages.json | 139 +++++++++- apps/web/src/locales/mr/messages.json | 139 +++++++++- apps/web/src/locales/my/messages.json | 139 +++++++++- apps/web/src/locales/nb/messages.json | 139 +++++++++- apps/web/src/locales/ne/messages.json | 139 +++++++++- apps/web/src/locales/nl/messages.json | 139 +++++++++- apps/web/src/locales/nn/messages.json | 139 +++++++++- apps/web/src/locales/or/messages.json | 139 +++++++++- apps/web/src/locales/pl/messages.json | 293 ++++++++++++++------ apps/web/src/locales/pt_BR/messages.json | 323 ++++++++++++++++------- apps/web/src/locales/pt_PT/messages.json | 139 +++++++++- apps/web/src/locales/ro/messages.json | 139 +++++++++- apps/web/src/locales/ru/messages.json | 139 +++++++++- apps/web/src/locales/si/messages.json | 139 +++++++++- apps/web/src/locales/sk/messages.json | 139 +++++++++- apps/web/src/locales/sl/messages.json | 139 +++++++++- apps/web/src/locales/sr/messages.json | 141 +++++++++- apps/web/src/locales/sr_CS/messages.json | 139 +++++++++- apps/web/src/locales/sv/messages.json | 139 +++++++++- apps/web/src/locales/te/messages.json | 139 +++++++++- apps/web/src/locales/th/messages.json | 139 +++++++++- apps/web/src/locales/tr/messages.json | 139 +++++++++- apps/web/src/locales/uk/messages.json | 141 +++++++++- apps/web/src/locales/vi/messages.json | 139 +++++++++- apps/web/src/locales/zh_CN/messages.json | 145 +++++++++- apps/web/src/locales/zh_TW/messages.json | 139 +++++++++- 62 files changed, 8691 insertions(+), 321 deletions(-) diff --git a/apps/web/src/locales/af/messages.json b/apps/web/src/locales/af/messages.json index 6dfb5ec30d5..51ba16e5e0a 100644 --- a/apps/web/src/locales/af/messages.json +++ b/apps/web/src/locales/af/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/ar/messages.json b/apps/web/src/locales/ar/messages.json index 1923502b1af..4377e5655b8 100644 --- a/apps/web/src/locales/ar/messages.json +++ b/apps/web/src/locales/ar/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index 911031f0f1a..2e94d219d81 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -1474,7 +1474,7 @@ "message": "Veb anbarda istifadə olunan dili dəyişdirin." }, "enableFavicon": { - "message": "Veb sayt nişanlarını göstər" + "message": "Veb sayt ikonlarını göstər" }, "faviconDesc": { "message": "Hər girişin yanında tanına bilən təsvir göstər." @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provayder Portal" }, + "success": { + "message": "Uğurlu" + }, "viewCollection": { "message": "Kolleksiyaya bax" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Maşın hesabına müraciət güncəlləndi" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "Özünüzü bir qrupa əlavə edə bilməzsiniz." }, "unassignedItemsBannerSelfHost": { "message": "Bildiriş: 2 May 2024-cü ildən etibarən təyin edilməmiş təşkilat elementləri artıq cihazlar arasında Bütün Anbarlar görünüşündə görünməyən və yalnız Admin Konsolu vasitəsilə əlçatan olacaq. Bu elementləri görünən etmək üçün Admin Konsolundan bir kolleksiyaya təyin edin." + }, + "unassignedItemsBannerNotice": { + "message": "Bildiriş: Təyin edilməyən təşkilat elementləri artıq Bütün Anbarlar görünüşündə görünən olmayacaq və yalnız Admin Konsolu vasitəsilə əlçatan olacaq." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Bildiriş: 16 May 2024-cü il tarixindən etibarən, təyin edilməyən təşkilat elementləri cihazlar arasında və Bütün Anbarlar görünüşündə görünən olmayacaq və yalnız Admin Konsolu vasitəsilə əlçatan olacaq." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Bu elementləri görünən etmək üçün", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "bir kolleksiyaya təyin edin.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Provayderi sil" + }, + "deleteProviderConfirmation": { + "message": "Bir provayderin silinməsi daimi və geri qaytarıla bilməyən prosesdir. Provayderin və əlaqəli bütün datanın silinməsini təsdiqləmək üçün ana parolunuzu daxil edin." + }, + "deleteProviderName": { + "message": "$ID$ silinə bilmir", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "$ID$ silinməzdən əvvəl bütün müştəriləri (client) ayırmalısınız", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provayder silindi" + }, + "providerDeletedDesc": { + "message": "Provayder və bütün əlaqəli datalar silindi." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Bu Provayderi silmək üçün tələb göndərdiniz. Təsdiqləmək üçün aşağıdakı düyməni istifadə edin." + }, + "deleteProviderWarning": { + "message": "Provayderin silinməsi daimi prosesdir. Geri dönüşü olmayacaq." + }, + "errorAssigningTargetCollection": { + "message": "Hədəf kolleksiyaya təyin etmə xətası." + }, + "errorAssigningTargetFolder": { + "message": "Hədəf qovluğa təyin etmə xətası." + }, + "integrationsAndSdks": { + "message": "İnteqrasiyalar və SDK-lar", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "İnteqrasiyalar" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDK-lar" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Github Actions qur" + }, + "setUpGitlabCICD": { + "message": "GitLab CI/CD qur" + }, + "setUpAnsible": { + "message": "Ansible qur" + }, + "cSharpSDKRepo": { + "message": "C# repozitoriyasına bax" + }, + "cPlusPlusSDKRepo": { + "message": "C++ repozitoriyasına bax" + }, + "jsWebAssemblySDKRepo": { + "message": "JS WebAssembly repozitoriyasına bax" + }, + "javaSDKRepo": { + "message": "Java repozitoriyasına bax" + }, + "pythonSDKRepo": { + "message": "Python repozitoriyasına bax" + }, + "phpSDKRepo": { + "message": "php repozitoriyasına bax" + }, + "rubySDKRepo": { + "message": "Ruby repozitoriyasına bax" + }, + "goSDKRepo": { + "message": "Go repozitoriyasına bax" + }, + "createNewClientToManageAsProvider": { + "message": "Provayder kimi idarə etmək üçün yeni bir client təşkilatı yaradın. Əlavə yerlər növbəti faktura dövründə əks olunacaq." + }, + "selectAPlan": { + "message": "Bir plan seçin" + }, + "thirtyFivePercentDiscount": { + "message": "35% endirim" + }, + "monthPerMember": { + "message": "üzv başına ay" + }, + "seats": { + "message": "Yer" + }, + "addOrganization": { + "message": "Təşkilat əlavə et" + }, + "createdNewClient": { + "message": "Yeni client uğurla yaradıldı" + }, + "noAccess": { + "message": "Müraciət yoxdur" + }, + "collectionAdminConsoleManaged": { + "message": "Bu kolleksiya yalnız admin konsolundan əlçatandır" } } diff --git a/apps/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index c93f7733822..2cfb8af125c 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index b3c20f50e2f..05a819f97a8 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Портал за доставчици" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "Преглед на колекцията" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Достъпът на машинния акаунт е променен" }, - "unassignedItemsBanner": { - "message": "Известие: неразпределените елементи на организацията вече не се виждат в изгледа с „Всички трезори“ на различните устройства, а са достъпни само през Административната конзола. Добавете тези елементи към някоя колекция в Административната конзола, за да станат видими." + "restrictedGroupAccessDesc": { + "message": "Не може да добавяте себе си към групи." }, "unassignedItemsBannerSelfHost": { - "message": "Известие: от 2 май 2024г. неразпределените елементи на организациите вече няма се виждат в изгледа с „Всички трезори“ на различните устройства, а ще бъдат достъпни само през Административната конзола. Добавете тези елементи към някоя колекция в Административната конзола, за да станат видими." + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Известие: неразпределените елементи в организацията вече няма да се виждат в изгледа с всички трезори на различните устройства, а са достъпни само през Административната конзола." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Известие: след 16 май 2024, неразпределените елементи в организацията вече няма да се виждат в изгледа с всички трезори на различните устройства, а ще бъдат достъпни само през Административната конзола." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Добавете тези елементи към колекция в", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "за да ги направите видими.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Изтриване на доставчик" + }, + "deleteProviderConfirmation": { + "message": "Изтриването на доставчик е окончателно и необратимо. Въведете главната си парола, за да потвърдите изтриването на доставчика и всички свързани данни." + }, + "deleteProviderName": { + "message": "Изтриването на $ID$ е невъзможно", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "Трябва да разкачите всички клиенти, преди да можете да изтриете $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Доставчикът е изтрит" + }, + "providerDeletedDesc": { + "message": "Доставчикът и всички свързани данни са изтрити." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Заявили сте, че искате да изтриете този доставчик. Използвайте бутона по-долу, за да потвърдите това решение." + }, + "deleteProviderWarning": { + "message": "Изтриването на доставчика Ви е окончателно и необратимо." + }, + "errorAssigningTargetCollection": { + "message": "Грешка при задаването на целева колекция." + }, + "errorAssigningTargetFolder": { + "message": "Грешка при задаването на целева папка." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/bn/messages.json b/apps/web/src/locales/bn/messages.json index afde1e40d4d..46d0e574cea 100644 --- a/apps/web/src/locales/bn/messages.json +++ b/apps/web/src/locales/bn/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/bs/messages.json b/apps/web/src/locales/bs/messages.json index f5cddc79db6..85880f9cbf7 100644 --- a/apps/web/src/locales/bs/messages.json +++ b/apps/web/src/locales/bs/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index 2bec515116d..cc072c3b966 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Portal del proveïdor" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "Mostra col·lecció" }, @@ -7625,13 +7628,13 @@ "message": "Assigna a aquestes col·leccions" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Seleccioneu les col·leccions amb les quals es compartiran els elements. Una vegada que un element s'actualitza en una col·lecció, es reflectirà a totes les col·leccions. Només els membres de l'organització amb accés a aquestes col·leccions podran veure els elements." }, "selectCollectionsToAssign": { - "message": "Select collections to assign" + "message": "Seleccioneu les col·leccions per assignar" }, "noCollectionsAssigned": { - "message": "No collections have been assigned" + "message": "No s'ha assignat cap col·lecció" }, "successfullyAssignedCollections": { "message": "Successfully assigned collections" @@ -7650,7 +7653,7 @@ } }, "items": { - "message": "Items" + "message": "Elements" }, "assignedSeats": { "message": "Assigned seats" @@ -7659,10 +7662,10 @@ "message": "Assigned" }, "used": { - "message": "Used" + "message": "Utilitzat" }, "remaining": { - "message": "Remaining" + "message": "Queden" }, "unlinkOrganization": { "message": "Unlink organization" @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index 82dfe2ecae5..6b82766cd31 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Portál poskytovatele" }, + "success": { + "message": "Úspěch" + }, "viewCollection": { "message": "Zobrazit kolekci" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Přístup strojového účtu byl aktualizován" }, - "unassignedItemsBanner": { - "message": "Upozornění: Nepřiřazené položky organizace již nejsou viditelné ve Vašem zobrazení všech trezorů napříč zařízeními a jsou nyní přístupné jen v konzoli správce. Přiřaďte tyto položky do kolekce z konzole pro správce, aby byly viditelné." + "restrictedGroupAccessDesc": { + "message": "Do skupiny nemůžete přidat sami sebe." }, "unassignedItemsBannerSelfHost": { - "message": "Upozornění: Dne 2. května 2024 již nebudou nepřiřazené položky organizace viditelné v zobrazení Všechny trezory ve všech zařízeních a budou přístupné jen prostřednictvím konzoly správce. Přiřaďte tyto položky do kolekce z konzoly pro správce, aby byly viditelné." + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Upozornění: Nepřiřazené položky organizace již nejsou viditelné ve vašem zobrazení všech trezorů napříč zařízeními a jsou nyní přístupné pouze v konzoli správce." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Upozornění: 16. květba 2024 již nebudou nepřiřazené položky organizace viditelné ve vašem zobrazení všech trezorů napříč zařízeními a budou přístupné pouze v konzoli správce." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Přiřadit tyto položky ke kolekci z", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "aby byly viditelné.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Smazat poskytovatele" + }, + "deleteProviderConfirmation": { + "message": "Smazání poskytovatele je trvalé a nevratné. Zadejte hlavní heslo pro potvrzení smazání poskytovatele a všech souvisejících dat." + }, + "deleteProviderName": { + "message": "Nelze smazat $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "Před smazáním $ID$ musíte odpojit všechny klienty", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Poskytovatel byl smazán" + }, + "providerDeletedDesc": { + "message": "Poskytovatel a veškerá související data byla smazána." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Požádali jste o smazání tohoto poskytovatele. Pro potvrzení použijte tlačítko níže." + }, + "deleteProviderWarning": { + "message": "Smazání poskytovatele je trvalé. Tuto akci nelze vrátit zpět." + }, + "errorAssigningTargetCollection": { + "message": "Chyba při přiřazování cílové kolekce." + }, + "errorAssigningTargetFolder": { + "message": "Chyba při přiřazování cílové složky." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Vytvořte novou klientskou organizaci pro správu jako poskytovatele. Další uživatelé budou reflektováni v dalším platebním cyklu." + }, + "selectAPlan": { + "message": "Vyberte plán" + }, + "thirtyFivePercentDiscount": { + "message": "35% sleva" + }, + "monthPerMember": { + "message": "měsíčně za člena" + }, + "seats": { + "message": "Počet" + }, + "addOrganization": { + "message": "Přidat organizaci" + }, + "createdNewClient": { + "message": "Nový klient byl úspěšně vytvořen" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/cy/messages.json b/apps/web/src/locales/cy/messages.json index 8ee7ab35694..7394c3fe2a9 100644 --- a/apps/web/src/locales/cy/messages.json +++ b/apps/web/src/locales/cy/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index e52510ed1c9..081fef78654 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Udbyderportal" }, + "success": { + "message": "Gennemført" + }, "viewCollection": { "message": "Vis samling" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Maskinekontoadgang opdateret" }, - "unassignedItemsBanner": { - "message": "Bemærk: Utildelte organisationsemner er ikke længere synlige i Alle Bokse-visningen og er kun tilgængelige via Adminkonsollen. Føj disse emner til en samling fra Adminkonsollen for at gøre dem synlige." + "restrictedGroupAccessDesc": { + "message": "Man kan ikke føje sig selv til en gruppe." }, "unassignedItemsBannerSelfHost": { "message": "Bemærk: Pr. 2. maj 2024 vil utildelte organisationsemner ikke længere være synlige i Alle Bokse-visningen på tværs af enheder og vil kun være tilgængelige via Admin-konsollen. Tildel disse emner til en samling via Admin-konsollen for at gøre dem synlige." + }, + "unassignedItemsBannerNotice": { + "message": "Bemærk: Utildelte organisationsemner er ikke længere synlige i Alle Bokse-visningen, men er kun tilgængelige via Admin-konsol." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Bemærk: Pr. 16. maj 2024 er utildelte organisationsemner er ikke længere synlige i Alle Bokse-visningen, men er kun tilgængelige via Admin-konsol." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Tildel disse emner til en samling via", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "for at gøre dem synlige.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Slet udbyder" + }, + "deleteProviderConfirmation": { + "message": "Sletning af en udbyder er permanent og irreversibel. Angiv hovedadgangskoden for at bekræfte sletningen af udbyderen og alle tilknyttede data." + }, + "deleteProviderName": { + "message": "Kan ikke slette $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "Alle klienttilknytninger skal fjernes, før $ID$ kan slettes", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Udbyder er hermed slettet" + }, + "providerDeletedDesc": { + "message": "Udbyderen og alle tilknyttede data er hermed slettet." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Der er anmodet sletning af din Bitwarden-konto. Klik på knappen nedenfor for at bekræfte." + }, + "deleteProviderWarning": { + "message": "Sletning af kontoen er permanent og irreversibel." + }, + "errorAssigningTargetCollection": { + "message": "Fejl ved tildeling af målsamling." + }, + "errorAssigningTargetFolder": { + "message": "Fejl ved tildeling af målmappe." + }, + "integrationsAndSdks": { + "message": "Integrationer og SDK'er", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrationer" + }, + "integrationsDesc": { + "message": "Synk automatisk hemmeligheder fra Bitwarden Secrets Manager til en tredjepartstjeneste." + }, + "sdks": { + "message": "SDK'er" + }, + "sdksDesc": { + "message": "Brug Bitwarden Secrets Manager SDK i flg. programmeringssprog til bygning af egne applikationer." + }, + "setUpGithubActions": { + "message": "Opsæt Github-handlinger" + }, + "setUpGitlabCICD": { + "message": "Opsæt GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Opsæt Ansible" + }, + "cSharpSDKRepo": { + "message": "Vis C#-repo" + }, + "cPlusPlusSDKRepo": { + "message": "Vis C++-repo" + }, + "jsWebAssemblySDKRepo": { + "message": "VIs JS WebAssembly-repo" + }, + "javaSDKRepo": { + "message": "Vis Java-repo" + }, + "pythonSDKRepo": { + "message": "Vis Python-repo" + }, + "phpSDKRepo": { + "message": "Vis php-repo" + }, + "rubySDKRepo": { + "message": "Vis Ruby-repo" + }, + "goSDKRepo": { + "message": "Vis Go-repo" + }, + "createNewClientToManageAsProvider": { + "message": "Opret en ny kundeorganisation til at håndtere som udbyder. Yderligere pladser afspejles i næste faktureringscyklus." + }, + "selectAPlan": { + "message": "Vælg en abonnementstype" + }, + "thirtyFivePercentDiscount": { + "message": "35% rabat" + }, + "monthPerMember": { + "message": "måned pr. medlem" + }, + "seats": { + "message": "Pladser" + }, + "addOrganization": { + "message": "Tilføj organisation" + }, + "createdNewClient": { + "message": "Ny kunde er hermed oprettet" + }, + "noAccess": { + "message": "Ingen adgang" + }, + "collectionAdminConsoleManaged": { + "message": "Denne samling er kun tilgængelig via Admin-konsol" } } diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index 0e9b0a983f2..3fb75984168 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Anbieterportal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "Sammlung anzeigen" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Zugriff auf Gerätekonto aktualisiert" }, - "unassignedItemsBanner": { - "message": "Hinweis: Nicht zugewiesene Organisationseinträge sind nicht mehr geräteübergreifend in der Ansicht aller Tresore sichtbar und sind nun nur über die Administrator-Konsole zugänglich. Weise diese Einträge einer Sammlung aus der Administrator-Konsole zu, um sie sichtbar zu machen." + "restrictedGroupAccessDesc": { + "message": "Du kannst dich nicht selbst zu einer Gruppe hinzufügen." }, "unassignedItemsBannerSelfHost": { - "message": "Hinweis: Ab dem 2. Mai 2024 werden nicht zugewiesene Organisationseinträge nicht mehr geräteübergreifend in der Ansicht aller Tresore sichtbar sein und sind nur über die Administrator-Konsole zugänglich. Weise diese Elemente einer Sammlung aus der Administrator-Konsole zu, um sie sichtbar zu machen." + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Fehler beim Zuweisen der Ziel-Sammlung." + }, + "errorAssigningTargetFolder": { + "message": "Fehler beim Zuweisen des Ziel-Ordners." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/el/messages.json b/apps/web/src/locales/el/messages.json index 256a5644d69..742a76402d5 100644 --- a/apps/web/src/locales/el/messages.json +++ b/apps/web/src/locales/el/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index 152c6a89e1d..66f728cb4db 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/en_IN/messages.json b/apps/web/src/locales/en_IN/messages.json index d4d22f6aba1..c0c77d3d784 100644 --- a/apps/web/src/locales/en_IN/messages.json +++ b/apps/web/src/locales/en_IN/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/eo/messages.json b/apps/web/src/locales/eo/messages.json index 899558160f5..2a6db2ea4bb 100644 --- a/apps/web/src/locales/eo/messages.json +++ b/apps/web/src/locales/eo/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/es/messages.json b/apps/web/src/locales/es/messages.json index 78b051d5884..9f0f085e9d4 100644 --- a/apps/web/src/locales/es/messages.json +++ b/apps/web/src/locales/es/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/et/messages.json b/apps/web/src/locales/et/messages.json index fdb656f31eb..0c3f7cb300d 100644 --- a/apps/web/src/locales/et/messages.json +++ b/apps/web/src/locales/et/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/eu/messages.json b/apps/web/src/locales/eu/messages.json index 0125ceec25b..c315d625209 100644 --- a/apps/web/src/locales/eu/messages.json +++ b/apps/web/src/locales/eu/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/fa/messages.json b/apps/web/src/locales/fa/messages.json index e91ea40d05f..32feda3edd1 100644 --- a/apps/web/src/locales/fa/messages.json +++ b/apps/web/src/locales/fa/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/fi/messages.json b/apps/web/src/locales/fi/messages.json index 18fa9c15fb2..377f7b084ff 100644 --- a/apps/web/src/locales/fi/messages.json +++ b/apps/web/src/locales/fi/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Toimittajaportaali" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "Tarkastele kokoelmaa" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Konetilin oikeuksia muutettiin" }, - "unassignedItemsBanner": { - "message": "Huomautus: Organisaatioiden kokoelmiin määrittämättömät kohteet eivät enää näy laitteiden \"Kaikki holvit\" -näkymissä, vaan ne ovat nähtävissä vain Hallintapaneelista. Määritä kohteet kokoelmiin Hallintapaneelista, jotta ne ovat jatkossakin käytettävissä kaikilta laitteilta." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { - "message": "Huomautus: 2.5.2024 alkaen kokoelmiin määrittämättömät organisaatioiden kohteet eivät enää näy laitteiden \"Kaikki holvit\" -näkymissä, vaan ne ovat nähtävissä vain Hallintapaneelista. Määritä kohteet kokoelmiin Hallintapaneelista, jotta ne ovat jatkossakin käytettävissä kaikilta laitteilta." + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/fil/messages.json b/apps/web/src/locales/fil/messages.json index 2cf9eb3b7e5..50c9f8d4e87 100644 --- a/apps/web/src/locales/fil/messages.json +++ b/apps/web/src/locales/fil/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index 27b529442c2..29341769560 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Portail fournisseur" }, + "success": { + "message": "Succès" + }, "viewCollection": { "message": "Afficher la Collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Accès au compte machine mis à jour" }, - "unassignedItemsBanner": { - "message": "Remarque : les éléments d'organisation non assignés ne sont plus visibles dans votre vue Tous les coffres sur tous les appareils et sont uniquement accessibles via la Console d'administration. Assignez ces éléments à une collection à partir de la Console d'administration pour les rendre visibles." + "restrictedGroupAccessDesc": { + "message": "Vous ne pouvez pas vous ajouter vous-même à un groupe." }, "unassignedItemsBannerSelfHost": { - "message": "Remarque : au 2 mai 2024, les éléments d'organisation non assignés ne sont plus visibles dans votre vue Tous les coffres sur tous les appareils et sont uniquement accessibles via la Console d'administration. Assignez ces éléments à une collection à partir de la Console d'administration pour les rendre visibles." + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Remarque : Les éléments d'organisation non assignés ne sont plus visibles dans la vue Tous les coffres sur les appareils et ne sont maintenant accessibles que via la Console Admin." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Remarque : À partir du 16 mai 2024, les éléments d'organisation non assignés ne seront plus visibles dans la vue Tous les coffres sur les appareils et ne seront maintenant accessibles que via la Console Admin." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assigner ces éléments à une collection depuis", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "pour les rendre visibles.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Supprimer le fournisseur" + }, + "deleteProviderConfirmation": { + "message": "La suppression d'un fournisseur est permanente et irréversible. Entrez votre mot de passe principal pour confirmer la suppression du fournisseur et de toutes les données associées." + }, + "deleteProviderName": { + "message": "Impossible de supprimer $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "Vous devez dissocier tous les clients avant de pouvoir supprimer $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Fournisseur supprimé" + }, + "providerDeletedDesc": { + "message": "Le fournisseur et toutes les données associées ont été supprimés." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Vous avez demandé à supprimer ce fournisseur. Utilisez le bouton ci-dessous pour confirmer." + }, + "deleteProviderWarning": { + "message": "La suppression de votre fournisseur est permanente. Elle ne peut pas être annulée." + }, + "errorAssigningTargetCollection": { + "message": "Erreur lors de l'assignation de la collection cible." + }, + "errorAssigningTargetFolder": { + "message": "Erreur lors de l'assignation du dossier cible." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Créez une nouvelle organisation de clients à gérer en tant que Fournisseur. Des sièges supplémentaires seront reflétés lors du prochain cycle de facturation." + }, + "selectAPlan": { + "message": "Sélectionnez un plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% de réduction" + }, + "monthPerMember": { + "message": "mois par membre" + }, + "seats": { + "message": "Licences" + }, + "addOrganization": { + "message": "Ajouter une organisation" + }, + "createdNewClient": { + "message": "Nouveau client créé avec succès" + }, + "noAccess": { + "message": "Aucun accès" + }, + "collectionAdminConsoleManaged": { + "message": "Cette collection n'est accessible qu'à partir de la Console Admin" } } diff --git a/apps/web/src/locales/gl/messages.json b/apps/web/src/locales/gl/messages.json index 781c9000cb3..ed04c3a3ef3 100644 --- a/apps/web/src/locales/gl/messages.json +++ b/apps/web/src/locales/gl/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/he/messages.json b/apps/web/src/locales/he/messages.json index fd42338a28d..b7b4d592273 100644 --- a/apps/web/src/locales/he/messages.json +++ b/apps/web/src/locales/he/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/hi/messages.json b/apps/web/src/locales/hi/messages.json index f0487066d63..23321b27ef2 100644 --- a/apps/web/src/locales/hi/messages.json +++ b/apps/web/src/locales/hi/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/hr/messages.json b/apps/web/src/locales/hr/messages.json index 20a62053f0f..edc6adaf3b8 100644 --- a/apps/web/src/locales/hr/messages.json +++ b/apps/web/src/locales/hr/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index 35781b814ac..4ec6f150297 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Szolgáltató portál" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "Gyűjtemény megtekintése" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "A gépi fiók elérése frissítésre került." }, - "unassignedItemsBanner": { - "message": "Megjegyzés: A nem hozzá rendelt szervezeti elemek már nem láthatók az Összes széf nézetben a különböző eszközökön és mostantól csak a Felügyeleti konzolon keresztül érhetők el. Rendeljük ezeket az elemeket egy gyűjteményhez az Adminisztrátori konzolból, hogy láthatóvá tegyeük azokat." + "restrictedGroupAccessDesc": { + "message": "Nem adhadjuk magunkat a csoporthoz." }, "unassignedItemsBannerSelfHost": { - "message": "Megjegyzés: A nem hozzá rendelt szervezeti elemek már nem láthatók az Összes széf nézetben a különböző eszközökön és mostantól csak a Felügyeleti konzolon keresztül érhetők el. Rendeljük ezeket az elemeket egy gyűjteményhez az Adminisztrátori konzolból, hogy láthatóvá tegyeük azokat." + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Megjegyzés: A nem hozzárendelt szervezeti elemek már nem láthatók az Összes széf nézetben a különböző eszközökön és csak az Adminisztrátori konzolon keresztül érhetők el." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Megjegyzés: 2024. május 16-tól a nem hozzárendelt szervezeti elemek többé nem lesznek láthatók az Összes széf nézetben a különböző eszközökön és csak az Adminisztrátoir konzolon keresztül lesznek elérhetők." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Rendeljük hozzá ezeket az elemeket a gyűjteményhez", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "a láthatósághoz.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Hiba történt a célgyűjtemény hozzárendelése során." + }, + "errorAssigningTargetFolder": { + "message": "Hiba történt a célmappa hozzárendelése során." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "Nincs hozzáférés." + }, + "collectionAdminConsoleManaged": { + "message": "Ez a gyűjtemény csak az adminisztrátori konzolról érhető el." } } diff --git a/apps/web/src/locales/id/messages.json b/apps/web/src/locales/id/messages.json index 4cf235eb16f..3bc9ef4e62b 100644 --- a/apps/web/src/locales/id/messages.json +++ b/apps/web/src/locales/id/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/it/messages.json b/apps/web/src/locales/it/messages.json index 1cdee4420dc..11dec69959d 100644 --- a/apps/web/src/locales/it/messages.json +++ b/apps/web/src/locales/it/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Portale Fornitori" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "Visualizza raccolta" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Accesso all'account macchina aggiornato" }, - "unassignedItemsBanner": { - "message": "Avviso: gli elementi dell'organizzazione non assegnati non sono più visibili nella visualizzazione Tutte le Cassaforti su tutti i dispositivi e sono ora accessibili solo tramite la Console di amministrazione. Assegna questi elementi ad una raccolta dalla Console di amministrazione per renderli visibili." + "restrictedGroupAccessDesc": { + "message": "Non puoi aggiungerti a un gruppo." }, "unassignedItemsBannerSelfHost": { - "message": "Avviso: dal 2 maggio 2024, gli elementi dell'organizzazione non assegnati non saranno più visibili nella tua visualizzazione Tutte le Cassaforti su tutti i dispositivi e saranno accessibili solo tramite la Console di amministrazione. Assegna questi elementi ad una raccolta dalla Console di amministrazione per renderli visibili." + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Avviso: gli elementi dell'organizzazione non assegnati non sono più visibili nella visualizzazione Tutte le Cassaforti su tutti i dispositivi e sono ora accessibili solo tramite la Console di amministrazione." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Avviso: dal 2 maggio 2024, gli elementi dell'organizzazione non assegnati non saranno più visibili nella tua visualizzazione Tutte le Cassaforti su tutti i dispositivi e saranno accessibili solo tramite la Console di amministrazione." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assegna questi elementi ad una raccolta dalla", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "per renderli visibili.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Elimina fornitore" + }, + "deleteProviderConfirmation": { + "message": "La cancellazione di un fornitore è permanente e irreversibile. Inserisci la tua password principale per confermare l'eliminazione del fornitore e di tutti i dati associati." + }, + "deleteProviderName": { + "message": "Impossibile eliminare $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "Devi scollegare tutti i client prima di poter eliminare $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Fornitore eliminato" + }, + "providerDeletedDesc": { + "message": "Il fornitore e tutti i dati associati sono stati eliminati." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Hai richiesto di eliminare il questo fornitore. Clicca qui sotto per confermare." + }, + "deleteProviderWarning": { + "message": "L'eliminazione del fornitore è permanente. Questa azione non è reversibile." + }, + "errorAssigningTargetCollection": { + "message": "Errore nell'assegnazione della raccolta di destinazione." + }, + "errorAssigningTargetFolder": { + "message": "Errore nell'assegnazione della cartella di destinazione." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Crea una nuova organizzazione cliente da gestire come fornitore. Gli slot aggiuntivi saranno riflessi nel prossimo ciclo di fatturazione." + }, + "selectAPlan": { + "message": "Seleziona un piano" + }, + "thirtyFivePercentDiscount": { + "message": "Sconto del 35%" + }, + "monthPerMember": { + "message": "mese per membro" + }, + "seats": { + "message": "Slot" + }, + "addOrganization": { + "message": "Aggiungi organizzazione" + }, + "createdNewClient": { + "message": "Nuovo cliente creato con successo" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index 3561c81bd1a..17b58a4299d 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -7601,11 +7601,14 @@ "message": "リリースブログを読む" }, "adminConsole": { - "message": "管理者コンソール" + "message": "管理コンソール" }, "providerPortal": { "message": "プロバイダーポータル" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "コレクションを表示" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "マシンアカウントへのアクセス権限を更新しました" }, - "unassignedItemsBanner": { - "message": "注意: 割り当てられていない組織項目は、デバイス間のすべての保管庫のビューでは表示されなくなり、管理コンソールからのみアクセスできます。 管理コンソールからコレクションにこれらのアイテムを割り当てると、表示するようにできます。" + "restrictedGroupAccessDesc": { + "message": "あなた自身をグループに追加することはできません。" }, "unassignedItemsBannerSelfHost": { - "message": "お知らせ:2024年5月2日に、 割り当てられていない組織アイテムはデバイス間のすべての保管庫ビューに表示されなくなり、管理コンソールからのみアクセス可能になります。 管理コンソールからコレクションにこれらのアイテムを割り当てると、表示できるようになります。" + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "注意: 割り当てられていない組織アイテムは、デバイス間のすべての保管庫ビューでは表示されなくなり、管理コンソールからのみアクセスできるようになりました。" + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "お知らせ:2024年5月16日に、 割り当てられていない組織アイテムは、デバイス間のすべての保管庫ビューに表示されなくなり、管理コンソールからのみアクセス可能になります。" + }, + "unassignedItemsBannerCTAPartOne": { + "message": "これらのアイテムのコレクションへの割り当てを", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "で実行すると表示できるようになります。", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "プロバイダを削除" + }, + "deleteProviderConfirmation": { + "message": "プロバイダの削除は恒久的で元に戻せません。マスターパスワードを入力して、プロバイダと関連するすべてのデータの削除を確認してください。" + }, + "deleteProviderName": { + "message": "$ID$ を削除できません", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "$ID$ を削除する前に、すべてのクライアントのリンクを解除してください", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "プロバイダを削除しました" + }, + "providerDeletedDesc": { + "message": "プロバイダと関連するすべてのデータを削除しました。" + }, + "deleteProviderRecoverConfirmDesc": { + "message": "このプロバイダの削除をリクエストしました。下のボタンを使うと確認できます。" + }, + "deleteProviderWarning": { + "message": "プロバイダを削除すると元に戻すことはできません。" + }, + "errorAssigningTargetCollection": { + "message": "ターゲットコレクションの割り当てに失敗しました。" + }, + "errorAssigningTargetFolder": { + "message": "ターゲットフォルダーの割り当てに失敗しました。" + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/ka/messages.json b/apps/web/src/locales/ka/messages.json index d94a7014100..6d34252b88c 100644 --- a/apps/web/src/locales/ka/messages.json +++ b/apps/web/src/locales/ka/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/km/messages.json b/apps/web/src/locales/km/messages.json index 781c9000cb3..ed04c3a3ef3 100644 --- a/apps/web/src/locales/km/messages.json +++ b/apps/web/src/locales/km/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/kn/messages.json b/apps/web/src/locales/kn/messages.json index f2a5bdd8299..ae6f2c2a897 100644 --- a/apps/web/src/locales/kn/messages.json +++ b/apps/web/src/locales/kn/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/ko/messages.json b/apps/web/src/locales/ko/messages.json index b54268adbd0..f9240f21aa6 100644 --- a/apps/web/src/locales/ko/messages.json +++ b/apps/web/src/locales/ko/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index d68a62bdcf7..0dc0dff097b 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -3360,7 +3360,7 @@ "message": "Ja konts pastāv, tika nosūtīts e-pasta ziņojums ar turpmākām norādēm." }, "deleteRecoverConfirmDesc": { - "message": "Tika pieprasīts izdzēst Bitwarden kontu. Jānospiež zemāk esošā poga, lai apstiprinātu." + "message": "Tika pieprasīts izdzēst Bitwarden kontu. Jāizmanto zemāk esošā poga, lai apstiprinātu." }, "myOrganization": { "message": "Mana apvienība" @@ -3396,7 +3396,7 @@ "message": "Apvienība izdzēsta" }, "organizationDeletedDesc": { - "message": "Apvienība un visi ar to saistītie dati ir izdzēsti." + "message": "Apvienība un visi ar to saistītie dati tika izdzēsti." }, "organizationUpdated": { "message": "Apvienība atjaunināta" @@ -4927,7 +4927,7 @@ "message": "Uziacināt jaunu nodrošinātāja lietotāju, zemāk esošajā laukā ievadot tā Bitwarden konta e-pasta adresi. Ja tam vēl nav Bitwarden konta, tiks vaicāts izveidot jaunu." }, "joinProvider": { - "message": "Pievienot nodrošinātāju" + "message": "Pievienoties nodrošinātāju" }, "joinProviderDesc": { "message": "Tu esi uzaicināts pievienoties augstāk norādītajam nodrošinātājam. Lai to pieņemtu, jāpiesakās vai jāizveido jauns Bitwarden konts." @@ -4945,7 +4945,7 @@ "message": "Nodrošinātājs" }, "newClientOrganization": { - "message": "Jauna sniedzēja apvienība" + "message": "Jauna pasūtītāja apvienība" }, "newClientOrganizationDesc": { "message": "Izveidot jaunu pasūtītāja apvienību, kas būs piesaistīta šim kontam kā nodrošinātājam. Tas sniegs iespēju piekļūt šai apvienībai un to pārvaldīt." @@ -5661,7 +5661,7 @@ "message": "Rēķinu vēsture" }, "backToReports": { - "message": "Atgriezties pie atskaitēm" + "message": "Atgriezties pie pārskatiem" }, "organizationPicker": { "message": "Apvienību atlasītājs" @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Nodrošinātāju portāls" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "Skatīt krājumu" }, @@ -7757,7 +7760,7 @@ "description": "Message to encourage the user to start creating machine accounts." }, "machineAccountsNoItemsTitle": { - "message": "Nothing to show yet", + "message": "Vēl nav nekā, ko parādīt", "description": "Title to indicate that there are no machine accounts to display." }, "deleteMachineAccounts": { @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Jāņem vērā: nepiešķirti apvienības vienumi vairs nav redzami skatā \"Visas glabātavas\" dažādās ierīcēs un ir sasniedzami tikai no pārvaldības konsoles, kur šie vienumi jāpiešķir krājumam, lai padarītu tos redzamus." + "restrictedGroupAccessDesc": { + "message": "Sevi nevar pievienot kopai." }, "unassignedItemsBannerSelfHost": { - "message": "Jāņem vērā: 2024. gada 2. maijā nepiešķirti apvienības vienumi vairs nebūs redzami skatā \"Visas glabātavas\" dažādās ierīcēs un būs sasniedzami tikai no pārvaldības konsoles, kur šie vienumi jāpiešķir krājumam, lai padarītu tos redzamus." + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Jāņem vērā: nepiešķirti apvienības vienumi vairs nav redzami skatā \"Visas glabātavas\" dažādās ierīcēs un tagad ir pieejami tikai pārvaldības konsolē." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Jāņem vērā: no 2024. gada 16. maija nepiešķirti apvienības vienumi vairs nebūs redzami skatā \"Visas glabātavas\" dažādās ierīcēs un būs pieejami tikai pārvaldības konsolē." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Piešķirt šos vienumus krājumam", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": ", lai padarītu tos redzamus.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Izdzēst nodrošinātāju" + }, + "deleteProviderConfirmation": { + "message": "Nodrošinātāja izdzēšana ir paliekoša un neatgriezeniska. Jāievada sava galvenā parole, lai apliecinātu nodrošinātāja un visu saistīto datu izdzēšanu." + }, + "deleteProviderName": { + "message": "Nevar izdzēst $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "Ir jāatsaista visi klienti, pirms var izdzēst $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Nodrošinātājs izdzēsts" + }, + "providerDeletedDesc": { + "message": "Nodrošinātājs un visi ar to saistītie dati tika izdzēsti." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Tika pieprasīts izdzēst šo nodrošinātāju. Jāizmanto zemāk esošā poga, lai apstiprinātu." + }, + "deleteProviderWarning": { + "message": "Nodrošinātāja izdzēšana ir paliekoša. To nevar atsaukt." + }, + "errorAssigningTargetCollection": { + "message": "Kļūda mērķa krājuma piešķiršanā." + }, + "errorAssigningTargetFolder": { + "message": "Kļūda mērķa mapes piešķiršanā." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/ml/messages.json b/apps/web/src/locales/ml/messages.json index 4f815327968..9b6d1ab5655 100644 --- a/apps/web/src/locales/ml/messages.json +++ b/apps/web/src/locales/ml/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/mr/messages.json b/apps/web/src/locales/mr/messages.json index 781c9000cb3..ed04c3a3ef3 100644 --- a/apps/web/src/locales/mr/messages.json +++ b/apps/web/src/locales/mr/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/my/messages.json b/apps/web/src/locales/my/messages.json index 781c9000cb3..ed04c3a3ef3 100644 --- a/apps/web/src/locales/my/messages.json +++ b/apps/web/src/locales/my/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/nb/messages.json b/apps/web/src/locales/nb/messages.json index 0719b129b34..4e5fb13d15c 100644 --- a/apps/web/src/locales/nb/messages.json +++ b/apps/web/src/locales/nb/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/ne/messages.json b/apps/web/src/locales/ne/messages.json index e0ca2b4f0aa..f4118475717 100644 --- a/apps/web/src/locales/ne/messages.json +++ b/apps/web/src/locales/ne/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index 3ab66e763be..7bf9b4018fd 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Providerportaal" }, + "success": { + "message": "Succes" + }, "viewCollection": { "message": "Collectie weergeven" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Toegang tot machine-account bijgewerkt" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "Je kunt jezelf niet aan een groep toevoegen." }, "unassignedItemsBannerSelfHost": { "message": "Kennisgeving: Vanaf 2 mei 2024 zijn niet-toegewezen organisatie-items op geen enkel apparaat meer zichtbaar in de weergave van alle kluisjes en alleen toegankelijk via de Admin Console. Je kunt deze items in het Admin Console aan een collectie toewijzen om ze zichtbaar te maken." + }, + "unassignedItemsBannerNotice": { + "message": "Let op: Niet-toegewezen organisatie-items zijn niet langer zichtbaar in de weergave van alle kluissen op verschillende apparaten en zijn nu alleen toegankelijk via de Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Let op: Vanaf 16 mei 2024 zijn niet-toegewezen organisatie-items niet langer zichtbaar in de weergave van alle kluissen op verschillende apparaten en alleen toegankelijk via de Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Deze items toewijzen aan een collectie van de", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "om ze zichtbaar te maken.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Provider verwijderen" + }, + "deleteProviderConfirmation": { + "message": "Het verwijderen van een provider is definitief en onomkeerbaar. Voer je hoofdwachtwoord in om het verwijderen van de provider en alle bijbehorende gegevens te bevestigen." + }, + "deleteProviderName": { + "message": "Kan $ID$ niet verwijderen", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "Je moet alle clients ontkoppelen voordat je $ID$ kunt verwijderen", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider verwijderd" + }, + "providerDeletedDesc": { + "message": "De organisatie en alle bijhorende gegevens zijn verwijderd." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Je hebt het verwijderen van deze provider aangevraagd. Gebruik deonderstaande knop om te bevestigen." + }, + "deleteProviderWarning": { + "message": "Het verwijderen van een provider is definitief. Je kunt het niet ongedaan maken." + }, + "errorAssigningTargetCollection": { + "message": "Fout bij toewijzen doelverzameling." + }, + "errorAssigningTargetFolder": { + "message": "Fout bij toewijzen doelmap." + }, + "integrationsAndSdks": { + "message": "Integraties & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integraties" + }, + "integrationsDesc": { + "message": "Automatisch secrets synchroniseren van Bitwarden Secrets Manager met een dienst van derden." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Gebruik Bitwarden Secrets Manager SDK in de volgende programmeertalen om je eigen applicaties te bouwen." + }, + "setUpGithubActions": { + "message": "GitHub Actions instellen" + }, + "setUpGitlabCICD": { + "message": "GitLab CI/CD instellen" + }, + "setUpAnsible": { + "message": "Ansibel instellen" + }, + "cSharpSDKRepo": { + "message": "C#-repository bekijken" + }, + "cPlusPlusSDKRepo": { + "message": "C++-repository bekijken" + }, + "jsWebAssemblySDKRepo": { + "message": "JS WebAssembly-repository bekijken" + }, + "javaSDKRepo": { + "message": "Java-repository bekijken" + }, + "pythonSDKRepo": { + "message": "Python-repository bekijken" + }, + "phpSDKRepo": { + "message": "Php-repository bekijken" + }, + "rubySDKRepo": { + "message": "Ruby-repository bekijken" + }, + "goSDKRepo": { + "message": "Go-repository bekijken" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "Geen toegang" + }, + "collectionAdminConsoleManaged": { + "message": "Deze collectie is alleen toegankelijk vanaf de admin console" } } diff --git a/apps/web/src/locales/nn/messages.json b/apps/web/src/locales/nn/messages.json index a80816b511c..3d54ea70fd6 100644 --- a/apps/web/src/locales/nn/messages.json +++ b/apps/web/src/locales/nn/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/or/messages.json b/apps/web/src/locales/or/messages.json index 781c9000cb3..ed04c3a3ef3 100644 --- a/apps/web/src/locales/or/messages.json +++ b/apps/web/src/locales/or/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index 9532e8d25ce..57e0eb1929b 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -296,7 +296,7 @@ "message": "Szukaj w organizacji" }, "searchMembers": { - "message": "Szukaj w użytkownikach" + "message": "Szukaj członków" }, "searchGroups": { "message": "Szukaj w grupach" @@ -854,7 +854,7 @@ "message": "Brak użytkowników do wyświetlenia." }, "noMembersInList": { - "message": "Brak użytkowników do wyświetlenia." + "message": "Brak członków do wyświetlenia." }, "noEventsInList": { "message": "Brak wydarzeń do wyświetlenia." @@ -1534,7 +1534,7 @@ "message": "Włącz dwustopniowe logowanie dla swojej organizacji." }, "twoStepLoginEnterpriseDescStart": { - "message": "Wymuszaj opcje logowania dwustopniowego Bitwarden dla użytkowników za pomocą ", + "message": "Wymuszaj opcje logowania dwustopniowego Bitwarden dla członków za pomocą ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Enforce Bitwarden Two-step Login options for members by using the Two-step Login Policy.'" }, "twoStepLoginPolicy": { @@ -2085,7 +2085,7 @@ "message": "Konto Premium" }, "premiumAccessDesc": { - "message": "Możesz przyznać dostęp premium wszystkim użytkownikom w Twojej organizacji za $PRICE$ /$INTERVAL$.", + "message": "Możesz przyznać dostęp premium wszystkim członkom Twojej organizacji za $PRICE$ /$INTERVAL$.", "placeholders": { "price": { "content": "$1", @@ -2541,7 +2541,7 @@ } }, "trialSecretsManagerThankYou": { - "message": "Thanks for signing up for Bitwarden Secrets Manager for $PLAN$!", + "message": "Dziękujemy za zarejestrowanie się do Menedżera Sekretów Bitwarden w planie $PLAN$!", "placeholders": { "plan": { "content": "$1", @@ -2691,7 +2691,7 @@ "message": "Czy na pewno chcesz usunąć tę kolekcję?" }, "editMember": { - "message": "Edytuj użytkownika" + "message": "Edytuj członka" }, "fieldOnTabRequiresAttention": { "message": "Pole na karcie '$TAB$' wymaga uwagi.", @@ -2751,7 +2751,7 @@ "message": "Administrator" }, "adminDesc": { - "message": "Administratorzy mają dostęp do wszystkich elementów, kolekcji i użytkowników w Twojej organizacji." + "message": "Administratorzy mają dostęp do wszystkich elementów, kolekcji i członków Twojej organizacji" }, "user": { "message": "Użytkownik" @@ -3234,7 +3234,7 @@ "message": "Dostęp grupowy" }, "groupAccessUserDesc": { - "message": "Zmień grupy, do których należy użytkownik." + "message": "Przyznaj członkowi dostęp do kolekcji, dodając go do 1 lub więcej grup." }, "invitedUsers": { "message": "Użytkownicy zostali zaproszeni" @@ -3270,10 +3270,10 @@ } }, "confirmUsers": { - "message": "Zatwierdź użytkowników" + "message": "Zatwierdź członków" }, "usersNeedConfirmed": { - "message": "Posiadasz użytkowników, którzy zaakceptowali zaproszenie, ale muszą jeszcze zostać potwierdzeni. Użytkownicy nie będą posiadali dostępu do organizacji, dopóki nie zostaną potwierdzeni." + "message": "Posiadasz członków, którzy zaakceptowali zaproszenie, ale muszą jeszcze zostać potwierdzeni. Członkowie nie będą posiadali dostępu do organizacji, dopóki nie zostaną potwierdzeni." }, "startDate": { "message": "Data rozpoczęcia" @@ -3486,10 +3486,10 @@ "message": "Wpisz identyfikator instalacji" }, "limitSubscriptionDesc": { - "message": "Ustaw limit liczby stanowisk subskrypcji. Po osiągnięciu tego limitu nie będziesz mógł zapraszać nowych użytkowników." + "message": "Ustaw limit liczby subskrypcji. Po osiągnięciu tego limitu nie będziesz mógł zapraszać nowych członków." }, "limitSmSubscriptionDesc": { - "message": "Ustaw limit liczby subskrypcji menedżera sekretów. Po osiągnięciu tego limitu nie będziesz mógł zapraszać nowych użytkowników." + "message": "Ustaw limit liczby subskrypcji menedżera sekretów. Po osiągnięciu tego limitu nie będziesz mógł zapraszać nowych członków." }, "maxSeatLimit": { "message": "Maksymalna liczba stanowisk (opcjonalnie)", @@ -3510,7 +3510,7 @@ "message": "Zmiany w subskrypcji spowodują proporcjonalne zmiany w rozliczeniach. Jeśli nowo zaproszeni użytkownicy przekroczą liczbę stanowisk w subskrypcji, otrzymasz proporcjonalną opłatę za dodatkowych użytkowników." }, "subscriptionUserSeats": { - "message": "Twoja subskrypcja pozwala na łączną liczbę $COUNT$ użytkowników.", + "message": "Twoja subskrypcja pozwala na łączną liczbę $COUNT$ członków.", "placeholders": { "count": { "content": "$1", @@ -3537,10 +3537,10 @@ "message": "Aby uzyskać dodatkową pomoc w zarządzaniu subskrypcją, skontaktuj się z działem obsługi klienta." }, "subscriptionUserSeatsUnlimitedAutoscale": { - "message": "Zmiany w subskrypcji spowodują proporcjonalne zmiany w rozliczeniach. Jeśli nowo zaproszeni użytkownicy przekroczą liczbę stanowisk w subskrypcji, otrzymasz proporcjonalną opłatę za dodatkowych użytkowników." + "message": "Zmiany w subskrypcji spowodują proporcjonalne zmiany w rozliczeniach. Jeśli nowo zaproszeni członkowie przekroczą liczbę miejsc w subskrypcji, otrzymasz proporcjonalną opłatę za dodatkowych członków." }, "subscriptionUserSeatsLimitedAutoscale": { - "message": "Korekty subskrypcji spowodują proporcjonalne zmiany w sumach rozliczeniowych. Jeśli nowo zaproszeni użytkownicy przekroczą liczbę miejsc objętych subskrypcją, natychmiast otrzymasz proporcjonalną opłatę za dodatkowych użytkowników, aż do osiągnięcia limitu miejsc $MAX$.", + "message": "Korekty subskrypcji spowodują proporcjonalne zmiany w sumach rozliczeniowych. Jeśli nowo zaproszeni członkowie przekroczą liczbę miejsc objętych subskrypcją, natychmiast otrzymasz proporcjonalną opłatę za dodatkowych członków, aż do osiągnięcia limitu miejsc $MAX$.", "placeholders": { "max": { "content": "$1", @@ -4083,7 +4083,7 @@ "message": "Identyfikator SSO" }, "ssoIdentifierHintPartOne": { - "message": "Podaj ten identyfikator swoim użytkownikom, aby zalogować się za pomocą SSO. Aby pominąć ten krok, ustaw ", + "message": "Podaj ten identyfikator swoim członkowm, aby zalogować się za pomocą SSO. Aby pominąć ten krok, ustaw ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" }, "unlinkSso": { @@ -5210,7 +5210,7 @@ "message": "Zweryfikuj certyfikaty" }, "spUniqueEntityId": { - "message": "Set a unique SP entity ID" + "message": "Ustaw unikalny identyfikator jednostki SP" }, "spUniqueEntityIdDesc": { "message": "Wygeneruj identyfikator unikalny dla Twojej organizacji" @@ -5426,13 +5426,13 @@ "message": "Opcje odszyfrowania użytkownika" }, "memberDecryptionPassDesc": { - "message": "Po uwierzytelnieniu użytkownicy odszyfrują dane sejfu za pomocą hasła głównego." + "message": "Po uwierzytelnieniu członkowie odszyfrują dane sejfu za pomocą hasła głównego." }, "keyConnector": { "message": "Key Connector" }, "memberDecryptionKeyConnectorDescStart": { - "message": "Połącz logowanie za pomocą SSO z Twoim serwerem kluczy odszyfrowania. Używając tej opcji, użytkownicy nie będą musieli używać swoich haseł głównych, aby odszyfrować dane sejfu.", + "message": "Połącz logowanie za pomocą SSO z Twoim serwerem kluczy odszyfrowania. Używając tej opcji, członkowie nie będą musieli używać swoich haseł głównych, aby odszyfrować dane sejfu.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Connect login with SSO to your self-hosted decryption key server. Using this option, members won’t need to use their master passwords to decrypt vault data. The require SSO authentication and single organization policies are required to set up Key Connector decryption. Contact Bitwarden Support for set up assistance.'" }, "memberDecryptionKeyConnectorDescLink": { @@ -6455,7 +6455,7 @@ "message": "Informacje o grupie" }, "editGroupMembersDesc": { - "message": "Przyznaj użytkownikom dostęp do kolekcji przypisanych do grupy." + "message": "Przyznaj członkom dostęp do kolekcji przypisanych do grupy." }, "editGroupCollectionsDesc": { "message": "Przyznaj dostęp do kolekcji, dodając je do tej grupy." @@ -6467,7 +6467,7 @@ "message": "Jeśli zaznaczone, zastąpi to wszystkie inne uprawnienia kolekcji." }, "selectMembers": { - "message": "Wybierz użytkowników" + "message": "Wybierz członków" }, "selectCollections": { "message": "Wybierz kolekcje" @@ -6476,7 +6476,7 @@ "message": "Rola" }, "removeMember": { - "message": "Usuń użytkownika" + "message": "Usuń członka" }, "collection": { "message": "Kolekcja" @@ -6500,7 +6500,7 @@ "message": "Nie dodano kolekcji" }, "noMembersAdded": { - "message": "Nie dodano użytkowników" + "message": "Nie dodano członków" }, "noGroupsAdded": { "message": "Nie dodano grup" @@ -6680,13 +6680,13 @@ "message": "Wysłać kod ponownie" }, "memberColumnHeader": { - "message": "Member" + "message": "Członek" }, "groupSlashMemberColumnHeader": { - "message": "Group/Member" + "message": "Grupa/Członek" }, "selectGroupsAndMembers": { - "message": "Wybierz grupy i użytkowników" + "message": "Wybierz grupy i członków" }, "selectGroups": { "message": "Wybierz grupy" @@ -6695,22 +6695,22 @@ "message": "Uprawnienia ustawione dla użytkownika zastąpią uprawnienia ustawione przez grupę tego użytkownika" }, "noMembersOrGroupsAdded": { - "message": "Nie dodano użytkowników lub grup" + "message": "Nie dodano członków lub grup" }, "deleted": { "message": "Usunięto" }, "memberStatusFilter": { - "message": "Filtr statusu użytkownika" + "message": "Filtr statusu członka" }, "inviteMember": { - "message": "Zaproś użytkownika" + "message": "Zaproś członka" }, "needsConfirmation": { "message": "Wymaga potwierdzenia" }, "memberRole": { - "message": "Rola użytkownika" + "message": "Rola członka" }, "moreFromBitwarden": { "message": "Więcej od Bitwarden" @@ -7105,7 +7105,7 @@ "message": "Zaufane urządzenia" }, "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Po uwierzytelnieniu użytkownicy odszyfrowają dane sejfu przy użyciu klucza zapisanego na ich urządzeniu.", + "message": "Po uwierzytelnieniu członkowie odszyfrowają dane sejfu przy użyciu klucza zapisanego na ich urządzeniu.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" }, "memberDecryptionOptionTdeDescriptionLinkOne": { @@ -7229,7 +7229,7 @@ "message": "Brak hasła głównego" }, "removeMembersWithoutMasterPasswordWarning": { - "message": "Usuwanie użytkowników, którzy nie mają hasła głównego bez ustawienia go, może ograniczyć dostęp do ich pełnych kont." + "message": "Usuwanie członków, którzy nie mają hasła głównego bez ustawienia go, może ograniczyć dostęp do ich pełnych kont." }, "approvedAuthRequest": { "message": "Zatwierdzone urządzenie dla $ID$.", @@ -7262,7 +7262,7 @@ } }, "startYour7DayFreeTrialOfBitwardenSecretsManagerFor": { - "message": "Start your 7-Day free trial of Bitwarden Secrets Manager for $ORG$", + "message": "Rozpocznij 7-dniowy darmowy okres próbny Menedżera Sekretów Bitwarden dla $ORG$", "placeholders": { "org": { "content": "$1", @@ -7508,7 +7508,7 @@ "message": "Przyznaj grupom lub członkom dostęp do tej kolekcji." }, "grantCollectionAccessMembersOnly": { - "message": "Grant members access to this collection." + "message": "Przyznaj członkom dostęp do tej kolekcji." }, "adminCollectionAccess": { "message": "Administratorzy mogą mieć dostęp do kolekcji i zarządzać nimi." @@ -7548,7 +7548,7 @@ "message": "Szczegóły potwierdzenia" }, "smFreeTrialThankYou": { - "message": "Thank you for signing up for Bitwarden Secrets Manager!" + "message": "Dziękujemy za zarejestrowanie się do Menedżera Sekretów Bitwarden!" }, "smFreeTrialConfirmationEmail": { "message": "Wysłaliśmy e-mail weryfikacyjny na adres " @@ -7557,7 +7557,7 @@ "message": "Ta akcja jest nieodwracalna" }, "confirmCollectionEnhancementsDialogContent": { - "message": "Turning on this feature will deprecate the manager role and replace it with a Can manage permission. This will take a few moments. Do not make any organization changes until it is complete. Are you sure you want to proceed?" + "message": "Włączenie tej funkcji spowoduje deprekację roli menedżera i zastąpi ją uprawnieniami do zarządzania uprawnieniami. To zajmie kilka chwil. Nie wprowadzaj żadnych zmian w organizacji dopóki nie zostanie zakończone. Czy na pewno chcesz kontynuować?" }, "sorryToSeeYouGo": { "message": "Przykro nam, że nas opuszczasz! Pomóż ulepszyć Bitwarden udostępniając informacje dlaczego anulujesz.", @@ -7598,7 +7598,7 @@ "message": "Witaj w nowej i ulepszonej aplikacji internetowej. Dowiedz się więcej o tym, co się zmieniło." }, "releaseBlog": { - "message": "Read release blog" + "message": "Przeczytaj blog z informacjami o wydaniu" }, "adminConsole": { "message": "Konsola administratora" @@ -7606,14 +7606,17 @@ "providerPortal": { "message": "Portal dostawcy" }, + "success": { + "message": "Sukces" + }, "viewCollection": { "message": "Zobacz kolekcję" }, "restrictedGroupAccess": { - "message": "You cannot add yourself to groups." + "message": "Nie możesz dodać siebie do grup." }, "restrictedCollectionAccess": { - "message": "You cannot add yourself to collections." + "message": "Nie możesz dodać siebie do kolekcji." }, "assign": { "message": "Przypisz" @@ -7738,42 +7741,42 @@ "description": "The date header used when a subscription is cancelled." }, "machineAccountsCannotCreate": { - "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + "message": "Konta dla maszyn nie mogą być tworzone w zawieszonych organizacjach. Skontaktuj się z właścicielem organizacji, aby uzyskać pomoc." }, "machineAccount": { - "message": "Machine account", + "message": "Konto dla maszyny", "description": "A machine user which can be used to automate processes and access secrets in the system." }, "machineAccounts": { - "message": "Machine accounts", + "message": "Konta dla maszyn", "description": "The title for the section that deals with machine accounts." }, "newMachineAccount": { - "message": "New machine account", + "message": "Nowe konto dla maszyny", "description": "Title for creating a new machine account." }, "machineAccountsNoItemsMessage": { - "message": "Create a new machine account to get started automating secret access.", + "message": "Utwórz nowe konto dla maszyny, aby rozpocząć automatyzację dostępu do sekretu.", "description": "Message to encourage the user to start creating machine accounts." }, "machineAccountsNoItemsTitle": { - "message": "Nothing to show yet", + "message": "Nie ma nic do pokazania", "description": "Title to indicate that there are no machine accounts to display." }, "deleteMachineAccounts": { - "message": "Delete machine accounts", + "message": "Usuń konta maszyn", "description": "Title for the action to delete one or multiple machine accounts." }, "deleteMachineAccount": { - "message": "Delete machine account", + "message": "Usuń konto maszyny", "description": "Title for the action to delete a single machine account." }, "viewMachineAccount": { - "message": "View machine account", + "message": "Zobacz konto maszyny", "description": "Action to view the details of a machine account." }, "deleteMachineAccountDialogMessage": { - "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "message": "Usuwanie konta maszyny $MACHINE_ACCOUNT$ jest trwałe i nieodwracalne.", "placeholders": { "machine_account": { "content": "$1", @@ -7782,10 +7785,10 @@ } }, "deleteMachineAccountsDialogMessage": { - "message": "Deleting machine accounts is permanent and irreversible." + "message": "Usuwanie kont maszyn jest trwałe i nieodwracalne." }, "deleteMachineAccountsConfirmMessage": { - "message": "Delete $COUNT$ machine accounts", + "message": "Usuń $COUNT$ kont maszyn", "placeholders": { "count": { "content": "$1", @@ -7794,60 +7797,60 @@ } }, "deleteMachineAccountToast": { - "message": "Machine account deleted" + "message": "Konto maszyny usunięte" }, "deleteMachineAccountsToast": { - "message": "Machine accounts deleted" + "message": "Konta maszyn usunięte" }, "searchMachineAccounts": { - "message": "Search machine accounts", + "message": "Szukaj kont maszyn", "description": "Placeholder text for searching machine accounts." }, "editMachineAccount": { - "message": "Edit machine account", + "message": "Edytuj konto maszyny", "description": "Title for editing a machine account." }, "machineAccountName": { - "message": "Machine account name", + "message": "Nazwa konta maszyny", "description": "Label for the name of a machine account" }, "machineAccountCreated": { - "message": "Machine account created", + "message": "Konto maszyny utworzone", "description": "Notifies that a new machine account has been created" }, "machineAccountUpdated": { - "message": "Machine account updated", + "message": "Konto maszyny zaktualizowane", "description": "Notifies that a machine account has been updated" }, "projectMachineAccountsDescription": { - "message": "Grant machine accounts access to this project." + "message": "Przyznaj dostęp kontom maszynowym do tego projektu." }, "projectMachineAccountsSelectHint": { - "message": "Type or select machine accounts" + "message": "Wpisz lub wybierz konta maszyn" }, "projectEmptyMachineAccountAccessPolicies": { - "message": "Add machine accounts to grant access" + "message": "Dodaj konta maszyn, aby udzielić dostępu" }, "machineAccountPeopleDescription": { - "message": "Grant groups or people access to this machine account." + "message": "Przyznaj grupom lub ludziom dostęp do tego konta maszyny." }, "machineAccountProjectsDescription": { - "message": "Assign projects to this machine account. " + "message": "Przypisz projekty do tego konta maszyny. " }, "createMachineAccount": { - "message": "Create a machine account" + "message": "Utwórz konto dla maszyny" }, "maPeopleWarningMessage": { - "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + "message": "Usuwanie osób z konta maszyny nie usuwa tokenów dostępu. Dla zapewnienia najlepszej praktyki bezpieczeństwa zaleca się cofnięcie tokenów dostępu stworzonych przez osoby usunięte z konta maszyny." }, "smAccessRemovalWarningMaTitle": { - "message": "Remove access to this machine account" + "message": "Usuń dostęp do tego konta maszyny" }, "smAccessRemovalWarningMaMessage": { - "message": "This action will remove your access to the machine account." + "message": "Ta akcja usunie Twój dostęp do konta maszyny." }, "machineAccountsIncluded": { - "message": "$COUNT$ machine accounts included", + "message": "Uwzględnione konta maszyn: $COUNT$", "placeholders": { "count": { "content": "$1", @@ -7856,7 +7859,7 @@ } }, "additionalMachineAccountCost": { - "message": "$COST$ per month for additional machine accounts", + "message": "$COST$ miesięcznie dla dodatkowych kont dla maszyn", "placeholders": { "cost": { "content": "$1", @@ -7865,10 +7868,10 @@ } }, "additionalMachineAccounts": { - "message": "Additional machine accounts" + "message": "Dodatkowe konta dla maszyn" }, "includedMachineAccounts": { - "message": "Your plan comes with $COUNT$ machine accounts.", + "message": "Twój plan obejmuje $COUNT$ kont dla maszyn.", "placeholders": { "count": { "content": "$1", @@ -7877,7 +7880,7 @@ } }, "addAdditionalMachineAccounts": { - "message": "You can add additional machine accounts for $COST$ per month.", + "message": "Możesz dodać dodatkowe konta dla maszyn za $COST$ miesięcznie.", "placeholders": { "cost": { "content": "$1", @@ -7886,24 +7889,156 @@ } }, "limitMachineAccounts": { - "message": "Limit machine accounts (optional)" + "message": "Limit kont dla maszyn (opcjonalnie)" }, "limitMachineAccountsDesc": { - "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + "message": "Ustaw limit liczby kont dla maszyn. Po osiągnięciu tego limitu nie będziesz mógł tworzyć nowych kont." }, "machineAccountLimit": { - "message": "Machine account limit (optional)" + "message": "Limit konta dla maszyn (opcjonalnie)" }, "maxMachineAccountCost": { - "message": "Max potential machine account cost" + "message": "Maksymalny potencjalny koszt kont dla maszyn" }, "machineAccountAccessUpdated": { - "message": "Machine account access updated" + "message": "Zaktualizowano dostęp do konta dla maszyny" }, - "unassignedItemsBanner": { - "message": "Uwaga: Nieprzypisane elementy w organizacji nie są już widoczne w widoku Wszystkie sejfy na urządzeniach i są teraz dostępne tylko przez Konsolę Administracyjną. Przypisz te elementy do kolekcji z konsoli administracyjnej, aby były one widoczne." + "restrictedGroupAccessDesc": { + "message": "Nie możesz dodać siebie do grupy." }, "unassignedItemsBannerSelfHost": { "message": "Uwaga: 2 maja 2024 r. nieprzypisane elementy w organizacji nie będą już widoczne w widoku Wszystkie sejfy na urządzeniach i będą dostępne tylko przez Konsolę Administracyjną. Przypisz te elementy do kolekcji z konsoli administracyjnej, aby były one widoczne." + }, + "unassignedItemsBannerNotice": { + "message": "Uwaga: Nieprzypisane elementy organizacji nie są już widoczne w widoku Wszystkie sejfy na urządzeniach i są teraz dostępne tylko przez Konsolę Administracyjną." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Uwaga: 16 maja 2024 r. nieprzypisana elementy w organizacji nie będą już widoczne w widoku Wszystkie sejfy na urządzeniach i będą dostępne tylko przez Konsolę Administracyjną." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Przypisz te elementy do kolekcji z", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": ", aby były widoczne.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Usuń dostawcę" + }, + "deleteProviderConfirmation": { + "message": "Usunięcie dostawcy jest trwałe i nieodwracalne. Wprowadź swoje hasło główne, aby potwierdzić usunięcie dostawcy i wszystkich powiązanych danych." + }, + "deleteProviderName": { + "message": "Nie można usunąć $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "Musisz odłączyć wszystkich klientów zanim będziesz mógł usunąć $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Dostawca został usunięty" + }, + "providerDeletedDesc": { + "message": "Dostawca i wszystkie powiązane dane zostały usunięte." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Poprosiłeś o usunięcie tego dostawcy. Kliknij poniższy przycisk, aby to potwierdzić." + }, + "deleteProviderWarning": { + "message": "Usunięcie dostawcy jest nieodwracalne. Ta czynność nie może zostać cofnięta." + }, + "errorAssigningTargetCollection": { + "message": "Wystąpił błąd podczas przypisywania kolekcji." + }, + "errorAssigningTargetFolder": { + "message": "Wystąpił błąd podczas przypisywania folderu." + }, + "integrationsAndSdks": { + "message": "Integracje i SDK", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integracje" + }, + "integrationsDesc": { + "message": "Automatycznie synchronizuj sekrety z Menedżera Sekretnych Bitwarden do usługi innej firmy." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Użyj SDK Menedżera Sekretów Bitwarden w następujących językach programowania, aby zbudować własne aplikacje." + }, + "setUpGithubActions": { + "message": "Skonfiguruj Github Actions" + }, + "setUpGitlabCICD": { + "message": "Skonfiguruj GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Skonfiguruj Ansible" + }, + "cSharpSDKRepo": { + "message": "Zobacz repozytorium C#" + }, + "cPlusPlusSDKRepo": { + "message": "Zobacz repozytorium C++" + }, + "jsWebAssemblySDKRepo": { + "message": "Zobacz repozytorium JS WebAssembly" + }, + "javaSDKRepo": { + "message": "Zobacz repozytorium Java" + }, + "pythonSDKRepo": { + "message": "Zobacz repozytorium Pythona" + }, + "phpSDKRepo": { + "message": "Zobacz repozytorium php" + }, + "rubySDKRepo": { + "message": "Zobacz repozytorium Ruby" + }, + "goSDKRepo": { + "message": "Zobacz repozytorium Go" + }, + "createNewClientToManageAsProvider": { + "message": "Utwórz nową organizację klienta do zarządzania jako dostawca. Dodatkowe miejsca zostaną odzwierciedlone w następnym cyklu rozliczeniowym." + }, + "selectAPlan": { + "message": "Wybierz plan" + }, + "thirtyFivePercentDiscount": { + "message": "Zniżka 35%" + }, + "monthPerMember": { + "message": "miesięcznie za członka" + }, + "seats": { + "message": "Miejsca" + }, + "addOrganization": { + "message": "Dodaj organizację" + }, + "createdNewClient": { + "message": "Pomyślnie utworzono nowego klienta" + }, + "noAccess": { + "message": "Brak dostępu" + }, + "collectionAdminConsoleManaged": { + "message": "Ta kolekcja jest dostępna tylko z konsoli administracyjnej" } } diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index 084d4c95d92..4f43c2aad73 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -579,7 +579,7 @@ "message": "Acesso" }, "accessLevel": { - "message": "Access level" + "message": "Nível de acesso" }, "loggedOut": { "message": "Sessão encerrada" @@ -672,7 +672,7 @@ "message": "Criptografia não suportada" }, "enablePasskeyEncryption": { - "message": "Set up encryption" + "message": "Configurar criptografia" }, "usedForEncryption": { "message": "Usado para criptografia" @@ -4951,13 +4951,13 @@ "message": "Crie uma nova organização de cliente que será associada a você como o provedor. Você poderá acessar e gerenciar esta organização." }, "newClient": { - "message": "New client" + "message": "Novo cliente" }, "addExistingOrganization": { "message": "Adicionar Organização Existente" }, "addNewOrganization": { - "message": "Add new organization" + "message": "Adicionar nova organização" }, "myProvider": { "message": "Meu Provedor" @@ -5940,13 +5940,13 @@ } }, "launchDuoAndFollowStepsToFinishLoggingIn": { - "message": "Launch Duo and follow the steps to finish logging in." + "message": "Inicie o Duo e siga os passos para finalizar o login." }, "duoRequiredByOrgForAccount": { - "message": "Duo two-step login is required for your account." + "message": "A autenticação em duas etapas do Duo é necessária para sua conta." }, "launchDuo": { - "message": "Launch Duo" + "message": "Abrir o Duo" }, "turnOn": { "message": "Ligar" @@ -6680,10 +6680,10 @@ "message": "Reenviar código" }, "memberColumnHeader": { - "message": "Member" + "message": "Membro" }, "groupSlashMemberColumnHeader": { - "message": "Group/Member" + "message": "Grupo/Membro" }, "selectGroupsAndMembers": { "message": "Selecione grupos e membros" @@ -7450,7 +7450,7 @@ "message": "Já tem uma conta?" }, "skipToContent": { - "message": "Skip to content" + "message": "Pular para o conteúdo" }, "managePermissionRequired": { "message": "Pelo menos um membro ou grupo deve ter poder gerenciar a permissão." @@ -7502,13 +7502,13 @@ "message": "O acesso à coleção está restrito" }, "readOnlyCollectionAccess": { - "message": "You do not have access to manage this collection." + "message": "Você não tem acesso para gerenciar esta coleção." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, "grantCollectionAccessMembersOnly": { - "message": "Grant members access to this collection." + "message": "Conceder acesso a essa coleção." }, "adminCollectionAccess": { "message": "Administrators can access and manage collections." @@ -7557,57 +7557,60 @@ "message": "Esta ação é irreversível" }, "confirmCollectionEnhancementsDialogContent": { - "message": "Turning on this feature will deprecate the manager role and replace it with a Can manage permission. This will take a few moments. Do not make any organization changes until it is complete. Are you sure you want to proceed?" + "message": "Ativar este recurso irá depreciar a função de gerente e substituí-lo por uma Permissão de para Gerenciar. Isso levará algum tempo. Não faça nenhuma alteração de organização até que esteja concluída. Tem certeza que deseja continuar?" }, "sorryToSeeYouGo": { - "message": "Sorry to see you go! Help improve Bitwarden by sharing why you're canceling.", + "message": "Lamentamos vê-lo ir! Ajude a melhorar o Bitwarden compartilhando o motivo de você estar cancelando.", "description": "A message shown to users as part of an offboarding survey asking them to provide more information on their subscription cancelation." }, "selectCancellationReason": { - "message": "Select a reason for canceling", + "message": "Selecione o motivo do cancelamento", "description": "Used as a form field label for a select input on the offboarding survey." }, "anyOtherFeedback": { - "message": "Is there any other feedback you'd like to share?", + "message": "Existe algum outro comentário que você gostaria de compartilhar?", "description": "Used as a form field label for a textarea input on the offboarding survey." }, "missingFeatures": { - "message": "Missing features", + "message": "Recursos ausentes", "description": "An option for the offboarding survey shown when a user cancels their subscription." }, "movingToAnotherTool": { - "message": "Moving to another tool", + "message": "Trocando de ferramenta", "description": "An option for the offboarding survey shown when a user cancels their subscription." }, "tooDifficultToUse": { - "message": "Too difficult to use", + "message": "Muito difícil de usar", "description": "An option for the offboarding survey shown when a user cancels their subscription." }, "notUsingEnough": { - "message": "Not using enough", + "message": "Não está usando o suficiente", "description": "An option for the offboarding survey shown when a user cancels their subscription." }, "tooExpensive": { - "message": "Too expensive", + "message": "Muito caro", "description": "An option for the offboarding survey shown when a user cancels their subscription." }, "freeForOneYear": { "message": "Grátis por 1 ano" }, "newWebApp": { - "message": "Welcome to the new and improved web app. Learn more about what’s changed." + "message": "Bem-vindo ao novo e melhorado aplicativo da web. Saiba mais sobre o que mudou." }, "releaseBlog": { - "message": "Read release blog" + "message": "Ler o blog do lançamento" }, "adminConsole": { - "message": "Admin Console" + "message": "Painel de administração" }, "providerPortal": { - "message": "Provider Portal" + "message": "Portal do provedor" + }, + "success": { + "message": "Success" }, "viewCollection": { - "message": "View collection" + "message": "Ver Coleção" }, "restrictedGroupAccess": { "message": "Você não pode se adicionar aos grupos." @@ -7616,28 +7619,28 @@ "message": "Você não pode se adicionar às coleções." }, "assign": { - "message": "Assign" + "message": "Atribuir" }, "assignToCollections": { - "message": "Assign to collections" + "message": "Atribuir à coleções" }, "assignToTheseCollections": { - "message": "Assign to these collections" + "message": "Atribuir a estas coleções" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Selecione as coleções com as quais os itens serão compartilhados. Assim que um item for atualizado em uma coleção, isso será refletido em todas as coleções. Apenas membros da organização com acesso a essas coleções poderão ver os itens." }, "selectCollectionsToAssign": { - "message": "Select collections to assign" + "message": "Selecione as coleções para atribuir" }, "noCollectionsAssigned": { - "message": "No collections have been assigned" + "message": "Nenhuma coleção foi atribuída" }, "successfullyAssignedCollections": { - "message": "Successfully assigned collections" + "message": "Coleções atribuídas com sucesso" }, "bulkCollectionAssignmentWarning": { - "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "message": "Você selecionou $TOTAL_COUNT$ itens. Você não pode atualizar $READONLY_COUNT$ destes itens porque você não tem permissão para edição.", "placeholders": { "total_count": { "content": "$1", @@ -7650,55 +7653,55 @@ } }, "items": { - "message": "Items" + "message": "Itens" }, "assignedSeats": { - "message": "Assigned seats" + "message": "Lugares Atribuídos" }, "assigned": { - "message": "Assigned" + "message": "Atribuído" }, "used": { - "message": "Used" + "message": "Utilizado" }, "remaining": { - "message": "Remaining" + "message": "Restante" }, "unlinkOrganization": { - "message": "Unlink organization" + "message": "Desvincular organização" }, "manageSeats": { - "message": "MANAGE SEATS" + "message": "GERENCIAR LUGARES" }, "manageSeatsDescription": { - "message": "Adjustments to seats will be reflected in the next billing cycle." + "message": "Os ajustes nos lugares serão refletidos no próximo ciclo de faturamento." }, "unassignedSeatsDescription": { - "message": "Unassigned subscription seats" + "message": "Assinatura de assentos desvinculada" }, "purchaseSeatDescription": { - "message": "Additional seats purchased" + "message": "Assentos adicionais comprados" }, "assignedSeatCannotUpdate": { - "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + "message": "Os assentos atribuídos não podem ser atualizados. Por favor, entre em contato com o proprietário da sua organização para obter assistência." }, "subscriptionUpdateFailed": { - "message": "Subscription update failed" + "message": "Atualização da assinatura falhou" }, "trial": { - "message": "Trial", + "message": "Avaliação", "description": "A subscription status label." }, "pastDue": { - "message": "Past due", + "message": "Atrasado", "description": "A subscription status label" }, "subscriptionExpired": { - "message": "Subscription expired", + "message": "Assinatura expirada", "description": "The date header used when a subscription is past due." }, "pastDueWarningForChargeAutomatically": { - "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "message": "Você tem um período de carência de $DAYS$ dias a contar da data de expiração para manter sua assinatura. Por favor, resolva as faturas vencidas até $SUSPENSION_DATE$.", "placeholders": { "days": { "content": "$1", @@ -7712,7 +7715,7 @@ "description": "A warning shown to the user when their subscription is past due and they are charged automatically." }, "pastDueWarningForSendInvoice": { - "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "message": "Você tem um período de licença de $DAYS$, contados a partir da data de vencimento da sua primeira fatura em aberto para manter a sua assinatura. Por favor, resolva as faturas vencidas até $SUSPENSION_DATE$.", "placeholders": { "days": { "content": "$1", @@ -7726,54 +7729,54 @@ "description": "A warning shown to the user when their subscription is past due and they pay via invoice." }, "unpaidInvoice": { - "message": "Unpaid invoice", + "message": "Fatura não paga", "description": "The header of a warning box shown to a user whose subscription is unpaid." }, "toReactivateYourSubscription": { - "message": "To reactivate your subscription, please resolve the past due invoices.", + "message": "Para reativar sua assinatura, por favor resolva as faturas vencidas.", "description": "The body of a warning box shown to a user whose subscription is unpaid." }, "cancellationDate": { - "message": "Cancellation date", + "message": "Data de cancelamento", "description": "The date header used when a subscription is cancelled." }, "machineAccountsCannotCreate": { - "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + "message": "Contas de máquina não podem ser criadas em organizações suspensas. Entre em contato com o proprietário da organização para obter assistência." }, "machineAccount": { - "message": "Machine account", + "message": "Conta de máquina", "description": "A machine user which can be used to automate processes and access secrets in the system." }, "machineAccounts": { - "message": "Machine accounts", + "message": "Contas de máquina", "description": "The title for the section that deals with machine accounts." }, "newMachineAccount": { - "message": "New machine account", + "message": "Nova conta de máquina", "description": "Title for creating a new machine account." }, "machineAccountsNoItemsMessage": { - "message": "Create a new machine account to get started automating secret access.", + "message": "Crie uma nova conta de máquina para começar a automatizar o acesso secreto.", "description": "Message to encourage the user to start creating machine accounts." }, "machineAccountsNoItemsTitle": { - "message": "Nothing to show yet", + "message": "Ainda não há nada a ser exibido", "description": "Title to indicate that there are no machine accounts to display." }, "deleteMachineAccounts": { - "message": "Delete machine accounts", + "message": "Excluir contas de máquina", "description": "Title for the action to delete one or multiple machine accounts." }, "deleteMachineAccount": { - "message": "Delete machine account", + "message": "Excluir conta de máquina", "description": "Title for the action to delete a single machine account." }, "viewMachineAccount": { - "message": "View machine account", + "message": "Ver conta de máquina", "description": "Action to view the details of a machine account." }, "deleteMachineAccountDialogMessage": { - "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "message": "A exclusão da conta de máquina $MACHINE_ACCOUNT$ é permanente e irreversível.", "placeholders": { "machine_account": { "content": "$1", @@ -7782,10 +7785,10 @@ } }, "deleteMachineAccountsDialogMessage": { - "message": "Deleting machine accounts is permanent and irreversible." + "message": "Excluir contas de máquina é permanente e irreversível." }, "deleteMachineAccountsConfirmMessage": { - "message": "Delete $COUNT$ machine accounts", + "message": "Excluir $COUNT$ contas de máquina", "placeholders": { "count": { "content": "$1", @@ -7794,60 +7797,60 @@ } }, "deleteMachineAccountToast": { - "message": "Machine account deleted" + "message": "Conta de máquina excluída" }, "deleteMachineAccountsToast": { - "message": "Machine accounts deleted" + "message": "Contas de máquina excluídas" }, "searchMachineAccounts": { - "message": "Search machine accounts", + "message": "Pesquisar contas de máquina", "description": "Placeholder text for searching machine accounts." }, "editMachineAccount": { - "message": "Edit machine account", + "message": "Editar conta da máquina", "description": "Title for editing a machine account." }, "machineAccountName": { - "message": "Machine account name", + "message": "Nome da conta de máquina", "description": "Label for the name of a machine account" }, "machineAccountCreated": { - "message": "Machine account created", + "message": "Conta de máquina criada", "description": "Notifies that a new machine account has been created" }, "machineAccountUpdated": { - "message": "Machine account updated", + "message": "Conta de máquina atualizada", "description": "Notifies that a machine account has been updated" }, "projectMachineAccountsDescription": { - "message": "Grant machine accounts access to this project." + "message": "Conceder acesso a contas de máquina a este projeto." }, "projectMachineAccountsSelectHint": { - "message": "Type or select machine accounts" + "message": "Digite ou selecione contas de máquina" }, "projectEmptyMachineAccountAccessPolicies": { - "message": "Add machine accounts to grant access" + "message": "Adicione contas de máquina para conceder acesso" }, "machineAccountPeopleDescription": { - "message": "Grant groups or people access to this machine account." + "message": "Conceder acesso de grupos ou pessoas a esta conta de máquina." }, "machineAccountProjectsDescription": { - "message": "Assign projects to this machine account. " + "message": "Atribuir projetos a esta conta de máquina. " }, "createMachineAccount": { - "message": "Create a machine account" + "message": "Criar uma conta de máquina" }, "maPeopleWarningMessage": { - "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + "message": "Remover pessoas de uma conta de máquina não remove os tokens de acesso que elas criaram. Por melhores práticas de segurança, é recomendado revogar os tokens de acesso criados por pessoas que foram removidas de uma conta de máquina." }, "smAccessRemovalWarningMaTitle": { - "message": "Remove access to this machine account" + "message": "Remover acesso a esta conta de máquina" }, "smAccessRemovalWarningMaMessage": { - "message": "This action will remove your access to the machine account." + "message": "Esta ação irá remover seu acesso à conta de máquina." }, "machineAccountsIncluded": { - "message": "$COUNT$ machine accounts included", + "message": "$COUNT$ contas de serviço incluídas", "placeholders": { "count": { "content": "$1", @@ -7856,7 +7859,7 @@ } }, "additionalMachineAccountCost": { - "message": "$COST$ per month for additional machine accounts", + "message": "$COST$ mensal para contas de máquina adicionais", "placeholders": { "cost": { "content": "$1", @@ -7865,10 +7868,10 @@ } }, "additionalMachineAccounts": { - "message": "Additional machine accounts" + "message": "Contas de máquina adicionais" }, "includedMachineAccounts": { - "message": "Your plan comes with $COUNT$ machine accounts.", + "message": "Seu plano vem com $COUNT$ contas de máquina.", "placeholders": { "count": { "content": "$1", @@ -7877,7 +7880,7 @@ } }, "addAdditionalMachineAccounts": { - "message": "You can add additional machine accounts for $COST$ per month.", + "message": "Você pode adicionar contas de máquina extras por $COST$ mensais.", "placeholders": { "cost": { "content": "$1", @@ -7886,24 +7889,156 @@ } }, "limitMachineAccounts": { - "message": "Limit machine accounts (optional)" + "message": "Limitar contas de máquina (opcional)" }, "limitMachineAccountsDesc": { - "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + "message": "Defina um limite para suas contas de máquina. Quando este limite for atingido, você não poderá criar novas contas de máquina." }, "machineAccountLimit": { - "message": "Machine account limit (optional)" + "message": "Limite de conta de máquina (opcional)" }, "maxMachineAccountCost": { - "message": "Max potential machine account cost" + "message": "Custo máximo de conta de máquina" }, "machineAccountAccessUpdated": { - "message": "Machine account access updated" + "message": "Acesso a conta de máquina atualizado" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "Você não pode adicionar você mesmo a um grupo." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Aviso: Itens de organização não atribuídos não estão mais visíveis na sua tela Todos os Cofres através dos dispositivos e agora só são acessíveis por meio do Console de Administração." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Aviso: Em 16 de maio, 2024, itens da organização que não foram atribuídos não estarão mais visíveis em sua visualização de Todos os Cofres dos dispositivos e só serão acessíveis por meio do painel de administração." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Atribua estes itens a uma coleção da", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "para torná-los visíveis.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Excluir Provedor" + }, + "deleteProviderConfirmation": { + "message": "A exclusão de um provedor é permanente e irreversível. Digite sua senha mestra para confirmar a exclusão do provedor e de todos os dados associados." + }, + "deleteProviderName": { + "message": "Não é possível excluir $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "Você deve desvincular todos os clientes antes de excluir $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provedor excluído" + }, + "providerDeletedDesc": { + "message": "O provedor e todos os dados associados foram excluídos." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Você pediu para excluir este Provedor. Clique no botão abaixo para confirmar." + }, + "deleteProviderWarning": { + "message": "A exclusão do seu provedor é permanente. Não pode ser desfeita." + }, + "errorAssigningTargetCollection": { + "message": "Erro ao atribuir coleção de destino." + }, + "errorAssigningTargetFolder": { + "message": "Erro ao atribuir pasta de destino." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Crie uma nova organização de cliente para gerenciar como um Provedor. Posições adicionais serão refletidas no próximo ciclo de faturamento." + }, + "selectAPlan": { + "message": "Selecione um plano" + }, + "thirtyFivePercentDiscount": { + "message": "35% de Desconto" + }, + "monthPerMember": { + "message": "mês por membro" + }, + "seats": { + "message": "Lugares" + }, + "addOrganization": { + "message": "Adicionar Organização" + }, + "createdNewClient": { + "message": "Novo cliente criado com sucesso" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/pt_PT/messages.json b/apps/web/src/locales/pt_PT/messages.json index cf1ad10d9bd..c2f2fbeee37 100644 --- a/apps/web/src/locales/pt_PT/messages.json +++ b/apps/web/src/locales/pt_PT/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Portal do fornecedor" }, + "success": { + "message": "Com sucesso" + }, "viewCollection": { "message": "Ver coleção" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Acesso à conta automática atualizado" }, - "unassignedItemsBanner": { - "message": "Aviso: Os itens da organização não atribuídos já não são visíveis na sua vista Todos os cofres em todos os dispositivos e agora só estão acessíveis através da Consola de administração. Atribua estes itens a uma coleção a partir da Consola de administração para os tornar visíveis." + "restrictedGroupAccessDesc": { + "message": "Não se pode adicionar a si próprio a um grupo." }, "unassignedItemsBannerSelfHost": { "message": "Aviso: A 2 de maio de 2024, os itens da organização não atribuídos deixarão de estar visíveis na vista Todos os cofres em todos os dispositivos e só estarão acessíveis através da Consola de administração. Atribua estes itens a uma coleção a partir da Consola de administração para os tornar visíveis." + }, + "unassignedItemsBannerNotice": { + "message": "Aviso: Os itens da organização não atribuídos já não são visíveis na vista Todos os cofres em todos os dispositivos e agora só estão acessíveis através da Consola de administração." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Aviso: A 16 de maio de 2024, os itens da organização não atribuídos deixarão de estar visíveis na vista Todos os cofres em todos os dispositivos e só estarão acessíveis através da Consola de administração." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Atribua estes itens a uma coleção a partir da", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "para os tornar visíveis.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Eliminar fornecedor" + }, + "deleteProviderConfirmation": { + "message": "A eliminação de um fornecedor é permanente e irreversível. Introduza a sua palavra-passe mestra para confirmar a eliminação do fornecedor e de todos os dados associados." + }, + "deleteProviderName": { + "message": "Não é possível eliminar $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "É necessário desvincular todos os clientes antes de poder eliminar $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Fornecedor eliminado" + }, + "providerDeletedDesc": { + "message": "O fornecedor e todos os dados associados foram eliminados." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Pediu para eliminar este fornecedor. Clique no botão abaixo para confirmar." + }, + "deleteProviderWarning": { + "message": "A eliminação do seu fornecedor é permanente. Não pode ser anulada." + }, + "errorAssigningTargetCollection": { + "message": "Erro ao atribuir a coleção de destino." + }, + "errorAssigningTargetFolder": { + "message": "Erro ao atribuir a pasta de destino." + }, + "integrationsAndSdks": { + "message": "Integrações e SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrações" + }, + "integrationsDesc": { + "message": "Sincronize automaticamente segredos do Gestor de Segredos do Bitwarden para um serviço de terceiros." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Utilize o SDK do Gestor de Segredos do Bitwarden nas seguintes linguagens de programação para criar as suas próprias aplicações." + }, + "setUpGithubActions": { + "message": "Configurar ações do Github" + }, + "setUpGitlabCICD": { + "message": "Configurar o GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Configurar o Ansible" + }, + "cSharpSDKRepo": { + "message": "Ver repositório de C#" + }, + "cPlusPlusSDKRepo": { + "message": "Ver repositório de C++" + }, + "jsWebAssemblySDKRepo": { + "message": "Ver repositório de JS WebAssembly" + }, + "javaSDKRepo": { + "message": "Ver repositório de Java" + }, + "pythonSDKRepo": { + "message": "Ver repositório de Python" + }, + "phpSDKRepo": { + "message": "Ver repositório de php" + }, + "rubySDKRepo": { + "message": "Ver repositório de Ruby" + }, + "goSDKRepo": { + "message": "Ver repositório de Go" + }, + "createNewClientToManageAsProvider": { + "message": "Crie uma nova organização de clientes para gerir como Fornecedor. Os lugares adicionais serão refletidos na próxima faturação." + }, + "selectAPlan": { + "message": "Selecionar um plano" + }, + "thirtyFivePercentDiscount": { + "message": "Desconto de 35%" + }, + "monthPerMember": { + "message": "mês por membro" + }, + "seats": { + "message": "Lugares" + }, + "addOrganization": { + "message": "Adicionar organização" + }, + "createdNewClient": { + "message": "Novo cliente criado com sucesso" + }, + "noAccess": { + "message": "Sem acesso" + }, + "collectionAdminConsoleManaged": { + "message": "Esta coleção só é acessível a partir da Consola de administração" } } diff --git a/apps/web/src/locales/ro/messages.json b/apps/web/src/locales/ro/messages.json index ba5badd908f..1a47866ebb7 100644 --- a/apps/web/src/locales/ro/messages.json +++ b/apps/web/src/locales/ro/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index eeb89acf558..85ba7d57852 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Портал провайдера" }, + "success": { + "message": "Успешно" + }, "viewCollection": { "message": "Посмотреть коллекцию" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Доступ аккаунта компьютера обновлен" }, - "unassignedItemsBanner": { - "message": "Обратите внимание: неприсвоенные элементы организации больше не видны в представлении \"Все хранилища\" на всех устройствах и теперь доступны только через консоль администратора. Назначьте эти элементы коллекции в консоли администратора, чтобы сделать их видимыми." + "restrictedGroupAccessDesc": { + "message": "Нельзя добавить самого себя в группу." }, "unassignedItemsBannerSelfHost": { "message": "Уведомление: 2 мая 2024 года неприсвоенные элементы организации больше не будут отображаться в представлении \"Все хранилища\" на всех устройствах и будут доступны только через консоль администратора. Назначьте эти элементы коллекции в консоли администратора, чтобы сделать их видимыми." + }, + "unassignedItemsBannerNotice": { + "message": "Уведомление: Неприсвоенные элементы организации больше не отображаются в представлении \"Все хранилища\" на всех устройствах и теперь доступны только через консоль администратора." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Уведомление: с 16 мая 2024 года неназначенные элементы организации больше не будут отображаться в представлении \"Все хранилища\" на всех устройствах и будут доступны только через консоль администратора." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Назначьте эти элементы в коллекцию из", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "чтобы сделать их видимыми.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Удалить провайдера" + }, + "deleteProviderConfirmation": { + "message": "Удаление провайдера является постоянным и необратимым. Введите свой мастер-пароль, чтобы подтвердить удаление провайдера и всех связанных с ним данных." + }, + "deleteProviderName": { + "message": "Невозможно удалить $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "Перед удалением $ID$ необходимо отвязать всех клиентов", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Провайдер удален" + }, + "providerDeletedDesc": { + "message": "Провайдер и все связанные с ним данные были удалены." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Вы запросили удаление этого провайдера. Воспользуйтесь кнопкой ниже для подтверждения." + }, + "deleteProviderWarning": { + "message": "Удаление вашего провайдера необратимо. Это нельзя отменить." + }, + "errorAssigningTargetCollection": { + "message": "Ошибка при назначении целевой коллекции." + }, + "errorAssigningTargetFolder": { + "message": "Ошибка при назначении целевой папки." + }, + "integrationsAndSdks": { + "message": "Интеграции и SDK", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Интеграции" + }, + "integrationsDesc": { + "message": "Автоматическая синхронизация секретов из Bitwarden Secrets Manager со сторонним сервисом." + }, + "sdks": { + "message": "SDK" + }, + "sdksDesc": { + "message": "Используйте SDK Bitwarden Secrets Manager на следующих языках программирования для создания собственных приложений." + }, + "setUpGithubActions": { + "message": "Настроить Github Actions" + }, + "setUpGitlabCICD": { + "message": "Настроить GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Настроить Ansible" + }, + "cSharpSDKRepo": { + "message": "Просмотр репозитория C#" + }, + "cPlusPlusSDKRepo": { + "message": "Просмотр репозитория C++" + }, + "jsWebAssemblySDKRepo": { + "message": "Просмотр репозитория JS WebAssembly" + }, + "javaSDKRepo": { + "message": "Просмотр репозитория Java" + }, + "pythonSDKRepo": { + "message": "Просмотр репозитория Python" + }, + "phpSDKRepo": { + "message": "Просмотр репозитория php" + }, + "rubySDKRepo": { + "message": "Просмотр репозитория Ruby" + }, + "goSDKRepo": { + "message": "Просмотр репозитория Go" + }, + "createNewClientToManageAsProvider": { + "message": "Создайте новую клиентскую организацию для управления ею в качестве провайдера. Дополнительные места будут отражены в следующем биллинговом цикле." + }, + "selectAPlan": { + "message": "Выберите план" + }, + "thirtyFivePercentDiscount": { + "message": "Скидка 35%" + }, + "monthPerMember": { + "message": "в месяц за пользователя" + }, + "seats": { + "message": "Места" + }, + "addOrganization": { + "message": "Добавить организацию" + }, + "createdNewClient": { + "message": "Новый клиент успешно создан" + }, + "noAccess": { + "message": "Нет доступа" + }, + "collectionAdminConsoleManaged": { + "message": "Эта коллекция доступна только из консоли администратора" } } diff --git a/apps/web/src/locales/si/messages.json b/apps/web/src/locales/si/messages.json index 0faa92c08da..e2c1e424272 100644 --- a/apps/web/src/locales/si/messages.json +++ b/apps/web/src/locales/si/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index 67f5abdc7d1..234a4670b95 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Portál poskytovateľa" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "Pozrieť zbierku" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Upozornenie: Nepriradené položky organizácie už nie sú viditeľné v zobrazení Všetky trezory a sú prístupné iba cez Správcovskú konzolu." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Upozornenie: 16. mája 2024 nepriradené položky organizácie už nebudú viditeľné v zobrazení Všetky trezory a budú prístupné iba cez Správcovskú konzolu." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Priradiť tieto položky do zbierky zo", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": ", aby boli viditeľné.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Chyba pri priraďovaní cieľovej kolekcie." + }, + "errorAssigningTargetFolder": { + "message": "Chyba pri priraďovaní cieľového priečinka." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/sl/messages.json b/apps/web/src/locales/sl/messages.json index 6a55868df21..c209ef05bd6 100644 --- a/apps/web/src/locales/sl/messages.json +++ b/apps/web/src/locales/sl/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/sr/messages.json b/apps/web/src/locales/sr/messages.json index 9683c587b77..a59b6a88550 100644 --- a/apps/web/src/locales/sr/messages.json +++ b/apps/web/src/locales/sr/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Портал провајдера" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "Преглед колекције" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Приступ налога машине ажуриран" }, - "unassignedItemsBanner": { - "message": "Напомена: Недодељене ставке организације више нису видљиве у вашем приказу Сви сефови на свим уређајима и сада су доступне само преко Админ конзоле. Доделите ове ставке колекцији са Админ конзолом да бисте их учинили видљивим." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { - "message": "Обавештење: 2. маја 2024. недодељене ставке организације више неће бити видљиве у вашем приказу Сви сефови на свим уређајима и биће им доступне само преко Админ конзоле. Доделите ове ставке колекцији са Админ конзолом да бисте их учинили видљивим." + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/sr_CS/messages.json b/apps/web/src/locales/sr_CS/messages.json index 15770437ce2..1ff89501f3e 100644 --- a/apps/web/src/locales/sr_CS/messages.json +++ b/apps/web/src/locales/sr_CS/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index 57321d33764..389f1682a85 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "Visa samling" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "Du kan inte lägga till dig själv i en grupp." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Radera leverantör" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/te/messages.json b/apps/web/src/locales/te/messages.json index 781c9000cb3..ed04c3a3ef3 100644 --- a/apps/web/src/locales/te/messages.json +++ b/apps/web/src/locales/te/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/th/messages.json b/apps/web/src/locales/th/messages.json index 1ecd45a9e4a..ec114aad906 100644 --- a/apps/web/src/locales/th/messages.json +++ b/apps/web/src/locales/th/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/tr/messages.json b/apps/web/src/locales/tr/messages.json index 65ef93eef7d..9177321cb5f 100644 --- a/apps/web/src/locales/tr/messages.json +++ b/apps/web/src/locales/tr/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Sağlayıcı Portalı" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index 1baaa5c67b1..ecb31b6da01 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -7601,11 +7601,14 @@ "message": "Читати блог випусків" }, "adminConsole": { - "message": "Консоль адміністратора" + "message": "консолі адміністратора," }, "providerPortal": { "message": "Портал провайдера" }, + "success": { + "message": "Успішно" + }, "viewCollection": { "message": "Переглянути збірку" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Доступ до машинного облікового запису оновлено" }, - "unassignedItemsBanner": { - "message": "Увага: непризначені елементи організації більше не видимі у поданні \"Усі сховища\" на різних пристроях і тепер доступні лише в консолі адміністратора. Щоб зробити їх видимими, призначте ці елементи збірці в консолі адміністратора." + "restrictedGroupAccessDesc": { + "message": "Ви не можете додати себе до групи." }, "unassignedItemsBannerSelfHost": { "message": "Сповіщення: 2 травня 2024 року, непризначені елементи організації більше не будуть видимі на ваших пристроях у поданні \"Усі сховища\", і будуть доступні лише через консоль адміністратора. Щоб зробити їх видимими, призначте ці елементи збірці в консолі адміністратора." + }, + "unassignedItemsBannerNotice": { + "message": "Примітка: непризначені елементи організації більше не видимі на ваших пристроях у поданні \"Усі сховища\", і тепер доступні лише через консоль адміністратора." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Примітка: 16 травня 2024 року непризначені елементи організації більше не будуть видимі на ваших пристроях у поданні \"Усі сховища\", і будуть доступні лише через консоль адміністратора." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Призначте ці елементи збірці в", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "щоб зробити їх видимими.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Видалити провайдера" + }, + "deleteProviderConfirmation": { + "message": "Видалення провайдера є остаточною і незворотною дією. Введіть свій головний пароль, щоб підтвердити видалення провайдера і всі пов'язані з ним дані." + }, + "deleteProviderName": { + "message": "Неможливо видалити $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "Перш ніж видалити $ID$, ви повинні від'єднати всіх клієнтів", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Провайдера видалено" + }, + "providerDeletedDesc": { + "message": "Провайдера і всі пов'язані дані було видалено." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Ви відправили запит на видалення цього провайдера. Натисніть кнопку внизу для підтвердження." + }, + "deleteProviderWarning": { + "message": "Видалення провайдера є незворотною дією. Це не можна буде скасувати." + }, + "errorAssigningTargetCollection": { + "message": "Помилка призначення цільової збірки." + }, + "errorAssigningTargetFolder": { + "message": "Помилка призначення цільової теки." + }, + "integrationsAndSdks": { + "message": "Інтеграції та SDK", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Інтеграції" + }, + "integrationsDesc": { + "message": "Автоматична синхронізація секретів між менеджером секретів Bitwarden і стороннім сервісом." + }, + "sdks": { + "message": "SDK" + }, + "sdksDesc": { + "message": "Використовуйте SDK менеджера секретів Bitwarden із зазначеними мовами програмування для створення власних програм." + }, + "setUpGithubActions": { + "message": "Налаштувати дії для Github" + }, + "setUpGitlabCICD": { + "message": "Налаштувати GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Налаштувати Ansible" + }, + "cSharpSDKRepo": { + "message": "Перегляд репозиторію C#" + }, + "cPlusPlusSDKRepo": { + "message": "Перегляд репозиторію C++" + }, + "jsWebAssemblySDKRepo": { + "message": "Перегляд репозиторію JS WebAssembly" + }, + "javaSDKRepo": { + "message": "Перегляд репозиторію Java" + }, + "pythonSDKRepo": { + "message": "Перегляд репозиторію Python" + }, + "phpSDKRepo": { + "message": "Перегляд репозиторію php" + }, + "rubySDKRepo": { + "message": "Перегляд репозиторію Ruby" + }, + "goSDKRepo": { + "message": "Перегляд репозиторію Go" + }, + "createNewClientToManageAsProvider": { + "message": "Створіть нову організацію клієнта, щоб керувати нею як провайдер. Додаткові місця будуть відображені в наступному платіжному циклі." + }, + "selectAPlan": { + "message": "Оберіть тарифний план" + }, + "thirtyFivePercentDiscount": { + "message": "Знижка 35%" + }, + "monthPerMember": { + "message": "на місяць за учасника" + }, + "seats": { + "message": "Місця" + }, + "addOrganization": { + "message": "Додати організацію" + }, + "createdNewClient": { + "message": "Нового клієнта успішно створено" + }, + "noAccess": { + "message": "Немає доступу" + }, + "collectionAdminConsoleManaged": { + "message": "Ця збірка доступна тільки з консолі адміністратора" } } diff --git a/apps/web/src/locales/vi/messages.json b/apps/web/src/locales/vi/messages.json index ffbd72ce9da..0696e226e01 100644 --- a/apps/web/src/locales/vi/messages.json +++ b/apps/web/src/locales/vi/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index e584785a5a3..f6093d9bc77 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "提供商门户" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "查看集合" }, @@ -7698,7 +7701,7 @@ "description": "The date header used when a subscription is past due." }, "pastDueWarningForChargeAutomatically": { - "message": "从您的订阅到期之日起,您有 $DAYS$ 天的宽限期来保留您的订购。请在 $SUSPENSION_DATE$ 之前处理逾期未支付的账单。", + "message": "从您的订阅到期之日起,您有 $DAYS$ 天的宽限期来保留您的订阅。请在 $SUSPENSION_DATE$ 之前处理逾期未支付的账单。", "placeholders": { "days": { "content": "$1", @@ -7712,7 +7715,7 @@ "description": "A warning shown to the user when their subscription is past due and they are charged automatically." }, "pastDueWarningForSendInvoice": { - "message": "从第一笔未支付的账单到期之日起,您有 $DAYS$ 天的宽限期来保留您的订购。请在 $SUSPENSION_DATE$ 之前处理逾期未支付的账单。", + "message": "从第一笔未支付的账单到期之日起,您有 $DAYS$ 天的宽限期来保留您的订阅。请在 $SUSPENSION_DATE$ 之前处理逾期未支付的账单。", "placeholders": { "days": { "content": "$1", @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "机器账户访问权限已更新" }, - "unassignedItemsBanner": { - "message": "注意:未分配的组织项目在您所有设备的「所有密码库」视图中不再可见,现在只能通过管理控制台访问。通过管理控制台将这些项目分配给集合以使其可见。" + "restrictedGroupAccessDesc": { + "message": "您不能将自己添加到群组。" }, "unassignedItemsBannerSelfHost": { - "message": "注意:从 2024 年 5 月 2 日起,未分配的组织项目在您所有设备的「所有密码库」视图中将不再可见,只能通过管理控制台访问。通过管理控制台将这些项目分配给集合以使其可见。" + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "删除提供商" + }, + "deleteProviderConfirmation": { + "message": "删除提供商是永久性操作,无法撤销!输入您的主密码以确认删除提供商及所有关联的数据。" + }, + "deleteProviderName": { + "message": "无法删除 $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "提供商已删除" + }, + "providerDeletedDesc": { + "message": "提供商和所有关联数据已被删除。" + }, + "deleteProviderRecoverConfirmDesc": { + "message": "您已请求删除此提供商。请使用下面的按钮确认。" + }, + "deleteProviderWarning": { + "message": "删除您的提供商是永久性操作,无法撤销!" + }, + "errorAssigningTargetCollection": { + "message": "分配目标集合时出错。" + }, + "errorAssigningTargetFolder": { + "message": "分配目标文件夹时出错。" + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/zh_TW/messages.json b/apps/web/src/locales/zh_TW/messages.json index 89bb26cacd3..1bc56d2157d 100644 --- a/apps/web/src/locales/zh_TW/messages.json +++ b/apps/web/src/locales/zh_TW/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } From 2916fc7404c18a6ca3914b1546c63d0b6859c8b4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 19 Apr 2024 10:42:33 +0000 Subject: [PATCH 007/110] Autosync the updated translations (#8825) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/af/messages.json | 22 ++++++- apps/desktop/src/locales/ar/messages.json | 22 ++++++- apps/desktop/src/locales/az/messages.json | 66 +++++++++++++------- apps/desktop/src/locales/be/messages.json | 22 ++++++- apps/desktop/src/locales/bg/messages.json | 22 ++++++- apps/desktop/src/locales/bn/messages.json | 22 ++++++- apps/desktop/src/locales/bs/messages.json | 22 ++++++- apps/desktop/src/locales/ca/messages.json | 22 ++++++- apps/desktop/src/locales/cs/messages.json | 22 ++++++- apps/desktop/src/locales/cy/messages.json | 22 ++++++- apps/desktop/src/locales/da/messages.json | 22 ++++++- apps/desktop/src/locales/de/messages.json | 22 ++++++- apps/desktop/src/locales/el/messages.json | 22 ++++++- apps/desktop/src/locales/en_GB/messages.json | 22 ++++++- apps/desktop/src/locales/en_IN/messages.json | 22 ++++++- apps/desktop/src/locales/eo/messages.json | 22 ++++++- apps/desktop/src/locales/es/messages.json | 22 ++++++- apps/desktop/src/locales/et/messages.json | 22 ++++++- apps/desktop/src/locales/eu/messages.json | 22 ++++++- apps/desktop/src/locales/fa/messages.json | 22 ++++++- apps/desktop/src/locales/fi/messages.json | 22 ++++++- apps/desktop/src/locales/fil/messages.json | 22 ++++++- apps/desktop/src/locales/fr/messages.json | 22 ++++++- apps/desktop/src/locales/gl/messages.json | 22 ++++++- apps/desktop/src/locales/he/messages.json | 22 ++++++- apps/desktop/src/locales/hi/messages.json | 22 ++++++- apps/desktop/src/locales/hr/messages.json | 22 ++++++- apps/desktop/src/locales/hu/messages.json | 22 ++++++- apps/desktop/src/locales/id/messages.json | 22 ++++++- apps/desktop/src/locales/it/messages.json | 22 ++++++- apps/desktop/src/locales/ja/messages.json | 22 ++++++- apps/desktop/src/locales/ka/messages.json | 22 ++++++- apps/desktop/src/locales/km/messages.json | 22 ++++++- apps/desktop/src/locales/kn/messages.json | 22 ++++++- apps/desktop/src/locales/ko/messages.json | 22 ++++++- apps/desktop/src/locales/lt/messages.json | 22 ++++++- apps/desktop/src/locales/lv/messages.json | 22 ++++++- apps/desktop/src/locales/me/messages.json | 22 ++++++- apps/desktop/src/locales/ml/messages.json | 22 ++++++- apps/desktop/src/locales/mr/messages.json | 22 ++++++- apps/desktop/src/locales/my/messages.json | 22 ++++++- apps/desktop/src/locales/nb/messages.json | 22 ++++++- apps/desktop/src/locales/ne/messages.json | 22 ++++++- apps/desktop/src/locales/nl/messages.json | 22 ++++++- apps/desktop/src/locales/nn/messages.json | 22 ++++++- apps/desktop/src/locales/or/messages.json | 22 ++++++- apps/desktop/src/locales/pl/messages.json | 22 ++++++- apps/desktop/src/locales/pt_BR/messages.json | 22 ++++++- apps/desktop/src/locales/pt_PT/messages.json | 22 ++++++- apps/desktop/src/locales/ro/messages.json | 22 ++++++- apps/desktop/src/locales/ru/messages.json | 22 ++++++- apps/desktop/src/locales/si/messages.json | 22 ++++++- apps/desktop/src/locales/sk/messages.json | 24 ++++++- apps/desktop/src/locales/sl/messages.json | 22 ++++++- apps/desktop/src/locales/sr/messages.json | 22 ++++++- apps/desktop/src/locales/sv/messages.json | 22 ++++++- apps/desktop/src/locales/te/messages.json | 22 ++++++- apps/desktop/src/locales/th/messages.json | 22 ++++++- apps/desktop/src/locales/tr/messages.json | 22 ++++++- apps/desktop/src/locales/uk/messages.json | 22 ++++++- apps/desktop/src/locales/vi/messages.json | 22 ++++++- apps/desktop/src/locales/zh_CN/messages.json | 22 ++++++- apps/desktop/src/locales/zh_TW/messages.json | 22 ++++++- 63 files changed, 1283 insertions(+), 149 deletions(-) diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json index ee32c045c99..97067b788a5 100644 --- a/apps/desktop/src/locales/af/messages.json +++ b/apps/desktop/src/locales/af/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Verander Hoofwagwoord" }, - "changeMasterPasswordConfirmation": { - "message": "U kan u hoofwagwoord op die bitwarden.com-webkluis verander. Wil u die webwerf nou besoek?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Vingerafdrukfrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Blaaierintegrasie word nie ondersteun nie" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Ongelukkig word blaaierintegrasie tans slegs in die weergawe vir die Mac-toepwinkel ondersteun." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json index 104a9f77803..2d25269fffe 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "تغيير كلمة المرور الرئيسية" }, - "changeMasterPasswordConfirmation": { - "message": "يمكنك تغيير كلمة المرور الرئيسية من خزنة الويب في bitwarden.com. هل تريد زيارة الموقع الآن؟" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "عبارة بصمة الإصبع", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "تكامل المتصفح غير مدعوم" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "للأسف، لا يتم دعم تكامل المتصفح إلا في إصدار متجر تطبيقات ماك في الوقت الحالي." }, @@ -2688,6 +2697,9 @@ "message": "تنسيقات مشتركة", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index f404c7f95ac..1ecd18eee75 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -717,7 +717,7 @@ "message": "Bildiriş server URL-si" }, "iconsUrl": { - "message": "Nişan server URL-si" + "message": "İkon server URL-si" }, "environmentSaved": { "message": "Mühit URL-ləri saxlanıldı." @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Ana parolu dəyişdir" }, - "changeMasterPasswordConfirmation": { - "message": "Ana parolunuzu bitwarden.com veb anbarında dəyişdirə bilərsiniz. İndi saytı ziyarət etmək istəyirsiniz?" + "continueToWebApp": { + "message": "Veb tətbiqlə davam edilsin?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Ana parolunuzu Bitwarden veb tətbiqində dəyişdirə bilərsiniz." }, "fingerprintPhrase": { "message": "Barmaq izi ifadəsi", @@ -920,52 +923,52 @@ "description": "Clipboard is the operating system thing where you copy/paste data to on your device." }, "enableFavicon": { - "message": "Veb sayt nişanlarını göstər" + "message": "Veb sayt ikonlarını göstər" }, "faviconDesc": { "message": "Hər girişin yanında tanına bilən bir təsvir göstər." }, "enableMinToTray": { - "message": "Bildiriş nişanına kiçildin" + "message": "Bildiriş sahəsi ikonuna kiçilt" }, "enableMinToTrayDesc": { - "message": "Pəncərə kiçildiləndə, bildiriş sahəsində bir nişan göstər." + "message": "Pəncərə kiçildiləndə, bunun əvəzinə bildiriş sahəsində bir ikon göstər." }, "enableMinToMenuBar": { - "message": "Menyu sətrinə kiçilt" + "message": "Menyu çubuğuna kiçilt" }, "enableMinToMenuBarDesc": { - "message": "Pəncərəni kiçildəndə, menyu sətrində bir nişan göstər." + "message": "Pəncərəni kiçildəndə, bunun əvəzinə menyu çubuğunda bir ikon göstər." }, "enableCloseToTray": { - "message": "Bildiriş nişanına bağla" + "message": "Bildiriş ikonu üçün bağla" }, "enableCloseToTrayDesc": { - "message": "Pəncərə bağlananda, bildiriş sahəsində bir nişan göstər." + "message": "Pəncərə bağladılanda, bunun əvəzinə bildiriş sahəsində bir ikon göstər." }, "enableCloseToMenuBar": { - "message": "Menyu sətrini bağla" + "message": "Menyu çubuğunda bağla" }, "enableCloseToMenuBarDesc": { - "message": "Pəncərəni bağlananda, menyu sətrində bir nişan göstər." + "message": "Pəncərəni bağladılanda, bunun əvəzinə menyu çubuğunda bir ikon göstər." }, "enableTray": { - "message": "Bildiriş sahəsi nişanını fəallaşdır" + "message": "Bildiriş sahəsi ikonunu göstər" }, "enableTrayDesc": { - "message": "Bildiriş sahəsində həmişə bir nişan göstər." + "message": "Bildiriş sahəsində həmişə bir ikon göstər." }, "startToTray": { - "message": "Bildiriş sahəsi nişanı kimi başlat" + "message": "Bildiriş sahəsi ikonu kimi başlat" }, "startToTrayDesc": { - "message": "Tətbiq ilk başladılanda, yalnız bildiriş sahəsi nişanı görünsün." + "message": "Tətbiq ilk başladılanda, sistem bildiriş sahəsində yalnız ikon olaraq görünsün." }, "startToMenuBar": { - "message": "Menyu sətrini başlat" + "message": "Menyu çubuğunda başlat" }, "startToMenuBarDesc": { - "message": "Tətbiq ilk başladılanda, menyu sətri sadəcə nişan kimi görünsün." + "message": "Tətbiq ilk başladılanda, menyu çubuğunda yalnız ikon olaraq görünsün." }, "openAtLogin": { "message": "Giriş ediləndə avtomatik başlat" @@ -977,7 +980,7 @@ "message": "\"Dock\"da həmişə göstər" }, "alwaysShowDockDesc": { - "message": "Menyu sətrinə kiçildiləndə belə Bitwarden nişanını \"Dock\"da göstər." + "message": "Menyu çubuğuna kiçildiləndə belə Bitwarden ikonunu Yuvada göstər." }, "confirmTrayTitle": { "message": "Bildiriş sahəsi nişanını ləğv et" @@ -1450,16 +1453,16 @@ "message": "Hesabınız bağlandı və bütün əlaqəli datalar silindi." }, "preferences": { - "message": "Tercihlər" + "message": "Tərcihlər" }, "enableMenuBar": { - "message": "Menyu sətri nişanını fəallaşdır" + "message": "Menyu çubuğu ikonunu göstər" }, "enableMenuBarDesc": { - "message": "Menyu sətrində həmişə bir nişan göstər." + "message": "Menyu çubuğunda həmişə bir ikon göstər." }, "hideToMenuBar": { - "message": "Menyu sətrini gizlət" + "message": "Menyu çubuğunda gizlət" }, "selectOneCollection": { "message": "Ən azı bir kolleksiya seçməlisiniz." @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Brauzer inteqrasiyası dəstəklənmir" }, + "browserIntegrationErrorTitle": { + "message": "Brauzer inteqrasiyasını fəallaşdırma xətası" + }, + "browserIntegrationErrorDesc": { + "message": "Brauzer inteqrasiyasını fəallaşdırarkən bir xəta baş verdi." + }, "browserIntegrationMasOnlyDesc": { "message": "Təəssüf ki, brauzer inteqrasiyası indilik yalnız Mac App Store versiyasında dəstəklənir." }, @@ -2021,7 +2030,7 @@ "message": "Eyni vaxtda 5-dən çox hesaba giriş edilə bilməz." }, "accountPreferences": { - "message": "Tercihlər" + "message": "Tərcihlər" }, "appPreferences": { "message": "Tətbiq ayarları (bütün hesablar)" @@ -2688,6 +2697,9 @@ "message": "Ortaq formatlar", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Uğurlu" + }, "troubleshooting": { "message": "Problemlərin aradan qaldırılması" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Parol silindi" + }, + "errorAssigningTargetCollection": { + "message": "Hədəf kolleksiyaya təyin etmə xətası." + }, + "errorAssigningTargetFolder": { + "message": "Hədəf qovluğa təyin etmə xətası." } } diff --git a/apps/desktop/src/locales/be/messages.json b/apps/desktop/src/locales/be/messages.json index 2bc33f2b281..e0133e5a742 100644 --- a/apps/desktop/src/locales/be/messages.json +++ b/apps/desktop/src/locales/be/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Змяніць асноўны пароль" }, - "changeMasterPasswordConfirmation": { - "message": "Вы можаце змяніць свой асноўны пароль у вэб-сховішчы на bitwarden.com. Перайсці на вэб-сайт зараз?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Фраза адбітка пальца", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Інтэграцыя з браўзерам не падтрымліваецца" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "На жаль, інтэграцыя з браўзерам зараз падтрымліваецца толькі ў версіі для Mac App Store." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index 9f6d5bdd364..20f47d4bcd8 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Промяна на главната парола" }, - "changeMasterPasswordConfirmation": { - "message": "Главната парола на трезор може да се промени чрез сайта bitwarden.com. Искате ли да го посетите?" + "continueToWebApp": { + "message": "Продължаване към уеб приложението?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Може да промените главната си парола в уеб приложението на Битуорден." }, "fingerprintPhrase": { "message": "Уникална фраза", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Интеграцията с браузър не се поддържа" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "За жалост в момента интеграцията с браузър не се поддържа във версията за магазина на Mac." }, @@ -2688,6 +2697,9 @@ "message": "Често използвани формати", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Отстраняване на проблеми" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Секретният ключ е премахнат" + }, + "errorAssigningTargetCollection": { + "message": "Грешка при задаването на целева колекция." + }, + "errorAssigningTargetFolder": { + "message": "Грешка при задаването на целева папка." } } diff --git a/apps/desktop/src/locales/bn/messages.json b/apps/desktop/src/locales/bn/messages.json index 22893aadab8..626734ebffc 100644 --- a/apps/desktop/src/locales/bn/messages.json +++ b/apps/desktop/src/locales/bn/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "মূল পাসওয়ার্ড পরিবর্তন" }, - "changeMasterPasswordConfirmation": { - "message": "আপনি bitwarden.com ওয়েব ভল্ট থেকে মূল পাসওয়ার্ডটি পরিবর্তন করতে পারেন। আপনি কি এখনই ওয়েবসাইটটি দেখতে চান?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "ফিঙ্গারপ্রিন্ট ফ্রেজ", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/bs/messages.json b/apps/desktop/src/locales/bs/messages.json index 6adb5bbce33..9d5685cca90 100644 --- a/apps/desktop/src/locales/bs/messages.json +++ b/apps/desktop/src/locales/bs/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Promijenite glavnu lozinku" }, - "changeMasterPasswordConfirmation": { - "message": "Možete da promjenite svoju glavnu lozinku na bitwarden.com web trezoru. Da li želite da posjetite web stranicu sada?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Jedinstvena fraza", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Nažalost, za sada je integracija sa preglednikom podržana samo u Mac App Store verziji aplikacije." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index c76111b53c7..d8c0f32948b 100644 --- a/apps/desktop/src/locales/ca/messages.json +++ b/apps/desktop/src/locales/ca/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Canvia la contrasenya mestra" }, - "changeMasterPasswordConfirmation": { - "message": "Podeu canviar la contrasenya mestra a la caixa forta web de bitwarden.com. Voleu visitar el lloc web ara?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Frase d'empremta digital", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "La integració en el navegador no és compatible" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Malauradament, la integració del navegador només és compatible amb la versió de Mac App Store." }, @@ -2688,6 +2697,9 @@ "message": "Formats comuns", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Resolució de problemes" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Clau de pas suprimida" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index e7ba56e81ce..e68fe8fffc4 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Změnit hlavní heslo" }, - "changeMasterPasswordConfirmation": { - "message": "Hlavní heslo si můžete změnit na webové stránce bitwarden.com. Chcete tuto stránku nyní otevřít?" + "continueToWebApp": { + "message": "Pokračovat do webové aplikace?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Hlavní heslo můžete změnit ve webové aplikaci Bitwardenu." }, "fingerprintPhrase": { "message": "Fráze otisku prstu", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Integrace prohlížeče není podporována" }, + "browserIntegrationErrorTitle": { + "message": "Chyba při povolování integrace prohlížeče" + }, + "browserIntegrationErrorDesc": { + "message": "Vyskytla se chyba při povolování integrace prohlížeče." + }, "browserIntegrationMasOnlyDesc": { "message": "Integrace prohlížeče je podporována jen ve verzi pro Mac App Store." }, @@ -2688,6 +2697,9 @@ "message": "Společné formáty", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Úspěch" + }, "troubleshooting": { "message": "Řešení problémů" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Přístupový klíč byl odebrán" + }, + "errorAssigningTargetCollection": { + "message": "Chyba při přiřazování cílové kolekce." + }, + "errorAssigningTargetFolder": { + "message": "Chyba při přiřazování cílové složky." } } diff --git a/apps/desktop/src/locales/cy/messages.json b/apps/desktop/src/locales/cy/messages.json index e87b805d0bc..62f2e608bbf 100644 --- a/apps/desktop/src/locales/cy/messages.json +++ b/apps/desktop/src/locales/cy/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index 1f994cf8eb9..0e578a6f66e 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Skift hovedadgangskode" }, - "changeMasterPasswordConfirmation": { - "message": "Man kan ændre sin hovedadgangskode via bitwarden.com web-boksen. Besøg webstedet nu?" + "continueToWebApp": { + "message": "Fortsæt til web-app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Hovedadgangskoden kan ændres via Bitwarden web-appen." }, "fingerprintPhrase": { "message": "Fingeraftrykssætning", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browserintegration understøttes ikke" }, + "browserIntegrationErrorTitle": { + "message": "Fejl ved aktivering af webbrowserintegration" + }, + "browserIntegrationErrorDesc": { + "message": "En fejl opstod under aktivering af webbrowserintegration." + }, "browserIntegrationMasOnlyDesc": { "message": "Desværre understøttes browserintegration indtil videre kun i Mac App Store-versionen." }, @@ -2688,6 +2697,9 @@ "message": "Almindelige formater", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Gennemført" + }, "troubleshooting": { "message": "Fejlfinding" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Adgangsnøgle fjernet" + }, + "errorAssigningTargetCollection": { + "message": "Fejl ved tildeling af målsamling." + }, + "errorAssigningTargetFolder": { + "message": "Fejl ved tildeling af målmappe." } } diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index 428cfd6a278..bba1ccec151 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Master-Passwort ändern" }, - "changeMasterPasswordConfirmation": { - "message": "Du kannst dein Master-Passwort im bitwarden.com-Web-Tresor ändern. Möchtest du die Seite jetzt öffnen?" + "continueToWebApp": { + "message": "Weiter zur Web-App?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Du kannst dein Master-Passwort in der Bitwarden Web-App ändern." }, "fingerprintPhrase": { "message": "Fingerabdruck-Phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser-Integration wird nicht unterstützt" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Leider wird die Browser-Integration derzeit nur in der Mac App Store Version unterstützt." }, @@ -2688,6 +2697,9 @@ "message": "Gängigste Formate", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Problembehandlung" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey entfernt" + }, + "errorAssigningTargetCollection": { + "message": "Fehler beim Zuweisen der Ziel-Sammlung." + }, + "errorAssigningTargetFolder": { + "message": "Fehler beim Zuweisen des Ziel-Ordners." } } diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index 63b1f21c2e3..87360c33ce0 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Αλλαγή Κύριου Κωδικού" }, - "changeMasterPasswordConfirmation": { - "message": "Μπορείτε να αλλάξετε τον κύριο κωδικό στο bitwarden.com. Θέλετε να επισκεφθείτε την ιστοσελίδα τώρα;" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Φράση Δακτυλικών Αποτυπωμάτων", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Η ενσωμάτωση του περιηγητή δεν υποστηρίζεται" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Δυστυχώς η ενσωμάτωση του προγράμματος περιήγησης υποστηρίζεται μόνο στην έκδοση Mac App Store για τώρα." }, @@ -2688,6 +2697,9 @@ "message": "Κοινές μορφές", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Αντιμετώπιση Προβλημάτων" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index 53958bca579..5c8c32b7c14 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json index f6011c301f6..abfa0b1c0da 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/eo/messages.json b/apps/desktop/src/locales/eo/messages.json index 772eb70985e..427f08f8052 100644 --- a/apps/desktop/src/locales/eo/messages.json +++ b/apps/desktop/src/locales/eo/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index e3dcd0dc4ce..f7df93bdd72 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Cambiar contraseña maestra" }, - "changeMasterPasswordConfirmation": { - "message": "Puedes cambiar tu contraseña maestra en la caja fuerte web de bitwarden.com. ¿Quieres visitar ahora el sitio web?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Frase de huella digital", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "La integración con el navegador no está soportada" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Por desgracia la integración del navegador sólo está soportada por ahora en la versión de la Mac App Store." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/et/messages.json b/apps/desktop/src/locales/et/messages.json index 2b54df2a91b..02cd737baac 100644 --- a/apps/desktop/src/locales/et/messages.json +++ b/apps/desktop/src/locales/et/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Muuda ülemparooli" }, - "changeMasterPasswordConfirmation": { - "message": "Saad oma ülemparooli muuta bitwarden.com veebihoidlas. Soovid seda kohe teha?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Sõrmejälje fraas", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Brauseri integratsioon ei ole toetatud" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Paraku on brauseri integratsioon hetkel toetatud ainult Mac App Store'i versioonis." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json index d66d5265e13..2067b2dcc2e 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Aldatu pasahitz nagusia" }, - "changeMasterPasswordConfirmation": { - "message": "Zure pasahitz nagusia alda dezakezu bitwarden.com webgunean. Orain joan nahi duzu webgunera?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Hatz-marka digitalaren esaldia", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Ez da nabigatzailearen integrazioa onartzen" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Zoritxarrez, Mac App Storeren bertsioan soilik onartzen da oraingoz nabigatzailearen integrazioa." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/fa/messages.json b/apps/desktop/src/locales/fa/messages.json index c62bb99b2d0..ef34f8222ae 100644 --- a/apps/desktop/src/locales/fa/messages.json +++ b/apps/desktop/src/locales/fa/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "تغییر کلمه عبور اصلی" }, - "changeMasterPasswordConfirmation": { - "message": "شما می‌توانید کلمه عبور اصلی خود را در bitwarden.com تغییر دهید. آیا می‌خواهید از سایت بازدید کنید؟" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "عبارت اثر انگشت", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "ادغام مرورگر پشتیبانی نمی‌شود" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "متأسفانه در حال حاضر ادغام مرورگر فقط در نسخه Mac App Store پشتیبانی می‌شود." }, @@ -2688,6 +2697,9 @@ "message": "فرمت‌های رایج", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index f74136aedca..03059f7ef31 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Vaihda pääsalasana" }, - "changeMasterPasswordConfirmation": { - "message": "Voit vaihtaa pääsalasanasi bitwarden.com-verkkoholvissa. Haluatko käydä sivustolla nyt?" + "continueToWebApp": { + "message": "Avataanko verkkosovellus?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Voit vaihtaa pääsalasanasi Bitwardenin verkkosovelluksessa." }, "fingerprintPhrase": { "message": "Tunnistelauseke", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Selainintegraatiota ei tueta" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Valitettavasti selainintegraatiota tuetaan toistaiseksi vain Mac App Store -versiossa." }, @@ -2688,6 +2697,9 @@ "message": "Yleiset muodot", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Vianetsintä" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Suojausavain poistettiin" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/fil/messages.json b/apps/desktop/src/locales/fil/messages.json index 170559fc64f..d28a4b568c3 100644 --- a/apps/desktop/src/locales/fil/messages.json +++ b/apps/desktop/src/locales/fil/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Palitan ang master password" }, - "changeMasterPasswordConfirmation": { - "message": "Maaari mong palitan ang iyong master password sa bitwarden.com web vault. Gusto mo bang bisitahin ang website ngayon?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Hulmabig ng Hilik ng Dako", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Hindi suportado ang pagsasama ng browser" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Sa kasamaang palad ang pagsasama ng browser ay suportado lamang sa bersyon ng Mac App Store para sa ngayon." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index 20353d2d863..86550b736f3 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Changer le mot de passe principal" }, - "changeMasterPasswordConfirmation": { - "message": "Vous pouvez changer votre mot de passe principal depuis le coffre web de bitwarden.com. Voulez-vous visiter le site web maintenant ?" + "continueToWebApp": { + "message": "Poursuivre vers l'application web ?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Vous pouvez modifier votre mot de passe principal sur l'application web de Bitwarden." }, "fingerprintPhrase": { "message": "Phrase d'empreinte", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Intégration dans le navigateur non supportée" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Malheureusement l'intégration avec le navigateur est uniquement supportée dans la version Mac App Store pour le moment." }, @@ -2688,6 +2697,9 @@ "message": "Formats communs", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Résolution de problèmes" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Clé d'identification (passkey) retirée" + }, + "errorAssigningTargetCollection": { + "message": "Erreur lors de l'assignation de la collection cible." + }, + "errorAssigningTargetFolder": { + "message": "Erreur lors de l'assignation du dossier cible." } } diff --git a/apps/desktop/src/locales/gl/messages.json b/apps/desktop/src/locales/gl/messages.json index f96260c0056..889a2beeee0 100644 --- a/apps/desktop/src/locales/gl/messages.json +++ b/apps/desktop/src/locales/gl/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json index cc5a0f011dc..3b155ffdf3e 100644 --- a/apps/desktop/src/locales/he/messages.json +++ b/apps/desktop/src/locales/he/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "החלף סיסמה ראשית" }, - "changeMasterPasswordConfirmation": { - "message": "באפשרותך לשנות את הסיסמה הראשית שלך דרך הכספת באתר bitwarden.com. האם ברצונך לפתוח את האתר כעת?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "סיסמת טביעת אצבע", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "שילוב הדפדפן אינו נתמך" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "למצער, אינטגרצייה עם הדפדפן בשלב זה נתמכת רק על ידי גרסת חנות האפליקציות של מקינטוש." }, @@ -2688,6 +2697,9 @@ "message": "תסדירים נפוצים", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/hi/messages.json b/apps/desktop/src/locales/hi/messages.json index 57ef1d32ef5..af28c666813 100644 --- a/apps/desktop/src/locales/hi/messages.json +++ b/apps/desktop/src/locales/hi/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json index 1e501cee78e..01983d5891e 100644 --- a/apps/desktop/src/locales/hr/messages.json +++ b/apps/desktop/src/locales/hr/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Promjeni glavnu lozinku" }, - "changeMasterPasswordConfirmation": { - "message": "Svoju glavnu lozinku možeš promijeniti na web trezoru. Želiš li sada posjetiti bitwarden.com?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Jedinstvena fraza", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Integracija preglednika nije podržana" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Nažalost, za sada je integracija s preglednikom podržana samo u Mac App Store verziji aplikacije." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index 0b443c9a6bc..ecf77e2f347 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Mesterjelszó módosítása" }, - "changeMasterPasswordConfirmation": { - "message": "A mesterjelszó megváltoztatható a bitwarden.com webes széfben. Szeretnénk felkeresni a webhelyet mos?" + "continueToWebApp": { + "message": "Tovább a webes alkalmazáshoz?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "A mesterjelszó a Bitwarden webalkalmazásban módosítható." }, "fingerprintPhrase": { "message": "Azonosítókifejezés", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "A böngésző integráció nem támogatott." }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Sajnos a böngésző integrációt egyelőre csak a Mac App Store verzió támogatja." }, @@ -2688,6 +2697,9 @@ "message": "Általános formátumok", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Hibaelhárítás" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Hiba történt a célgyűjtemény hozzárendelése során." + }, + "errorAssigningTargetFolder": { + "message": "Hiba történt a célmappa hozzárendelése során." } } diff --git a/apps/desktop/src/locales/id/messages.json b/apps/desktop/src/locales/id/messages.json index 008b6b369aa..cd36126b050 100644 --- a/apps/desktop/src/locales/id/messages.json +++ b/apps/desktop/src/locales/id/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Ubah Kata Sandi Utama" }, - "changeMasterPasswordConfirmation": { - "message": "Anda dapat mengubah kata sandi utama Anda di brankas web bitwarden.com. Anda ingin mengunjungi situs web sekarang?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Frase Fingerprint", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Integrasi browser tidak didukung" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Sayangnya integrasi browser hanya didukung di versi Mac App Store untuk saat ini." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index a3a6f771fec..0eeea259e2e 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Cambia password principale" }, - "changeMasterPasswordConfirmation": { - "message": "Puoi cambiare la tua password principale sulla cassaforte online di bitwarden.com. Vuoi visitare ora il sito?" + "continueToWebApp": { + "message": "Passa al sito web?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Puoi modificare la tua password principale sul sito web di Bitwarden." }, "fingerprintPhrase": { "message": "Frase impronta", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "L'integrazione del browser non è supportata" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Purtroppo l'integrazione del browser è supportata solo nella versione nell'App Store per ora." }, @@ -2688,6 +2697,9 @@ "message": "Formati comuni", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Risoluzione problemi" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey rimossa" + }, + "errorAssigningTargetCollection": { + "message": "Errore nell'assegnazione della raccolta di destinazione." + }, + "errorAssigningTargetFolder": { + "message": "Errore nell'assegnazione della cartella di destinazione." } } diff --git a/apps/desktop/src/locales/ja/messages.json b/apps/desktop/src/locales/ja/messages.json index f0a95d4e359..ab6c0be95f7 100644 --- a/apps/desktop/src/locales/ja/messages.json +++ b/apps/desktop/src/locales/ja/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "マスターパスワードの変更" }, - "changeMasterPasswordConfirmation": { - "message": "マスターパスワードは bitwarden.com ウェブ保管庫で変更できます。ウェブサイトを開きますか?" + "continueToWebApp": { + "message": "ウェブアプリに進みますか?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Bitwarden ウェブアプリでマスターパスワードを変更できます。" }, "fingerprintPhrase": { "message": "パスフレーズ", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "ブラウザー統合はサポートされていません" }, + "browserIntegrationErrorTitle": { + "message": "ブラウザー連携を有効にする際にエラーが発生しました" + }, + "browserIntegrationErrorDesc": { + "message": "ブラウザー統合の有効化中にエラーが発生しました。" + }, "browserIntegrationMasOnlyDesc": { "message": "残念ながら、ブラウザ統合は、Mac App Storeのバージョンでのみサポートされています。" }, @@ -2688,6 +2697,9 @@ "message": "一般的な形式", "description": "Label indicating the most common import formats" }, + "success": { + "message": "成功" + }, "troubleshooting": { "message": "トラブルシューティング" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "パスキーを削除しました" + }, + "errorAssigningTargetCollection": { + "message": "ターゲットコレクションの割り当てに失敗しました。" + }, + "errorAssigningTargetFolder": { + "message": "ターゲットフォルダーの割り当てに失敗しました。" } } diff --git a/apps/desktop/src/locales/ka/messages.json b/apps/desktop/src/locales/ka/messages.json index f96260c0056..889a2beeee0 100644 --- a/apps/desktop/src/locales/ka/messages.json +++ b/apps/desktop/src/locales/ka/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/km/messages.json b/apps/desktop/src/locales/km/messages.json index f96260c0056..889a2beeee0 100644 --- a/apps/desktop/src/locales/km/messages.json +++ b/apps/desktop/src/locales/km/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/kn/messages.json b/apps/desktop/src/locales/kn/messages.json index 281d64cbc29..eb0cbcf6be7 100644 --- a/apps/desktop/src/locales/kn/messages.json +++ b/apps/desktop/src/locales/kn/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "ಮಾಸ್ಟರ್ ಪಾಸ್ವರ್ಡ್ ಬದಲಾಯಿಸಿ" }, - "changeMasterPasswordConfirmation": { - "message": "ನಿಮ್ಮ ಮಾಸ್ಟರ್ ಪಾಸ್‌ವರ್ಡ್ ಅನ್ನು ನೀವು bitwarden.com ವೆಬ್ ವಾಲ್ಟ್‌ನಲ್ಲಿ ಬದಲಾಯಿಸಬಹುದು. ನೀವು ಈಗ ವೆಬ್‌ಸೈಟ್‌ಗೆ ಭೇಟಿ ನೀಡಲು ಬಯಸುವಿರಾ?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "ಫಿಂಗರ್ಪ್ರಿಂಟ್ ಫ್ರೇಸ್", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "ದುರದೃಷ್ಟವಶಾತ್ ಬ್ರೌಸರ್ ಏಕೀಕರಣವನ್ನು ಇದೀಗ ಮ್ಯಾಕ್ ಆಪ್ ಸ್ಟೋರ್ ಆವೃತ್ತಿಯಲ್ಲಿ ಮಾತ್ರ ಬೆಂಬಲಿಸಲಾಗುತ್ತದೆ." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json index a09d53b1ddb..8e50ade96cd 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "마스터 비밀번호 변경" }, - "changeMasterPasswordConfirmation": { - "message": "bitwarden.com 웹 보관함에서 마스터 비밀번호를 바꿀 수 있습니다. 지금 웹 사이트를 방문하시겠습니까?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "지문 구절", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "브라우저와 연결이 지원되지 않음" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "브라우저와 연결은 현재 Mac App Store 버전에서만 지원됩니다." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json index dbc2e13d1cc..e9de6970054 100644 --- a/apps/desktop/src/locales/lt/messages.json +++ b/apps/desktop/src/locales/lt/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Keisti pagrindinį slaptažodį" }, - "changeMasterPasswordConfirmation": { - "message": "Pagrindinį slaptažodį galite pakeisti bitwarden.com žiniatinklio saugykloje. Ar norite dabar apsilankyti svetainėje?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Piršto antspaudo frazė", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Naršyklės integravimas nepalaikomas" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Deja, bet naršyklės integravimas kol kas palaikomas tik Mac App Store versijoje." }, @@ -2688,6 +2697,9 @@ "message": "Dažni formatai", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index 0a3501ddedd..0227a8c5241 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Mainīt galveno paroli" }, - "changeMasterPasswordConfirmation": { - "message": "Galveno paroli ir iespējams mainīt bitwarden.com tīmekļa glabātavā. Vai tagad apmeklēt tīmekļvietni?" + "continueToWebApp": { + "message": "Pāriet uz tīmekļa lietotni?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Savu galveno paroli var mainīt Bitwarden tīmekļa lietotnē." }, "fingerprintPhrase": { "message": "Atpazīšanas vārdkopa", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Sasaistīšana ar pārlūku nav atbalstīta" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Diemžēl sasaistīšāna ar pārlūku pagaidām ir nodrošināta tikai Mac App Store laidienā." }, @@ -2688,6 +2697,9 @@ "message": "Izplatīti veidoli", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Sarežģījumu novēršana" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Piekļuves atslēga noņemta" + }, + "errorAssigningTargetCollection": { + "message": "Kļūda mērķa krājuma piešķiršanā." + }, + "errorAssigningTargetFolder": { + "message": "Kļūda mērķa mapes piešķiršanā." } } diff --git a/apps/desktop/src/locales/me/messages.json b/apps/desktop/src/locales/me/messages.json index d5e3bddf8ea..1f49961b469 100644 --- a/apps/desktop/src/locales/me/messages.json +++ b/apps/desktop/src/locales/me/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Promjena glavne lozinke" }, - "changeMasterPasswordConfirmation": { - "message": "Možete promijeniti svoju glavnu lozinku u trezoru na internet strani bitwarden.com. Da li želite da posjetite internet lokaciju sada?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fraza računa", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/ml/messages.json b/apps/desktop/src/locales/ml/messages.json index 6b1137d2328..96811b9dba8 100644 --- a/apps/desktop/src/locales/ml/messages.json +++ b/apps/desktop/src/locales/ml/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "പ്രാഥമിക പാസ്‌വേഡ് മാറ്റുക" }, - "changeMasterPasswordConfirmation": { - "message": "തങ്ങൾക്കു Bitwarden വെബ് വാൾട്ടിൽ പ്രാഥമിക പാസ്‌വേഡ് മാറ്റാൻ സാധിക്കും.വെബ്സൈറ്റ് ഇപ്പോൾ സന്ദർശിക്കാൻ ആഗ്രഹിക്കുന്നുവോ?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "ഫിംഗർപ്രിന്റ് ഫ്രേസ്‌", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/mr/messages.json b/apps/desktop/src/locales/mr/messages.json index f96260c0056..889a2beeee0 100644 --- a/apps/desktop/src/locales/mr/messages.json +++ b/apps/desktop/src/locales/mr/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/my/messages.json b/apps/desktop/src/locales/my/messages.json index 2626e93c240..0ee0db69ef2 100644 --- a/apps/desktop/src/locales/my/messages.json +++ b/apps/desktop/src/locales/my/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/nb/messages.json b/apps/desktop/src/locales/nb/messages.json index 8e8e2e2cbb8..7bf132bdac1 100644 --- a/apps/desktop/src/locales/nb/messages.json +++ b/apps/desktop/src/locales/nb/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Endre hovedpassordet" }, - "changeMasterPasswordConfirmation": { - "message": "Du kan endre superpassordet ditt på bitwarden.net-netthvelvet. Vil du besøke det nettstedet nå?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingeravtrykksfrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Nettleserintegrasjon støttes ikke" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Nettleserintegrasjon støttes dessverre bare i Mac App Store-versjonen for øyeblikket." }, @@ -2688,6 +2697,9 @@ "message": "Vanlige formater", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/ne/messages.json b/apps/desktop/src/locales/ne/messages.json index e7d586023e8..13e14668054 100644 --- a/apps/desktop/src/locales/ne/messages.json +++ b/apps/desktop/src/locales/ne/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index 9c4ba780364..b5f2a413d6f 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Hoofdwachtwoord wijzigen" }, - "changeMasterPasswordConfirmation": { - "message": "Je kunt je hoofdwachtwoord wijzigen in de kluis op bitwarden.com. Wil je de website nu bezoeken?" + "continueToWebApp": { + "message": "Doorgaan naar web-app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Je kunt je hoofdwachtwoord wijzigen in de Bitwarden-webapp." }, "fingerprintPhrase": { "message": "Vingerafdrukzin", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browserintegratie niet ondersteund" }, + "browserIntegrationErrorTitle": { + "message": "Fout bij inschakelen van de browserintegratie" + }, + "browserIntegrationErrorDesc": { + "message": "Er is iets misgegaan bij het tijdens het inschakelen van de browserintegratie." + }, "browserIntegrationMasOnlyDesc": { "message": "Helaas wordt browserintegratie momenteel alleen ondersteund in de Mac App Store-versie." }, @@ -2688,6 +2697,9 @@ "message": "Veelvoorkomende formaten", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Probleemoplossing" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey verwijderd" + }, + "errorAssigningTargetCollection": { + "message": "Fout bij toewijzen doelverzameling." + }, + "errorAssigningTargetFolder": { + "message": "Fout bij toewijzen doelmap." } } diff --git a/apps/desktop/src/locales/nn/messages.json b/apps/desktop/src/locales/nn/messages.json index ea55378f5bc..35e7173d74a 100644 --- a/apps/desktop/src/locales/nn/messages.json +++ b/apps/desktop/src/locales/nn/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Endre hovudpassord" }, - "changeMasterPasswordConfirmation": { - "message": "Du kan endre hovudpassordet ditt i Bitwarden sin nettkvelv. Vil du gå til nettstaden no?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/or/messages.json b/apps/desktop/src/locales/or/messages.json index c6c0c2fb0c0..cd83d2ea698 100644 --- a/apps/desktop/src/locales/or/messages.json +++ b/apps/desktop/src/locales/or/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index a0626a6c906..250c557309c 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Zmień hasło główne" }, - "changeMasterPasswordConfirmation": { - "message": "Hasło główne możesz zmienić na stronie sejfu bitwarden.com. Czy chcesz przejść do tej strony?" + "continueToWebApp": { + "message": "Kontynuować do aplikacji internetowej?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Możesz zmienić swoje hasło główne w aplikacji internetowej Bitwarden." }, "fingerprintPhrase": { "message": "Unikalny identyfikator konta", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Połączenie z przeglądarką nie jest obsługiwane" }, + "browserIntegrationErrorTitle": { + "message": "Błąd podczas włączania integracji z przeglądarką" + }, + "browserIntegrationErrorDesc": { + "message": "Wystąpił błąd podczas włączania integracji z przeglądarką." + }, "browserIntegrationMasOnlyDesc": { "message": "Połączenie z przeglądarką jest obsługiwane tylko z wersją aplikacji ze sklepu Mac App Store." }, @@ -2688,6 +2697,9 @@ "message": "Popularne formaty", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Sukces" + }, "troubleshooting": { "message": "Rozwiązywanie problemów" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey został usunięty" + }, + "errorAssigningTargetCollection": { + "message": "Wystąpił błąd podczas przypisywania kolekcji." + }, + "errorAssigningTargetFolder": { + "message": "Wystąpił błąd podczas przypisywania folderu." } } diff --git a/apps/desktop/src/locales/pt_BR/messages.json b/apps/desktop/src/locales/pt_BR/messages.json index c8f8316e6d1..9a79ad665eb 100644 --- a/apps/desktop/src/locales/pt_BR/messages.json +++ b/apps/desktop/src/locales/pt_BR/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Alterar Senha Mestra" }, - "changeMasterPasswordConfirmation": { - "message": "Você pode alterar a sua senha mestra no cofre web em bitwarden.com. Você deseja visitar o site agora?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Frase biométrica", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Integração com o navegador não suportado" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Infelizmente, por ora, a integração do navegador só é suportada na versão da Mac App Store." }, @@ -2688,6 +2697,9 @@ "message": "Formatos comuns", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index 5fbd7636d15..14f0ec5d2f7 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Alterar palavra-passe mestra" }, - "changeMasterPasswordConfirmation": { - "message": "Pode alterar o seu endereço de e-mail no cofre do site bitwarden.com. Deseja visitar o site agora?" + "continueToWebApp": { + "message": "Continuar para a aplicação Web?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Pode alterar a sua palavra-passe mestra na aplicação Web Bitwarden." }, "fingerprintPhrase": { "message": "Frase de impressão digital", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Integração com o navegador não suportada" }, + "browserIntegrationErrorTitle": { + "message": "Erro ao ativar a integração do navegador" + }, + "browserIntegrationErrorDesc": { + "message": "Ocorreu um erro ao ativar a integração do navegador." + }, "browserIntegrationMasOnlyDesc": { "message": "Infelizmente, a integração do navegador só é suportada na versão da Mac App Store por enquanto." }, @@ -2688,6 +2697,9 @@ "message": "Formatos comuns", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Com sucesso" + }, "troubleshooting": { "message": "Resolução de problemas" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Chave de acesso removida" + }, + "errorAssigningTargetCollection": { + "message": "Erro ao atribuir a coleção de destino." + }, + "errorAssigningTargetFolder": { + "message": "Erro ao atribuir a pasta de destino." } } diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json index 7af8f7ec162..978f57eb9b7 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Schimbare parolă principală" }, - "changeMasterPasswordConfirmation": { - "message": "Puteți modifica parola principală pe saitul web bitwarden.com. Doriți să vizitați saitul acum?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Frază amprentă", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Integrarea browserului nu este acceptată" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Din păcate, integrarea browserului este acceptată numai în versiunea Mac App Store pentru moment." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index 0e28c2cf90c..c9b3b95b39c 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Изменить мастер-пароль" }, - "changeMasterPasswordConfirmation": { - "message": "Вы можете изменить свой мастер-пароль на bitwarden.com. Перейти на сайт сейчас?" + "continueToWebApp": { + "message": "Перейти к веб-приложению?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Изменить мастер-пароль можно в веб-приложении Bitwarden." }, "fingerprintPhrase": { "message": "Фраза отпечатка", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Интеграция с браузером не поддерживается" }, + "browserIntegrationErrorTitle": { + "message": "Ошибка при включении интеграции с браузером" + }, + "browserIntegrationErrorDesc": { + "message": "Произошла ошибка при включении интеграции с браузером." + }, "browserIntegrationMasOnlyDesc": { "message": "К сожалению, интеграция браузера пока поддерживается только в версии Mac App Store." }, @@ -2688,6 +2697,9 @@ "message": "Основные форматы", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Успешно" + }, "troubleshooting": { "message": "Устранение проблем" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey удален" + }, + "errorAssigningTargetCollection": { + "message": "Ошибка при назначении целевой коллекции." + }, + "errorAssigningTargetFolder": { + "message": "Ошибка при назначении целевой папки." } } diff --git a/apps/desktop/src/locales/si/messages.json b/apps/desktop/src/locales/si/messages.json index 60e8aea93cb..3d439971442 100644 --- a/apps/desktop/src/locales/si/messages.json +++ b/apps/desktop/src/locales/si/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index 240b883254c..6499486b9d5 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -445,7 +445,7 @@ "message": "Vyhnúť sa zameniteľným znakom" }, "searchCollection": { - "message": "Search collection" + "message": "Vyhľadať zbierku" }, "searchFolder": { "message": "Search folder" @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Zmeniť hlavné heslo" }, - "changeMasterPasswordConfirmation": { - "message": "Svoje hlavné heslo môžete zmeniť vo webovom trezore bitwarden.com. Chcete teraz navštíviť túto stránku?" + "continueToWebApp": { + "message": "Pokračovať vo webovej aplikácii?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Hlavné heslo si môžete zmeniť vo webovej aplikácii Bitwarden." }, "fingerprintPhrase": { "message": "Fráza odtlačku", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Integrácia v prehliadači nie je podporovaná" }, + "browserIntegrationErrorTitle": { + "message": "Chyba pri povoľovaní integrácie v prehliadači" + }, + "browserIntegrationErrorDesc": { + "message": "Pri povoľovaní integrácie v prehliadači sa vyskytla chyba." + }, "browserIntegrationMasOnlyDesc": { "message": "Bohužiaľ, integrácia v prehliadači je zatiaľ podporovaná iba vo verzii Mac App Store." }, @@ -2688,6 +2697,9 @@ "message": "Bežné formáty", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Úspech" + }, "troubleshooting": { "message": "Riešenie problémov" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Prístupový kľúč bol odstránený" + }, + "errorAssigningTargetCollection": { + "message": "Chyba pri priraďovaní cieľovej kolekcie." + }, + "errorAssigningTargetFolder": { + "message": "Chyba pri priraďovaní cieľového priečinka." } } diff --git a/apps/desktop/src/locales/sl/messages.json b/apps/desktop/src/locales/sl/messages.json index 528274cf290..8cb06dcf0ca 100644 --- a/apps/desktop/src/locales/sl/messages.json +++ b/apps/desktop/src/locales/sl/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Spremeni glavno geslo" }, - "changeMasterPasswordConfirmation": { - "message": "Svoje glavno geslo lahko spremenite v bitwarden.com spletnem trezorju. Želite obiskati spletno stran zdaj?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Identifikacijsko geslo", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index 77b5f7221d6..37c5dfa3827 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Промени главну лозинку" }, - "changeMasterPasswordConfirmation": { - "message": "Можете променити главну лозинку у Вашем сефу на bitwarden.com. Да ли желите да посетите веб страницу сада?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Сигурносна Фраза Сефа", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Интеграција са претраживачем није подржана" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Нажалост, интеграција прегледача за сада је подржана само у верзији Mac App Store." }, @@ -2688,6 +2697,9 @@ "message": "Уобичајени формати", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Решавање проблема" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Приступачни кључ је уклоњен" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index c07f7efef3b..bd21c0f328a 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Ändra huvudlösenord" }, - "changeMasterPasswordConfirmation": { - "message": "Du kan ändra ditt huvudlösenord i Bitwardens webbvalv. Vill du besöka webbplatsen nu?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingeravtrycksfras", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Webbläsarintegration stöds inte" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Tyvärr stöds webbläsarintegration för tillfället endast i versionen från Mac App Store." }, @@ -2688,6 +2697,9 @@ "message": "Vanliga format", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Felsökning" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Nyckel borttagen" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json index f96260c0056..889a2beeee0 100644 --- a/apps/desktop/src/locales/te/messages.json +++ b/apps/desktop/src/locales/te/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/th/messages.json b/apps/desktop/src/locales/th/messages.json index efbfc86b338..f1cd5351f7f 100644 --- a/apps/desktop/src/locales/th/messages.json +++ b/apps/desktop/src/locales/th/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "เปลี่ยนรหัสผ่านหลัก" }, - "changeMasterPasswordConfirmation": { - "message": "คุณสามารถเปลี่ยนรหัสผ่านหลักของคุณได้ที่เว็บ bitwarden.com คุณต้องการไปที่เว็บไซต์ตอนนี้ไหม?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint Phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index 36d62ed2a54..3e7229c41b8 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Ana parolayı değiştir" }, - "changeMasterPasswordConfirmation": { - "message": "Ana parolanızı bitwarden.com web kasası üzerinden değiştirebilirsiniz. Siteyi şimdi ziyaret etmek ister misiniz?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Parmak izi ifadesi", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Tarayıcı entegrasyonu desteklenmiyor" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Ne yazık ki tarayıcı entegrasyonu şu anda sadece Mac App Store sürümünde destekleniyor." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Sorun giderme" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index 377fd23b0b0..9ee76520933 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Змінити головний пароль" }, - "changeMasterPasswordConfirmation": { - "message": "Ви можете змінити головний пароль в сховищі на bitwarden.com. Хочете перейти на вебсайт зараз?" + "continueToWebApp": { + "message": "Продовжити у вебпрограмі?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Ви можете змінити головний пароль у вебпрограмі Bitwarden." }, "fingerprintPhrase": { "message": "Фраза відбитка", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Інтеграція з браузером не підтримується" }, + "browserIntegrationErrorTitle": { + "message": "Помилка увімкнення інтеграції з браузером" + }, + "browserIntegrationErrorDesc": { + "message": "Під час увімкнення інтеграції з браузером сталася помилка." + }, "browserIntegrationMasOnlyDesc": { "message": "На жаль, зараз інтеграція з браузером підтримується лише у версії для Mac з App Store." }, @@ -2688,6 +2697,9 @@ "message": "Поширені формати", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Успішно" + }, "troubleshooting": { "message": "Усунення проблем" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Ключ доступу вилучено" + }, + "errorAssigningTargetCollection": { + "message": "Помилка призначення цільової збірки." + }, + "errorAssigningTargetFolder": { + "message": "Помилка призначення цільової теки." } } diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json index 138773d40ac..0c0e6f6df73 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Thay đổi mật khẩu chính" }, - "changeMasterPasswordConfirmation": { - "message": "Bạn có thể thay đổi mật khẩu chính trong kho bitwarden nền web. Bạn có muốn truy cập trang web bây giờ?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint Phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Không hỗ trợ tích hợp trình duyệt" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Rất tiếc, tính năng tích hợp trình duyệt hiện chỉ được hỗ trợ trong phiên bản App Store trên Mac." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index 8725fa0f21c..3819cb967ca 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "修改主密码" }, - "changeMasterPasswordConfirmation": { - "message": "您可以在 bitwarden.com 网页密码库修改您的主密码。现在要访问吗?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "指纹短语", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "不支持浏览器集成" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "很遗憾,目前仅 Mac App Store 版本支持浏览器集成。" }, @@ -2688,6 +2697,9 @@ "message": "常规格式", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "故障排除" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "通行密钥已移除" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index c93f2369769..5f768b0a43e 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "變更主密碼" }, - "changeMasterPasswordConfirmation": { - "message": "您可以在 bitwarden.com 網頁版密碼庫變更主密碼。現在要前往嗎?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "指紋短語", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "不支援瀏覽器整合" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "很遺憾,目前僅 Mac App Store 版本支援瀏覽器整合功能。" }, @@ -2688,6 +2697,9 @@ "message": "常見格式", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "疑難排解" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "金鑰已移除" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } From c1c6afb0f48e9cc8852448a0826263810f7a5cae Mon Sep 17 00:00:00 2001 From: Robyn MacCallum Date: Fri, 19 Apr 2024 09:45:09 -0400 Subject: [PATCH 008/110] [PM-7562] Add DuckDuckGo back to State Service (#8791) * Add ddg back to state service * Remove getters --- .../desktop/src/app/accounts/settings.component.ts | 5 +++++ .../src/platform/abstractions/state.service.ts | 7 +++++++ .../src/platform/models/domain/global-state.ts | 1 + libs/common/src/platform/services/state.service.ts | 14 ++++++++++++++ 4 files changed, 27 insertions(+) diff --git a/apps/desktop/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts index 06533e18fcb..f3958d7c871 100644 --- a/apps/desktop/src/app/accounts/settings.component.ts +++ b/apps/desktop/src/app/accounts/settings.component.ts @@ -660,6 +660,11 @@ export class SettingsComponent implements OnInit { this.form.value.enableDuckDuckGoBrowserIntegration, ); + // Adding to cover users on a previous version of DDG + await this.stateService.setEnableDuckDuckGoBrowserIntegration( + this.form.value.enableDuckDuckGoBrowserIntegration, + ); + if (!this.form.value.enableBrowserIntegration) { await this.stateService.setDuckDuckGoSharedKey(null); } diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index 2348c8844a6..27026ac0ea9 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -74,6 +74,13 @@ export abstract class StateService { * Used when Lock with MP on Restart is enabled */ setPinKeyEncryptedUserKeyEphemeral: (value: EncString, options?: StorageOptions) => Promise; + /** + * @deprecated For backwards compatible purposes only, use DesktopAutofillSettingsService + */ + setEnableDuckDuckGoBrowserIntegration: ( + value: boolean, + options?: StorageOptions, + ) => Promise; /** * @deprecated For migration purposes only, use getUserKeyMasterKey instead */ diff --git a/libs/common/src/platform/models/domain/global-state.ts b/libs/common/src/platform/models/domain/global-state.ts index 703a998d1c3..cd7cf7d1747 100644 --- a/libs/common/src/platform/models/domain/global-state.ts +++ b/libs/common/src/platform/models/domain/global-state.ts @@ -4,4 +4,5 @@ export class GlobalState { vaultTimeoutAction?: string; enableBrowserIntegration?: boolean; enableBrowserIntegrationFingerprint?: boolean; + enableDuckDuckGoBrowserIntegration?: boolean; } diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 9edc9ed1e31..1fb2d716705 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -571,6 +571,20 @@ export class StateService< ); } + async setEnableDuckDuckGoBrowserIntegration( + value: boolean, + options?: StorageOptions, + ): Promise { + const globals = await this.getGlobals( + this.reconcileOptions(options, await this.defaultOnDiskOptions()), + ); + globals.enableDuckDuckGoBrowserIntegration = value; + await this.saveGlobals( + globals, + this.reconcileOptions(options, await this.defaultOnDiskOptions()), + ); + } + /** * @deprecated Use UserKey instead */ From 6f2bed63a63ed3b6ddf49066bc8c7b000be44688 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Gon=C3=A7alves?= Date: Fri, 19 Apr 2024 14:53:34 +0100 Subject: [PATCH 009/110] [PM-7569] Fix ciphers view update race on desktop (#8821) * PM-7569 Wait for the update before allow reading ciphers$ * PM-7569 Remove commented line --- .../src/vault/services/cipher.service.ts | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 4a13196c9c3..0b44636ea6c 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -1,4 +1,4 @@ -import { Observable, firstValueFrom, map } from "rxjs"; +import { Observable, firstValueFrom, map, share, skipWhile, switchMap } from "rxjs"; import { SemVer } from "semver"; import { ApiService } from "../../abstractions/api.service"; @@ -21,7 +21,13 @@ import Domain from "../../platform/models/domain/domain-base"; import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer"; import { EncString } from "../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; -import { ActiveUserState, StateProvider } from "../../platform/state"; +import { + ActiveUserState, + CIPHERS_MEMORY, + DeriveDefinition, + DerivedState, + StateProvider, +} from "../../platform/state"; import { CipherId, CollectionId, OrganizationId } from "../../types/guid"; import { UserKey, OrgKey } from "../../types/key"; import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service"; @@ -71,10 +77,14 @@ export class CipherService implements CipherServiceAbstraction { private sortedCiphersCache: SortedCiphersCache = new SortedCiphersCache( this.sortCiphersByLastUsed, ); + private ciphersExpectingUpdate: DerivedState; localData$: Observable>; ciphers$: Observable>; cipherViews$: Observable>; + viewFor$(id: CipherId) { + return this.cipherViews$.pipe(map((views) => views[id])); + } addEditCipherInfo$: Observable; private localDataState: ActiveUserState>; @@ -99,10 +109,29 @@ export class CipherService implements CipherServiceAbstraction { this.encryptedCiphersState = this.stateProvider.getActive(ENCRYPTED_CIPHERS); this.decryptedCiphersState = this.stateProvider.getActive(DECRYPTED_CIPHERS); this.addEditCipherInfoState = this.stateProvider.getActive(ADD_EDIT_CIPHER_INFO_KEY); + this.ciphersExpectingUpdate = this.stateProvider.getDerived( + this.encryptedCiphersState.state$, + new DeriveDefinition(CIPHERS_MEMORY, "ciphersExpectingUpdate", { + derive: (_: Record) => false, + deserializer: (value) => value, + }), + {}, + ); this.localData$ = this.localDataState.state$.pipe(map((data) => data ?? {})); - this.ciphers$ = this.encryptedCiphersState.state$.pipe(map((ciphers) => ciphers ?? {})); - this.cipherViews$ = this.decryptedCiphersState.state$.pipe(map((views) => views ?? {})); + // First wait for ciphersExpectingUpdate to be false before emitting ciphers + this.ciphers$ = this.ciphersExpectingUpdate.state$.pipe( + skipWhile((expectingUpdate) => expectingUpdate), + switchMap(() => this.encryptedCiphersState.state$), + map((ciphers) => ciphers ?? {}), + ); + this.cipherViews$ = this.decryptedCiphersState.state$.pipe( + map((views) => views ?? {}), + + share({ + resetOnRefCountZero: true, + }), + ); this.addEditCipherInfo$ = this.addEditCipherInfoState.state$; } @@ -807,6 +836,8 @@ export class CipherService implements CipherServiceAbstraction { private async updateEncryptedCipherState( update: (current: Record) => Record, ): Promise> { + // Store that we should wait for an update to return any ciphers + await this.ciphersExpectingUpdate.forceValue(true); await this.clearDecryptedCiphersState(); const [, updatedCiphers] = await this.encryptedCiphersState.update((current) => { const result = update(current ?? {}); From 8cb16fb406b735e4fb97bcfe4dc46b00c0a913d5 Mon Sep 17 00:00:00 2001 From: Joseph Flinn <58369717+joseph-flinn@users.noreply.github.com> Date: Fri, 19 Apr 2024 07:02:48 -0700 Subject: [PATCH 010/110] Make extension copy updates for Marketing (#8822) --- apps/browser/src/_locales/en/messages.json | 4 +- apps/browser/store/locales/en/copy.resx | 103 ++++++++++++-------- apps/browser/store/windows/AppxManifest.xml | 8 +- 3 files changed, 66 insertions(+), 49 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 8c81088fc50..7e6e333689f 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/store/locales/en/copy.resx b/apps/browser/store/locales/en/copy.resx index 191198691d4..df8d63835c4 100644 --- a/apps/browser/store/locales/en/copy.resx +++ b/apps/browser/store/locales/en/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 50 languages. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/windows/AppxManifest.xml b/apps/browser/store/windows/AppxManifest.xml index f57b3db9887..df02ea085cf 100644 --- a/apps/browser/store/windows/AppxManifest.xml +++ b/apps/browser/store/windows/AppxManifest.xml @@ -11,7 +11,7 @@ Version="0.0.0.0"/> - Bitwarden Extension - Free Password Manager + Bitwarden Password Manager 8bit Solutions LLC Assets/icon_50.png @@ -30,10 +30,10 @@ @@ -41,7 +41,7 @@ + DisplayName="Bitwarden Password Manager"> From 6cafb1d28fb5dd1fe4e524c6f2f203e3fa23b2a0 Mon Sep 17 00:00:00 2001 From: Merissa Weinstein Date: Fri, 19 Apr 2024 09:09:58 -0500 Subject: [PATCH 011/110] [PM-2870] [PM-2865] Accessibility updates: add labels to buttons & form checkboxes (#8358) * organization-options: add area-labels to links * vault-cipher-row: add aria-label to input checkbox * vault-collection-row: add aria-label to collection item * add internationalizatino to org options menu * add internationlization to checkbox aria-labels for vault and collection items * organization-options-component: remove added aria-lables to buttons inside of toggle --------- Co-authored-by: Merissa Weinstein --- .../vault-items/vault-cipher-row.component.html | 1 + .../vault-items/vault-collection-row.component.html | 1 + .../components/organization-options.component.html | 7 ++++++- apps/web/src/locales/en/messages.json | 9 +++++++++ 4 files changed, 17 insertions(+), 1 deletion(-) 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 5ddabf05574..ae22d89f7f4 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 @@ -6,6 +6,7 @@ [disabled]="disabled" [checked]="checked" (change)="$event ? this.checkedToggled.next() : null" + [attr.aria-label]="'vaultItemSelect' | i18n" /> diff --git a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html index d333f92d5c2..d03b6dcc385 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html @@ -7,6 +7,7 @@ [disabled]="disabled" [checked]="checked" (change)="$event ? this.checkedToggled.next() : null" + [attr.aria-label]="'collectionItemSelect' | i18n" /> diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.html b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.html index f4fb2cc040a..0b94b6e2be2 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.html +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.html @@ -1,5 +1,10 @@ - diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 845816562bf..63069a83de8 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } From fffef95c5ef0ef3f2a9112eb3e2f49d5412362a0 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Fri, 19 Apr 2024 11:20:13 -0400 Subject: [PATCH 012/110] Auth/PM-7235 - Refactor AuthService.getAuthStatus, deprecate everBeenUnlocked, and handle initialization of auto user key on client init (#8590) * PM-7235 - AuthSvc - Refactor getAuthStatus to simply use the cryptoService.hasUserKey check to determine the user's auth status. * PM-7235 - CryptoSvc - getUserKey - remove setUserKey side effect if auto key is stored. Will move to app init * PM-7235 - For each client init service, add setUserKeyInMemoryIfAutoUserKeySet logic * PM-7235 - CryptoSvc tests - remove uncessary test. * PM-7235 - Create UserKeyInitService and inject into all init services with new listening logic to support acct switching. * PM-7235 - UserKeyInitSvc - minor refactor of setUserKeyInMemoryIfAutoUserKeySet * PM-7235 - Add test suite for UserKeyInitService * PM-7235 - Remove everBeenUnlocked as it is no longer needed * PM-7235 - Fix tests * PM-7235 - UserKeyInitSvc - per PR feedback, add error handling to protect observable stream from being cancelled in case of an error * PM-7235 - Fix tests * Update libs/common/src/platform/services/user-key-init.service.ts Co-authored-by: Matt Gibson * Update libs/common/src/platform/services/user-key-init.service.ts Co-authored-by: Matt Gibson * PM-7235 - AuthSvc - Per PR review, for getAuthStatus, only check user key existence in memory. * PM-7235 - remove not useful test per PR feedback. * PM-7235 - Per PR feedback, update cryptoService.hasUserKey to only check memory for the user key. * PM-7235 - Per PR feedback, move user key init service listener to main.background instead of init service * PM-7235 - UserKeyInitSvc tests - fix tests to plass --------- Co-authored-by: Matt Gibson --- .../browser/src/background/main.background.ts | 12 ++ .../src/popup/services/init.service.ts | 1 - apps/desktop/src/app/services/init.service.ts | 4 + apps/web/src/app/core/init.service.ts | 3 + .../src/auth/components/lock.component.ts | 1 - .../src/services/jslib-services.module.ts | 7 + libs/common/src/auth/services/auth.service.ts | 29 +--- .../platform/abstractions/state.service.ts | 2 - .../src/platform/models/domain/account.ts | 1 - .../platform/services/crypto.service.spec.ts | 27 +-- .../src/platform/services/crypto.service.ts | 20 +-- .../src/platform/services/state.service.ts | 18 -- .../services/user-key-init.service.spec.ts | 162 ++++++++++++++++++ .../services/user-key-init.service.ts | 57 ++++++ .../vault-timeout-settings.service.ts | 1 - .../vault-timeout.service.spec.ts | 1 - .../vault-timeout/vault-timeout.service.ts | 1 - 17 files changed, 253 insertions(+), 94 deletions(-) create mode 100644 libs/common/src/platform/services/user-key-init.service.spec.ts create mode 100644 libs/common/src/platform/services/user-key-init.service.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 325a7f19432..8432c398b73 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -110,6 +110,7 @@ import { MigrationBuilderService } from "@bitwarden/common/platform/services/mig import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; import { SystemService } from "@bitwarden/common/platform/services/system.service"; +import { UserKeyInitService } from "@bitwarden/common/platform/services/user-key-init.service"; import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service"; import { ActiveUserStateProvider, @@ -325,6 +326,7 @@ export default class MainBackground { stateEventRunnerService: StateEventRunnerService; ssoLoginService: SsoLoginServiceAbstraction; billingAccountProfileStateService: BillingAccountProfileStateService; + userKeyInitService: UserKeyInitService; scriptInjectorService: BrowserScriptInjectorService; onUpdatedRan: boolean; @@ -1046,6 +1048,12 @@ export default class MainBackground { ); } } + + this.userKeyInitService = new UserKeyInitService( + this.accountService, + this.cryptoService, + this.logService, + ); } async bootstrap() { @@ -1053,6 +1061,10 @@ export default class MainBackground { await this.stateService.init({ runMigrations: !this.isPrivateMode }); + // This is here instead of in in the InitService b/c we don't plan for + // side effects to run in the Browser InitService. + this.userKeyInitService.listenForActiveUserChangesToSetUserKey(); + await (this.i18nService as I18nService).init(); await (this.eventUploadService as EventUploadService).init(true); this.twoFactorService.init(); diff --git a/apps/browser/src/popup/services/init.service.ts b/apps/browser/src/popup/services/init.service.ts index 4036ace31fd..c9e6d66c2af 100644 --- a/apps/browser/src/popup/services/init.service.ts +++ b/apps/browser/src/popup/services/init.service.ts @@ -9,7 +9,6 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { BrowserApi } from "../../platform/browser/browser-api"; import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; import { BrowserStateService as StateServiceAbstraction } from "../../platform/services/abstractions/browser-state.service"; - @Injectable() export class InitService { constructor( diff --git a/apps/desktop/src/app/services/init.service.ts b/apps/desktop/src/app/services/init.service.ts index d1a83d468c1..ae2e1ba97cb 100644 --- a/apps/desktop/src/app/services/init.service.ts +++ b/apps/desktop/src/app/services/init.service.ts @@ -12,6 +12,7 @@ import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platfor import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; +import { UserKeyInitService } from "@bitwarden/common/platform/services/user-key-init.service"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service"; import { SyncService as SyncServiceAbstraction } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; @@ -35,6 +36,7 @@ export class InitService { private nativeMessagingService: NativeMessagingService, private themingService: AbstractThemingService, private encryptService: EncryptService, + private userKeyInitService: UserKeyInitService, @Inject(DOCUMENT) private document: Document, ) {} @@ -42,6 +44,8 @@ export class InitService { return async () => { this.nativeMessagingService.init(); await this.stateService.init({ runMigrations: false }); // Desktop will run them in main process + this.userKeyInitService.listenForActiveUserChangesToSetUserKey(); + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.syncService.fullSync(true); diff --git a/apps/web/src/app/core/init.service.ts b/apps/web/src/app/core/init.service.ts index d5576d3bf70..dab6ed5e3de 100644 --- a/apps/web/src/app/core/init.service.ts +++ b/apps/web/src/app/core/init.service.ts @@ -11,6 +11,7 @@ import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt. import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; +import { UserKeyInitService } from "@bitwarden/common/platform/services/user-key-init.service"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service"; @@ -27,12 +28,14 @@ export class InitService { private cryptoService: CryptoServiceAbstraction, private themingService: AbstractThemingService, private encryptService: EncryptService, + private userKeyInitService: UserKeyInitService, @Inject(DOCUMENT) private document: Document, ) {} init() { return async () => { await this.stateService.init(); + this.userKeyInitService.listenForActiveUserChangesToSetUserKey(); setTimeout(() => this.notificationsService.init(), 3000); await this.vaultTimeoutService.init(true); diff --git a/libs/angular/src/auth/components/lock.component.ts b/libs/angular/src/auth/components/lock.component.ts index 6602a917c91..9c2ed55357f 100644 --- a/libs/angular/src/auth/components/lock.component.ts +++ b/libs/angular/src/auth/components/lock.component.ts @@ -283,7 +283,6 @@ export class LockComponent implements OnInit, OnDestroy { } private async doContinue(evaluatePasswordAfterUnlock: boolean) { - await this.stateService.setEverBeenUnlocked(true); await this.biometricStateService.resetUserPromptCancelled(); this.messagingService.send("unlocked"); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 859103474df..b0d84e7c3b2 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -52,6 +52,7 @@ import { ProviderApiService } from "@bitwarden/common/admin-console/services/pro import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service"; import { AccountApiService as AccountApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/account-api.service"; import { + AccountService, AccountService as AccountServiceAbstraction, InternalAccountService, } from "@bitwarden/common/auth/abstractions/account.service"; @@ -154,6 +155,7 @@ import { MigrationRunner } from "@bitwarden/common/platform/services/migration-r import { NoopNotificationsService } from "@bitwarden/common/platform/services/noop-notifications.service"; import { StateService } from "@bitwarden/common/platform/services/state.service"; import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; +import { UserKeyInitService } from "@bitwarden/common/platform/services/user-key-init.service"; import { ValidationService } from "@bitwarden/common/platform/services/validation.service"; import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service"; import { @@ -1115,6 +1117,11 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultOrganizationManagementPreferencesService, deps: [StateProvider], }), + safeProvider({ + provide: UserKeyInitService, + useClass: UserKeyInitService, + deps: [AccountService, CryptoServiceAbstraction, LogService], + }), safeProvider({ provide: ErrorHandler, useClass: LoggingErrorHandler, diff --git a/libs/common/src/auth/services/auth.service.ts b/libs/common/src/auth/services/auth.service.ts index 7a29d313e7f..c9e711b4cc5 100644 --- a/libs/common/src/auth/services/auth.service.ts +++ b/libs/common/src/auth/services/auth.service.ts @@ -12,7 +12,6 @@ import { ApiService } from "../../abstractions/api.service"; import { CryptoService } from "../../platform/abstractions/crypto.service"; import { MessagingService } from "../../platform/abstractions/messaging.service"; import { StateService } from "../../platform/abstractions/state.service"; -import { KeySuffixOptions } from "../../platform/enums"; import { UserId } from "../../types/guid"; import { AccountService } from "../abstractions/account.service"; import { AuthService as AuthServiceAbstraction } from "../abstractions/auth.service"; @@ -91,31 +90,11 @@ export class AuthService implements AuthServiceAbstraction { return AuthenticationStatus.LoggedOut; } - // If we don't have a user key in memory, we're locked - if (!(await this.cryptoService.hasUserKeyInMemory(userId))) { - // Check if the user has vault timeout set to never and verify that - // they've never unlocked their vault - const neverLock = - (await this.cryptoService.hasUserKeyStored(KeySuffixOptions.Auto, userId)) && - !(await this.stateService.getEverBeenUnlocked({ userId: userId })); + // Note: since we aggresively set the auto user key to memory if it exists on app init (see InitService) + // we only need to check if the user key is in memory. + const hasUserKey = await this.cryptoService.hasUserKeyInMemory(userId as UserId); - if (neverLock) { - // Attempt to get the key from storage and set it in memory - const userKey = await this.cryptoService.getUserKeyFromStorage( - KeySuffixOptions.Auto, - userId, - ); - await this.cryptoService.setUserKey(userKey, userId); - } - } - - // We do another check here in case setting the auto key failed - const hasKeyInMemory = await this.cryptoService.hasUserKeyInMemory(userId); - if (!hasKeyInMemory) { - return AuthenticationStatus.Locked; - } - - return AuthenticationStatus.Unlocked; + return hasUserKey ? AuthenticationStatus.Unlocked : AuthenticationStatus.Locked; } logOut(callback: () => void) { diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index 27026ac0ea9..051604f0ae5 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -148,8 +148,6 @@ export abstract class StateService { * @deprecated For migration purposes only, use setEncryptedUserKeyPin instead */ setEncryptedPinProtected: (value: string, options?: StorageOptions) => Promise; - getEverBeenUnlocked: (options?: StorageOptions) => Promise; - setEverBeenUnlocked: (value: boolean, options?: StorageOptions) => Promise; getIsAuthenticated: (options?: StorageOptions) => Promise; getKdfConfig: (options?: StorageOptions) => Promise; setKdfConfig: (kdfConfig: KdfConfig, options?: StorageOptions) => Promise; diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index ae7780ada47..5a9a7646962 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -126,7 +126,6 @@ export class AccountProfile { name?: string; email?: string; emailVerified?: boolean; - everBeenUnlocked?: boolean; lastSync?: string; userId?: string; kdfIterations?: number; diff --git a/libs/common/src/platform/services/crypto.service.spec.ts b/libs/common/src/platform/services/crypto.service.spec.ts index 16e6d4aa633..2f68cf2ce7c 100644 --- a/libs/common/src/platform/services/crypto.service.spec.ts +++ b/libs/common/src/platform/services/crypto.service.spec.ts @@ -91,21 +91,7 @@ describe("cryptoService", () => { expect(userKey).toEqual(mockUserKey); }); - it("sets from the Auto key if the User Key if not set", async () => { - const autoKeyB64 = - "IT5cA1i5Hncd953pb00E58D2FqJX+fWTj4AvoI67qkGHSQPgulAqKv+LaKRAo9Bg0xzP9Nw00wk4TqjMmGSM+g=="; - stateService.getUserKeyAutoUnlock.mockResolvedValue(autoKeyB64); - const setKeySpy = jest.spyOn(cryptoService, "setUserKey"); - - const userKey = await cryptoService.getUserKey(mockUserId); - - expect(setKeySpy).toHaveBeenCalledWith(expect.any(SymmetricCryptoKey), mockUserId); - expect(setKeySpy).toHaveBeenCalledTimes(1); - - expect(userKey.keyB64).toEqual(autoKeyB64); - }); - - it("returns nullish if there is no auto key and the user key is not set", async () => { + it("returns nullish if the user key is not set", async () => { const userKey = await cryptoService.getUserKey(mockUserId); expect(userKey).toBeFalsy(); @@ -147,17 +133,6 @@ describe("cryptoService", () => { }, ); - describe("hasUserKey", () => { - it.each([true, false])( - "returns %s when the user key is not in memory, but the auto key is set", - async (hasKey) => { - stateProvider.singleUser.getFake(mockUserId, USER_KEY).nextState(null); - cryptoService.hasUserKeyStored = jest.fn().mockResolvedValue(hasKey); - expect(await cryptoService.hasUserKey(mockUserId)).toBe(hasKey); - }, - ); - }); - describe("getUserKeyWithLegacySupport", () => { let mockUserKey: UserKey; let mockMasterKey: MasterKey; diff --git a/libs/common/src/platform/services/crypto.service.ts b/libs/common/src/platform/services/crypto.service.ts index c091b6a5a9d..3cd443c0738 100644 --- a/libs/common/src/platform/services/crypto.service.ts +++ b/libs/common/src/platform/services/crypto.service.ts @@ -164,19 +164,8 @@ export class CryptoService implements CryptoServiceAbstraction { } async getUserKey(userId?: UserId): Promise { - let userKey = await firstValueFrom(this.stateProvider.getUserState$(USER_KEY, userId)); - if (userKey) { - return userKey; - } - - // If the user has set their vault timeout to 'Never', we can load the user key from storage - if (await this.hasUserKeyStored(KeySuffixOptions.Auto, userId)) { - userKey = await this.getKeyFromStorage(KeySuffixOptions.Auto, userId); - if (userKey) { - await this.setUserKey(userKey, userId); - return userKey; - } - } + const userKey = await firstValueFrom(this.stateProvider.getUserState$(USER_KEY, userId)); + return userKey; } async isLegacyUser(masterKey?: MasterKey, userId?: UserId): Promise { @@ -217,10 +206,7 @@ export class CryptoService implements CryptoServiceAbstraction { if (userId == null) { return false; } - return ( - (await this.hasUserKeyInMemory(userId)) || - (await this.hasUserKeyStored(KeySuffixOptions.Auto, userId)) - ); + return await this.hasUserKeyInMemory(userId); } async hasUserKeyInMemory(userId?: UserId): Promise { diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 1fb2d716705..f660cd7a342 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -634,24 +634,6 @@ export class StateService< ); } - async getEverBeenUnlocked(options?: StorageOptions): Promise { - return ( - (await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))) - ?.profile?.everBeenUnlocked ?? false - ); - } - - async setEverBeenUnlocked(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - account.profile.everBeenUnlocked = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - } - async getIsAuthenticated(options?: StorageOptions): Promise { return ( (await this.tokenService.getAccessToken(options?.userId as UserId)) != null && diff --git a/libs/common/src/platform/services/user-key-init.service.spec.ts b/libs/common/src/platform/services/user-key-init.service.spec.ts new file mode 100644 index 00000000000..567320ded6a --- /dev/null +++ b/libs/common/src/platform/services/user-key-init.service.spec.ts @@ -0,0 +1,162 @@ +import { mock } from "jest-mock-extended"; + +import { FakeAccountService, mockAccountServiceWith } from "../../../spec"; +import { CsprngArray } from "../../types/csprng"; +import { UserId } from "../../types/guid"; +import { UserKey } from "../../types/key"; +import { LogService } from "../abstractions/log.service"; +import { KeySuffixOptions } from "../enums"; +import { Utils } from "../misc/utils"; +import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; + +import { CryptoService } from "./crypto.service"; +import { UserKeyInitService } from "./user-key-init.service"; + +describe("UserKeyInitService", () => { + let userKeyInitService: UserKeyInitService; + + const mockUserId = Utils.newGuid() as UserId; + + const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); + + const cryptoService = mock(); + const logService = mock(); + + beforeEach(() => { + userKeyInitService = new UserKeyInitService(accountService, cryptoService, logService); + }); + + describe("listenForActiveUserChangesToSetUserKey()", () => { + it("calls setUserKeyInMemoryIfAutoUserKeySet if there is an active user", () => { + // Arrange + accountService.activeAccountSubject.next({ + id: mockUserId, + name: "name", + email: "email", + }); + + (userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet = jest.fn(); + + // Act + + const subscription = userKeyInitService.listenForActiveUserChangesToSetUserKey(); + + // Assert + + expect(subscription).not.toBeFalsy(); + + expect((userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet).toHaveBeenCalledWith( + mockUserId, + ); + }); + + it("calls setUserKeyInMemoryIfAutoUserKeySet if there is an active user and tracks subsequent emissions", () => { + // Arrange + accountService.activeAccountSubject.next({ + id: mockUserId, + name: "name", + email: "email", + }); + + const mockUser2Id = Utils.newGuid() as UserId; + + jest + .spyOn(userKeyInitService as any, "setUserKeyInMemoryIfAutoUserKeySet") + .mockImplementation(() => Promise.resolve()); + + // Act + + const subscription = userKeyInitService.listenForActiveUserChangesToSetUserKey(); + + accountService.activeAccountSubject.next({ + id: mockUser2Id, + name: "name", + email: "email", + }); + + // Assert + + expect(subscription).not.toBeFalsy(); + + expect((userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet).toHaveBeenCalledTimes( + 2, + ); + + expect( + (userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet, + ).toHaveBeenNthCalledWith(1, mockUserId); + expect( + (userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet, + ).toHaveBeenNthCalledWith(2, mockUser2Id); + + subscription.unsubscribe(); + }); + + it("does not call setUserKeyInMemoryIfAutoUserKeySet if there is not an active user", () => { + // Arrange + accountService.activeAccountSubject.next(null); + + (userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet = jest.fn(); + + // Act + + const subscription = userKeyInitService.listenForActiveUserChangesToSetUserKey(); + + // Assert + + expect(subscription).not.toBeFalsy(); + + expect( + (userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet, + ).not.toHaveBeenCalledWith(mockUserId); + }); + }); + + describe("setUserKeyInMemoryIfAutoUserKeySet", () => { + it("does nothing if the userId is null", async () => { + // Act + await (userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet(null); + + // Assert + expect(cryptoService.getUserKeyFromStorage).not.toHaveBeenCalled(); + expect(cryptoService.setUserKey).not.toHaveBeenCalled(); + }); + + it("does nothing if the autoUserKey is null", async () => { + // Arrange + const userId = mockUserId; + + cryptoService.getUserKeyFromStorage.mockResolvedValue(null); + + // Act + await (userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet(userId); + + // Assert + expect(cryptoService.getUserKeyFromStorage).toHaveBeenCalledWith( + KeySuffixOptions.Auto, + userId, + ); + expect(cryptoService.setUserKey).not.toHaveBeenCalled(); + }); + + it("sets the user key in memory if the autoUserKey is not null", async () => { + // Arrange + const userId = mockUserId; + + const mockRandomBytes = new Uint8Array(64) as CsprngArray; + const mockAutoUserKey: UserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey; + + cryptoService.getUserKeyFromStorage.mockResolvedValue(mockAutoUserKey); + + // Act + await (userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet(userId); + + // Assert + expect(cryptoService.getUserKeyFromStorage).toHaveBeenCalledWith( + KeySuffixOptions.Auto, + userId, + ); + expect(cryptoService.setUserKey).toHaveBeenCalledWith(mockAutoUserKey, userId); + }); + }); +}); diff --git a/libs/common/src/platform/services/user-key-init.service.ts b/libs/common/src/platform/services/user-key-init.service.ts new file mode 100644 index 00000000000..1f6aacce8f3 --- /dev/null +++ b/libs/common/src/platform/services/user-key-init.service.ts @@ -0,0 +1,57 @@ +import { EMPTY, Subscription, catchError, filter, from, switchMap } from "rxjs"; + +import { AccountService } from "../../auth/abstractions/account.service"; +import { UserId } from "../../types/guid"; +import { CryptoService } from "../abstractions/crypto.service"; +import { LogService } from "../abstractions/log.service"; +import { KeySuffixOptions } from "../enums"; + +// TODO: this is a half measure improvement which allows us to reduce some side effects today (cryptoService.getUserKey setting user key in memory if auto key exists) +// but ideally, in the future, we would be able to put this logic into the cryptoService +// after the vault timeout settings service is transitioned to state provider so that +// the getUserKey logic can simply go to the correct location based on the vault timeout settings +// similar to the TokenService (it would either go to secure storage for the auto user key or memory for the user key) + +export class UserKeyInitService { + constructor( + private accountService: AccountService, + private cryptoService: CryptoService, + private logService: LogService, + ) {} + + // Note: must listen for changes to support account switching + listenForActiveUserChangesToSetUserKey(): Subscription { + return this.accountService.activeAccount$ + .pipe( + filter((activeAccount) => activeAccount != null), + switchMap((activeAccount) => + from(this.setUserKeyInMemoryIfAutoUserKeySet(activeAccount?.id)).pipe( + catchError((err: unknown) => { + this.logService.warning( + `setUserKeyInMemoryIfAutoUserKeySet failed with error: ${err}`, + ); + // Returning EMPTY to protect observable chain from cancellation in case of error + return EMPTY; + }), + ), + ), + ) + .subscribe(); + } + + private async setUserKeyInMemoryIfAutoUserKeySet(userId: UserId) { + if (userId == null) { + return; + } + + const autoUserKey = await this.cryptoService.getUserKeyFromStorage( + KeySuffixOptions.Auto, + userId, + ); + if (autoUserKey == null) { + return; + } + + await this.cryptoService.setUserKey(autoUserKey, userId); + } +} diff --git a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts index a8afc632972..4fac3be9c97 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts @@ -172,7 +172,6 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA } async clear(userId?: string): Promise { - await this.stateService.setEverBeenUnlocked(false, { userId: userId }); await this.cryptoService.clearPinKeys(userId); } diff --git a/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts b/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts index 243b644dd84..5344093a25a 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts @@ -174,7 +174,6 @@ describe("VaultTimeoutService", () => { // This does NOT assert all the things that the lock process does expect(stateService.getIsAuthenticated).toHaveBeenCalledWith({ userId: userId }); expect(vaultTimeoutSettingsService.availableVaultTimeoutActions$).toHaveBeenCalledWith(userId); - expect(stateService.setEverBeenUnlocked).toHaveBeenCalledWith(true, { userId: userId }); expect(stateService.setUserKeyAutoUnlock).toHaveBeenCalledWith(null, { userId: userId }); expect(masterPasswordService.mock.clearMasterKey).toHaveBeenCalledWith(userId); expect(cipherService.clearCache).toHaveBeenCalledWith(userId); diff --git a/libs/common/src/services/vault-timeout/vault-timeout.service.ts b/libs/common/src/services/vault-timeout/vault-timeout.service.ts index 35faf0fceeb..8baf6c04c49 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.ts @@ -98,7 +98,6 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { await this.masterPasswordService.clearMasterKey((userId ?? currentUserId) as UserId); - await this.stateService.setEverBeenUnlocked(true, { userId: userId }); await this.stateService.setUserKeyAutoUnlock(null, { userId: userId }); await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId }); From 1e67014158267477adb8cb563be49189cb7624be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= Date: Fri, 19 Apr 2024 13:12:17 -0400 Subject: [PATCH 013/110] fix update loop when overwriting state from buffer (#8834) --- .../state/buffered-key-definition.ts | 6 +- .../generator/state/buffered-state.spec.ts | 26 +++--- .../tools/generator/state/buffered-state.ts | 87 ++++++++----------- 3 files changed, 57 insertions(+), 62 deletions(-) diff --git a/libs/common/src/tools/generator/state/buffered-key-definition.ts b/libs/common/src/tools/generator/state/buffered-key-definition.ts index 5457410f80a..1f112808392 100644 --- a/libs/common/src/tools/generator/state/buffered-key-definition.ts +++ b/libs/common/src/tools/generator/state/buffered-key-definition.ts @@ -87,9 +87,13 @@ export class BufferedKeyDefinition { } /** Checks whether the input type can be converted to the output type. - * @returns `true` if the definition is valid, otherwise `false`. + * @returns `true` if the definition is defined and valid, otherwise `false`. */ isValid(input: Input, dependency: Dependency) { + if (input === null) { + return Promise.resolve(false); + } + const isValid = this.options?.isValid; if (isValid) { return isValid(input, dependency); diff --git a/libs/common/src/tools/generator/state/buffered-state.spec.ts b/libs/common/src/tools/generator/state/buffered-state.spec.ts index 7f9722d3846..46e132c1bde 100644 --- a/libs/common/src/tools/generator/state/buffered-state.spec.ts +++ b/libs/common/src/tools/generator/state/buffered-state.spec.ts @@ -75,14 +75,16 @@ describe("BufferedState", () => { it("rolls over pending values from the buffered state immediately by default", async () => { const provider = new FakeStateProvider(accountService); const outputState = provider.getUser(SomeUser, SOME_KEY); - await outputState.update(() => ({ foo: true, bar: false })); + const initialValue = { foo: true, bar: false }; + await outputState.update(() => initialValue); const bufferedState = new BufferedState(provider, BUFFER_KEY, outputState); const bufferedValue = { foo: true, bar: true }; await provider.setUserState(BUFFER_KEY.toKeyDefinition(), bufferedValue, SomeUser); - const result = await firstValueFrom(bufferedState.state$); + const result = await trackEmissions(bufferedState.state$); + await awaitAsync(); - expect(result).toEqual(bufferedValue); + expect(result).toEqual([initialValue, bufferedValue]); }); // also important for data migrations @@ -131,14 +133,16 @@ describe("BufferedState", () => { }); const provider = new FakeStateProvider(accountService); const outputState = provider.getUser(SomeUser, SOME_KEY); - await outputState.update(() => ({ foo: true, bar: false })); + const initialValue = { foo: true, bar: false }; + await outputState.update(() => initialValue); const bufferedState = new BufferedState(provider, bufferedKey, outputState); const bufferedValue = { foo: true, bar: true }; await provider.setUserState(bufferedKey.toKeyDefinition(), bufferedValue, SomeUser); - const result = await firstValueFrom(bufferedState.state$); + const result = await trackEmissions(bufferedState.state$); + await awaitAsync(); - expect(result).toEqual(bufferedValue); + expect(result).toEqual([initialValue, bufferedValue]); }); it("reads from the output state when shouldOverwrite returns a falsy value", async () => { @@ -274,7 +278,7 @@ describe("BufferedState", () => { await bufferedState.buffer(bufferedValue); await awaitAsync(); - expect(result).toEqual([firstValue, firstValue]); + expect(result).toEqual([firstValue]); }); it("replaces the output state when its dependency becomes true", async () => { @@ -296,7 +300,7 @@ describe("BufferedState", () => { dependency.next(true); await awaitAsync(); - expect(result).toEqual([firstValue, firstValue, bufferedValue]); + expect(result).toEqual([firstValue, bufferedValue]); }); it.each([[null], [undefined]])("ignores `%p`", async (bufferedValue) => { @@ -325,11 +329,13 @@ describe("BufferedState", () => { await outputState.update(() => firstValue); const bufferedState = new BufferedState(provider, bufferedKey, outputState); - const result = trackEmissions(bufferedState.state$); + const stateResult = trackEmissions(bufferedState.state$); await bufferedState.buffer({ foo: true, bar: true }); await awaitAsync(); + const bufferedResult = await firstValueFrom(bufferedState.bufferedState$); - expect(result).toEqual([firstValue, firstValue]); + expect(stateResult).toEqual([firstValue]); + expect(bufferedResult).toBeNull(); }); it("overwrites the output when isValid returns true", async () => { diff --git a/libs/common/src/tools/generator/state/buffered-state.ts b/libs/common/src/tools/generator/state/buffered-state.ts index 42b14b815cb..bb4de645e9c 100644 --- a/libs/common/src/tools/generator/state/buffered-state.ts +++ b/libs/common/src/tools/generator/state/buffered-state.ts @@ -1,4 +1,4 @@ -import { Observable, combineLatest, concatMap, filter, map, of } from "rxjs"; +import { Observable, combineLatest, concatMap, filter, map, of, concat, merge } from "rxjs"; import { StateProvider, @@ -33,68 +33,53 @@ export class BufferedState implements SingleUserState private output: SingleUserState, dependency$: Observable = null, ) { - this.bufferState = provider.getUser(output.userId, key.toKeyDefinition()); + this.bufferedState = provider.getUser(output.userId, key.toKeyDefinition()); - const watching = [ - this.bufferState.state$, - this.output.state$, - dependency$ ?? of(true as unknown as Dependency), - ] as const; - - this.state$ = combineLatest(watching).pipe( - concatMap(async ([input, output, dependency]) => { - const normalized = input ?? null; - - const canOverwrite = normalized !== null && key.shouldOverwrite(dependency); - if (canOverwrite) { - await this.updateOutput(dependency); - - // prevent duplicate updates by suppressing the update - return [false, output] as const; + // overwrite the output value + const hasValue$ = concat(of(null), this.bufferedState.state$).pipe( + map((buffer) => (buffer ?? null) !== null), + ); + const overwriteDependency$ = (dependency$ ?? of(true as unknown as Dependency)).pipe( + map((dependency) => [key.shouldOverwrite(dependency), dependency] as const), + ); + const overwrite$ = combineLatest([hasValue$, overwriteDependency$]).pipe( + concatMap(async ([hasValue, [shouldOverwrite, dependency]]) => { + if (hasValue && shouldOverwrite) { + await this.overwriteOutput(dependency); } - - return [true, output] as const; + return [false, null] as const; }), - filter(([updated]) => updated), + ); + + // drive overwrites only when there's a subscription; + // the output state determines when emissions occur + const output$ = this.output.state$.pipe(map((output) => [true, output] as const)); + this.state$ = merge(overwrite$, output$).pipe( + filter(([emit]) => emit), map(([, output]) => output), ); this.combinedState$ = this.state$.pipe(map((state) => [this.output.userId, state])); - this.bufferState$ = this.bufferState.state$; + this.bufferedState$ = this.bufferedState.state$; } - private bufferState: SingleUserState; + private bufferedState: SingleUserState; - private async updateOutput(dependency: Dependency) { - // retrieve the latest input value - let input: Input; - await this.bufferState.update((state) => state, { - shouldUpdate: (state) => { - input = state; - return false; - }, + private async overwriteOutput(dependency: Dependency) { + // take the latest value from the buffer + let buffered: Input; + await this.bufferedState.update((state) => { + buffered = state ?? null; + return null; }); - // bail if this update lost the race with the last update - if (input === null) { - return; + // update the output state + const isValid = await this.key.isValid(buffered, dependency); + if (isValid) { + const output = await this.key.map(buffered, dependency); + await this.output.update(() => output); } - - // destroy invalid data and bail - if (!(await this.key.isValid(input, dependency))) { - await this.bufferState.update(() => null); - return; - } - - // overwrite anything left to the output; the updates need to be awaited with `Promise.all` - // so that `inputState.update(() => null)` runs before `shouldUpdate` reads the value (above). - // This lets the emission from `this.outputState.update` renter the `concatMap`. If the - // awaits run in sequence, it can win the race and cause a double emission. - const output = await this.key.map(input, dependency); - await Promise.all([this.output.update(() => output), this.bufferState.update(() => null)]); - - return; } /** {@link SingleUserState.userId} */ @@ -119,14 +104,14 @@ export class BufferedState implements SingleUserState async buffer(value: Input): Promise { const normalized = value ?? null; if (normalized !== null) { - await this.bufferState.update(() => normalized); + await this.bufferedState.update(() => normalized); } } /** The data presently being buffered. This emits the pending value each time * new buffer data is provided. It emits null when the buffer is empty. */ - readonly bufferState$: Observable; + readonly bufferedState$: Observable; /** Updates the output state. * @param configureState a callback that returns an updated output From ddee74fdee4d79541bd4404a72ab25efa5cb51d2 Mon Sep 17 00:00:00 2001 From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Date: Fri, 19 Apr 2024 13:15:30 -0400 Subject: [PATCH 014/110] Removed 2023 plans for view for grandfathered 2020 providers (#8804) --- .../organizations/organization-plans.component.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.ts b/apps/web/src/app/billing/organizations/organization-plans.component.ts index 30691ce87d5..645b1f29ac4 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.ts +++ b/apps/web/src/app/billing/organizations/organization-plans.component.ts @@ -48,11 +48,7 @@ interface OnSuccessArgs { organizationId: string; } -const AllowedLegacyPlanTypes = [ - PlanType.TeamsMonthly2023, - PlanType.TeamsAnnually2023, - PlanType.EnterpriseAnnually2023, - PlanType.EnterpriseMonthly2023, +const Allowed2020PlansForLegacyProviders = [ PlanType.TeamsMonthly2020, PlanType.TeamsAnnually2020, PlanType.EnterpriseAnnually2020, @@ -283,7 +279,8 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { (!this.currentPlan || this.currentPlan.upgradeSortOrder < plan.upgradeSortOrder) && (!this.hasProvider || plan.product !== ProductType.TeamsStarter) && ((!this.isProviderQualifiedFor2020Plan() && this.planIsEnabled(plan)) || - (this.isProviderQualifiedFor2020Plan() && AllowedLegacyPlanTypes.includes(plan.type))), + (this.isProviderQualifiedFor2020Plan() && + Allowed2020PlansForLegacyProviders.includes(plan.type))), ); result.sort((planA, planB) => planA.displaySortOrder - planB.displaySortOrder); @@ -298,7 +295,8 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { (plan) => plan.product === selectedProductType && ((!this.isProviderQualifiedFor2020Plan() && this.planIsEnabled(plan)) || - (this.isProviderQualifiedFor2020Plan() && AllowedLegacyPlanTypes.includes(plan.type))), + (this.isProviderQualifiedFor2020Plan() && + Allowed2020PlansForLegacyProviders.includes(plan.type))), ) || []; result.sort((planA, planB) => planA.displaySortOrder - planB.displaySortOrder); From d55d240b1886c7a6b26e65a09468e8c3fe7f6d8f Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Fri, 19 Apr 2024 13:23:42 -0400 Subject: [PATCH 015/110] Update host permission to all urls (#8831) Discussions on this permission here: https://github.com/bitwarden/clients/pull/5985 --- apps/browser/src/manifest.v3.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index 6c58b405f42..cdd0869fc52 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -64,7 +64,7 @@ "offscreen" ], "optional_permissions": ["nativeMessaging", "privacy"], - "host_permissions": ["*://*/*"], + "host_permissions": [""], "content_security_policy": { "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'", "sandbox": "sandbox allow-scripts; script-src 'self'" From c1bbf675e2545173704be7e6cd1d6a3b57dc985e Mon Sep 17 00:00:00 2001 From: Joseph Flinn <58369717+joseph-flinn@users.noreply.github.com> Date: Fri, 19 Apr 2024 10:29:50 -0700 Subject: [PATCH 016/110] Update number of translations and give credit to our translators (#8835) --- apps/browser/store/locales/en/copy.resx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/browser/store/locales/en/copy.resx b/apps/browser/store/locales/en/copy.resx index df8d63835c4..82e4eb1d88e 100644 --- a/apps/browser/store/locales/en/copy.resx +++ b/apps/browser/store/locales/en/copy.resx @@ -159,7 +159,7 @@ Built-in Generator Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. Global Translations -Bitwarden translations exist for more than 50 languages. +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. From 9a4279c8bbb50a9c68503621d0b61811afdc2716 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 19 Apr 2024 17:41:41 +0000 Subject: [PATCH 017/110] Autosync the updated translations (#8836) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/af/messages.json | 3 --- apps/desktop/src/locales/ar/messages.json | 3 --- apps/desktop/src/locales/az/messages.json | 3 --- apps/desktop/src/locales/be/messages.json | 3 --- apps/desktop/src/locales/bg/messages.json | 7 ++----- apps/desktop/src/locales/bn/messages.json | 3 --- apps/desktop/src/locales/bs/messages.json | 3 --- apps/desktop/src/locales/ca/messages.json | 3 --- apps/desktop/src/locales/cs/messages.json | 3 --- apps/desktop/src/locales/cy/messages.json | 3 --- apps/desktop/src/locales/da/messages.json | 3 --- apps/desktop/src/locales/de/messages.json | 3 --- apps/desktop/src/locales/el/messages.json | 3 --- apps/desktop/src/locales/en_GB/messages.json | 3 --- apps/desktop/src/locales/en_IN/messages.json | 3 --- apps/desktop/src/locales/eo/messages.json | 3 --- apps/desktop/src/locales/es/messages.json | 3 --- apps/desktop/src/locales/et/messages.json | 3 --- apps/desktop/src/locales/eu/messages.json | 3 --- apps/desktop/src/locales/fa/messages.json | 3 --- apps/desktop/src/locales/fi/messages.json | 11 ++++------- apps/desktop/src/locales/fil/messages.json | 3 --- apps/desktop/src/locales/fr/messages.json | 3 --- apps/desktop/src/locales/gl/messages.json | 3 --- apps/desktop/src/locales/he/messages.json | 3 --- apps/desktop/src/locales/hi/messages.json | 3 --- apps/desktop/src/locales/hr/messages.json | 3 --- apps/desktop/src/locales/hu/messages.json | 7 ++----- apps/desktop/src/locales/id/messages.json | 13 +++++-------- apps/desktop/src/locales/it/messages.json | 7 ++----- apps/desktop/src/locales/ja/messages.json | 3 --- apps/desktop/src/locales/ka/messages.json | 3 --- apps/desktop/src/locales/km/messages.json | 3 --- apps/desktop/src/locales/kn/messages.json | 3 --- apps/desktop/src/locales/ko/messages.json | 3 --- apps/desktop/src/locales/lt/messages.json | 3 --- apps/desktop/src/locales/lv/messages.json | 7 ++----- apps/desktop/src/locales/me/messages.json | 3 --- apps/desktop/src/locales/ml/messages.json | 3 --- apps/desktop/src/locales/mr/messages.json | 3 --- apps/desktop/src/locales/my/messages.json | 3 --- apps/desktop/src/locales/nb/messages.json | 3 --- apps/desktop/src/locales/ne/messages.json | 3 --- apps/desktop/src/locales/nl/messages.json | 3 --- apps/desktop/src/locales/nn/messages.json | 3 --- apps/desktop/src/locales/or/messages.json | 3 --- apps/desktop/src/locales/pl/messages.json | 3 --- apps/desktop/src/locales/pt_BR/messages.json | 3 --- apps/desktop/src/locales/pt_PT/messages.json | 3 --- apps/desktop/src/locales/ro/messages.json | 3 --- apps/desktop/src/locales/ru/messages.json | 3 --- apps/desktop/src/locales/si/messages.json | 3 --- apps/desktop/src/locales/sk/messages.json | 3 --- apps/desktop/src/locales/sl/messages.json | 3 --- apps/desktop/src/locales/sr/messages.json | 3 --- apps/desktop/src/locales/sv/messages.json | 3 --- apps/desktop/src/locales/te/messages.json | 3 --- apps/desktop/src/locales/th/messages.json | 3 --- apps/desktop/src/locales/tr/messages.json | 3 --- apps/desktop/src/locales/uk/messages.json | 3 --- apps/desktop/src/locales/vi/messages.json | 3 --- apps/desktop/src/locales/zh_CN/messages.json | 11 ++++------- apps/desktop/src/locales/zh_TW/messages.json | 3 --- 63 files changed, 21 insertions(+), 210 deletions(-) diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json index 97067b788a5..afdfc90d766 100644 --- a/apps/desktop/src/locales/af/messages.json +++ b/apps/desktop/src/locales/af/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json index 2d25269fffe..7869b0894bc 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -2697,9 +2697,6 @@ "message": "تنسيقات مشتركة", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index 1ecd18eee75..d4cea4f06ec 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -2697,9 +2697,6 @@ "message": "Ortaq formatlar", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Uğurlu" - }, "troubleshooting": { "message": "Problemlərin aradan qaldırılması" }, diff --git a/apps/desktop/src/locales/be/messages.json b/apps/desktop/src/locales/be/messages.json index e0133e5a742..53e3ec2d127 100644 --- a/apps/desktop/src/locales/be/messages.json +++ b/apps/desktop/src/locales/be/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index 20f47d4bcd8..f4886c420f6 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -1633,10 +1633,10 @@ "message": "Интеграцията с браузър не се поддържа" }, "browserIntegrationErrorTitle": { - "message": "Error enabling browser integration" + "message": "Грешка при включването на интеграцията с браузъра" }, "browserIntegrationErrorDesc": { - "message": "An error has occurred while enabling browser integration." + "message": "Възникна грешка при включването на интеграцията с браузъра." }, "browserIntegrationMasOnlyDesc": { "message": "За жалост в момента интеграцията с браузър не се поддържа във версията за магазина на Mac." @@ -2697,9 +2697,6 @@ "message": "Често използвани формати", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Отстраняване на проблеми" }, diff --git a/apps/desktop/src/locales/bn/messages.json b/apps/desktop/src/locales/bn/messages.json index 626734ebffc..abd2c1cfaed 100644 --- a/apps/desktop/src/locales/bn/messages.json +++ b/apps/desktop/src/locales/bn/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/bs/messages.json b/apps/desktop/src/locales/bs/messages.json index 9d5685cca90..825bd6344e0 100644 --- a/apps/desktop/src/locales/bs/messages.json +++ b/apps/desktop/src/locales/bs/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index d8c0f32948b..6c48d6cb0b6 100644 --- a/apps/desktop/src/locales/ca/messages.json +++ b/apps/desktop/src/locales/ca/messages.json @@ -2697,9 +2697,6 @@ "message": "Formats comuns", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Resolució de problemes" }, diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index e68fe8fffc4..550b10a31c1 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -2697,9 +2697,6 @@ "message": "Společné formáty", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Úspěch" - }, "troubleshooting": { "message": "Řešení problémů" }, diff --git a/apps/desktop/src/locales/cy/messages.json b/apps/desktop/src/locales/cy/messages.json index 62f2e608bbf..b1cc9e63d31 100644 --- a/apps/desktop/src/locales/cy/messages.json +++ b/apps/desktop/src/locales/cy/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index 0e578a6f66e..f2a84a3c293 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -2697,9 +2697,6 @@ "message": "Almindelige formater", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Gennemført" - }, "troubleshooting": { "message": "Fejlfinding" }, diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index bba1ccec151..e5e3945abca 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -2697,9 +2697,6 @@ "message": "Gängigste Formate", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Problembehandlung" }, diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index 87360c33ce0..41e6a62a2fd 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -2697,9 +2697,6 @@ "message": "Κοινές μορφές", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Αντιμετώπιση Προβλημάτων" }, diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index 5c8c32b7c14..2658610df37 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json index abfa0b1c0da..0542da9ddc8 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/eo/messages.json b/apps/desktop/src/locales/eo/messages.json index 427f08f8052..1c4cc4f0bef 100644 --- a/apps/desktop/src/locales/eo/messages.json +++ b/apps/desktop/src/locales/eo/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index f7df93bdd72..ec5da442931 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/et/messages.json b/apps/desktop/src/locales/et/messages.json index 02cd737baac..3850cc1d858 100644 --- a/apps/desktop/src/locales/et/messages.json +++ b/apps/desktop/src/locales/et/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json index 2067b2dcc2e..b21108b6add 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/fa/messages.json b/apps/desktop/src/locales/fa/messages.json index ef34f8222ae..08356d410d7 100644 --- a/apps/desktop/src/locales/fa/messages.json +++ b/apps/desktop/src/locales/fa/messages.json @@ -2697,9 +2697,6 @@ "message": "فرمت‌های رایج", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index 03059f7ef31..fca24c197a1 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -1633,10 +1633,10 @@ "message": "Selainintegraatiota ei tueta" }, "browserIntegrationErrorTitle": { - "message": "Error enabling browser integration" + "message": "Virhe otettaessa selainintegrointia käyttöön" }, "browserIntegrationErrorDesc": { - "message": "An error has occurred while enabling browser integration." + "message": "Otettaessa selainintegraatiota käyttöön tapahtui virhe." }, "browserIntegrationMasOnlyDesc": { "message": "Valitettavasti selainintegraatiota tuetaan toistaiseksi vain Mac App Store -versiossa." @@ -2697,9 +2697,6 @@ "message": "Yleiset muodot", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Vianetsintä" }, @@ -2716,9 +2713,9 @@ "message": "Suojausavain poistettiin" }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "Virhe määritettäessä kohdekokoelmaa." }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "Virhe määritettäessä kohdekansiota." } } diff --git a/apps/desktop/src/locales/fil/messages.json b/apps/desktop/src/locales/fil/messages.json index d28a4b568c3..6d5f85fca8c 100644 --- a/apps/desktop/src/locales/fil/messages.json +++ b/apps/desktop/src/locales/fil/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index 86550b736f3..1097624b147 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -2697,9 +2697,6 @@ "message": "Formats communs", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Résolution de problèmes" }, diff --git a/apps/desktop/src/locales/gl/messages.json b/apps/desktop/src/locales/gl/messages.json index 889a2beeee0..90648699c0f 100644 --- a/apps/desktop/src/locales/gl/messages.json +++ b/apps/desktop/src/locales/gl/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json index 3b155ffdf3e..73599c012ee 100644 --- a/apps/desktop/src/locales/he/messages.json +++ b/apps/desktop/src/locales/he/messages.json @@ -2697,9 +2697,6 @@ "message": "תסדירים נפוצים", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/hi/messages.json b/apps/desktop/src/locales/hi/messages.json index af28c666813..4f8bb9b4bb8 100644 --- a/apps/desktop/src/locales/hi/messages.json +++ b/apps/desktop/src/locales/hi/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json index 01983d5891e..220c8bfab23 100644 --- a/apps/desktop/src/locales/hr/messages.json +++ b/apps/desktop/src/locales/hr/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index ecf77e2f347..149d48284ed 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -1633,10 +1633,10 @@ "message": "A böngésző integráció nem támogatott." }, "browserIntegrationErrorTitle": { - "message": "Error enabling browser integration" + "message": "Böngésző integráció engedélyezése" }, "browserIntegrationErrorDesc": { - "message": "An error has occurred while enabling browser integration." + "message": "Hiba történt a böngésző integrációjának engedélyezése közben." }, "browserIntegrationMasOnlyDesc": { "message": "Sajnos a böngésző integrációt egyelőre csak a Mac App Store verzió támogatja." @@ -2697,9 +2697,6 @@ "message": "Általános formátumok", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Hibaelhárítás" }, diff --git a/apps/desktop/src/locales/id/messages.json b/apps/desktop/src/locales/id/messages.json index cd36126b050..3194b0f7d31 100644 --- a/apps/desktop/src/locales/id/messages.json +++ b/apps/desktop/src/locales/id/messages.json @@ -404,7 +404,7 @@ "message": "Panjang" }, "passwordMinLength": { - "message": "Minimum password length" + "message": "Panjang kata sandi minimum" }, "uppercase": { "message": "Huruf Kapital (A-Z)" @@ -545,7 +545,7 @@ "message": "Diperlukan pengetikan ulang kata sandi utama." }, "masterPasswordMinlength": { - "message": "Master password must be at least $VALUE$ characters long.", + "message": "Kata sandi utama minimal harus $VALUE$ karakter.", "description": "The Master Password must be at least a specific number of characters long.", "placeholders": { "value": { @@ -561,7 +561,7 @@ "message": "Akun baru Anda telah dibuat! Sekarang Anda bisa masuk." }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "Anda berhasil masuk" }, "youMayCloseThisWindow": { "message": "You may close this window" @@ -801,10 +801,10 @@ "message": "Ubah Kata Sandi Utama" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "Lanjutkan ke aplikasi web?" }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "Anda bisa mengganti kata sandi utama Anda di aplikasi web Bitwarden." }, "fingerprintPhrase": { "message": "Frase Fingerprint", @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index 0eeea259e2e..08ae2d9da85 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -1633,10 +1633,10 @@ "message": "L'integrazione del browser non è supportata" }, "browserIntegrationErrorTitle": { - "message": "Error enabling browser integration" + "message": "Errore durante l'attivazione dell'integrazione del browser" }, "browserIntegrationErrorDesc": { - "message": "An error has occurred while enabling browser integration." + "message": "Si è verificato un errore durante l'attivazione dell'integrazione del browser." }, "browserIntegrationMasOnlyDesc": { "message": "Purtroppo l'integrazione del browser è supportata solo nella versione nell'App Store per ora." @@ -2697,9 +2697,6 @@ "message": "Formati comuni", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Risoluzione problemi" }, diff --git a/apps/desktop/src/locales/ja/messages.json b/apps/desktop/src/locales/ja/messages.json index ab6c0be95f7..b07eab20cc7 100644 --- a/apps/desktop/src/locales/ja/messages.json +++ b/apps/desktop/src/locales/ja/messages.json @@ -2697,9 +2697,6 @@ "message": "一般的な形式", "description": "Label indicating the most common import formats" }, - "success": { - "message": "成功" - }, "troubleshooting": { "message": "トラブルシューティング" }, diff --git a/apps/desktop/src/locales/ka/messages.json b/apps/desktop/src/locales/ka/messages.json index 889a2beeee0..90648699c0f 100644 --- a/apps/desktop/src/locales/ka/messages.json +++ b/apps/desktop/src/locales/ka/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/km/messages.json b/apps/desktop/src/locales/km/messages.json index 889a2beeee0..90648699c0f 100644 --- a/apps/desktop/src/locales/km/messages.json +++ b/apps/desktop/src/locales/km/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/kn/messages.json b/apps/desktop/src/locales/kn/messages.json index eb0cbcf6be7..162cba3a75e 100644 --- a/apps/desktop/src/locales/kn/messages.json +++ b/apps/desktop/src/locales/kn/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json index 8e50ade96cd..09b1767af0b 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json index e9de6970054..de77aa8fbf3 100644 --- a/apps/desktop/src/locales/lt/messages.json +++ b/apps/desktop/src/locales/lt/messages.json @@ -2697,9 +2697,6 @@ "message": "Dažni formatai", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index 0227a8c5241..521a0afcf20 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -1633,10 +1633,10 @@ "message": "Sasaistīšana ar pārlūku nav atbalstīta" }, "browserIntegrationErrorTitle": { - "message": "Error enabling browser integration" + "message": "Kļūda pārlūga saistīšanas iespējošanā" }, "browserIntegrationErrorDesc": { - "message": "An error has occurred while enabling browser integration." + "message": "Atgadījās kļūda pārlūka saistīšanas iespējošanas laikā." }, "browserIntegrationMasOnlyDesc": { "message": "Diemžēl sasaistīšāna ar pārlūku pagaidām ir nodrošināta tikai Mac App Store laidienā." @@ -2697,9 +2697,6 @@ "message": "Izplatīti veidoli", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Sarežģījumu novēršana" }, diff --git a/apps/desktop/src/locales/me/messages.json b/apps/desktop/src/locales/me/messages.json index 1f49961b469..ed458379b8b 100644 --- a/apps/desktop/src/locales/me/messages.json +++ b/apps/desktop/src/locales/me/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/ml/messages.json b/apps/desktop/src/locales/ml/messages.json index 96811b9dba8..b94b9d1b796 100644 --- a/apps/desktop/src/locales/ml/messages.json +++ b/apps/desktop/src/locales/ml/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/mr/messages.json b/apps/desktop/src/locales/mr/messages.json index 889a2beeee0..90648699c0f 100644 --- a/apps/desktop/src/locales/mr/messages.json +++ b/apps/desktop/src/locales/mr/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/my/messages.json b/apps/desktop/src/locales/my/messages.json index 0ee0db69ef2..5142c8e61f7 100644 --- a/apps/desktop/src/locales/my/messages.json +++ b/apps/desktop/src/locales/my/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/nb/messages.json b/apps/desktop/src/locales/nb/messages.json index 7bf132bdac1..e190cfc236a 100644 --- a/apps/desktop/src/locales/nb/messages.json +++ b/apps/desktop/src/locales/nb/messages.json @@ -2697,9 +2697,6 @@ "message": "Vanlige formater", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/ne/messages.json b/apps/desktop/src/locales/ne/messages.json index 13e14668054..bd58a18b0d3 100644 --- a/apps/desktop/src/locales/ne/messages.json +++ b/apps/desktop/src/locales/ne/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index b5f2a413d6f..3ca07307105 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -2697,9 +2697,6 @@ "message": "Veelvoorkomende formaten", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Probleemoplossing" }, diff --git a/apps/desktop/src/locales/nn/messages.json b/apps/desktop/src/locales/nn/messages.json index 35e7173d74a..12e11b32c11 100644 --- a/apps/desktop/src/locales/nn/messages.json +++ b/apps/desktop/src/locales/nn/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/or/messages.json b/apps/desktop/src/locales/or/messages.json index cd83d2ea698..73635585516 100644 --- a/apps/desktop/src/locales/or/messages.json +++ b/apps/desktop/src/locales/or/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index 250c557309c..df7a158a3a9 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -2697,9 +2697,6 @@ "message": "Popularne formaty", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Sukces" - }, "troubleshooting": { "message": "Rozwiązywanie problemów" }, diff --git a/apps/desktop/src/locales/pt_BR/messages.json b/apps/desktop/src/locales/pt_BR/messages.json index 9a79ad665eb..f651e0b0607 100644 --- a/apps/desktop/src/locales/pt_BR/messages.json +++ b/apps/desktop/src/locales/pt_BR/messages.json @@ -2697,9 +2697,6 @@ "message": "Formatos comuns", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index 14f0ec5d2f7..4e58dad2cf3 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -2697,9 +2697,6 @@ "message": "Formatos comuns", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Com sucesso" - }, "troubleshooting": { "message": "Resolução de problemas" }, diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json index 978f57eb9b7..3fe73f28a75 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index c9b3b95b39c..cc182812a6b 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -2697,9 +2697,6 @@ "message": "Основные форматы", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Успешно" - }, "troubleshooting": { "message": "Устранение проблем" }, diff --git a/apps/desktop/src/locales/si/messages.json b/apps/desktop/src/locales/si/messages.json index 3d439971442..261ae1c9b8d 100644 --- a/apps/desktop/src/locales/si/messages.json +++ b/apps/desktop/src/locales/si/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index 6499486b9d5..6ef52a83eef 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -2697,9 +2697,6 @@ "message": "Bežné formáty", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Úspech" - }, "troubleshooting": { "message": "Riešenie problémov" }, diff --git a/apps/desktop/src/locales/sl/messages.json b/apps/desktop/src/locales/sl/messages.json index 8cb06dcf0ca..8c9c158c87b 100644 --- a/apps/desktop/src/locales/sl/messages.json +++ b/apps/desktop/src/locales/sl/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index 37c5dfa3827..edafdb55a28 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -2697,9 +2697,6 @@ "message": "Уобичајени формати", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Решавање проблема" }, diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index bd21c0f328a..6342424fd44 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -2697,9 +2697,6 @@ "message": "Vanliga format", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Felsökning" }, diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json index 889a2beeee0..90648699c0f 100644 --- a/apps/desktop/src/locales/te/messages.json +++ b/apps/desktop/src/locales/te/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/th/messages.json b/apps/desktop/src/locales/th/messages.json index f1cd5351f7f..5b6a1b9d0b7 100644 --- a/apps/desktop/src/locales/th/messages.json +++ b/apps/desktop/src/locales/th/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index 3e7229c41b8..42a8f207c73 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Sorun giderme" }, diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index 9ee76520933..ac7b7c32435 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -2697,9 +2697,6 @@ "message": "Поширені формати", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Успішно" - }, "troubleshooting": { "message": "Усунення проблем" }, diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json index 0c0e6f6df73..aac9995db13 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index 3819cb967ca..0560466cf81 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -1633,10 +1633,10 @@ "message": "不支持浏览器集成" }, "browserIntegrationErrorTitle": { - "message": "Error enabling browser integration" + "message": "启用浏览器集成时出错" }, "browserIntegrationErrorDesc": { - "message": "An error has occurred while enabling browser integration." + "message": "启用浏览器集成时出错。" }, "browserIntegrationMasOnlyDesc": { "message": "很遗憾,目前仅 Mac App Store 版本支持浏览器集成。" @@ -2697,9 +2697,6 @@ "message": "常规格式", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "故障排除" }, @@ -2716,9 +2713,9 @@ "message": "通行密钥已移除" }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "分配目标集合时出错。" }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "分配目标文件夹时出错。" } } diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index 5f768b0a43e..9eb12e23cf8 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -2697,9 +2697,6 @@ "message": "常見格式", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "疑難排解" }, From 395ed3f5d464355d448204d944eee51094bdc28c Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Fri, 19 Apr 2024 14:02:40 -0500 Subject: [PATCH 018/110] [PM-7489] Introduce `MessageSender` & `MessageListener` (#8709) * Introduce MessageSender * Update `messageSenderFactory` * Remove Comment * Use BrowserApi * Update Comment * Rename to CommandDefinition * Add More Documentation to MessageSender * Add `EMPTY` helpers and remove NoopMessageSender * Calm Down Logging * Limit Logging On Known Errors * Use `messageStream` Parameter Co-authored-by: Matt Gibson * Add eslint rules * Update Error Handling Co-authored-by: Cesar Gonzalez * Delete Lazy Classes In Favor of Observable Factories * Remove Fido Messages --------- Co-authored-by: Matt Gibson Co-authored-by: Cesar Gonzalez --- .eslintrc.json | 16 ++ .../browser/src/background/main.background.ts | 54 +++--- .../background/nativeMessaging.background.ts | 2 +- .../src/background/runtime.background.ts | 156 ++++++++++-------- .../message-sender.factory.ts | 17 ++ .../messaging-service.factory.ts | 24 +-- .../messaging/chrome-message.sender.ts | 37 +++++ ...ssaging-private-mode-background.service.ts | 8 - ...er-messaging-private-mode-popup.service.ts | 8 - .../services/browser-messaging.service.ts | 9 - .../utils/from-chrome-runtime-messaging.ts | 26 +++ apps/browser/src/popup/app.component.ts | 145 ++++++++-------- .../src/popup/services/services.module.ts | 81 +++++++-- apps/cli/src/bw.ts | 10 +- .../src/app/services/services.module.ts | 30 +++- apps/desktop/src/main.ts | 18 +- apps/desktop/src/main/power-monitor.main.ts | 5 +- apps/desktop/src/platform/preload.ts | 21 ++- .../electron-renderer-message.sender.ts | 12 ++ .../electron-renderer-messaging.service.ts | 20 --- .../src/platform/utils/from-ipc-messaging.ts | 15 ++ .../electron-main-messaging.service.ts | 17 +- .../trial-billing-step.component.ts | 3 +- .../organization-plans.component.ts | 3 +- .../app/core/broadcaster-messaging.service.ts | 14 -- apps/web/src/app/core/core.module.ts | 7 - .../vault/components/premium-badge.stories.ts | 10 +- .../platform/services/broadcaster.service.ts | 6 - libs/angular/src/services/injection-tokens.ts | 6 +- .../src/services/jslib-services.module.ts | 30 +++- .../abstractions/messaging.service.ts | 6 +- .../src/platform/messaging/helpers.spec.ts | 46 ++++++ libs/common/src/platform/messaging/helpers.ts | 23 +++ libs/common/src/platform/messaging/index.ts | 4 + .../common/src/platform/messaging/internal.ts | 5 + .../messaging/message.listener.spec.ts | 47 ++++++ .../platform/messaging/message.listener.ts | 41 +++++ .../src/platform/messaging/message.sender.ts | 62 +++++++ .../messaging/subject-message.sender.spec.ts | 65 ++++++++ .../messaging/subject-message.sender.ts | 17 ++ libs/common/src/platform/messaging/types.ts | 13 ++ .../platform/services/broadcaster.service.ts | 34 ---- .../services/default-broadcaster.service.ts | 36 ++++ .../services/noop-messaging.service.ts | 7 - 44 files changed, 855 insertions(+), 361 deletions(-) create mode 100644 apps/browser/src/platform/background/service-factories/message-sender.factory.ts create mode 100644 apps/browser/src/platform/messaging/chrome-message.sender.ts delete mode 100644 apps/browser/src/platform/services/browser-messaging-private-mode-background.service.ts delete mode 100644 apps/browser/src/platform/services/browser-messaging-private-mode-popup.service.ts delete mode 100644 apps/browser/src/platform/services/browser-messaging.service.ts create mode 100644 apps/browser/src/platform/utils/from-chrome-runtime-messaging.ts create mode 100644 apps/desktop/src/platform/services/electron-renderer-message.sender.ts delete mode 100644 apps/desktop/src/platform/services/electron-renderer-messaging.service.ts create mode 100644 apps/desktop/src/platform/utils/from-ipc-messaging.ts delete mode 100644 apps/web/src/app/core/broadcaster-messaging.service.ts delete mode 100644 libs/angular/src/platform/services/broadcaster.service.ts create mode 100644 libs/common/src/platform/messaging/helpers.spec.ts create mode 100644 libs/common/src/platform/messaging/helpers.ts create mode 100644 libs/common/src/platform/messaging/index.ts create mode 100644 libs/common/src/platform/messaging/internal.ts create mode 100644 libs/common/src/platform/messaging/message.listener.spec.ts create mode 100644 libs/common/src/platform/messaging/message.listener.ts create mode 100644 libs/common/src/platform/messaging/message.sender.ts create mode 100644 libs/common/src/platform/messaging/subject-message.sender.spec.ts create mode 100644 libs/common/src/platform/messaging/subject-message.sender.ts create mode 100644 libs/common/src/platform/messaging/types.ts delete mode 100644 libs/common/src/platform/services/broadcaster.service.ts create mode 100644 libs/common/src/platform/services/default-broadcaster.service.ts delete mode 100644 libs/common/src/platform/services/noop-messaging.service.ts diff --git a/.eslintrc.json b/.eslintrc.json index 671e7b2fab0..61bebbf4836 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -246,6 +246,22 @@ } ] } + }, + { + "files": ["**/*.ts"], + "excludedFiles": ["**/platform/**/*.ts"], + "rules": { + "no-restricted-imports": [ + "error", + { + "patterns": [ + "**/platform/**/internal", // General internal pattern + // All features that have been converted to barrel files + "**/platform/messaging/**" + ] + } + ] + } } ] } diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 8432c398b73..f2003f9621c 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1,4 +1,4 @@ -import { firstValueFrom } from "rxjs"; +import { Subject, firstValueFrom, merge } from "rxjs"; import { PinCryptoServiceAbstraction, @@ -82,7 +82,6 @@ import { FileUploadService as FileUploadServiceAbstraction } from "@bitwarden/co import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { AbstractMemoryStorageService, @@ -95,6 +94,9 @@ import { DefaultBiometricStateService, } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; +import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; +// eslint-disable-next-line no-restricted-imports -- Used for dependency creation +import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { AppIdService } from "@bitwarden/common/platform/services/app-id.service"; import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service"; @@ -208,13 +210,14 @@ import { Account } from "../models/account"; import { BrowserApi } from "../platform/browser/browser-api"; import { flagEnabled } from "../platform/flags"; import { UpdateBadge } from "../platform/listeners/update-badge"; +/* eslint-disable no-restricted-imports */ +import { ChromeMessageSender } from "../platform/messaging/chrome-message.sender"; +/* eslint-enable no-restricted-imports */ import { BrowserStateService as StateServiceAbstraction } from "../platform/services/abstractions/browser-state.service"; import { BrowserCryptoService } from "../platform/services/browser-crypto.service"; import { BrowserEnvironmentService } from "../platform/services/browser-environment.service"; import BrowserLocalStorageService from "../platform/services/browser-local-storage.service"; import BrowserMemoryStorageService from "../platform/services/browser-memory-storage.service"; -import BrowserMessagingPrivateModeBackgroundService from "../platform/services/browser-messaging-private-mode-background.service"; -import BrowserMessagingService from "../platform/services/browser-messaging.service"; import { BrowserScriptInjectorService } from "../platform/services/browser-script-injector.service"; import { DefaultBrowserStateService } from "../platform/services/default-browser-state.service"; import I18nService from "../platform/services/i18n.service"; @@ -223,6 +226,7 @@ import { BackgroundPlatformUtilsService } from "../platform/services/platform-ut import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service"; import { BackgroundDerivedStateProvider } from "../platform/state/background-derived-state.provider"; import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service"; +import { fromChromeRuntimeMessaging } from "../platform/utils/from-chrome-runtime-messaging"; import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service"; import FilelessImporterBackground from "../tools/background/fileless-importer.background"; import { Fido2Background as Fido2BackgroundAbstraction } from "../vault/fido2/background/abstractions/fido2.background"; @@ -236,7 +240,7 @@ import { NativeMessagingBackground } from "./nativeMessaging.background"; import RuntimeBackground from "./runtime.background"; export default class MainBackground { - messagingService: MessagingServiceAbstraction; + messagingService: MessageSender; storageService: BrowserLocalStorageService; secureStorageService: AbstractStorageService; memoryStorageService: AbstractMemoryStorageService; @@ -326,6 +330,8 @@ export default class MainBackground { stateEventRunnerService: StateEventRunnerService; ssoLoginService: SsoLoginServiceAbstraction; billingAccountProfileStateService: BillingAccountProfileStateService; + // eslint-disable-next-line rxjs/no-exposed-subjects -- Needed to give access to services module + intraprocessMessagingSubject: Subject>; userKeyInitService: UserKeyInitService; scriptInjectorService: BrowserScriptInjectorService; @@ -369,15 +375,25 @@ export default class MainBackground { const logoutCallback = async (expired: boolean, userId?: UserId) => await this.logout(expired, userId); - this.messagingService = - this.isPrivateMode && BrowserApi.isManifestVersion(2) - ? new BrowserMessagingPrivateModeBackgroundService() - : new BrowserMessagingService(); this.logService = new ConsoleLogService(false); this.cryptoFunctionService = new WebCryptoFunctionService(self); this.keyGenerationService = new KeyGenerationService(this.cryptoFunctionService); this.storageService = new BrowserLocalStorageService(); + this.intraprocessMessagingSubject = new Subject>(); + + this.messagingService = MessageSender.combine( + new SubjectMessageSender(this.intraprocessMessagingSubject), + new ChromeMessageSender(this.logService), + ); + + const messageListener = new MessageListener( + merge( + this.intraprocessMessagingSubject.asObservable(), // For messages from the same context + fromChromeRuntimeMessaging(), // For messages from other contexts + ), + ); + const mv3MemoryStorageCreator = (partitionName: string) => { // TODO: Consider using multithreaded encrypt service in popup only context return new LocalBackedSessionStorageService( @@ -560,21 +576,6 @@ export default class MainBackground { this.twoFactorService = new TwoFactorService(this.i18nService, this.platformUtilsService); - // eslint-disable-next-line - const that = this; - const backgroundMessagingService = new (class extends MessagingServiceAbstraction { - // AuthService should send the messages to the background not popup. - send = (subscriber: string, arg: any = {}) => { - if (BrowserApi.isManifestVersion(3)) { - that.messagingService.send(subscriber, arg); - return; - } - - const message = Object.assign({}, { command: subscriber }, arg); - void that.runtimeBackground.processMessage(message, that as any); - }; - })(); - this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); this.devicesApiService = new DevicesApiServiceImplementation(this.apiService); @@ -605,7 +606,7 @@ export default class MainBackground { this.authService = new AuthService( this.accountService, - backgroundMessagingService, + this.messagingService, this.cryptoService, this.apiService, this.stateService, @@ -626,7 +627,7 @@ export default class MainBackground { this.tokenService, this.appIdService, this.platformUtilsService, - backgroundMessagingService, + this.messagingService, this.logService, this.keyConnectorService, this.environmentService, @@ -914,6 +915,7 @@ export default class MainBackground { this.logService, this.configService, this.fido2Background, + messageListener, ); this.nativeMessagingBackground = new NativeMessagingBackground( this.accountService, diff --git a/apps/browser/src/background/nativeMessaging.background.ts b/apps/browser/src/background/nativeMessaging.background.ts index e5eed06c21b..5ac99611476 100644 --- a/apps/browser/src/background/nativeMessaging.background.ts +++ b/apps/browser/src/background/nativeMessaging.background.ts @@ -399,7 +399,7 @@ export class NativeMessagingBackground { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.runtimeBackground.processMessage({ command: "unlocked" }, null); + this.runtimeBackground.processMessage({ command: "unlocked" }); } break; } diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index 44fe4818e0c..f457889e963 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -1,4 +1,4 @@ -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, mergeMap } from "rxjs"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; @@ -10,6 +10,7 @@ import { SystemService } from "@bitwarden/common/platform/abstractions/system.se import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherType } from "@bitwarden/common/vault/enums"; +import { MessageListener } from "../../../../libs/common/src/platform/messaging"; import { closeUnlockPopout, openSsoAuthResultPopout, @@ -44,6 +45,7 @@ export default class RuntimeBackground { private logService: LogService, private configService: ConfigService, private fido2Background: Fido2Background, + private messageListener: MessageListener, ) { // onInstalled listener must be wired up before anything else, so we do it in the ctor chrome.runtime.onInstalled.addListener((details: any) => { @@ -60,92 +62,47 @@ export default class RuntimeBackground { const backgroundMessageListener = ( msg: any, sender: chrome.runtime.MessageSender, - sendResponse: any, + sendResponse: (response: any) => void, ) => { const messagesWithResponse = ["biometricUnlock"]; if (messagesWithResponse.includes(msg.command)) { - this.processMessage(msg, sender).then( + this.processMessageWithSender(msg, sender).then( (value) => sendResponse({ result: value }), (error) => sendResponse({ error: { ...error, message: error.message } }), ); return true; } - this.processMessage(msg, sender).catch((e) => this.logService.error(e)); + void this.processMessageWithSender(msg, sender).catch((err) => + this.logService.error( + `Error while processing message in RuntimeBackground '${msg?.command}'. Error: ${err?.message ?? "Unknown Error"}`, + ), + ); + return false; }; + this.messageListener.allMessages$ + .pipe( + mergeMap(async (message: any) => { + await this.processMessage(message); + }), + ) + .subscribe(); + + // For messages that require the full on message interface BrowserApi.messageListener("runtime.background", backgroundMessageListener); - if (this.main.popupOnlyContext) { - (self as any).bitwardenBackgroundMessageListener = backgroundMessageListener; - } } - async processMessage(msg: any, sender: chrome.runtime.MessageSender) { + // Messages that need the chrome sender and send back a response need to be registered in this method. + async processMessageWithSender(msg: any, sender: chrome.runtime.MessageSender) { switch (msg.command) { - case "loggedIn": - case "unlocked": { - let item: LockedVaultPendingNotificationsData; - - if (msg.command === "loggedIn") { - await this.sendBwInstalledMessageToVault(); - } - - if (this.lockedVaultPendingNotifications?.length > 0) { - item = this.lockedVaultPendingNotifications.pop(); - await closeUnlockPopout(); - } - - await this.notificationsService.updateConnection(msg.command === "loggedIn"); - await this.main.refreshBadge(); - await this.main.refreshMenu(false); - this.systemService.cancelProcessReload(); - - if (item) { - await BrowserApi.focusWindow(item.commandToRetry.sender.tab.windowId); - await BrowserApi.focusTab(item.commandToRetry.sender.tab.id); - await BrowserApi.tabSendMessageData( - item.commandToRetry.sender.tab, - "unlockCompleted", - item, - ); - } - break; - } - case "addToLockedVaultPendingNotifications": - this.lockedVaultPendingNotifications.push(msg.data); - break; - case "logout": - await this.main.logout(msg.expired, msg.userId); - break; - case "syncCompleted": - if (msg.successfully) { - setTimeout(async () => { - await this.main.refreshBadge(); - await this.main.refreshMenu(); - }, 2000); - await this.configService.ensureConfigFetched(); - } - break; - case "openPopup": - await this.main.openPopup(); - break; case "triggerAutofillScriptInjection": await this.autofillService.injectAutofillScripts(sender.tab, sender.frameId); break; case "bgCollectPageDetails": await this.main.collectPageDetailsForContentScript(sender.tab, msg.sender, sender.frameId); break; - case "bgUpdateContextMenu": - case "editedCipher": - case "addedCipher": - case "deletedCipher": - await this.main.refreshBadge(); - await this.main.refreshMenu(); - break; - case "bgReseedStorage": - await this.main.reseedStorage(); - break; case "collectPageDetailsResponse": switch (msg.sender) { case "autofiller": @@ -209,6 +166,72 @@ export default class RuntimeBackground { break; } break; + case "biometricUnlock": { + const result = await this.main.biometricUnlock(); + return result; + } + } + } + + async processMessage(msg: any) { + switch (msg.command) { + case "loggedIn": + case "unlocked": { + let item: LockedVaultPendingNotificationsData; + + if (msg.command === "loggedIn") { + await this.sendBwInstalledMessageToVault(); + } + + if (this.lockedVaultPendingNotifications?.length > 0) { + item = this.lockedVaultPendingNotifications.pop(); + await closeUnlockPopout(); + } + + await this.notificationsService.updateConnection(msg.command === "loggedIn"); + await this.main.refreshBadge(); + await this.main.refreshMenu(false); + this.systemService.cancelProcessReload(); + + if (item) { + await BrowserApi.focusWindow(item.commandToRetry.sender.tab.windowId); + await BrowserApi.focusTab(item.commandToRetry.sender.tab.id); + await BrowserApi.tabSendMessageData( + item.commandToRetry.sender.tab, + "unlockCompleted", + item, + ); + } + break; + } + case "addToLockedVaultPendingNotifications": + this.lockedVaultPendingNotifications.push(msg.data); + break; + case "logout": + await this.main.logout(msg.expired, msg.userId); + break; + case "syncCompleted": + if (msg.successfully) { + setTimeout(async () => { + await this.main.refreshBadge(); + await this.main.refreshMenu(); + }, 2000); + await this.configService.ensureConfigFetched(); + } + break; + case "openPopup": + await this.main.openPopup(); + break; + case "bgUpdateContextMenu": + case "editedCipher": + case "addedCipher": + case "deletedCipher": + await this.main.refreshBadge(); + await this.main.refreshMenu(); + break; + case "bgReseedStorage": + await this.main.reseedStorage(); + break; case "authResult": { const env = await firstValueFrom(this.environmentService.environment$); const vaultUrl = env.getWebVaultUrl(); @@ -265,9 +288,6 @@ export default class RuntimeBackground { await this.main.clearClipboard(msg.clipboardValue, msg.timeoutMs); break; } - case "biometricUnlock": { - return await this.main.biometricUnlock(); - } } } diff --git a/apps/browser/src/platform/background/service-factories/message-sender.factory.ts b/apps/browser/src/platform/background/service-factories/message-sender.factory.ts new file mode 100644 index 00000000000..6f50b4b8f57 --- /dev/null +++ b/apps/browser/src/platform/background/service-factories/message-sender.factory.ts @@ -0,0 +1,17 @@ +import { MessageSender } from "@bitwarden/common/platform/messaging"; + +import { CachedServices, factory, FactoryOptions } from "./factory-options"; + +type MessagingServiceFactoryOptions = FactoryOptions; + +export type MessageSenderInitOptions = MessagingServiceFactoryOptions; + +export function messageSenderFactory( + cache: { messagingService?: MessageSender } & CachedServices, + opts: MessageSenderInitOptions, +): Promise { + // NOTE: Name needs to match that of MainBackground property until we delete these. + return factory(cache, "messagingService", opts, () => { + throw new Error("Not implemented, not expected to be used."); + }); +} diff --git a/apps/browser/src/platform/background/service-factories/messaging-service.factory.ts b/apps/browser/src/platform/background/service-factories/messaging-service.factory.ts index 46852712aa8..20c6e3f424b 100644 --- a/apps/browser/src/platform/background/service-factories/messaging-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/messaging-service.factory.ts @@ -1,19 +1,5 @@ -import { MessagingService as AbstractMessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; - -import { - CachedServices, - factory, - FactoryOptions, -} from "../../background/service-factories/factory-options"; -import BrowserMessagingService from "../../services/browser-messaging.service"; - -type MessagingServiceFactoryOptions = FactoryOptions; - -export type MessagingServiceInitOptions = MessagingServiceFactoryOptions; - -export function messagingServiceFactory( - cache: { messagingService?: AbstractMessagingService } & CachedServices, - opts: MessagingServiceInitOptions, -): Promise { - return factory(cache, "messagingService", opts, () => new BrowserMessagingService()); -} +// Export old messaging service stuff to minimize changes +export { + messageSenderFactory as messagingServiceFactory, + MessageSenderInitOptions as MessagingServiceInitOptions, +} from "./message-sender.factory"; diff --git a/apps/browser/src/platform/messaging/chrome-message.sender.ts b/apps/browser/src/platform/messaging/chrome-message.sender.ts new file mode 100644 index 00000000000..0e57ecfb4ec --- /dev/null +++ b/apps/browser/src/platform/messaging/chrome-message.sender.ts @@ -0,0 +1,37 @@ +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { CommandDefinition, MessageSender } from "@bitwarden/common/platform/messaging"; +import { getCommand } from "@bitwarden/common/platform/messaging/internal"; + +type ErrorHandler = (logger: LogService, command: string) => void; + +const HANDLED_ERRORS: Record = { + "Could not establish connection. Receiving end does not exist.": (logger, command) => + logger.debug(`Receiving end didn't exist for command '${command}'`), + + "The message port closed before a response was received.": (logger, command) => + logger.debug(`Port was closed for command '${command}'`), +}; + +export class ChromeMessageSender implements MessageSender { + constructor(private readonly logService: LogService) {} + + send( + commandDefinition: string | CommandDefinition, + payload: object | T = {}, + ): void { + const command = getCommand(commandDefinition); + chrome.runtime.sendMessage(Object.assign(payload, { command: command }), () => { + if (chrome.runtime.lastError) { + const errorHandler = HANDLED_ERRORS[chrome.runtime.lastError.message]; + if (errorHandler != null) { + errorHandler(this.logService, command); + return; + } + + this.logService.warning( + `Unhandled error while sending message with command '${command}': ${chrome.runtime.lastError.message}`, + ); + } + }); + } +} diff --git a/apps/browser/src/platform/services/browser-messaging-private-mode-background.service.ts b/apps/browser/src/platform/services/browser-messaging-private-mode-background.service.ts deleted file mode 100644 index 0c7008473bb..00000000000 --- a/apps/browser/src/platform/services/browser-messaging-private-mode-background.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; - -export default class BrowserMessagingPrivateModeBackgroundService implements MessagingService { - send(subscriber: string, arg: any = {}) { - const message = Object.assign({}, { command: subscriber }, arg); - (self as any).bitwardenPopupMainMessageListener(message); - } -} diff --git a/apps/browser/src/platform/services/browser-messaging-private-mode-popup.service.ts b/apps/browser/src/platform/services/browser-messaging-private-mode-popup.service.ts deleted file mode 100644 index 5883f611970..00000000000 --- a/apps/browser/src/platform/services/browser-messaging-private-mode-popup.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; - -export default class BrowserMessagingPrivateModePopupService implements MessagingService { - send(subscriber: string, arg: any = {}) { - const message = Object.assign({}, { command: subscriber }, arg); - (self as any).bitwardenBackgroundMessageListener(message); - } -} diff --git a/apps/browser/src/platform/services/browser-messaging.service.ts b/apps/browser/src/platform/services/browser-messaging.service.ts deleted file mode 100644 index 5eff957cb50..00000000000 --- a/apps/browser/src/platform/services/browser-messaging.service.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; - -import { BrowserApi } from "../browser/browser-api"; - -export default class BrowserMessagingService implements MessagingService { - send(subscriber: string, arg: any = {}) { - return BrowserApi.sendMessage(subscriber, arg); - } -} diff --git a/apps/browser/src/platform/utils/from-chrome-runtime-messaging.ts b/apps/browser/src/platform/utils/from-chrome-runtime-messaging.ts new file mode 100644 index 00000000000..e30f35b680b --- /dev/null +++ b/apps/browser/src/platform/utils/from-chrome-runtime-messaging.ts @@ -0,0 +1,26 @@ +import { map, share } from "rxjs"; + +import { tagAsExternal } from "@bitwarden/common/platform/messaging/internal"; + +import { fromChromeEvent } from "../browser/from-chrome-event"; + +/** + * Creates an observable that listens to messages through `chrome.runtime.onMessage`. + * @returns An observable stream of messages. + */ +export const fromChromeRuntimeMessaging = () => { + return fromChromeEvent(chrome.runtime.onMessage).pipe( + map(([message, sender]) => { + message ??= {}; + + // Force the sender onto the message as long as we won't overwrite anything + if (!("webExtSender" in message)) { + message.webExtSender = sender; + } + + return message; + }), + tagAsExternal, + share(), + ); +}; diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index 2aba93ac951..7acaf1ba937 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -1,17 +1,16 @@ import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import { NavigationEnd, Router, RouterOutlet } from "@angular/router"; -import { filter, concatMap, Subject, takeUntil, firstValueFrom, map } from "rxjs"; +import { filter, concatMap, Subject, takeUntil, firstValueFrom, tap, map } from "rxjs"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { MessageListener } from "@bitwarden/common/platform/messaging"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/components"; import { BrowserApi } from "../platform/browser/browser-api"; -import { ZonedMessageListenerService } from "../platform/browser/zoned-message-listener.service"; import { BrowserStateService } from "../platform/services/abstractions/browser-state.service"; import { BrowserSendStateService } from "../tools/popup/services/browser-send-state.service"; import { VaultBrowserStateService } from "../vault/services/vault-browser-state.service"; @@ -34,7 +33,6 @@ export class AppComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); constructor( - private broadcasterService: BroadcasterService, private authService: AuthService, private i18nService: I18nService, private router: Router, @@ -46,7 +44,7 @@ export class AppComponent implements OnInit, OnDestroy { private ngZone: NgZone, private platformUtilsService: PlatformUtilsService, private dialogService: DialogService, - private browserMessagingApi: ZonedMessageListenerService, + private messageListener: MessageListener, private toastService: ToastService, ) {} @@ -78,77 +76,76 @@ export class AppComponent implements OnInit, OnDestroy { window.onkeypress = () => this.recordActivity(); }); - const bitwardenPopupMainMessageListener = (msg: any, sender: any) => { - if (msg.command === "doneLoggingOut") { - this.authService.logOut(async () => { - if (msg.expired) { - this.toastService.showToast({ - variant: "warning", - title: this.i18nService.t("loggedOut"), - message: this.i18nService.t("loginExpired"), + this.messageListener.allMessages$ + .pipe( + tap((msg: any) => { + if (msg.command === "doneLoggingOut") { + this.authService.logOut(async () => { + if (msg.expired) { + this.toastService.showToast({ + variant: "warning", + title: this.i18nService.t("loggedOut"), + message: this.i18nService.t("loginExpired"), + }); + } + + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.router.navigate(["home"]); }); + this.changeDetectorRef.detectChanges(); + } else if (msg.command === "authBlocked") { + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.router.navigate(["home"]); + } else if ( + msg.command === "locked" && + (msg.userId == null || msg.userId == this.activeUserId) + ) { + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.router.navigate(["lock"]); + } else if (msg.command === "showDialog") { + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.showDialog(msg); + } else if (msg.command === "showNativeMessagingFinterprintDialog") { + // TODO: Should be refactored to live in another service. + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.showNativeMessagingFingerprintDialog(msg); + } else if (msg.command === "showToast") { + this.toastService._showToast(msg); + } else if (msg.command === "reloadProcess") { + const forceWindowReload = + this.platformUtilsService.isSafari() || + this.platformUtilsService.isFirefox() || + this.platformUtilsService.isOpera(); + // Wait to make sure background has reloaded first. + window.setTimeout( + () => BrowserApi.reloadExtension(forceWindowReload ? window : null), + 2000, + ); + } else if (msg.command === "reloadPopup") { + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.router.navigate(["/"]); + } else if (msg.command === "convertAccountToKeyConnector") { + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.router.navigate(["/remove-password"]); + } else if (msg.command === "switchAccountFinish") { + // TODO: unset loading? + // this.loading = false; + } else if (msg.command == "update-temp-password") { + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.router.navigate(["/update-temp-password"]); } - - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["home"]); - }); - this.changeDetectorRef.detectChanges(); - } else if (msg.command === "authBlocked") { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["home"]); - } else if ( - msg.command === "locked" && - (msg.userId == null || msg.userId == this.activeUserId) - ) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["lock"]); - } else if (msg.command === "showDialog") { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.showDialog(msg); - } else if (msg.command === "showNativeMessagingFinterprintDialog") { - // TODO: Should be refactored to live in another service. - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.showNativeMessagingFingerprintDialog(msg); - } else if (msg.command === "showToast") { - this.toastService._showToast(msg); - } else if (msg.command === "reloadProcess") { - const forceWindowReload = - this.platformUtilsService.isSafari() || - this.platformUtilsService.isFirefox() || - this.platformUtilsService.isOpera(); - // Wait to make sure background has reloaded first. - window.setTimeout( - () => BrowserApi.reloadExtension(forceWindowReload ? window : null), - 2000, - ); - } else if (msg.command === "reloadPopup") { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/"]); - } else if (msg.command === "convertAccountToKeyConnector") { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/remove-password"]); - } else if (msg.command === "switchAccountFinish") { - // TODO: unset loading? - // this.loading = false; - } else if (msg.command == "update-temp-password") { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/update-temp-password"]); - } else { - msg.webExtSender = sender; - this.broadcasterService.send(msg); - } - }; - - (self as any).bitwardenPopupMainMessageListener = bitwardenPopupMainMessageListener; - this.browserMessagingApi.messageListener("app.component", bitwardenPopupMainMessageListener); + }), + takeUntil(this.destroy$), + ) + .subscribe(); // eslint-disable-next-line rxjs/no-async-subscribe this.router.events.pipe(takeUntil(this.destroy$)).subscribe(async (event) => { diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index f3be8490c12..4ab1fe23688 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -1,5 +1,6 @@ import { APP_INITIALIZER, NgModule, NgZone } from "@angular/core"; import { Router } from "@angular/router"; +import { Subject, merge } from "rxjs"; import { UnauthGuard as BaseUnauthGuardService } from "@bitwarden/angular/auth/guards"; import { AngularThemingService } from "@bitwarden/angular/platform/services/theming/angular-theming.service"; @@ -11,6 +12,7 @@ import { OBSERVABLE_MEMORY_STORAGE, SYSTEM_THEME_OBSERVABLE, SafeInjectionToken, + INTRAPROCESS_MESSAGING_SUBJECT, } from "@bitwarden/angular/services/injection-tokens"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; import { @@ -54,7 +56,6 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { @@ -63,6 +64,9 @@ import { ObservableStorageService, } from "@bitwarden/common/platform/abstractions/storage.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; +import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; +// eslint-disable-next-line no-restricted-imports -- Used for dependency injection +import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; @@ -89,20 +93,23 @@ import AutofillService from "../../autofill/services/autofill.service"; import MainBackground from "../../background/main.background"; import { Account } from "../../models/account"; import { BrowserApi } from "../../platform/browser/browser-api"; +import { runInsideAngular } from "../../platform/browser/run-inside-angular.operator"; +/* eslint-disable no-restricted-imports */ +import { ChromeMessageSender } from "../../platform/messaging/chrome-message.sender"; +/* eslint-enable no-restricted-imports */ import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; import { BrowserFileDownloadService } from "../../platform/popup/services/browser-file-download.service"; import { BrowserStateService as StateServiceAbstraction } from "../../platform/services/abstractions/browser-state.service"; import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service"; import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service"; import BrowserLocalStorageService from "../../platform/services/browser-local-storage.service"; -import BrowserMessagingPrivateModePopupService from "../../platform/services/browser-messaging-private-mode-popup.service"; -import BrowserMessagingService from "../../platform/services/browser-messaging.service"; import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service"; import { DefaultBrowserStateService } from "../../platform/services/default-browser-state.service"; import I18nService from "../../platform/services/i18n.service"; import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service"; import { ForegroundDerivedStateProvider } from "../../platform/state/foreground-derived-state.provider"; import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service"; +import { fromChromeRuntimeMessaging } from "../../platform/utils/from-chrome-runtime-messaging"; import { BrowserSendStateService } from "../../tools/popup/services/browser-send-state.service"; import { FilePopoutUtilsService } from "../../tools/popup/services/file-popout-utils.service"; import { VaultBrowserStateService } from "../../vault/services/vault-browser-state.service"; @@ -155,15 +162,6 @@ const safeProviders: SafeProvider[] = [ useClass: UnauthGuardService, deps: [AuthServiceAbstraction, Router], }), - safeProvider({ - provide: MessagingService, - useFactory: () => { - return needsBackgroundInit && BrowserApi.isManifestVersion(2) - ? new BrowserMessagingPrivateModePopupService() - : new BrowserMessagingService(); - }, - deps: [], - }), safeProvider({ provide: TwoFactorService, useFactory: getBgService("twoFactorService"), @@ -484,6 +482,65 @@ const safeProviders: SafeProvider[] = [ useClass: BrowserSendStateService, deps: [StateProvider], }), + safeProvider({ + provide: MessageListener, + useFactory: (subject: Subject>, ngZone: NgZone) => + new MessageListener( + merge( + subject.asObservable(), // For messages in the same context + fromChromeRuntimeMessaging().pipe(runInsideAngular(ngZone)), // For messages in the same context + ), + ), + deps: [INTRAPROCESS_MESSAGING_SUBJECT, NgZone], + }), + safeProvider({ + provide: MessageSender, + useFactory: (subject: Subject>, logService: LogService) => + MessageSender.combine( + new SubjectMessageSender(subject), // For sending messages in the same context + new ChromeMessageSender(logService), // For sending messages to different contexts + ), + deps: [INTRAPROCESS_MESSAGING_SUBJECT, LogService], + }), + safeProvider({ + provide: INTRAPROCESS_MESSAGING_SUBJECT, + useFactory: () => { + if (BrowserPopupUtils.backgroundInitializationRequired()) { + // There is no persistent main background which means we have one in memory, + // we need the same instance that our in memory background is utilizing. + return getBgService("intraprocessMessagingSubject")(); + } else { + return new Subject>(); + } + }, + deps: [], + }), + safeProvider({ + provide: MessageSender, + useFactory: (subject: Subject>, logService: LogService) => + MessageSender.combine( + new SubjectMessageSender(subject), // For sending messages in the same context + new ChromeMessageSender(logService), // For sending messages to different contexts + ), + deps: [INTRAPROCESS_MESSAGING_SUBJECT, LogService], + }), + safeProvider({ + provide: INTRAPROCESS_MESSAGING_SUBJECT, + useFactory: () => { + if (needsBackgroundInit) { + // We will have created a popup within this context, in that case + // we want to make sure we have the same subject as that context so we + // can message with it. + return getBgService("intraprocessMessagingSubject")(); + } else { + // There isn't a locally created background so we will communicate with + // the true background through chrome apis, in that case, we can just create + // one for ourself. + return new Subject>(); + } + }, + deps: [], + }), ]; @NgModule({ diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 7fbefc10e31..e784997d824 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -60,10 +60,10 @@ import { } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { KeySuffixOptions, LogLevelType } from "@bitwarden/common/platform/enums"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; +import { MessageSender } from "@bitwarden/common/platform/messaging"; import { Account } from "@bitwarden/common/platform/models/domain/account"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { AppIdService } from "@bitwarden/common/platform/services/app-id.service"; -import { BroadcasterService } from "@bitwarden/common/platform/services/broadcaster.service"; import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service"; import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; @@ -75,7 +75,6 @@ import { KeyGenerationService } from "@bitwarden/common/platform/services/key-ge import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; -import { NoopMessagingService } from "@bitwarden/common/platform/services/noop-messaging.service"; import { StateService } from "@bitwarden/common/platform/services/state.service"; import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; import { @@ -155,7 +154,7 @@ global.DOMParser = new jsdom.JSDOM().window.DOMParser; const packageJson = require("../package.json"); export class Main { - messagingService: NoopMessagingService; + messagingService: MessageSender; storageService: LowdbStorageService; secureStorageService: NodeEnvSecureStorageService; memoryStorageService: MemoryStorageService; @@ -212,7 +211,6 @@ export class Main { organizationService: OrganizationService; providerService: ProviderService; twoFactorService: TwoFactorService; - broadcasterService: BroadcasterService; folderApiService: FolderApiService; userVerificationApiService: UserVerificationApiService; organizationApiService: OrganizationApiServiceAbstraction; @@ -298,7 +296,7 @@ export class Main { stateEventRegistrarService, ); - this.messagingService = new NoopMessagingService(); + this.messagingService = MessageSender.EMPTY; this.accountService = new AccountServiceImplementation( this.messagingService, @@ -422,8 +420,6 @@ export class Main { this.searchService = new SearchService(this.logService, this.i18nService, this.stateProvider); - this.broadcasterService = new BroadcasterService(); - this.collectionService = new CollectionService( this.cryptoService, this.i18nService, diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 264f26cbe2c..d1d51c0f1c2 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -1,4 +1,5 @@ import { APP_INITIALIZER, NgModule } from "@angular/core"; +import { Subject, merge } from "rxjs"; import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; import { @@ -14,6 +15,7 @@ import { SYSTEM_THEME_OBSERVABLE, SafeInjectionToken, STATE_FACTORY, + INTRAPROCESS_MESSAGING_SUBJECT, } from "@bitwarden/angular/services/injection-tokens"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; @@ -23,7 +25,6 @@ import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/ab import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; -import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -42,6 +43,9 @@ import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/ import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/platform/abstractions/system.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; +import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; +// eslint-disable-next-line no-restricted-imports -- Used for dependency injection +import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; @@ -63,11 +67,12 @@ import { ELECTRON_SUPPORTS_SECURE_STORAGE, ElectronPlatformUtilsService, } from "../../platform/services/electron-platform-utils.service"; -import { ElectronRendererMessagingService } from "../../platform/services/electron-renderer-messaging.service"; +import { ElectronRendererMessageSender } from "../../platform/services/electron-renderer-message.sender"; import { ElectronRendererSecureStorageService } from "../../platform/services/electron-renderer-secure-storage.service"; import { ElectronRendererStorageService } from "../../platform/services/electron-renderer-storage.service"; import { ElectronStateService } from "../../platform/services/electron-state.service"; import { I18nRendererService } from "../../platform/services/i18n.renderer.service"; +import { fromIpcMessaging } from "../../platform/utils/from-ipc-messaging"; import { fromIpcSystemTheme } from "../../platform/utils/from-ipc-system-theme"; import { EncryptedMessageHandlerService } from "../../services/encrypted-message-handler.service"; import { NativeMessageHandlerService } from "../../services/native-message-handler.service"; @@ -138,9 +143,24 @@ const safeProviders: SafeProvider[] = [ deps: [SYSTEM_LANGUAGE, LOCALES_DIRECTORY, GlobalStateProvider], }), safeProvider({ - provide: MessagingServiceAbstraction, - useClass: ElectronRendererMessagingService, - deps: [BroadcasterServiceAbstraction], + provide: MessageSender, + useFactory: (subject: Subject>) => + MessageSender.combine( + new ElectronRendererMessageSender(), // Communication with main process + new SubjectMessageSender(subject), // Communication with ourself + ), + deps: [INTRAPROCESS_MESSAGING_SUBJECT], + }), + safeProvider({ + provide: MessageListener, + useFactory: (subject: Subject>) => + new MessageListener( + merge( + subject.asObservable(), // For messages from the same context + fromIpcMessaging(), // For messages from the main process + ), + ), + deps: [INTRAPROCESS_MESSAGING_SUBJECT], }), safeProvider({ provide: AbstractStorageService, diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index a4783e05738..0655e5600d2 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -1,7 +1,7 @@ import * as path from "path"; import { app } from "electron"; -import { firstValueFrom } from "rxjs"; +import { Subject, firstValueFrom } from "rxjs"; import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service"; import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service"; @@ -11,6 +11,9 @@ import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwar import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { DefaultBiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; +import { Message, MessageSender } from "@bitwarden/common/platform/messaging"; +// eslint-disable-next-line no-restricted-imports -- For dependency creation +import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service"; @@ -18,7 +21,6 @@ import { KeyGenerationService } from "@bitwarden/common/platform/services/key-ge import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; -import { NoopMessagingService } from "@bitwarden/common/platform/services/noop-messaging.service"; /* eslint-disable import/no-restricted-paths -- We need the implementation to inject, but generally this should not be accessed */ import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-active-user-state.provider"; @@ -59,7 +61,7 @@ export class Main { storageService: ElectronStorageService; memoryStorageService: MemoryStorageService; memoryStorageForStateProviders: MemoryStorageServiceForStateProviders; - messagingService: ElectronMainMessagingService; + messagingService: MessageSender; stateService: StateService; environmentService: DefaultEnvironmentService; mainCryptoFunctionService: MainCryptoFunctionService; @@ -131,7 +133,7 @@ export class Main { this.i18nService = new I18nMainService("en", "./locales/", globalStateProvider); const accountService = new AccountServiceImplementation( - new NoopMessagingService(), + MessageSender.EMPTY, this.logService, globalStateProvider, ); @@ -223,7 +225,13 @@ export class Main { this.updaterMain = new UpdaterMain(this.i18nService, this.windowMain); this.trayMain = new TrayMain(this.windowMain, this.i18nService, this.desktopSettingsService); - this.messagingService = new ElectronMainMessagingService(this.windowMain, (message) => { + const messageSubject = new Subject>(); + this.messagingService = MessageSender.combine( + new SubjectMessageSender(messageSubject), // For local messages + new ElectronMainMessagingService(this.windowMain), + ); + + messageSubject.asObservable().subscribe((message) => { this.messagingMain.onMessage(message); }); diff --git a/apps/desktop/src/main/power-monitor.main.ts b/apps/desktop/src/main/power-monitor.main.ts index 067a380ba05..8cad5c1d9e2 100644 --- a/apps/desktop/src/main/power-monitor.main.ts +++ b/apps/desktop/src/main/power-monitor.main.ts @@ -1,6 +1,7 @@ import { powerMonitor } from "electron"; -import { ElectronMainMessagingService } from "../services/electron-main-messaging.service"; +import { MessageSender } from "@bitwarden/common/platform/messaging"; + import { isSnapStore } from "../utils"; // tslint:disable-next-line @@ -10,7 +11,7 @@ const IdleCheckInterval = 30 * 1000; // 30 seconds export class PowerMonitorMain { private idle = false; - constructor(private messagingService: ElectronMainMessagingService) {} + constructor(private messagingService: MessageSender) {} init() { // ref: https://github.com/electron/electron/issues/13767 diff --git a/apps/desktop/src/platform/preload.ts b/apps/desktop/src/platform/preload.ts index 04819998d5f..771d25ef0a8 100644 --- a/apps/desktop/src/platform/preload.ts +++ b/apps/desktop/src/platform/preload.ts @@ -124,12 +124,21 @@ export default { sendMessage: (message: { command: string } & any) => ipcRenderer.send("messagingService", message), - onMessage: (callback: (message: { command: string } & any) => void) => { - ipcRenderer.on("messagingService", (_event, message: any) => { - if (message.command) { - callback(message); - } - }); + onMessage: { + addListener: (callback: (message: { command: string } & any) => void) => { + ipcRenderer.addListener("messagingService", (_event, message: any) => { + if (message.command) { + callback(message); + } + }); + }, + removeListener: (callback: (message: { command: string } & any) => void) => { + ipcRenderer.removeListener("messagingService", (_event, message: any) => { + if (message.command) { + callback(message); + } + }); + }, }, launchUri: (uri: string) => ipcRenderer.invoke("launchUri", uri), diff --git a/apps/desktop/src/platform/services/electron-renderer-message.sender.ts b/apps/desktop/src/platform/services/electron-renderer-message.sender.ts new file mode 100644 index 00000000000..037c303b3b6 --- /dev/null +++ b/apps/desktop/src/platform/services/electron-renderer-message.sender.ts @@ -0,0 +1,12 @@ +import { MessageSender, CommandDefinition } from "@bitwarden/common/platform/messaging"; +import { getCommand } from "@bitwarden/common/platform/messaging/internal"; + +export class ElectronRendererMessageSender implements MessageSender { + send( + commandDefinition: CommandDefinition | string, + payload: object | T = {}, + ): void { + const command = getCommand(commandDefinition); + ipc.platform.sendMessage(Object.assign({}, { command: command }, payload)); + } +} diff --git a/apps/desktop/src/platform/services/electron-renderer-messaging.service.ts b/apps/desktop/src/platform/services/electron-renderer-messaging.service.ts deleted file mode 100644 index 192efc1dc6f..00000000000 --- a/apps/desktop/src/platform/services/electron-renderer-messaging.service.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; - -export class ElectronRendererMessagingService implements MessagingService { - constructor(private broadcasterService: BroadcasterService) { - ipc.platform.onMessage((message) => this.sendMessage(message.command, message, false)); - } - - send(subscriber: string, arg: any = {}) { - this.sendMessage(subscriber, arg, true); - } - - private sendMessage(subscriber: string, arg: any = {}, toMain: boolean) { - const message = Object.assign({}, { command: subscriber }, arg); - this.broadcasterService.send(message); - if (toMain) { - ipc.platform.sendMessage(message); - } - } -} diff --git a/apps/desktop/src/platform/utils/from-ipc-messaging.ts b/apps/desktop/src/platform/utils/from-ipc-messaging.ts new file mode 100644 index 00000000000..254a215ceb3 --- /dev/null +++ b/apps/desktop/src/platform/utils/from-ipc-messaging.ts @@ -0,0 +1,15 @@ +import { fromEventPattern, share } from "rxjs"; + +import { Message } from "@bitwarden/common/platform/messaging"; +import { tagAsExternal } from "@bitwarden/common/platform/messaging/internal"; + +/** + * Creates an observable that when subscribed to will listen to messaging events through IPC. + * @returns An observable stream of messages. + */ +export const fromIpcMessaging = () => { + return fromEventPattern>( + (handler) => ipc.platform.onMessage.addListener(handler), + (handler) => ipc.platform.onMessage.removeListener(handler), + ).pipe(tagAsExternal, share()); +}; diff --git a/apps/desktop/src/services/electron-main-messaging.service.ts b/apps/desktop/src/services/electron-main-messaging.service.ts index 71e1b1d7d56..ce4ffd903a8 100644 --- a/apps/desktop/src/services/electron-main-messaging.service.ts +++ b/apps/desktop/src/services/electron-main-messaging.service.ts @@ -2,18 +2,17 @@ import * as path from "path"; import { app, dialog, ipcMain, Menu, MenuItem, nativeTheme, Notification, shell } from "electron"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; +import { MessageSender, CommandDefinition } from "@bitwarden/common/platform/messaging"; +// eslint-disable-next-line no-restricted-imports -- Using implementation helper in implementation +import { getCommand } from "@bitwarden/common/platform/messaging/internal"; import { SafeUrls } from "@bitwarden/common/platform/misc/safe-urls"; import { WindowMain } from "../main/window.main"; import { RendererMenuItem } from "../utils"; -export class ElectronMainMessagingService implements MessagingService { - constructor( - private windowMain: WindowMain, - private onMessage: (message: any) => void, - ) { +export class ElectronMainMessagingService implements MessageSender { + constructor(private windowMain: WindowMain) { ipcMain.handle("appVersion", () => { return app.getVersion(); }); @@ -88,9 +87,9 @@ export class ElectronMainMessagingService implements MessagingService { }); } - send(subscriber: string, arg: any = {}) { - const message = Object.assign({}, { command: subscriber }, arg); - this.onMessage(message); + send(commandDefinition: CommandDefinition | string, arg: T | object = {}) { + const command = getCommand(commandDefinition); + const message = Object.assign({}, { command: command }, arg); if (this.windowMain.win != null) { this.windowMain.win.webContents.send("messagingService", message); } diff --git a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts index 0c5f1d706b3..bd138cad292 100644 --- a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts +++ b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts @@ -103,7 +103,8 @@ export class TrialBillingStepComponent implements OnInit { planDescription, }); - this.messagingService.send("organizationCreated", organizationId); + // TODO: No one actually listening to this? + this.messagingService.send("organizationCreated", { organizationId }); } protected changedCountry() { diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.ts b/apps/web/src/app/billing/organizations/organization-plans.component.ts index 645b1f29ac4..23d48d93be7 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.ts +++ b/apps/web/src/app/billing/organizations/organization-plans.component.ts @@ -587,7 +587,8 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { this.formPromise = doSubmit(); const organizationId = await this.formPromise; this.onSuccess.emit({ organizationId: organizationId }); - this.messagingService.send("organizationCreated", organizationId); + // TODO: No one actually listening to this message? + this.messagingService.send("organizationCreated", { organizationId }); } catch (e) { this.logService.error(e); } diff --git a/apps/web/src/app/core/broadcaster-messaging.service.ts b/apps/web/src/app/core/broadcaster-messaging.service.ts deleted file mode 100644 index 7c8e4eef43c..00000000000 --- a/apps/web/src/app/core/broadcaster-messaging.service.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Injectable } from "@angular/core"; - -import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; - -@Injectable() -export class BroadcasterMessagingService implements MessagingService { - constructor(private broadcasterService: BroadcasterService) {} - - send(subscriber: string, arg: any = {}) { - const message = Object.assign({}, { command: subscriber }, arg); - this.broadcasterService.send(message); - } -} diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index 9d53bc39f04..a2747647564 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -22,7 +22,6 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; @@ -51,7 +50,6 @@ import { WebStorageServiceProvider } from "../platform/web-storage-service.provi import { WindowStorageService } from "../platform/window-storage.service"; import { CollectionAdminService } from "../vault/core/collection-admin.service"; -import { BroadcasterMessagingService } from "./broadcaster-messaging.service"; import { EventService } from "./event.service"; import { InitService } from "./init.service"; import { ModalService } from "./modal.service"; @@ -117,11 +115,6 @@ const safeProviders: SafeProvider[] = [ useClass: WebPlatformUtilsService, useAngularDecorators: true, }), - safeProvider({ - provide: MessagingServiceAbstraction, - useClass: BroadcasterMessagingService, - useAngularDecorators: true, - }), safeProvider({ provide: ModalServiceAbstraction, useClass: ModalService, diff --git a/apps/web/src/app/vault/components/premium-badge.stories.ts b/apps/web/src/app/vault/components/premium-badge.stories.ts index 5433dd99813..c61bbb46a5e 100644 --- a/apps/web/src/app/vault/components/premium-badge.stories.ts +++ b/apps/web/src/app/vault/components/premium-badge.stories.ts @@ -4,15 +4,15 @@ import { of } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { MessageSender } from "@bitwarden/common/platform/messaging"; import { BadgeModule, I18nMockService } from "@bitwarden/components"; import { PremiumBadgeComponent } from "./premium-badge.component"; -class MockMessagingService implements MessagingService { - send(subscriber: string, arg?: any) { +class MockMessagingService implements MessageSender { + send = () => { alert("Clicked on badge"); - } + }; } export default { @@ -31,7 +31,7 @@ export default { }, }, { - provide: MessagingService, + provide: MessageSender, useFactory: () => { return new MockMessagingService(); }, diff --git a/libs/angular/src/platform/services/broadcaster.service.ts b/libs/angular/src/platform/services/broadcaster.service.ts deleted file mode 100644 index cf58d2b311c..00000000000 --- a/libs/angular/src/platform/services/broadcaster.service.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Injectable } from "@angular/core"; - -import { BroadcasterService as BaseBroadcasterService } from "@bitwarden/common/platform/services/broadcaster.service"; - -@Injectable() -export class BroadcasterService extends BaseBroadcasterService {} diff --git a/libs/angular/src/services/injection-tokens.ts b/libs/angular/src/services/injection-tokens.ts index 7d39078797e..6fffe722fbd 100644 --- a/libs/angular/src/services/injection-tokens.ts +++ b/libs/angular/src/services/injection-tokens.ts @@ -1,5 +1,5 @@ import { InjectionToken } from "@angular/core"; -import { Observable } from "rxjs"; +import { Observable, Subject } from "rxjs"; import { AbstractMemoryStorageService, @@ -8,6 +8,7 @@ import { } from "@bitwarden/common/platform/abstractions/storage.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; +import { Message } from "@bitwarden/common/platform/messaging"; declare const tag: unique symbol; /** @@ -49,3 +50,6 @@ export const LOG_MAC_FAILURES = new SafeInjectionToken("LOG_MAC_FAILURE export const SYSTEM_THEME_OBSERVABLE = new SafeInjectionToken>( "SYSTEM_THEME_OBSERVABLE", ); +export const INTRAPROCESS_MESSAGING_SUBJECT = new SafeInjectionToken>>( + "INTRAPROCESS_MESSAGING_SUBJECT", +); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index b0d84e7c3b2..204ff5a294b 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1,4 +1,5 @@ import { ErrorHandler, LOCALE_ID, NgModule } from "@angular/core"; +import { Subject } from "rxjs"; import { AuthRequestServiceAbstraction, @@ -116,7 +117,7 @@ import { BillingApiService } from "@bitwarden/common/billing/services/billing-ap import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service"; import { PaymentMethodWarningsService } from "@bitwarden/common/billing/services/payment-method-warnings.service"; import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service"; @@ -137,6 +138,9 @@ import { DefaultBiometricStateService, } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; +import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; +// eslint-disable-next-line no-restricted-imports -- Used for dependency injection +import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; import { devFlagEnabled, flagEnabled } from "@bitwarden/common/platform/misc/flags"; import { Account } from "@bitwarden/common/platform/models/domain/account"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; @@ -147,6 +151,7 @@ import { ConsoleLogService } from "@bitwarden/common/platform/services/console-l import { CryptoService } from "@bitwarden/common/platform/services/crypto.service"; import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation"; +import { DefaultBroadcasterService } from "@bitwarden/common/platform/services/default-broadcaster.service"; import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service"; import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service"; import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service"; @@ -247,7 +252,6 @@ import { import { AuthGuard } from "../auth/guards/auth.guard"; import { UnauthGuard } from "../auth/guards/unauth.guard"; import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service"; -import { BroadcasterService } from "../platform/services/broadcaster.service"; import { FormValidationErrorsService } from "../platform/services/form-validation-errors.service"; import { LoggingErrorHandler } from "../platform/services/logging-error-handler"; import { AngularThemingService } from "../platform/services/theming/angular-theming.service"; @@ -270,6 +274,7 @@ import { SYSTEM_LANGUAGE, SYSTEM_THEME_OBSERVABLE, WINDOW, + INTRAPROCESS_MESSAGING_SUBJECT, } from "./injection-tokens"; import { ModalService } from "./modal.service"; @@ -625,7 +630,11 @@ const safeProviders: SafeProvider[] = [ BillingAccountProfileStateService, ], }), - safeProvider({ provide: BroadcasterServiceAbstraction, useClass: BroadcasterService, deps: [] }), + safeProvider({ + provide: BroadcasterService, + useClass: DefaultBroadcasterService, + deps: [MessageSender, MessageListener], + }), safeProvider({ provide: VaultTimeoutSettingsServiceAbstraction, useClass: VaultTimeoutSettingsService, @@ -1127,6 +1136,21 @@ const safeProviders: SafeProvider[] = [ useClass: LoggingErrorHandler, deps: [], }), + safeProvider({ + provide: INTRAPROCESS_MESSAGING_SUBJECT, + useFactory: () => new Subject>(), + deps: [], + }), + safeProvider({ + provide: MessageListener, + useFactory: (subject: Subject>) => new MessageListener(subject.asObservable()), + deps: [INTRAPROCESS_MESSAGING_SUBJECT], + }), + safeProvider({ + provide: MessageSender, + useFactory: (subject: Subject>) => new SubjectMessageSender(subject), + deps: [INTRAPROCESS_MESSAGING_SUBJECT], + }), safeProvider({ provide: ProviderApiServiceAbstraction, useClass: ProviderApiService, diff --git a/libs/common/src/platform/abstractions/messaging.service.ts b/libs/common/src/platform/abstractions/messaging.service.ts index ab4332c2839..f24279f932a 100644 --- a/libs/common/src/platform/abstractions/messaging.service.ts +++ b/libs/common/src/platform/abstractions/messaging.service.ts @@ -1,3 +1,3 @@ -export abstract class MessagingService { - abstract send(subscriber: string, arg?: any): void; -} +// Export the new message sender as the legacy MessagingService to minimize changes in the initial PR, +// team specific PR's will come after. +export { MessageSender as MessagingService } from "../messaging/message.sender"; diff --git a/libs/common/src/platform/messaging/helpers.spec.ts b/libs/common/src/platform/messaging/helpers.spec.ts new file mode 100644 index 00000000000..fcd36b44111 --- /dev/null +++ b/libs/common/src/platform/messaging/helpers.spec.ts @@ -0,0 +1,46 @@ +import { Subject, firstValueFrom } from "rxjs"; + +import { getCommand, isExternalMessage, tagAsExternal } from "./helpers"; +import { Message, CommandDefinition } from "./types"; + +describe("helpers", () => { + describe("getCommand", () => { + it("can get the command from just a string", () => { + const command = getCommand("myCommand"); + + expect(command).toEqual("myCommand"); + }); + + it("can get the command from a message definition", () => { + const commandDefinition = new CommandDefinition("myCommand"); + + const command = getCommand(commandDefinition); + + expect(command).toEqual("myCommand"); + }); + }); + + describe("tag integration", () => { + it("can tag and identify as tagged", async () => { + const messagesSubject = new Subject>(); + + const taggedMessages = messagesSubject.asObservable().pipe(tagAsExternal); + + const firstValuePromise = firstValueFrom(taggedMessages); + + messagesSubject.next({ command: "test" }); + + const result = await firstValuePromise; + + expect(isExternalMessage(result)).toEqual(true); + }); + }); + + describe("isExternalMessage", () => { + it.each([null, { command: "myCommand", test: "object" }, undefined] as Message< + Record + >[])("returns false when value is %s", (value: Message) => { + expect(isExternalMessage(value)).toBe(false); + }); + }); +}); diff --git a/libs/common/src/platform/messaging/helpers.ts b/libs/common/src/platform/messaging/helpers.ts new file mode 100644 index 00000000000..bf119432e05 --- /dev/null +++ b/libs/common/src/platform/messaging/helpers.ts @@ -0,0 +1,23 @@ +import { MonoTypeOperatorFunction, map } from "rxjs"; + +import { Message, CommandDefinition } from "./types"; + +export const getCommand = (commandDefinition: CommandDefinition | string) => { + if (typeof commandDefinition === "string") { + return commandDefinition; + } else { + return commandDefinition.command; + } +}; + +export const EXTERNAL_SOURCE_TAG = Symbol("externalSource"); + +export const isExternalMessage = (message: Message) => { + return (message as Record)?.[EXTERNAL_SOURCE_TAG] === true; +}; + +export const tagAsExternal: MonoTypeOperatorFunction> = map( + (message: Message) => { + return Object.assign(message, { [EXTERNAL_SOURCE_TAG]: true }); + }, +); diff --git a/libs/common/src/platform/messaging/index.ts b/libs/common/src/platform/messaging/index.ts new file mode 100644 index 00000000000..a9b4eca5ae8 --- /dev/null +++ b/libs/common/src/platform/messaging/index.ts @@ -0,0 +1,4 @@ +export { MessageListener } from "./message.listener"; +export { MessageSender } from "./message.sender"; +export { Message, CommandDefinition } from "./types"; +export { isExternalMessage } from "./helpers"; diff --git a/libs/common/src/platform/messaging/internal.ts b/libs/common/src/platform/messaging/internal.ts new file mode 100644 index 00000000000..08763d48bc5 --- /dev/null +++ b/libs/common/src/platform/messaging/internal.ts @@ -0,0 +1,5 @@ +// Built in implementations +export { SubjectMessageSender } from "./subject-message.sender"; + +// Helpers meant to be used only by other implementations +export { tagAsExternal, getCommand } from "./helpers"; diff --git a/libs/common/src/platform/messaging/message.listener.spec.ts b/libs/common/src/platform/messaging/message.listener.spec.ts new file mode 100644 index 00000000000..98bbf1fdc82 --- /dev/null +++ b/libs/common/src/platform/messaging/message.listener.spec.ts @@ -0,0 +1,47 @@ +import { Subject } from "rxjs"; + +import { subscribeTo } from "../../../spec/observable-tracker"; + +import { MessageListener } from "./message.listener"; +import { Message, CommandDefinition } from "./types"; + +describe("MessageListener", () => { + const subject = new Subject>(); + const sut = new MessageListener(subject.asObservable()); + + const testCommandDefinition = new CommandDefinition<{ test: number }>("myCommand"); + + describe("allMessages$", () => { + it("runs on all nexts", async () => { + const tracker = subscribeTo(sut.allMessages$); + + const pausePromise = tracker.pauseUntilReceived(2); + + subject.next({ command: "command1", test: 1 }); + subject.next({ command: "command2", test: 2 }); + + await pausePromise; + + expect(tracker.emissions[0]).toEqual({ command: "command1", test: 1 }); + expect(tracker.emissions[1]).toEqual({ command: "command2", test: 2 }); + }); + }); + + describe("messages$", () => { + it("runs on only my commands", async () => { + const tracker = subscribeTo(sut.messages$(testCommandDefinition)); + + const pausePromise = tracker.pauseUntilReceived(2); + + subject.next({ command: "notMyCommand", test: 1 }); + subject.next({ command: "myCommand", test: 2 }); + subject.next({ command: "myCommand", test: 3 }); + subject.next({ command: "notMyCommand", test: 4 }); + + await pausePromise; + + expect(tracker.emissions[0]).toEqual({ command: "myCommand", test: 2 }); + expect(tracker.emissions[1]).toEqual({ command: "myCommand", test: 3 }); + }); + }); +}); diff --git a/libs/common/src/platform/messaging/message.listener.ts b/libs/common/src/platform/messaging/message.listener.ts new file mode 100644 index 00000000000..df453c84226 --- /dev/null +++ b/libs/common/src/platform/messaging/message.listener.ts @@ -0,0 +1,41 @@ +import { EMPTY, Observable, filter } from "rxjs"; + +import { Message, CommandDefinition } from "./types"; + +/** + * A class that allows for listening to messages coming through the application, + * allows for listening of all messages or just the messages you care about. + * + * @note Consider NOT using messaging at all if you can. State Providers offer an observable stream of + * data that is persisted. This can serve messages that might have been used to notify of settings changes + * or vault data changes and those observables should be preferred over messaging. + */ +export class MessageListener { + constructor(private readonly messageStream: Observable>) {} + + /** + * A stream of all messages sent through the application. It does not contain type information for the + * other properties on the messages. You are encouraged to instead subscribe to an individual message + * through {@link messages$}. + */ + allMessages$ = this.messageStream; + + /** + * Creates an observable stream filtered to just the command given via the {@link CommandDefinition} and typed + * to the generic contained in the CommandDefinition. Be careful using this method unless all your messages are being + * sent through `MessageSender.send`, if that isn't the case you should have lower confidence in the message + * payload being the expected type. + * + * @param commandDefinition The CommandDefinition containing the information about the message type you care about. + */ + messages$(commandDefinition: CommandDefinition): Observable { + return this.allMessages$.pipe( + filter((msg) => msg?.command === commandDefinition.command), + ) as Observable; + } + + /** + * A helper property for returning a MessageListener that will never emit any messages and will immediately complete. + */ + static readonly EMPTY = new MessageListener(EMPTY); +} diff --git a/libs/common/src/platform/messaging/message.sender.ts b/libs/common/src/platform/messaging/message.sender.ts new file mode 100644 index 00000000000..6bf26615807 --- /dev/null +++ b/libs/common/src/platform/messaging/message.sender.ts @@ -0,0 +1,62 @@ +import { CommandDefinition } from "./types"; + +class MultiMessageSender implements MessageSender { + constructor(private readonly innerMessageSenders: MessageSender[]) {} + + send( + commandDefinition: string | CommandDefinition, + payload: object | T = {}, + ): void { + for (const messageSender of this.innerMessageSenders) { + messageSender.send(commandDefinition, payload); + } + } +} + +export abstract class MessageSender { + /** + * A method for sending messages in a type safe manner. The passed in command definition + * will require you to provide a compatible type in the payload parameter. + * + * @example + * const MY_COMMAND = new CommandDefinition<{ test: number }>("myCommand"); + * + * this.messageSender.send(MY_COMMAND, { test: 14 }); + * + * @param commandDefinition + * @param payload + */ + abstract send(commandDefinition: CommandDefinition, payload: T): void; + + /** + * A legacy method for sending messages in a non-type safe way. + * + * @remarks Consider defining a {@link CommandDefinition} and passing that in for the first parameter to + * get compilation errors when defining an incompatible payload. + * + * @param command The string based command of your message. + * @param payload Extra contextual information regarding the message. Be aware that this payload may + * be serialized and lose all prototype information. + */ + abstract send(command: string, payload?: object): void; + + /** Implementation of the other two overloads, read their docs instead. */ + abstract send( + commandDefinition: CommandDefinition | string, + payload: T | object, + ): void; + + /** + * A helper method for combine multiple {@link MessageSender}'s. + * @param messageSenders The message senders that should be combined. + * @returns A message sender that will relay all messages to the given message senders. + */ + static combine(...messageSenders: MessageSender[]) { + return new MultiMessageSender(messageSenders); + } + + /** + * A helper property for creating a {@link MessageSender} that sends to nowhere. + */ + static readonly EMPTY: MessageSender = new MultiMessageSender([]); +} diff --git a/libs/common/src/platform/messaging/subject-message.sender.spec.ts b/libs/common/src/platform/messaging/subject-message.sender.spec.ts new file mode 100644 index 00000000000..4278fca7bc1 --- /dev/null +++ b/libs/common/src/platform/messaging/subject-message.sender.spec.ts @@ -0,0 +1,65 @@ +import { Subject } from "rxjs"; + +import { subscribeTo } from "../../../spec/observable-tracker"; + +import { SubjectMessageSender } from "./internal"; +import { MessageSender } from "./message.sender"; +import { Message, CommandDefinition } from "./types"; + +describe("SubjectMessageSender", () => { + const subject = new Subject>(); + const subjectObservable = subject.asObservable(); + + const sut: MessageSender = new SubjectMessageSender(subject); + + describe("send", () => { + it("will send message with command from message definition", async () => { + const commandDefinition = new CommandDefinition<{ test: number }>("myCommand"); + + const tracker = subscribeTo(subjectObservable); + const pausePromise = tracker.pauseUntilReceived(1); + + sut.send(commandDefinition, { test: 1 }); + + await pausePromise; + + expect(tracker.emissions[0]).toEqual({ command: "myCommand", test: 1 }); + }); + + it("will send message with command from normal string", async () => { + const tracker = subscribeTo(subjectObservable); + const pausePromise = tracker.pauseUntilReceived(1); + + sut.send("myCommand", { test: 1 }); + + await pausePromise; + + expect(tracker.emissions[0]).toEqual({ command: "myCommand", test: 1 }); + }); + + it("will send message with object even if payload not given", async () => { + const tracker = subscribeTo(subjectObservable); + const pausePromise = tracker.pauseUntilReceived(1); + + sut.send("myCommand"); + + await pausePromise; + + expect(tracker.emissions[0]).toEqual({ command: "myCommand" }); + }); + + it.each([null, undefined])( + "will send message with object even if payload is null-ish (%s)", + async (payloadValue) => { + const tracker = subscribeTo(subjectObservable); + const pausePromise = tracker.pauseUntilReceived(1); + + sut.send("myCommand", payloadValue); + + await pausePromise; + + expect(tracker.emissions[0]).toEqual({ command: "myCommand" }); + }, + ); + }); +}); diff --git a/libs/common/src/platform/messaging/subject-message.sender.ts b/libs/common/src/platform/messaging/subject-message.sender.ts new file mode 100644 index 00000000000..94ae6f27f3c --- /dev/null +++ b/libs/common/src/platform/messaging/subject-message.sender.ts @@ -0,0 +1,17 @@ +import { Subject } from "rxjs"; + +import { getCommand } from "./internal"; +import { MessageSender } from "./message.sender"; +import { Message, CommandDefinition } from "./types"; + +export class SubjectMessageSender implements MessageSender { + constructor(private readonly messagesSubject: Subject>) {} + + send( + commandDefinition: string | CommandDefinition, + payload: object | T = {}, + ): void { + const command = getCommand(commandDefinition); + this.messagesSubject.next(Object.assign(payload ?? {}, { command: command })); + } +} diff --git a/libs/common/src/platform/messaging/types.ts b/libs/common/src/platform/messaging/types.ts new file mode 100644 index 00000000000..f30163344fd --- /dev/null +++ b/libs/common/src/platform/messaging/types.ts @@ -0,0 +1,13 @@ +declare const tag: unique symbol; + +/** + * A class for defining information about a message, this is helpful + * alonside `MessageSender` and `MessageListener` for providing a type + * safe(-ish) way of sending and receiving messages. + */ +export class CommandDefinition { + [tag]: T; + constructor(readonly command: string) {} +} + +export type Message = { command: string } & T; diff --git a/libs/common/src/platform/services/broadcaster.service.ts b/libs/common/src/platform/services/broadcaster.service.ts deleted file mode 100644 index 9d823b00e04..00000000000 --- a/libs/common/src/platform/services/broadcaster.service.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { - BroadcasterService as BroadcasterServiceAbstraction, - MessageBase, -} from "../abstractions/broadcaster.service"; - -export class BroadcasterService implements BroadcasterServiceAbstraction { - subscribers: Map void> = new Map< - string, - (message: MessageBase) => void - >(); - - send(message: MessageBase, id?: string) { - if (id != null) { - if (this.subscribers.has(id)) { - this.subscribers.get(id)(message); - } - return; - } - - this.subscribers.forEach((value) => { - value(message); - }); - } - - subscribe(id: string, messageCallback: (message: MessageBase) => void) { - this.subscribers.set(id, messageCallback); - } - - unsubscribe(id: string) { - if (this.subscribers.has(id)) { - this.subscribers.delete(id); - } - } -} diff --git a/libs/common/src/platform/services/default-broadcaster.service.ts b/libs/common/src/platform/services/default-broadcaster.service.ts new file mode 100644 index 00000000000..a16745c643d --- /dev/null +++ b/libs/common/src/platform/services/default-broadcaster.service.ts @@ -0,0 +1,36 @@ +import { Subscription } from "rxjs"; + +import { BroadcasterService, MessageBase } from "../abstractions/broadcaster.service"; +import { MessageListener, MessageSender } from "../messaging"; + +/** + * Temporary implementation that just delegates to the message sender and message listener + * and manages their subscriptions. + */ +export class DefaultBroadcasterService implements BroadcasterService { + subscriptions = new Map(); + + constructor( + private readonly messageSender: MessageSender, + private readonly messageListener: MessageListener, + ) {} + + send(message: MessageBase, id?: string) { + this.messageSender.send(message?.command, message); + } + + subscribe(id: string, messageCallback: (message: MessageBase) => void) { + this.subscriptions.set( + id, + this.messageListener.allMessages$.subscribe((message) => { + messageCallback(message); + }), + ); + } + + unsubscribe(id: string) { + const subscription = this.subscriptions.get(id); + subscription?.unsubscribe(); + this.subscriptions.delete(id); + } +} diff --git a/libs/common/src/platform/services/noop-messaging.service.ts b/libs/common/src/platform/services/noop-messaging.service.ts deleted file mode 100644 index d1a60bc5bc1..00000000000 --- a/libs/common/src/platform/services/noop-messaging.service.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { MessagingService } from "../abstractions/messaging.service"; - -export class NoopMessagingService implements MessagingService { - send(subscriber: string, arg: any = {}) { - // Do nothing... - } -} From ec1af0cf9f7588e5ec476d573f2f041bbde0d722 Mon Sep 17 00:00:00 2001 From: Jonathan Prusik Date: Fri, 19 Apr 2024 15:21:54 -0400 Subject: [PATCH 019/110] [PM-7610] [MV3] Guard overlay visibility and autofill on page load settings from awaiting indefinitely when there is no active account (#8833) * guard overlay visibility and autofill on page load settings from awaiting indefinitely when there is no active account * cleanup --- .../autofill-service.factory.ts | 8 +++++++- .../services/autofill.service.spec.ts | 1 + .../src/autofill/services/autofill.service.ts | 20 +++++++++++++++++-- .../browser/src/background/main.background.ts | 3 ++- .../src/popup/services/services.module.ts | 1 + 5 files changed, 29 insertions(+), 4 deletions(-) diff --git a/apps/browser/src/autofill/background/service_factories/autofill-service.factory.ts b/apps/browser/src/autofill/background/service_factories/autofill-service.factory.ts index 7b423ca4f47..bee5da18b5b 100644 --- a/apps/browser/src/autofill/background/service_factories/autofill-service.factory.ts +++ b/apps/browser/src/autofill/background/service_factories/autofill-service.factory.ts @@ -1,3 +1,7 @@ +import { + accountServiceFactory, + AccountServiceInitOptions, +} from "../../../auth/background/service-factories/account-service.factory"; import { UserVerificationServiceInitOptions, userVerificationServiceFactory, @@ -50,7 +54,8 @@ export type AutoFillServiceInitOptions = AutoFillServiceOptions & LogServiceInitOptions & UserVerificationServiceInitOptions & DomainSettingsServiceInitOptions & - BrowserScriptInjectorServiceInitOptions; + BrowserScriptInjectorServiceInitOptions & + AccountServiceInitOptions; export function autofillServiceFactory( cache: { autofillService?: AbstractAutoFillService } & CachedServices, @@ -71,6 +76,7 @@ export function autofillServiceFactory( await userVerificationServiceFactory(cache, opts), await billingAccountProfileStateServiceFactory(cache, opts), await browserScriptInjectorServiceFactory(cache, opts), + await accountServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/autofill/services/autofill.service.spec.ts b/apps/browser/src/autofill/services/autofill.service.spec.ts index 5064d2e7df6..d1fbf79bfaa 100644 --- a/apps/browser/src/autofill/services/autofill.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill.service.spec.ts @@ -87,6 +87,7 @@ describe("AutofillService", () => { userVerificationService, billingAccountProfileStateService, scriptInjectorService, + accountService, ); domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider); diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index 6cf58558dc2..8f85d65692f 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -1,7 +1,9 @@ import { firstValueFrom } from "rxjs"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; @@ -57,6 +59,7 @@ export default class AutofillService implements AutofillServiceInterface { private userVerificationService: UserVerificationService, private billingAccountProfileStateService: BillingAccountProfileStateService, private scriptInjectorService: ScriptInjectorService, + private accountService: AccountService, ) {} /** @@ -104,13 +107,26 @@ export default class AutofillService implements AutofillServiceInterface { frameId = 0, triggeringOnPageLoad = true, ): Promise { - const mainAutofillScript = (await this.getOverlayVisibility()) + // Autofill settings loaded from state can await the active account state indefinitely if + // not guarded by an active account check (e.g. the user is logged in) + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + + // These settings are not available until the user logs in + let overlayVisibility: InlineMenuVisibilitySetting = AutofillOverlayVisibility.Off; + let autoFillOnPageLoadIsEnabled = false; + + if (activeAccount) { + overlayVisibility = await this.getOverlayVisibility(); + } + const mainAutofillScript = overlayVisibility ? "bootstrap-autofill-overlay.js" : "bootstrap-autofill.js"; const injectedScripts = [mainAutofillScript]; - const autoFillOnPageLoadIsEnabled = await this.getAutofillOnPageLoad(); + if (activeAccount) { + autoFillOnPageLoadIsEnabled = await this.getAutofillOnPageLoad(); + } if (triggeringOnPageLoad && autoFillOnPageLoadIsEnabled) { injectedScripts.push("autofiller.js"); diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index f2003f9621c..c627c0032bd 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -29,6 +29,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/services/policy/p import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service"; import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service"; +import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; @@ -137,7 +138,6 @@ import { EventUploadService } from "@bitwarden/common/services/event/event-uploa import { NotificationsService } from "@bitwarden/common/services/notifications.service"; import { SearchService } from "@bitwarden/common/services/search.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service"; -import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/src/auth/abstractions/avatar.service"; import { PasswordGenerationService, PasswordGenerationServiceAbstraction, @@ -807,6 +807,7 @@ export default class MainBackground { this.userVerificationService, this.billingAccountProfileStateService, this.scriptInjectorService, + this.accountService, ); this.auditService = new AuditService(this.cryptoFunctionService, this.apiService); diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 4ab1fe23688..123e901e4e3 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -312,6 +312,7 @@ const safeProviders: SafeProvider[] = [ UserVerificationService, BillingAccountProfileStateService, ScriptInjectorService, + AccountServiceAbstraction, ], }), safeProvider({ From c8a3cb5708429b9807f4917f1b57fa12a1b76a83 Mon Sep 17 00:00:00 2001 From: MtnBurrit0 <77340197+mimartin12@users.noreply.github.com> Date: Fri, 19 Apr 2024 13:39:06 -0600 Subject: [PATCH 020/110] [DEVOPS-1919] - Slack messages contain the incorrect git commit sha (#8813) * Initial run to see what data I can access * Update to use JQ * Use dev action * Implement artifact build sha - Moved notify job to happen post artifact check - Removed git sha job - Updated jobs to use real artifact sha * Update .github/workflows/deploy-web.yml Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com> * Handle web build triggers - Update GH environment with commit as well --------- Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com> --- .github/workflows/deploy-web.yml | 148 ++++++++++++++++--------------- 1 file changed, 77 insertions(+), 71 deletions(-) diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml index 769e7005881..6a5d9f14057 100644 --- a/.github/workflows/deploy-web.yml +++ b/.github/workflows/deploy-web.yml @@ -128,29 +128,90 @@ jobs: - name: Success Code run: exit 0 - get-branch-or-tag-sha: - name: Get Branch or Tag SHA + artifact-check: + name: Check if Web artifact is present runs-on: ubuntu-22.04 + needs: setup + env: + _ENVIRONMENT_ARTIFACT: ${{ needs.setup.outputs.environment-artifact }} outputs: - branch-or-tag-sha: ${{ steps.get-branch-or-tag-sha.outputs.sha }} + artifact-build-commit: ${{ steps.set-artifact-commit.outputs.commit }} steps: - - name: Checkout Branch - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: 'Download latest cloud asset using GitHub Run ID: ${{ inputs.build-web-run-id }}' + if: ${{ inputs.build-web-run-id }} + uses: bitwarden/gh-actions/download-artifacts@main + id: download-latest-artifacts-run-id + continue-on-error: true with: - ref: ${{ inputs.branch-or-tag }} - fetch-depth: 0 + workflow: build-web.yml + path: apps/web + workflow_conclusion: success + run_id: ${{ inputs.build-web-run-id }} + artifacts: ${{ env._ENVIRONMENT_ARTIFACT }} - - name: Get Branch or Tag SHA - id: get-branch-or-tag-sha + - name: 'Download latest cloud asset from branch/tag: ${{ inputs.branch-or-tag }}' + if: ${{ !inputs.build-web-run-id }} + uses: bitwarden/gh-actions/download-artifacts@main + id: download-latest-artifacts + continue-on-error: true + with: + workflow: build-web.yml + path: apps/web + workflow_conclusion: success + branch: ${{ inputs.branch-or-tag }} + artifacts: ${{ env._ENVIRONMENT_ARTIFACT }} + + - name: Login to Azure + if: ${{ steps.download-latest-artifacts.outcome == 'failure' }} + uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + with: + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + + - name: Retrieve secrets for Build trigger + if: ${{ steps.download-latest-artifacts.outcome == 'failure' }} + id: retrieve-secret + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: "bitwarden-ci" + secrets: "github-pat-bitwarden-devops-bot-repo-scope" + + - name: 'Trigger build web for missing branch/tag ${{ inputs.branch-or-tag }}' + if: ${{ steps.download-latest-artifacts.outcome == 'failure' }} + uses: convictional/trigger-workflow-and-wait@f69fa9eedd3c62a599220f4d5745230e237904be # v1.6.5 + id: trigger-build-web + with: + owner: bitwarden + repo: clients + github_token: ${{ steps.retrieve-secret.outputs.github-pat-bitwarden-devops-bot-repo-scope }} + workflow_file_name: build-web.yml + ref: ${{ inputs.branch-or-tag }} + wait_interval: 100 + + - name: Set artifact build commit + id: set-artifact-commit + env: + GH_TOKEN: ${{ github.token }} run: | - echo "sha=$(git rev-parse origin/${{ inputs.branch-or-tag }})" >> $GITHUB_OUTPUT + # If run-id was used, get the commit from the download-latest-artifacts-run-id step + if [ "${{ inputs.build-web-run-id }}" ]; then + echo "commit=${{ steps.download-latest-artifacts-run-id.outputs.artifact-build-commit }}" >> $GITHUB_OUTPUT + + elif [ "${{ steps.download-latest-artifacts.outcome }}" == "failure" ]; then + # If the download-latest-artifacts step failed, query the GH API to get the commit SHA of the artifact that was just built with trigger-build-web. + commit=$(gh api /repos/bitwarden/clients/actions/runs/${{ steps.trigger-build-web.outputs.workflow_id }}/artifacts --jq '.artifacts[0].workflow_run.head_sha') + echo "commit=$commit" >> $GITHUB_OUTPUT + + else + # Set the commit to the output of step download-latest-artifacts. + echo "commit=${{ steps.download-latest-artifacts.outputs.artifact-build-commit }}" >> $GITHUB_OUTPUT + fi notify-start: name: Notify Slack with start message needs: - approval - setup - - get-branch-or-tag-sha + - artifact-check runs-on: ubuntu-22.04 if: ${{ always() && contains( inputs.environment , 'QA' ) }} outputs: @@ -165,66 +226,10 @@ jobs: tag: ${{ inputs.branch-or-tag }} slack-channel: team-eng-qa-devops event: 'start' - commit-sha: ${{ needs.get-branch-or-tag-sha.outputs.branch-or-tag-sha }} + commit-sha: ${{ needs.artifact-check.outputs.artifact-build-commit }} url: https://github.com/bitwarden/clients/actions/runs/${{ github.run_id }} AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - artifact-check: - name: Check if Web artifact is present - runs-on: ubuntu-22.04 - needs: setup - env: - _ENVIRONMENT_ARTIFACT: ${{ needs.setup.outputs.environment-artifact }} - steps: - - name: 'Download latest cloud asset using GitHub Run ID: ${{ inputs.build-web-run-id }}' - if: ${{ inputs.build-web-run-id }} - uses: bitwarden/gh-actions/download-artifacts@main - id: download-latest-artifacts - continue-on-error: true - with: - workflow: build-web.yml - path: apps/web - workflow_conclusion: success - run_id: ${{ inputs.build-web-run-id }} - artifacts: ${{ env._ENVIRONMENT_ARTIFACT }} - - - name: 'Download latest cloud asset from branch/tag: ${{ inputs.branch-or-tag }}' - if: ${{ !inputs.build-web-run-id }} - uses: bitwarden/gh-actions/download-artifacts@main - id: download-artifacts - continue-on-error: true - with: - workflow: build-web.yml - path: apps/web - workflow_conclusion: success - branch: ${{ inputs.branch-or-tag }} - artifacts: ${{ env._ENVIRONMENT_ARTIFACT }} - - - name: Login to Azure - if: ${{ steps.download-artifacts.outcome == 'failure' }} - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 - with: - creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - - - name: Retrieve secrets for Build trigger - if: ${{ steps.download-artifacts.outcome == 'failure' }} - id: retrieve-secret - uses: bitwarden/gh-actions/get-keyvault-secrets@main - with: - keyvault: "bitwarden-ci" - secrets: "github-pat-bitwarden-devops-bot-repo-scope" - - - name: 'Trigger build web for missing branch/tag ${{ inputs.branch-or-tag }}' - if: ${{ steps.download-artifacts.outcome == 'failure' }} - uses: convictional/trigger-workflow-and-wait@f69fa9eedd3c62a599220f4d5745230e237904be # v1.6.5 - with: - owner: bitwarden - repo: clients - github_token: ${{ steps.retrieve-secret.outputs.github-pat-bitwarden-devops-bot-repo-scope }} - workflow_file_name: build-web.yml - ref: ${{ inputs.branch-or-tag }} - wait_interval: 100 - azure-deploy: name: Deploy Web Vault to ${{ inputs.environment }} Storage Account needs: @@ -248,6 +253,7 @@ jobs: environment: ${{ env._ENVIRONMENT_NAME }} task: 'deploy' description: 'Deployment from branch/tag: ${{ inputs.branch-or-tag }}' + ref: ${{ needs.artifact-check.outputs.artifact-build-commit }} - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -349,10 +355,10 @@ jobs: runs-on: ubuntu-22.04 if: ${{ always() && contains( inputs.environment , 'QA' ) }} needs: + - setup - notify-start - azure-deploy - - setup - - get-branch-or-tag-sha + - artifact-check steps: - uses: bitwarden/gh-actions/report-deployment-status-to-slack@main with: @@ -362,6 +368,6 @@ jobs: slack-channel: ${{ needs.notify-start.outputs.channel_id }} event: ${{ needs.azure-deploy.result }} url: https://github.com/bitwarden/clients/actions/runs/${{ github.run_id }} - commit-sha: ${{ needs.get-branch-or-tag-sha.outputs.branch-or-tag-sha }} + commit-sha: ${{ needs.artifact-check.outputs.artifact-build-commit }} update-ts: ${{ needs.notify-start.outputs.ts }} AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} From 26b3259c705178e499f55fbcbd29ccc77c2d8f94 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 19 Apr 2024 19:40:20 +0000 Subject: [PATCH 021/110] Autosync the updated translations (#8837) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/web/src/locales/af/messages.json | 9 ++ apps/web/src/locales/ar/messages.json | 9 ++ apps/web/src/locales/az/messages.json | 9 ++ apps/web/src/locales/be/messages.json | 9 ++ apps/web/src/locales/bg/messages.json | 63 +++++++----- apps/web/src/locales/bn/messages.json | 9 ++ apps/web/src/locales/bs/messages.json | 9 ++ apps/web/src/locales/ca/messages.json | 53 +++++----- apps/web/src/locales/cs/messages.json | 43 ++++---- apps/web/src/locales/cy/messages.json | 9 ++ apps/web/src/locales/da/messages.json | 9 ++ apps/web/src/locales/de/messages.json | 9 ++ apps/web/src/locales/el/messages.json | 9 ++ apps/web/src/locales/en_GB/messages.json | 9 ++ apps/web/src/locales/en_IN/messages.json | 9 ++ apps/web/src/locales/eo/messages.json | 9 ++ apps/web/src/locales/es/messages.json | 9 ++ apps/web/src/locales/et/messages.json | 9 ++ apps/web/src/locales/eu/messages.json | 9 ++ apps/web/src/locales/fa/messages.json | 9 ++ apps/web/src/locales/fi/messages.json | 119 ++++++++++++----------- apps/web/src/locales/fil/messages.json | 9 ++ apps/web/src/locales/fr/messages.json | 43 ++++---- apps/web/src/locales/gl/messages.json | 9 ++ apps/web/src/locales/he/messages.json | 9 ++ apps/web/src/locales/hi/messages.json | 9 ++ apps/web/src/locales/hr/messages.json | 9 ++ apps/web/src/locales/hu/messages.json | 11 ++- apps/web/src/locales/id/messages.json | 9 ++ apps/web/src/locales/it/messages.json | 11 ++- apps/web/src/locales/ja/messages.json | 63 +++++++----- apps/web/src/locales/ka/messages.json | 9 ++ apps/web/src/locales/km/messages.json | 9 ++ apps/web/src/locales/kn/messages.json | 9 ++ apps/web/src/locales/ko/messages.json | 9 ++ apps/web/src/locales/lv/messages.json | 15 ++- apps/web/src/locales/ml/messages.json | 9 ++ apps/web/src/locales/mr/messages.json | 9 ++ apps/web/src/locales/my/messages.json | 9 ++ apps/web/src/locales/nb/messages.json | 9 ++ apps/web/src/locales/ne/messages.json | 9 ++ apps/web/src/locales/nl/messages.json | 9 ++ apps/web/src/locales/nn/messages.json | 9 ++ apps/web/src/locales/or/messages.json | 9 ++ apps/web/src/locales/pl/messages.json | 9 ++ apps/web/src/locales/pt_BR/messages.json | 9 ++ apps/web/src/locales/pt_PT/messages.json | 9 ++ apps/web/src/locales/ro/messages.json | 9 ++ apps/web/src/locales/ru/messages.json | 9 ++ apps/web/src/locales/si/messages.json | 9 ++ apps/web/src/locales/sk/messages.json | 91 +++++++++-------- apps/web/src/locales/sl/messages.json | 9 ++ apps/web/src/locales/sr/messages.json | 9 ++ apps/web/src/locales/sr_CS/messages.json | 9 ++ apps/web/src/locales/sv/messages.json | 21 ++-- apps/web/src/locales/te/messages.json | 9 ++ apps/web/src/locales/th/messages.json | 9 ++ apps/web/src/locales/tr/messages.json | 9 ++ apps/web/src/locales/uk/messages.json | 9 ++ apps/web/src/locales/vi/messages.json | 9 ++ apps/web/src/locales/zh_CN/messages.json | 13 ++- apps/web/src/locales/zh_TW/messages.json | 9 ++ 62 files changed, 777 insertions(+), 219 deletions(-) diff --git a/apps/web/src/locales/af/messages.json b/apps/web/src/locales/af/messages.json index 51ba16e5e0a..2c226ec0b83 100644 --- a/apps/web/src/locales/af/messages.json +++ b/apps/web/src/locales/af/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/ar/messages.json b/apps/web/src/locales/ar/messages.json index 4377e5655b8..be25dead79b 100644 --- a/apps/web/src/locales/ar/messages.json +++ b/apps/web/src/locales/ar/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index 2e94d219d81..d53278e93a7 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "Bu kolleksiya yalnız admin konsolundan əlçatandır" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index 2cfb8af125c..7c3b76b6105 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index 05a819f97a8..f4ee30ba957 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -7607,7 +7607,7 @@ "message": "Портал за доставчици" }, "success": { - "message": "Success" + "message": "Успех" }, "viewCollection": { "message": "Преглед на колекцията" @@ -7907,7 +7907,7 @@ "message": "Не може да добавяте себе си към групи." }, "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "message": "Известие: от 2 май 2024г. неразпределените елементи на организациите вече няма се виждат в изгледа с „Всички трезори“ на различните устройства, а ще бъдат достъпни само през Административната конзола. Добавете тези елементи към някоя колекция в Административната конзола, за да станат видими." }, "unassignedItemsBannerNotice": { "message": "Известие: неразпределените елементи в организацията вече няма да се виждат в изгледа с всички трезори на различните устройства, а са достъпни само през Административната конзола." @@ -7966,79 +7966,88 @@ "message": "Грешка при задаването на целева папка." }, "integrationsAndSdks": { - "message": "Integrations & SDKs", + "message": "Интеграции и набори за разработка (SDK)", "description": "The title for the section that deals with integrations and SDKs." }, "integrations": { - "message": "Integrations" + "message": "Интеграции" }, "integrationsDesc": { - "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + "message": "Автоматично синхронизиране на тайните от „Управлението на тайни“ на Битуорден към външна услуга." }, "sdks": { - "message": "SDKs" + "message": "Набори за разработка (SDK)" }, "sdksDesc": { - "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + "message": "Използвайте набора за разработка (SDK) за Управлението на тайни на Битуорден със следните програмни езици, за да създадете свои собствени приложения." }, "setUpGithubActions": { - "message": "Set up Github Actions" + "message": "Настройка на действия в Github" }, "setUpGitlabCICD": { - "message": "Set up GitLab CI/CD" + "message": "Настройка на GitLab CI/CD" }, "setUpAnsible": { - "message": "Set up Ansible" + "message": "Настройка на Ansible" }, "cSharpSDKRepo": { - "message": "View C# repository" + "message": "Преглед на хранилището за C#" }, "cPlusPlusSDKRepo": { - "message": "View C++ repository" + "message": "Преглед на хранилището за C++" }, "jsWebAssemblySDKRepo": { - "message": "View JS WebAssembly repository" + "message": "Преглед на хранилището за JS WebAssembly" }, "javaSDKRepo": { - "message": "View Java repository" + "message": "Преглед на хранилището за Java" }, "pythonSDKRepo": { - "message": "View Python repository" + "message": "Преглед на хранилището за Python" }, "phpSDKRepo": { - "message": "View php repository" + "message": "Преглед на хранилището за php" }, "rubySDKRepo": { - "message": "View Ruby repository" + "message": "Преглед на хранилището за Ruby" }, "goSDKRepo": { - "message": "View Go repository" + "message": "Преглед на хранилището за Go" }, "createNewClientToManageAsProvider": { - "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + "message": "Създайте нова организация, която да управлявате като доставчик. Допълнителните места ще бъдат отразени в следващия платежен период." }, "selectAPlan": { - "message": "Select a plan" + "message": "Изберете план" }, "thirtyFivePercentDiscount": { - "message": "35% Discount" + "message": "Отстъпка от 35%" }, "monthPerMember": { - "message": "month per member" + "message": "на месец за член" }, "seats": { - "message": "Seats" + "message": "Места" }, "addOrganization": { - "message": "Add organization" + "message": "Добавяне на организация" }, "createdNewClient": { - "message": "Successfully created new client" + "message": "Новият клиент е създаден успешно" }, "noAccess": { - "message": "No access" + "message": "Нямате достъп" }, "collectionAdminConsoleManaged": { - "message": "This collection is only accessible from the admin console" + "message": "Тази колекция е достъпна само през административната конзола" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/bn/messages.json b/apps/web/src/locales/bn/messages.json index 46d0e574cea..9b60b1149b6 100644 --- a/apps/web/src/locales/bn/messages.json +++ b/apps/web/src/locales/bn/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/bs/messages.json b/apps/web/src/locales/bs/messages.json index 85880f9cbf7..3b2b0f91bfe 100644 --- a/apps/web/src/locales/bs/messages.json +++ b/apps/web/src/locales/bs/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index cc072c3b966..bf3071b506e 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -7607,7 +7607,7 @@ "message": "Portal del proveïdor" }, "success": { - "message": "Success" + "message": "Èxit" }, "viewCollection": { "message": "Mostra col·lecció" @@ -7637,10 +7637,10 @@ "message": "No s'ha assignat cap col·lecció" }, "successfullyAssignedCollections": { - "message": "Successfully assigned collections" + "message": "Col·leccions assignades correctament" }, "bulkCollectionAssignmentWarning": { - "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "message": "Heu seleccionat $TOTAL_COUNT$ elements. No podeu actualitzar-ne $READONLY_COUNT$ dels quals perquè no teniu permisos d'edició.", "placeholders": { "total_count": { "content": "$1", @@ -7656,10 +7656,10 @@ "message": "Elements" }, "assignedSeats": { - "message": "Assigned seats" + "message": "Seients assignats" }, "assigned": { - "message": "Assigned" + "message": "Assignat" }, "used": { "message": "Utilitzat" @@ -7668,40 +7668,40 @@ "message": "Queden" }, "unlinkOrganization": { - "message": "Unlink organization" + "message": "Desenllaça l'organització" }, "manageSeats": { - "message": "MANAGE SEATS" + "message": "GESTIONA SEIENTS" }, "manageSeatsDescription": { - "message": "Adjustments to seats will be reflected in the next billing cycle." + "message": "Els ajustos dels seients es reflectiran en el pròxim cicle de facturació." }, "unassignedSeatsDescription": { - "message": "Unassigned subscription seats" + "message": "Seients de subscripció no assignats" }, "purchaseSeatDescription": { - "message": "Additional seats purchased" + "message": "Seients addicionals adquirits" }, "assignedSeatCannotUpdate": { - "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + "message": "Els seients assignats no es poden actualitzar. Poseu-vos en contacte amb el propietari de l'organització per obtenir ajuda." }, "subscriptionUpdateFailed": { - "message": "Subscription update failed" + "message": "L'actualització de la subscripció ha fallat" }, "trial": { - "message": "Trial", + "message": "Prova", "description": "A subscription status label." }, "pastDue": { - "message": "Past due", + "message": "Vençuda", "description": "A subscription status label" }, "subscriptionExpired": { - "message": "Subscription expired", + "message": "La subscripció ha caducat", "description": "The date header used when a subscription is past due." }, "pastDueWarningForChargeAutomatically": { - "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "message": "Teniu un període de gràcia de $DAYS$ dies a partir de la data de caducitat de la vostra subscripció per mantenir-la. Resoleu les factures vençudes abans de $SUSPENSION_DATE$.", "placeholders": { "days": { "content": "$1", @@ -7715,7 +7715,7 @@ "description": "A warning shown to the user when their subscription is past due and they are charged automatically." }, "pastDueWarningForSendInvoice": { - "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "message": "Teniu un període de gràcia de $DAYS$ dies a partir de la data en què cal que la primera factura no pagada mantinga la subscripció. Resoleu les factures vençudes abans de $SUSPENSION_DATE$.", "placeholders": { "days": { "content": "$1", @@ -7729,22 +7729,22 @@ "description": "A warning shown to the user when their subscription is past due and they pay via invoice." }, "unpaidInvoice": { - "message": "Unpaid invoice", + "message": "Factura no pagada", "description": "The header of a warning box shown to a user whose subscription is unpaid." }, "toReactivateYourSubscription": { - "message": "To reactivate your subscription, please resolve the past due invoices.", + "message": "Per reactivar la subscripció, resoleu les factures vençudes.", "description": "The body of a warning box shown to a user whose subscription is unpaid." }, "cancellationDate": { - "message": "Cancellation date", + "message": "Data de cancel·lació", "description": "The date header used when a subscription is cancelled." }, "machineAccountsCannotCreate": { - "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + "message": "No es poden crear comptes de màquina en organitzacions suspeses. Poseu-vos en contacte amb el propietari de l'organització per obtenir ajuda." }, "machineAccount": { - "message": "Machine account", + "message": "Compte de màquina", "description": "A machine user which can be used to automate processes and access secrets in the system." }, "machineAccounts": { @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index 6b82766cd31..b6e62aef9aa 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -7907,7 +7907,7 @@ "message": "Do skupiny nemůžete přidat sami sebe." }, "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "message": "Upozornění: Dne 2. května 2024 již nebudou nepřiřazené položky organizace viditelné v zobrazení Všechny trezory ve všech zařízeních a budou přístupné jen prostřednictvím konzoly správce. Přiřaďte tyto položky do kolekce z konzoly pro správce, aby byly viditelné." }, "unassignedItemsBannerNotice": { "message": "Upozornění: Nepřiřazené položky organizace již nejsou viditelné ve vašem zobrazení všech trezorů napříč zařízeními a jsou nyní přístupné pouze v konzoli správce." @@ -7966,53 +7966,53 @@ "message": "Chyba při přiřazování cílové složky." }, "integrationsAndSdks": { - "message": "Integrations & SDKs", + "message": "Integrace a SDK", "description": "The title for the section that deals with integrations and SDKs." }, "integrations": { - "message": "Integrations" + "message": "Integrace" }, "integrationsDesc": { - "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + "message": "Automaticky synchronizuje tajné klíče se správce tajných klíčů Bitwardenu do služby třetí strany." }, "sdks": { - "message": "SDKs" + "message": "SDK" }, "sdksDesc": { - "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + "message": "Použije SDK správce tajných klíčů Bitwardenu v následujících programovacích jazycích k vytvoření vlastních aplikací." }, "setUpGithubActions": { - "message": "Set up Github Actions" + "message": "Nastavit akce GitHubu" }, "setUpGitlabCICD": { - "message": "Set up GitLab CI/CD" + "message": "Nastavit GitLab CI/CD" }, "setUpAnsible": { - "message": "Set up Ansible" + "message": "Nastavit Ansible" }, "cSharpSDKRepo": { - "message": "View C# repository" + "message": "Zobrazit repozitář C#" }, "cPlusPlusSDKRepo": { - "message": "View C++ repository" + "message": "Zobrazit repozitář C++" }, "jsWebAssemblySDKRepo": { - "message": "View JS WebAssembly repository" + "message": "Zobrazit repozitář JS WebAssembly" }, "javaSDKRepo": { - "message": "View Java repository" + "message": "Zobrazit repozitář Java" }, "pythonSDKRepo": { - "message": "View Python repository" + "message": "Zobrazit repozitář Python" }, "phpSDKRepo": { - "message": "View php repository" + "message": "Zobrazit repozitář PHP" }, "rubySDKRepo": { - "message": "View Ruby repository" + "message": "Zobrazit repozitář Ruby" }, "goSDKRepo": { - "message": "View Go repository" + "message": "Zobrazit repozitář Go" }, "createNewClientToManageAsProvider": { "message": "Vytvořte novou klientskou organizaci pro správu jako poskytovatele. Další uživatelé budou reflektováni v dalším platebním cyklu." @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/cy/messages.json b/apps/web/src/locales/cy/messages.json index 7394c3fe2a9..961b3c93a34 100644 --- a/apps/web/src/locales/cy/messages.json +++ b/apps/web/src/locales/cy/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index 081fef78654..e5042229fe8 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "Denne samling er kun tilgængelig via Admin-konsol" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index 3fb75984168..bd2e0946f61 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/el/messages.json b/apps/web/src/locales/el/messages.json index 742a76402d5..ed374326fee 100644 --- a/apps/web/src/locales/el/messages.json +++ b/apps/web/src/locales/el/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index 66f728cb4db..3b0a99715fb 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/en_IN/messages.json b/apps/web/src/locales/en_IN/messages.json index c0c77d3d784..1868130a1c5 100644 --- a/apps/web/src/locales/en_IN/messages.json +++ b/apps/web/src/locales/en_IN/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/eo/messages.json b/apps/web/src/locales/eo/messages.json index 2a6db2ea4bb..88509a82991 100644 --- a/apps/web/src/locales/eo/messages.json +++ b/apps/web/src/locales/eo/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/es/messages.json b/apps/web/src/locales/es/messages.json index 9f0f085e9d4..e851539df16 100644 --- a/apps/web/src/locales/es/messages.json +++ b/apps/web/src/locales/es/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/et/messages.json b/apps/web/src/locales/et/messages.json index 0c3f7cb300d..1cef83a8237 100644 --- a/apps/web/src/locales/et/messages.json +++ b/apps/web/src/locales/et/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/eu/messages.json b/apps/web/src/locales/eu/messages.json index c315d625209..bf79583e580 100644 --- a/apps/web/src/locales/eu/messages.json +++ b/apps/web/src/locales/eu/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/fa/messages.json b/apps/web/src/locales/fa/messages.json index 32feda3edd1..0621239c5e8 100644 --- a/apps/web/src/locales/fa/messages.json +++ b/apps/web/src/locales/fa/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/fi/messages.json b/apps/web/src/locales/fi/messages.json index 377f7b084ff..4cc682caa20 100644 --- a/apps/web/src/locales/fi/messages.json +++ b/apps/web/src/locales/fi/messages.json @@ -630,7 +630,7 @@ "message": "Suojausavain ei kelpaa. Yritä uudelleen." }, "twoFactorForPasskeysNotSupportedOnClientUpdateToLogIn": { - "message": "Kaksivaiheista salausavainkirjautumista ei tueta. Päivitä sovellus kirjautuaksesi sisään." + "message": "Kaksivaiheista suojausavainkirjautumista ei tueta. Päivitä sovellus kirjautuaksesi sisään." }, "loginWithPasskeyInfo": { "message": "Käytä generoitua suojausavainta, joka kirjaa sinut automaattisesti sisään ilman salasanaa. Henkilöllisyytesi vahvistetaan kasvojen tunnistuksen tai sormenjäljen kataisilla biometrisillä tiedoilla, tai jollakin muulla FIDO2-suojausratkaisulla." @@ -2799,7 +2799,7 @@ "message": "Verkkoholvi" }, "cli": { - "message": "komentorivi" + "message": "Komentorivi" }, "bitWebVault": { "message": "Bitwarden Verkkoholvi" @@ -4948,10 +4948,10 @@ "message": "Uusi asiakasorganisaatio" }, "newClientOrganizationDesc": { - "message": "Luo uusi asiakasorganisaatio, joka liitetään sinuun toimittajana. Voit käyttää ja hallita tätä organisaatiota." + "message": "Luo uusi asiakasorganisaatio, jonka toimittajaksi sinut määritetään. Voit käyttää ja hallita tätä organisaatiota." }, "newClient": { - "message": "Uusi pääte" + "message": "Uusi asiakas" }, "addExistingOrganization": { "message": "Lisää olemassa oleva organisaatio" @@ -6238,7 +6238,7 @@ "description": "Title for creating a new project." }, "softDeleteSecretWarning": { - "message": "Salaisuuksien poistaminen voi vaikuttaa olemassa oleviin integrointeihin.", + "message": "Salaisuuksien poistaminen voi vaikuttaa olemassa oleviin integraatioihin.", "description": "Warns that deleting secrets can have consequences on integrations" }, "softDeletesSuccessToast": { @@ -7071,7 +7071,7 @@ "description": "The individual description shown to the user when the user doesn't have access to delete a project." }, "smProjectsDeleteBulkConfirmation": { - "message": "Seuraavien projektien poistaminen ei ole mahdollista. Haluatko jatkaa?", + "message": "Seuraavia projekteja ei ole mahdollista poistaa. Haluatko jatkaa?", "description": "The message shown to the user when bulk deleting projects and the user doesn't have access to some projects." }, "updateKdfSettings": { @@ -7217,7 +7217,7 @@ "message": "Tilille ei ole asetettu pääsalasanaa" }, "removeOrgUserNoMasterPasswordDesc": { - "message": "Käyttäjän $USER$ poistaminen asettamatta hänen tililleen pääsalasanaa voi estää häntä kirjautumasta hänen omalle tililleen. Haluatko varmasti jatkaa?", + "message": "Käyttäjän $USER$ poistaminen asettamatta hänen tililleen pääsalasanaa voi estää häntä kirjautumasta heidän omalle tililleen. Haluatko varmasti jatkaa?", "placeholders": { "user": { "content": "$1", @@ -7484,7 +7484,7 @@ "message": "Laajennuksella voit tallentaa kirjautumistietoja ja automaattitäyttää lomakkeita avaamatta verkkosovellusta." }, "projectAccessUpdated": { - "message": "Projektin käyttöoikeudet on muutettu" + "message": "Projektin käyttöoikeuksia muutettiin" }, "unexpectedErrorSend": { "message": "Odottamaton virhe ladattaessa Sendiä. Yritä myöhemmin uudelleen." @@ -7514,7 +7514,7 @@ "message": "Ylläpitäjät voivat käyttää ja hallinnoida kokoelmia." }, "serviceAccountAccessUpdated": { - "message": "Palvelutilin oikeuksia muutettiin" + "message": "Palvelutilin käyttöoikeuksia muutettiin" }, "commonImportFormats": { "message": "Yleiset muodot", @@ -7595,7 +7595,7 @@ "message": "Ilmainen 1 vuoden ajan" }, "newWebApp": { - "message": "Tervetuloa uuteen, entistä parempaan verkkosovellukseen. Lue lisää siitä, mikä on muuttunut." + "message": "Tervetuloa uuteen, entistä parempaan verkkosovellukseen. Katso mikä on muuttunut." }, "releaseBlog": { "message": "Lue julkaisublogia" @@ -7607,10 +7607,10 @@ "message": "Toimittajaportaali" }, "success": { - "message": "Success" + "message": "Onnistui" }, "viewCollection": { - "message": "Tarkastele kokoelmaa" + "message": "Näytä kokoelma" }, "restrictedGroupAccess": { "message": "Et voi lisätä itseäsi ryhmiin." @@ -7640,7 +7640,7 @@ "message": "Kokoelmat määritettiin" }, "bulkCollectionAssignmentWarning": { - "message": "Olet valinnut $TOTAL_COUNT$ kohdetta. Et voi päivittää näistä $READONLY_COUNT$ kohdetta, koska käyttöoikeutesi eivät salli muokkausta.", + "message": "Olet valinnut $TOTAL_COUNT$ kohdetta. Näistä $READONLY_COUNT$ et voi muuttaa, koska käyttöoikeutesi eivät salli muokkausta.", "placeholders": { "total_count": { "content": "$1", @@ -7668,7 +7668,7 @@ "message": "Jäljellä" }, "unlinkOrganization": { - "message": "Irrota organisaatio" + "message": "Poista organisaatioliitos" }, "manageSeats": { "message": "HALLITSE KÄYTTÄJÄPAIKKOJA" @@ -7904,33 +7904,33 @@ "message": "Konetilin oikeuksia muutettiin" }, "restrictedGroupAccessDesc": { - "message": "You cannot add yourself to a group." + "message": "Et voi lisätä itseäsi ryhmään." }, "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "message": "Huomioi: Alkaen 2. toukokuuta 2024, määrittämättömät organisaatiokohteet eivät enää näy laitteidesi \"Kaikki holvit\" -näkymissä, vaan ne ovat käytettävissä vain Hallintapaneelista." }, "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + "message": "Huomioi: Määrittämättömät organisaatiokohteet eivät enää näy laitteidesi \"Kaikki holvit\" -näkymissä, vaan ne ovat käytettävissä vain Hallintapaneelista." }, "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + "message": "Huomioi: Alkaen 16. toukokuuta 2024, määrittämättömät organisaatiokohteet eivät enää näy laitteidesi \"Kaikki holvit\" -näkymissä, vaan ne ovat käytettävissä vain Hallintapaneelista." }, "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", + "message": "Määritä nämä kohteet kokoelmaan", "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." }, "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", + "message": ", jotta ne näkyvät.", "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." }, "deleteProvider": { - "message": "Delete provider" + "message": "Poista toimittaja" }, "deleteProviderConfirmation": { - "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + "message": "Toimittajan poisto on pysyvää ja peruuttamatonta. Vahvista palveluntarjoajan ja kaikkien siihen liittyvien tietojen poisto syöttämällä pääsalasanasi." }, "deleteProviderName": { - "message": "Cannot delete $ID$", + "message": "Toimittajaa $ID$ ei ole mahdollista poistaa", "placeholders": { "id": { "content": "$1", @@ -7939,7 +7939,7 @@ } }, "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "message": "Kaikki liitetyt asiakkaat on poistettava ennen toimittajan $ID$ poistoa.", "placeholders": { "id": { "content": "$1", @@ -7948,97 +7948,106 @@ } }, "providerDeleted": { - "message": "Provider deleted" + "message": "Toimittaja poistettiin" }, "providerDeletedDesc": { - "message": "The Provider and all associated data has been deleted." + "message": "Toimittaja ja kaikki siihen liittyvät tiedot on poistettu." }, "deleteProviderRecoverConfirmDesc": { - "message": "You have requested to delete this Provider. Use the button below to confirm." + "message": "Olet pyytänyt tämän toimittajan poistoa. Vahvista alla olevalla painikeella." }, "deleteProviderWarning": { - "message": "Deleting your provider is permanent. It cannot be undone." + "message": "Toimittajasi poisto on pysyvä toimenpide, eikä sen peruminen ole mahdollista." }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "Virhe määritettäessä kohdekokoelmaa." }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "Virhe määritettäessä kohdekansiota." }, "integrationsAndSdks": { - "message": "Integrations & SDKs", + "message": "Integraatiot ja SDK:t", "description": "The title for the section that deals with integrations and SDKs." }, "integrations": { - "message": "Integrations" + "message": "Integraatiot" }, "integrationsDesc": { - "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + "message": "Synkronoi salaisuudet automaattisesti Bitwardenin Salaisuushallinnan ja ulkopuolisen palvelun välillä." }, "sdks": { - "message": "SDKs" + "message": "SDK:t" }, "sdksDesc": { - "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + "message": "Bitwardenin Salaisuushallinnan SDK:n avulla voit kehittää omia sovelluksiasi seuraavilla ohjelmointikielillä." }, "setUpGithubActions": { - "message": "Set up Github Actions" + "message": "Määritä GitHub Actions" }, "setUpGitlabCICD": { - "message": "Set up GitLab CI/CD" + "message": "Määritä GitLab CI/CD" }, "setUpAnsible": { - "message": "Set up Ansible" + "message": "Määritä Ansible" }, "cSharpSDKRepo": { - "message": "View C# repository" + "message": "Näytä C#-arkisto" }, "cPlusPlusSDKRepo": { - "message": "View C++ repository" + "message": "Näytä C++-arkisto" }, "jsWebAssemblySDKRepo": { - "message": "View JS WebAssembly repository" + "message": "Näytä JS WebAssembly -arkisto" }, "javaSDKRepo": { - "message": "View Java repository" + "message": "Näytä Java-arkisto" }, "pythonSDKRepo": { - "message": "View Python repository" + "message": "Näytä Python-arkisto" }, "phpSDKRepo": { - "message": "View php repository" + "message": "Näytä php-arkisto" }, "rubySDKRepo": { - "message": "View Ruby repository" + "message": "Näytä Ruby-arkisto" }, "goSDKRepo": { - "message": "View Go repository" + "message": "Näytä Go-arkisto" }, "createNewClientToManageAsProvider": { - "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + "message": "Luo uusi asiakasorganisaatio, jota hallitset toimittajana. Uudet käyttäjäpaikat näkyvät seuraavalla laskutusjaksolla." }, "selectAPlan": { - "message": "Select a plan" + "message": "Valitse tilaus" }, "thirtyFivePercentDiscount": { - "message": "35% Discount" + "message": "35 %:n alennus" }, "monthPerMember": { - "message": "month per member" + "message": "kuukaudessa/jäsen" }, "seats": { - "message": "Seats" + "message": "Käyttäjäpaikat" }, "addOrganization": { - "message": "Add organization" + "message": "Lisää organisaatio" }, "createdNewClient": { - "message": "Successfully created new client" + "message": "Uuden asiakkaan luonti onnistui." }, "noAccess": { - "message": "No access" + "message": "Käyttöoikeutta ei ole" }, "collectionAdminConsoleManaged": { - "message": "This collection is only accessible from the admin console" + "message": "Tämä kokoelma on käytettävissä vain hallintakonsolista" + }, + "organizationOptionsMenu": { + "message": "Näytä/piilota organisaatiovalikko" + }, + "vaultItemSelect": { + "message": "Valitse holvin kohde" + }, + "collectionItemSelect": { + "message": "Valitse kokoelman kohde" } } diff --git a/apps/web/src/locales/fil/messages.json b/apps/web/src/locales/fil/messages.json index 50c9f8d4e87..eb3094e0439 100644 --- a/apps/web/src/locales/fil/messages.json +++ b/apps/web/src/locales/fil/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index 29341769560..1f1f3438974 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -7907,7 +7907,7 @@ "message": "Vous ne pouvez pas vous ajouter vous-même à un groupe." }, "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "message": "Remarque : le 2 mai 2024, les éléments d'organisation non assignés ne seront plus visibles dans votre vue Tous les coffres sur les appareils et seront uniquement accessibles via la Console Admin. Assignez ces éléments à une collection à partir de la Console Admin pour les rendre visibles." }, "unassignedItemsBannerNotice": { "message": "Remarque : Les éléments d'organisation non assignés ne sont plus visibles dans la vue Tous les coffres sur les appareils et ne sont maintenant accessibles que via la Console Admin." @@ -7966,53 +7966,53 @@ "message": "Erreur lors de l'assignation du dossier cible." }, "integrationsAndSdks": { - "message": "Integrations & SDKs", + "message": "Intégrations & SDKs", "description": "The title for the section that deals with integrations and SDKs." }, "integrations": { - "message": "Integrations" + "message": "Intégrations" }, "integrationsDesc": { - "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + "message": "Synchroniser automatiquement les secrets à partir du Secrets Manager de Bitwarden vers un service tiers." }, "sdks": { - "message": "SDKs" + "message": "SDK" }, "sdksDesc": { - "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + "message": "Utilisez Bitwarden Secrets Manager SDK dans les langages de programmation suivants pour construire vos propres applications." }, "setUpGithubActions": { - "message": "Set up Github Actions" + "message": "Configurer Github Actions" }, "setUpGitlabCICD": { - "message": "Set up GitLab CI/CD" + "message": "Configurer GitLab CI/CD" }, "setUpAnsible": { - "message": "Set up Ansible" + "message": "Configurer Ansible" }, "cSharpSDKRepo": { - "message": "View C# repository" + "message": "Afficher le dépôt C#" }, "cPlusPlusSDKRepo": { - "message": "View C++ repository" + "message": "Afficher le dépôt C++" }, "jsWebAssemblySDKRepo": { - "message": "View JS WebAssembly repository" + "message": "Afficher le dépôt JS WebAssembly" }, "javaSDKRepo": { - "message": "View Java repository" + "message": "Afficher le dépôt Java" }, "pythonSDKRepo": { - "message": "View Python repository" + "message": "Afficher le dépôt Python" }, "phpSDKRepo": { - "message": "View php repository" + "message": "Afficher le dépôt php" }, "rubySDKRepo": { - "message": "View Ruby repository" + "message": "Afficher le dépôt Ruby" }, "goSDKRepo": { - "message": "View Go repository" + "message": "Afficher le dépôt Go" }, "createNewClientToManageAsProvider": { "message": "Créez une nouvelle organisation de clients à gérer en tant que Fournisseur. Des sièges supplémentaires seront reflétés lors du prochain cycle de facturation." @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "Cette collection n'est accessible qu'à partir de la Console Admin" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/gl/messages.json b/apps/web/src/locales/gl/messages.json index ed04c3a3ef3..13fa5f60c02 100644 --- a/apps/web/src/locales/gl/messages.json +++ b/apps/web/src/locales/gl/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/he/messages.json b/apps/web/src/locales/he/messages.json index b7b4d592273..b57005a76b6 100644 --- a/apps/web/src/locales/he/messages.json +++ b/apps/web/src/locales/he/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/hi/messages.json b/apps/web/src/locales/hi/messages.json index 23321b27ef2..818cc1cef5a 100644 --- a/apps/web/src/locales/hi/messages.json +++ b/apps/web/src/locales/hi/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/hr/messages.json b/apps/web/src/locales/hr/messages.json index edc6adaf3b8..ffeb8c2201d 100644 --- a/apps/web/src/locales/hr/messages.json +++ b/apps/web/src/locales/hr/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index 4ec6f150297..b85733325b6 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -7607,7 +7607,7 @@ "message": "Szolgáltató portál" }, "success": { - "message": "Success" + "message": "Sikeres" }, "viewCollection": { "message": "Gyűjtemény megtekintése" @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "Ez a gyűjtemény csak az adminisztrátori konzolról érhető el." + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/id/messages.json b/apps/web/src/locales/id/messages.json index 3bc9ef4e62b..df1af272719 100644 --- a/apps/web/src/locales/id/messages.json +++ b/apps/web/src/locales/id/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/it/messages.json b/apps/web/src/locales/it/messages.json index 11dec69959d..04342f13b15 100644 --- a/apps/web/src/locales/it/messages.json +++ b/apps/web/src/locales/it/messages.json @@ -7607,7 +7607,7 @@ "message": "Portale Fornitori" }, "success": { - "message": "Success" + "message": "Successo" }, "viewCollection": { "message": "Visualizza raccolta" @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Attiva/Disattiva Menu Organizzazione" + }, + "vaultItemSelect": { + "message": "Seleziona elemento della cassaforte" + }, + "collectionItemSelect": { + "message": "Seleziona elemento della raccolta" } } diff --git a/apps/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index 17b58a4299d..a701dd6e8df 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -7607,7 +7607,7 @@ "message": "プロバイダーポータル" }, "success": { - "message": "Success" + "message": "成功" }, "viewCollection": { "message": "コレクションを表示" @@ -7907,7 +7907,7 @@ "message": "あなた自身をグループに追加することはできません。" }, "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "message": "お知らせ:2024年5月2日に、 割り当てられていない組織アイテムはデバイス間のすべての保管庫ビューに表示されなくなり、管理コンソールからのみアクセス可能になります。 管理コンソールからコレクションにこれらのアイテムを割り当てると、表示できるようになります。" }, "unassignedItemsBannerNotice": { "message": "注意: 割り当てられていない組織アイテムは、デバイス間のすべての保管庫ビューでは表示されなくなり、管理コンソールからのみアクセスできるようになりました。" @@ -7966,79 +7966,88 @@ "message": "ターゲットフォルダーの割り当てに失敗しました。" }, "integrationsAndSdks": { - "message": "Integrations & SDKs", + "message": "連携&SDK", "description": "The title for the section that deals with integrations and SDKs." }, "integrations": { - "message": "Integrations" + "message": "システム連携" }, "integrationsDesc": { - "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + "message": "Bitwarden シークレットマネージャーのシークレットを、サードパーティーのサービスに自動的に同期します。" }, "sdks": { - "message": "SDKs" + "message": "SDK" }, "sdksDesc": { - "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + "message": "Bitwarden シークレットマネージャー SDK を以下のプログラミング言語で使用して、独自のアプリを構築できます。" }, "setUpGithubActions": { - "message": "Set up Github Actions" + "message": "Github アクションを設定" }, "setUpGitlabCICD": { - "message": "Set up GitLab CI/CD" + "message": "GitLab CI/CD の設定" }, "setUpAnsible": { - "message": "Set up Ansible" + "message": "Ansible を設定" }, "cSharpSDKRepo": { - "message": "View C# repository" + "message": "C# リポジトリを表示" }, "cPlusPlusSDKRepo": { - "message": "View C++ repository" + "message": "C++ リポジトリを表示" }, "jsWebAssemblySDKRepo": { - "message": "View JS WebAssembly repository" + "message": "JS WebAssembly リポジトリを表示" }, "javaSDKRepo": { - "message": "View Java repository" + "message": "Java リポジトリを表示" }, "pythonSDKRepo": { - "message": "View Python repository" + "message": "Python リポジトリを表示" }, "phpSDKRepo": { - "message": "View php repository" + "message": "PHP リポジトリを表示" }, "rubySDKRepo": { - "message": "View Ruby repository" + "message": "Ruby リポジトリを表示" }, "goSDKRepo": { - "message": "View Go repository" + "message": "Go リポジトリを表示" }, "createNewClientToManageAsProvider": { - "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + "message": "プロバイダーとして管理するための新しいクライアント組織を作成します。次の請求サイクルに追加のシートが反映されます。" }, "selectAPlan": { - "message": "Select a plan" + "message": "プランを選択" }, "thirtyFivePercentDiscount": { - "message": "35% Discount" + "message": "35%割引" }, "monthPerMember": { - "message": "month per member" + "message": "月/メンバーあたり" }, "seats": { - "message": "Seats" + "message": "シート" }, "addOrganization": { - "message": "Add organization" + "message": "組織を追加" }, "createdNewClient": { - "message": "Successfully created new client" + "message": "新しいクライアントを作成しました" }, "noAccess": { - "message": "No access" + "message": "アクセス不可" }, "collectionAdminConsoleManaged": { - "message": "This collection is only accessible from the admin console" + "message": "このコレクションは管理コンソールからのみアクセス可能です" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/ka/messages.json b/apps/web/src/locales/ka/messages.json index 6d34252b88c..defe9e621df 100644 --- a/apps/web/src/locales/ka/messages.json +++ b/apps/web/src/locales/ka/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/km/messages.json b/apps/web/src/locales/km/messages.json index ed04c3a3ef3..13fa5f60c02 100644 --- a/apps/web/src/locales/km/messages.json +++ b/apps/web/src/locales/km/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/kn/messages.json b/apps/web/src/locales/kn/messages.json index ae6f2c2a897..cfb188b5801 100644 --- a/apps/web/src/locales/kn/messages.json +++ b/apps/web/src/locales/kn/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/ko/messages.json b/apps/web/src/locales/ko/messages.json index f9240f21aa6..9dd856e656e 100644 --- a/apps/web/src/locales/ko/messages.json +++ b/apps/web/src/locales/ko/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index 0dc0dff097b..b20f3502314 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -7607,7 +7607,7 @@ "message": "Nodrošinātāju portāls" }, "success": { - "message": "Success" + "message": "Izdevās" }, "viewCollection": { "message": "Skatīt krājumu" @@ -8036,9 +8036,18 @@ "message": "Successfully created new client" }, "noAccess": { - "message": "No access" + "message": "Nav piekļuves" }, "collectionAdminConsoleManaged": { - "message": "This collection is only accessible from the admin console" + "message": "Šis krājums ir pieejams tikai pārvaldības konsolē" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/ml/messages.json b/apps/web/src/locales/ml/messages.json index 9b6d1ab5655..c2b83a5e28d 100644 --- a/apps/web/src/locales/ml/messages.json +++ b/apps/web/src/locales/ml/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/mr/messages.json b/apps/web/src/locales/mr/messages.json index ed04c3a3ef3..13fa5f60c02 100644 --- a/apps/web/src/locales/mr/messages.json +++ b/apps/web/src/locales/mr/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/my/messages.json b/apps/web/src/locales/my/messages.json index ed04c3a3ef3..13fa5f60c02 100644 --- a/apps/web/src/locales/my/messages.json +++ b/apps/web/src/locales/my/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/nb/messages.json b/apps/web/src/locales/nb/messages.json index 4e5fb13d15c..2ffdc5214fa 100644 --- a/apps/web/src/locales/nb/messages.json +++ b/apps/web/src/locales/nb/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/ne/messages.json b/apps/web/src/locales/ne/messages.json index f4118475717..7854d49a7b3 100644 --- a/apps/web/src/locales/ne/messages.json +++ b/apps/web/src/locales/ne/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index 7bf9b4018fd..b9f405cb063 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "Deze collectie is alleen toegankelijk vanaf de admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/nn/messages.json b/apps/web/src/locales/nn/messages.json index 3d54ea70fd6..53ac66738e8 100644 --- a/apps/web/src/locales/nn/messages.json +++ b/apps/web/src/locales/nn/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/or/messages.json b/apps/web/src/locales/or/messages.json index ed04c3a3ef3..13fa5f60c02 100644 --- a/apps/web/src/locales/or/messages.json +++ b/apps/web/src/locales/or/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index 57e0eb1929b..c54b72c21cd 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "Ta kolekcja jest dostępna tylko z konsoli administracyjnej" + }, + "organizationOptionsMenu": { + "message": "Przełącz menu organizacji" + }, + "vaultItemSelect": { + "message": "Wybierz element sejfu" + }, + "collectionItemSelect": { + "message": "Wybierz element kolekcji" } } diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index 4f43c2aad73..2b296694bb9 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/pt_PT/messages.json b/apps/web/src/locales/pt_PT/messages.json index c2f2fbeee37..961d4531fa4 100644 --- a/apps/web/src/locales/pt_PT/messages.json +++ b/apps/web/src/locales/pt_PT/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "Esta coleção só é acessível a partir da Consola de administração" + }, + "organizationOptionsMenu": { + "message": "Alternar menu da organização" + }, + "vaultItemSelect": { + "message": "Selecionar item do cofre" + }, + "collectionItemSelect": { + "message": "Selecionar item da coleção" } } diff --git a/apps/web/src/locales/ro/messages.json b/apps/web/src/locales/ro/messages.json index 1a47866ebb7..bd735d8f75a 100644 --- a/apps/web/src/locales/ro/messages.json +++ b/apps/web/src/locales/ro/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index 85ba7d57852..67b23b0e89c 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "Эта коллекция доступна только из консоли администратора" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/si/messages.json b/apps/web/src/locales/si/messages.json index e2c1e424272..0cd67041d3b 100644 --- a/apps/web/src/locales/si/messages.json +++ b/apps/web/src/locales/si/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index 234a4670b95..756723129ff 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -344,7 +344,7 @@ "message": "Krstné meno" }, "middleName": { - "message": "Druhé meno" + "message": "Stredné meno" }, "lastName": { "message": "Priezvisko" @@ -2814,7 +2814,7 @@ "message": "Zmenené heslo k účtu." }, "enabledUpdated2fa": { - "message": "Dvojstupňové prihlasovanie zapnuté/aktualizované." + "message": "Dvojstupňové prihlasovanie uložené" }, "disabled2fa": { "message": "Dvojstupňové prihlasovanie vypnuté." @@ -3234,10 +3234,10 @@ "message": "Skupinový prístup" }, "groupAccessUserDesc": { - "message": "Upraviť skupiny, do ktorých patrí používateľ." + "message": "Povoľte členom prístup k zbierkam ich pridaním do jednej alebo viac skupín." }, "invitedUsers": { - "message": "Používatelia pozvaní." + "message": "Používatelia pozvaní" }, "resendInvitation": { "message": "Znovu poslať pozvánku" @@ -3246,7 +3246,7 @@ "message": "Znovu poslať e-mail" }, "hasBeenReinvited": { - "message": "$USER$ bol znovu pozvaný.", + "message": "$USER$ bol znovu pozvaný", "placeholders": { "user": { "content": "$1", @@ -3270,7 +3270,7 @@ } }, "confirmUsers": { - "message": "Potvrdiť používateľov" + "message": "Potvrdiť členov" }, "usersNeedConfirmed": { "message": "Máte používateľov, ktorí prijali pozvanie, ale ešte ich musíte potvrdiť. Používatelia nebudú mať prístup k organizácii, kým nebudú potvrdení." @@ -3294,13 +3294,13 @@ "message": "Skontrolujte si doručenú poštu, mali by ste obdržať odkaz pre verifikáciu." }, "emailVerified": { - "message": "Vaša emailová adresa bola overená." + "message": "Emailová adresa konta bola overená" }, "emailVerifiedFailed": { "message": "Overovanie zlyhalo. Skúste si odoslať nový verifikačný e-mail." }, "emailVerificationRequired": { - "message": "Vyžaduje sa overenie e-mailu" + "message": "Vyžaduje sa overenie e-mailom" }, "emailVerificationRequiredDesc": { "message": "Na používanie tejto funkcie musíte overiť svoj e-mail." @@ -7907,7 +7907,7 @@ "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "message": "Upozornenie: 2. mája nepriradené položky organizácie už nebudú viditeľné v zobrazení Všetky Trezory a budú prístupné len cez administrátorskú konzolu. Aby boli viditeľné, priraďte tieto položky do kolekcie z konzoly administrátora." }, "unassignedItemsBannerNotice": { "message": "Upozornenie: Nepriradené položky organizácie už nie sú viditeľné v zobrazení Všetky trezory a sú prístupné iba cez Správcovskú konzolu." @@ -7924,13 +7924,13 @@ "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." }, "deleteProvider": { - "message": "Delete provider" + "message": "Odstrániť poskytovateľa" }, "deleteProviderConfirmation": { - "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + "message": "Odstránenie poskytovateľa je trvalé a nenávratné. Zadajte vaše hlavné heslo pre potvrdenie odstránenia poskytovateľa a súvisiacich dát." }, "deleteProviderName": { - "message": "Cannot delete $ID$", + "message": "$ID$ sa nedá odstrániť", "placeholders": { "id": { "content": "$1", @@ -7939,7 +7939,7 @@ } }, "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "message": "Pred tým než budete môcť odstrániť $ID$, musíte odpojiť všetkých klientov", "placeholders": { "id": { "content": "$1", @@ -7948,16 +7948,16 @@ } }, "providerDeleted": { - "message": "Provider deleted" + "message": "Poskytovateľ odstránený" }, "providerDeletedDesc": { - "message": "The Provider and all associated data has been deleted." + "message": "Poskytovateľ a všetky súvisiace dáta boli odstránené." }, "deleteProviderRecoverConfirmDesc": { - "message": "You have requested to delete this Provider. Use the button below to confirm." + "message": "Požiadali ste o odstránenie tohto Poskytovateľa. Pre potvrdenie operácie použite tlačidlo nižšie." }, "deleteProviderWarning": { - "message": "Deleting your provider is permanent. It cannot be undone." + "message": "Odstránenie poskytovateľa je trvalé. Nedá sa vrátiť späť." }, "errorAssigningTargetCollection": { "message": "Chyba pri priraďovaní cieľovej kolekcie." @@ -7966,79 +7966,88 @@ "message": "Chyba pri priraďovaní cieľového priečinka." }, "integrationsAndSdks": { - "message": "Integrations & SDKs", + "message": "Integrácie a SDK", "description": "The title for the section that deals with integrations and SDKs." }, "integrations": { - "message": "Integrations" + "message": "Integrácie" }, "integrationsDesc": { - "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + "message": "Automaticky synchronizovať položky z Bitwarden Secrets Manager do služby tretej strany." }, "sdks": { - "message": "SDKs" + "message": "SDK" }, "sdksDesc": { - "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + "message": "Použite Bitwarden Secrets Manager SDK v následujúcich programovacích jazykoch pre vytvorenie vašej vlastnej aplikácie." }, "setUpGithubActions": { - "message": "Set up Github Actions" + "message": "Nastaviť Github Actions" }, "setUpGitlabCICD": { - "message": "Set up GitLab CI/CD" + "message": "Nastaviť GitLab CI/CD" }, "setUpAnsible": { - "message": "Set up Ansible" + "message": "Nastaviť Ansible" }, "cSharpSDKRepo": { - "message": "View C# repository" + "message": "Zobraziť C# repozitár" }, "cPlusPlusSDKRepo": { - "message": "View C++ repository" + "message": "Zobraziť C++ repozitár" }, "jsWebAssemblySDKRepo": { - "message": "View JS WebAssembly repository" + "message": "Zobraziť JS WebAssembly repozitár" }, "javaSDKRepo": { - "message": "View Java repository" + "message": "Zobraziť Java repozitár" }, "pythonSDKRepo": { - "message": "View Python repository" + "message": "Zobraziť Python repozitár" }, "phpSDKRepo": { - "message": "View php repository" + "message": "Zobraziť php repozitár" }, "rubySDKRepo": { - "message": "View Ruby repository" + "message": "Zobraziť Ruby repozitár" }, "goSDKRepo": { - "message": "View Go repository" + "message": "Zobraziť Go repozitár" }, "createNewClientToManageAsProvider": { - "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + "message": "Vytvoriť novú klientskú organizáciu ktorú môžete spravovať ako Poskytovateľ. Dodatočné sedenia sa prejavia v najbližšom fakturačnom období." }, "selectAPlan": { - "message": "Select a plan" + "message": "Vyberte plán" }, "thirtyFivePercentDiscount": { - "message": "35% Discount" + "message": "Zľava 35%" }, "monthPerMember": { "message": "month per member" }, "seats": { - "message": "Seats" + "message": "Sedenia" }, "addOrganization": { - "message": "Add organization" + "message": "Pridať organizáciu" }, "createdNewClient": { - "message": "Successfully created new client" + "message": "Nový klient úspešne vytvorený" }, "noAccess": { - "message": "No access" + "message": "Žiadny prístup" }, "collectionAdminConsoleManaged": { - "message": "This collection is only accessible from the admin console" + "message": "Táto zbierka je dostupná iba z administrátorskej konzoly" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/sl/messages.json b/apps/web/src/locales/sl/messages.json index c209ef05bd6..dfde4373801 100644 --- a/apps/web/src/locales/sl/messages.json +++ b/apps/web/src/locales/sl/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/sr/messages.json b/apps/web/src/locales/sr/messages.json index a59b6a88550..f927113fad9 100644 --- a/apps/web/src/locales/sr/messages.json +++ b/apps/web/src/locales/sr/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/sr_CS/messages.json b/apps/web/src/locales/sr_CS/messages.json index 1ff89501f3e..d6c354f4da8 100644 --- a/apps/web/src/locales/sr_CS/messages.json +++ b/apps/web/src/locales/sr_CS/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index 389f1682a85..21a100c5af5 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -7966,17 +7966,17 @@ "message": "Error assigning target folder." }, "integrationsAndSdks": { - "message": "Integrations & SDKs", + "message": "Integrationer och SDK:er", "description": "The title for the section that deals with integrations and SDKs." }, "integrations": { - "message": "Integrations" + "message": "Integrationer" }, "integrationsDesc": { - "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + "message": "Synkronisera automatiskt hemligheter från Bitwarden Secrets Manager till en tredjepartstjänst." }, "sdks": { - "message": "SDKs" + "message": "SDK:er" }, "sdksDesc": { "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." @@ -7985,10 +7985,10 @@ "message": "Set up Github Actions" }, "setUpGitlabCICD": { - "message": "Set up GitLab CI/CD" + "message": "Ställ in GitLab CI/CD" }, "setUpAnsible": { - "message": "Set up Ansible" + "message": "Ställ in Ansible" }, "cSharpSDKRepo": { "message": "View C# repository" @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/te/messages.json b/apps/web/src/locales/te/messages.json index ed04c3a3ef3..13fa5f60c02 100644 --- a/apps/web/src/locales/te/messages.json +++ b/apps/web/src/locales/te/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/th/messages.json b/apps/web/src/locales/th/messages.json index ec114aad906..929c91e74b7 100644 --- a/apps/web/src/locales/th/messages.json +++ b/apps/web/src/locales/th/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/tr/messages.json b/apps/web/src/locales/tr/messages.json index 9177321cb5f..1087bc89bb6 100644 --- a/apps/web/src/locales/tr/messages.json +++ b/apps/web/src/locales/tr/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index ecb31b6da01..c9434d72d61 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "Ця збірка доступна тільки з консолі адміністратора" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/vi/messages.json b/apps/web/src/locales/vi/messages.json index 0696e226e01..81fd3c2820d 100644 --- a/apps/web/src/locales/vi/messages.json +++ b/apps/web/src/locales/vi/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index f6093d9bc77..a9a0d854577 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -7939,7 +7939,7 @@ } }, "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "message": "删除 $ID$ 之前,您必须取消链接所有的客户端。", "placeholders": { "id": { "content": "$1", @@ -8018,7 +8018,7 @@ "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." }, "selectAPlan": { - "message": "Select a plan" + "message": "选择套餐" }, "thirtyFivePercentDiscount": { "message": "35% Discount" @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/zh_TW/messages.json b/apps/web/src/locales/zh_TW/messages.json index 1bc56d2157d..42e47a6e31e 100644 --- a/apps/web/src/locales/zh_TW/messages.json +++ b/apps/web/src/locales/zh_TW/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } From a2fc666823a696c39b6a0adcd7305d73250baf9a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 19 Apr 2024 19:40:56 +0000 Subject: [PATCH 022/110] Autosync the updated translations (#8838) 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 | 6 +- 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 | 4 +- 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 | 14 +-- 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 | 6 +- apps/browser/src/_locales/id/messages.json | 4 +- apps/browser/src/_locales/it/messages.json | 6 +- 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 | 6 +- 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 | 6 +- 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/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 | 12 +- apps/browser/src/_locales/zh_TW/messages.json | 4 +- apps/browser/store/locales/ar/copy.resx | 108 ++++++++++-------- apps/browser/store/locales/az/copy.resx | 107 +++++++++-------- apps/browser/store/locales/be/copy.resx | 104 +++++++++++------ apps/browser/store/locales/bg/copy.resx | 107 +++++++++-------- apps/browser/store/locales/bn/copy.resx | 103 ++++++++++------- apps/browser/store/locales/bs/copy.resx | 103 ++++++++++------- apps/browser/store/locales/ca/copy.resx | 107 +++++++++-------- apps/browser/store/locales/cs/copy.resx | 107 +++++++++-------- apps/browser/store/locales/cy/copy.resx | 107 +++++++++-------- apps/browser/store/locales/da/copy.resx | 107 +++++++++-------- apps/browser/store/locales/de/copy.resx | 107 +++++++++-------- apps/browser/store/locales/el/copy.resx | 107 +++++++++-------- apps/browser/store/locales/en_GB/copy.resx | 103 ++++++++++------- apps/browser/store/locales/en_IN/copy.resx | 103 ++++++++++------- apps/browser/store/locales/es/copy.resx | 107 +++++++++-------- apps/browser/store/locales/et/copy.resx | 103 ++++++++++------- apps/browser/store/locales/eu/copy.resx | 107 +++++++++-------- apps/browser/store/locales/fa/copy.resx | 108 ++++++++++-------- apps/browser/store/locales/fi/copy.resx | 107 +++++++++-------- apps/browser/store/locales/fil/copy.resx | 103 ++++++++++++----- apps/browser/store/locales/fr/copy.resx | 107 +++++++++-------- apps/browser/store/locales/gl/copy.resx | 107 +++++++++-------- apps/browser/store/locales/he/copy.resx | 103 ++++++++++------- apps/browser/store/locales/hi/copy.resx | 103 ++++++++++------- apps/browser/store/locales/hr/copy.resx | 108 ++++++++++-------- apps/browser/store/locales/hu/copy.resx | 104 ++++++++++------- apps/browser/store/locales/id/copy.resx | 103 ++++++++++------- apps/browser/store/locales/it/copy.resx | 107 +++++++++-------- apps/browser/store/locales/ja/copy.resx | 107 +++++++++-------- apps/browser/store/locales/ka/copy.resx | 103 ++++++++++------- apps/browser/store/locales/km/copy.resx | 103 ++++++++++------- apps/browser/store/locales/kn/copy.resx | 108 ++++++++++-------- apps/browser/store/locales/ko/copy.resx | 107 +++++++++-------- apps/browser/store/locales/lt/copy.resx | 107 +++++++++-------- apps/browser/store/locales/lv/copy.resx | 107 +++++++++-------- apps/browser/store/locales/ml/copy.resx | 99 ++++++++++------ apps/browser/store/locales/mr/copy.resx | 103 ++++++++++------- apps/browser/store/locales/my/copy.resx | 103 ++++++++++------- apps/browser/store/locales/nb/copy.resx | 103 ++++++++++------- apps/browser/store/locales/ne/copy.resx | 103 ++++++++++------- apps/browser/store/locales/nl/copy.resx | 108 ++++++++++-------- apps/browser/store/locales/nn/copy.resx | 103 ++++++++++------- apps/browser/store/locales/or/copy.resx | 103 ++++++++++------- apps/browser/store/locales/pl/copy.resx | 108 ++++++++++-------- apps/browser/store/locales/pt_BR/copy.resx | 107 +++++++++-------- apps/browser/store/locales/pt_PT/copy.resx | 107 +++++++++-------- apps/browser/store/locales/ro/copy.resx | 107 +++++++++-------- apps/browser/store/locales/ru/copy.resx | 108 ++++++++++-------- apps/browser/store/locales/si/copy.resx | 103 ++++++++++------- apps/browser/store/locales/sk/copy.resx | 107 +++++++++-------- apps/browser/store/locales/sl/copy.resx | 108 ++++++++++-------- apps/browser/store/locales/sr/copy.resx | 107 +++++++++-------- apps/browser/store/locales/sv/copy.resx | 107 +++++++++-------- apps/browser/store/locales/te/copy.resx | 103 ++++++++++------- apps/browser/store/locales/th/copy.resx | 107 +++++++++-------- apps/browser/store/locales/tr/copy.resx | 107 +++++++++-------- apps/browser/store/locales/uk/copy.resx | 107 +++++++++-------- apps/browser/store/locales/vi/copy.resx | 107 +++++++++-------- apps/browser/store/locales/zh_CN/copy.resx | 108 ++++++++++-------- apps/browser/store/locales/zh_TW/copy.resx | 108 ++++++++++-------- 120 files changed, 3848 insertions(+), 2760 deletions(-) diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index b7e26f33620..e08894be0b1 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - مدير كلمات مرور مجاني", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "مدير كلمات مرور مجاني وآمن لجميع أجهزتك.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 20834af27ff..1e5062d8c67 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Ödənişsiz Parol Meneceri", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bütün cihazlarınız üçün güvənli və ödənişsiz bir parol meneceri.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 630ad48ee63..91ff397b3ae 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden – бясплатны менеджар пароляў", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Бяспечны і бясплатны менеджар пароляў для ўсіх вашых прылад.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index edfcd8a9b40..33be2608b4b 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -3,11 +3,11 @@ "message": "Битуорден (Bitwarden)" }, "extName": { - "message": "Bitwarden", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Безопасно и безплатно управление за всичките ви устройства.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -3001,7 +3001,7 @@ "description": "Notification message for when saving credentials has failed." }, "success": { - "message": "Success" + "message": "Успех" }, "removePasskey": { "message": "Премахване на секретния ключ" diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index 2ab44bf7e14..a12308648a4 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "আপনার সমস্ত ডিভাইসের জন্য একটি সুরক্ষিত এবং বিনামূল্যের পাসওয়ার্ড ব্যবস্থাপক।", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index 8d2a25db9c6..7f406fabee9 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index 30d4c4e636f..7c8bd63aead 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Administrador de contrasenyes segur i gratuït per a tots els vostres dispositius.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index 42a4f9edb1a..bd3c6882df0 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden – Bezplatný správce hesel", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bezpečný a bezplatný správce hesel pro všechna Vaše zařízení.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 52d3cf7d56a..c718c1d8765 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Rheolydd cyfineiriau am ddim", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Rheolydd cyfrineiriau diogel a rhad ac am ddim ar gyfer eich holl ddyfeisiau.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index 08aebe98e14..777c3b484f6 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Gratis adgangskodemanager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "En sikker og gratis adgangskodemanager til alle dine enheder.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 4edca5557cf..8f2a59af1e9 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Kostenloser Passwortmanager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Ein sicherer und kostenloser Passwortmanager für all deine Geräte.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index 75cd5ef2fa8..8c65e61e53f 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Δωρεάν Διαχειριστής Κωδικών", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Ένας ασφαλής και δωρεάν διαχειριστής κωδικών, για όλες σας τις συσκευές.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index b4b7b314db4..e4d90adf1ab 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index 6dd78dc292e..7cc17240d20 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index 20b9a91814f..3e488bce4c7 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bitwarden es un gestor de contraseñas seguro y gratuito para todos tus dispositivos.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index c8325288479..785a3e49860 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Tasuta paroolihaldur", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Turvaline ja tasuta paroolihaldur kõikidele sinu seadmetele.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index 9504f06c65b..9a07b9d9aea 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Pasahitz kudeatzailea", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bitwarden, zure gailu guztietarako pasahitzen kudeatzaile seguru eta doakoa da.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index c96c5c35cf0..c68dc43ef4c 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - مدیریت کلمه عبور رایگان", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "یک مدیریت کننده کلمه عبور رایگان برای تمامی دستگاه‌هایتان.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index b3602afd82d..2cdb6a2379e 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden – Ilmainen salasanahallinta", + "message": "Bitwarden – Salasanahallinta", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Turvallinen ja ilmainen salasanahallinta kaikille laitteillesi.", + "message": "Kotona, töissä tai reissussa, Bitwarden suojaa helposti kaikki salasanasi, avainkoodisi ja arkaluonteiset tietosi.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -3001,7 +3001,7 @@ "description": "Notification message for when saving credentials has failed." }, "success": { - "message": "Success" + "message": "Onnistui" }, "removePasskey": { "message": "Poista suojausavain" @@ -3010,10 +3010,10 @@ "message": "Suojausavain poistettiin" }, "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + "message": "Huomioi: Määrittämättömät organisaatiokohteet eivät enää näy \"Kaikki holvit\" -näkymässä, vaan ne ovat käytettävissä vain Hallintapaneelista." }, "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + "message": "Huomioi: Alkaen 16. toukokuuta 2024, määrittämättömät organisaatiokohteet eivät enää näy \"Kaikki holvit\" -näkymässä, vaan ne ovat käytettävissä vain Hallintapaneelista." }, "unassignedItemsBannerCTAPartOne": { "message": "Määritä nämä kohteet kokoelmaan", @@ -3027,9 +3027,9 @@ "message": "Hallintapaneelista" }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "Virhe määritettäessä kohdekokoelmaa." }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "Virhe määritettäessä kohdekansiota." } } diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index 28418d984d1..0dfb4a39c91 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Libreng Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Isang ligtas at libreng password manager para sa lahat ng iyong mga aparato.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index 206f07800d8..742e31ee58c 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Gestion des mots de passe", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Un gestionnaire de mots de passe sécurisé et gratuit pour tous vos appareils.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index 95b880d1a54..b4c151eeb03 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index efb82e64ec1..61482da54a6 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - מנהל סיסמאות חינמי", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "מנהל סיסמאות חינמי ומאובטח עבור כל המכשירים שלך.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 9c13fa6efa3..b76405eed85 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -3,11 +3,11 @@ "message": "bitwarden" }, "extName": { - "message": "Bitwarden", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "bitwarden is a secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index ee4c4e7859e..2dc500bc1ee 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - besplatni upravitelj lozinki", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bitwarden je siguran i besplatan upravitelj lozinki za sve tvoje uređaje.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index 7d6a8a208be..e47f2cda1fc 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Ingyenes jelszókezelő", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Egy biztonságos és ingyenes jelszókezelő az összes eszközre.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -3001,7 +3001,7 @@ "description": "Notification message for when saving credentials has failed." }, "success": { - "message": "Success" + "message": "Sikeres" }, "removePasskey": { "message": "Jelszó eltávolítása" diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index 92b60324ad5..d4399d8e15d 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Pengelola Sandi Gratis", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bitwarden adalah sebuah pengelola sandi yang aman dan gratis untuk semua perangkat Anda.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 6887b134dfb..93ae6821909 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Password Manager Gratis", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Un password manager sicuro e gratis per tutti i tuoi dispositivi.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -3001,7 +3001,7 @@ "description": "Notification message for when saving credentials has failed." }, "success": { - "message": "Success" + "message": "Successo" }, "removePasskey": { "message": "Rimuovi passkey" diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 744c76a5098..52ff21727af 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - 無料パスワードマネージャー", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bitwarden はあらゆる端末で使える、安全な無料パスワードマネージャーです。", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index ab7b84d22ea..2c18502eca3 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index 67e1f247871..5d1b024c607 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index 9b363cba1f9..047270808e8 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -3,11 +3,11 @@ "message": "ಬಿಟ್ವಾರ್ಡೆನ್" }, "extName": { - "message": "ಬಿಟ್‌ವಾರ್ಡೆನ್ - ಉಚಿತ ಪಾಸ್‌ವರ್ಡ್ ನಿರ್ವಾಹಕ", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "ನಿಮ್ಮ ಎಲ್ಲಾ ಸಾಧನಗಳಿಗೆ ಸುರಕ್ಷಿತ ಮತ್ತು ಉಚಿತ ಪಾಸ್‌ವರ್ಡ್ ನಿರ್ವಾಹಕ.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index 3e4f5769c08..4bc4302f8b7 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - 무료 비밀번호 관리자", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "당신의 모든 기기에서 사용할 수 있는, 안전한 무료 비밀번호 관리자입니다.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index c690c2727b4..b1a2c857e02 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Saugi ir nemokama slaptažodžių tvarkyklė visiems įrenginiams.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index d26b10a30ce..4055693486f 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Drošs bezmaksas paroļu pārvaldnieks visām Tavām ierīcēm.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -3001,7 +3001,7 @@ "description": "Notification message for when saving credentials has failed." }, "success": { - "message": "Success" + "message": "Izdevās" }, "removePasskey": { "message": "Noņemt piekļuves atslēgu" diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index 1db5f6458b3..d9703137fec 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - സൗജന്യ പാസ്സ്‌വേഡ് മാനേജർ ", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "നിങ്ങളുടെ എല്ലാ ഉപകരണങ്ങൾക്കും സുരക്ഷിതവും സൗജന്യവുമായ പാസ്‌വേഡ് മാനേജർ.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index 06cf84efff3..f67f617d3b1 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - विनामूल्य पासवर्ड व्यवस्थापक", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "तुमच्या सर्व उपकरणांसाठी एक सुरक्षित व विनामूल्य पासवर्ड व्यवस्थापक.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index 67e1f247871..5d1b024c607 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 220fe95e233..649163a8dc1 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden — Fri passordbehandling", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bitwarden er en sikker og fri passordbehandler for alle dine PCer og mobiler.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index 67e1f247871..5d1b024c607 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index 808e599e70a..5a52b4f7ef7 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Gratis wachtwoordbeheer", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Een veilige en gratis oplossing voor wachtwoordbeheer voor al je apparaten.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index 67e1f247871..5d1b024c607 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index 67e1f247871..5d1b024c607 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index 1ef79bac42a..e768a70d528 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - darmowy menedżer haseł", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bezpieczny i darmowy menedżer haseł dla wszystkich Twoich urządzeń.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 9322e680d22..c6e62fbd4f3 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Um gerenciador de senhas seguro e gratuito para todos os seus dispositivos.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index d4fdc1be816..06ba8eed26b 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Gestor de Palavras-passe", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Um gestor de palavras-passe seguro e gratuito para todos os seus dispositivos.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 5e8c82e70b6..b3e0a2066fc 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Manager de parole gratuit", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Un manager de parole sigur și gratuit pentru toate dispozitivele dvs.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index 046fe2b9310..e594dbdce2c 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - бесплатный менеджер паролей", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Защищенный и бесплатный менеджер паролей для всех ваших устройств.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index e576a50dd73..05e2dc3edd4 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -3,11 +3,11 @@ "message": "බිට්වාඩන්" }, "extName": { - "message": "බිට්වාඩන් - නොමිලේ මුරපදය කළමනාකරු", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "ඔබගේ සියලු උපාංග සඳහා ආරක්ෂිත සහ නොමිලේ මුරපද කළමණාකරුවෙකු.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index e34fa525be0..eab1d105eb0 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Bezplatný správca hesiel", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "bitwarden je bezpečný a bezplatný správca hesiel pre všetky vaše zariadenia.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -1754,7 +1754,7 @@ } }, "send": { - "message": "Odoslať", + "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "searchSends": { diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index c7f8ed04816..935678efc82 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Brezplačni upravitelj gesel", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Varen in brezplačen upravitelj gesel za vse vaše naprave.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index 8d1ee8264fc..5819546800e 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - бесплатни менаџер лозинки", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Сигурни и бесплатни менаџер лозинки за све ваше уређаје.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index ebe5e6d2812..d96e86b8d3e 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Gratis lösenordshanterare", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bitwarden är en säker och gratis lösenordshanterare för alla dina enheter.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index 67e1f247871..5d1b024c607 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index 718440438e5..794d0e6c22d 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -3,11 +3,11 @@ "message": "bitwarden" }, "extName": { - "message": "bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "bitwarden is a secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 7633b472589..8408253b866 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Ücretsiz Parola Yöneticisi", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Tüm cihazlarınız için güvenli ve ücretsiz bir parola yöneticisi.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index b09d9661425..b590b92041b 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bitwarden - це захищений і безкоштовний менеджер паролів для всіх ваших пристроїв.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index af518878b91..ab1d0d515b7 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Quản lý mật khẩu miễn phí", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Trình quản lý mật khẩu an toàn và miễn phí cho mọi thiết bị của bạn.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index 5842d4f4c12..a2f856c31b8 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - 免费密码管理器", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "安全且免费的跨平台密码管理器。", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -3001,7 +3001,7 @@ "description": "Notification message for when saving credentials has failed." }, "success": { - "message": "Success" + "message": "成功" }, "removePasskey": { "message": "移除通行密钥" @@ -3024,12 +3024,12 @@ "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." }, "adminConsole": { - "message": "Admin Console" + "message": "管理控制台" }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "分配目标集合时出错。" }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "分配目标文件夹时出错。" } } diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index a8163dab98d..1ecfdfc50eb 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - 免費密碼管理工具", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bitwarden 是一款安全、免費、跨平台的密碼管理工具。", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/store/locales/ar/copy.resx b/apps/browser/store/locales/ar/copy.resx index e74606ff15d..e1bfa48b44f 100644 --- a/apps/browser/store/locales/ar/copy.resx +++ b/apps/browser/store/locales/ar/copy.resx @@ -1,17 +1,17 @@  - @@ -118,42 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - مدير كلمات مرور مجاني + Bitwarden Password Manager - مدير كلمات مرور مجاني وآمن لجميع أجهزتك + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - شركة Bitwarden, Inc هي الشركة الأم لشركة 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -تم تصنيف Bitwarden كأفصل مدير كلمات مرور بواسطة كل من The Verge، U.S News & World Report، CNET، وغيرهم. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -قم بادراة وحفظ وتأمين كلمات المرور الخاصة بك، ومشاركتها بين اجهزتك من اي مكان. -يوفر Bitwarden حل مفتوح المصدر لادارة كلمات المرور للجميع، سواء في المنزل، في العمل او في اي مكان. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -قم بانشاء كلمات مرور قوية وفريدة وعشوائية حسب متطلبات الأمان للصفحات التي تزورها. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -يوفر Bitwarden Send امكانية ارسال البيانات --- النصوص والملفات --- بطريقة مشفرة وسريعة لأي شخص. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -يوفر Bitwarden خطط خاصة للفرق والشركات والمؤسسات لتمكنك من مشاركة كلمات المرور مع زملائك في العمل. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -لماذا قد تختار Bitwarden: -تشفير على مستوى عالمي -كلمات المرور محمية بتشفير متقدم تام (end-to-end encryption) من نوعية AES-256 bit، مع salted hashing، و PBKDF2 SHA-256. كل هذا لابقاء بياناتك محمية وخاصة. +More reasons to choose Bitwarden: -مولد كلمات المرور المدمج -قم بانشاء كلمات مرور قوية وفريدة وعشوائية حسب متطلبات الأمان للصفحات التي تزورها. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -الترجمات العالمية -يتوفر Bitwarden باكثر من 40 لغة، وتتنامى الترجمات بفضل مجتمعنا العالمي. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -تطبيقات متعددة المنصات -قم بحماية ومشاركة بياناتاك الحساسة عبر خزنة Bitwarden من اي متصفح ويب، او هاتف ذكي، او جهاز كمبيوتر، وغيرها. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - مدير كلمات مرور مجاني وآمن لجميع أجهزتك + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. مزامنة خزنتك والوصول إليها من عدة أجهزة diff --git a/apps/browser/store/locales/az/copy.resx b/apps/browser/store/locales/az/copy.resx index cb05f8e5d9e..2a3d507df2c 100644 --- a/apps/browser/store/locales/az/copy.resx +++ b/apps/browser/store/locales/az/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Ödənişsiz Parol Meneceri + Bitwarden Password Manager - Bütün cihazlarınız üçün güvənli və ödənişsiz bir parol meneceri + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc., 8bit Solutions LLC-nin ana şirkətidir. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -THE VERGE, U.S. NEWS & WORLD REPORT, CNET VƏ BİR ÇOXUNA GÖRƏ ƏN YAXŞI PAROL MENECERİDİR. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Hər yerdən limitsiz cihazda limitsiz parolu idarə edin, saxlayın, qoruyun və paylaşın. Bitwarden evdə, işdə və ya yolda hər kəsə açıq mənbəli parol idarəetmə həllərini təqdim edir. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Çox istifadə etdiyiniz hər veb sayt üçün təhlükəsizlik tələblərinə görə güclü, unikal və təsadüfi parollar yaradın. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send şifrələnmiş məlumatların (fayl və sadə mətnləri) birbaşa və sürətli göndərilməsini təmin edir. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden, parolları iş yoldaşlarınızla təhlükəsiz paylaşa bilməyiniz üçün şirkətlərə Teams və Enterprise planları təklif edir. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Nəyə görə Bitwarden-i seçməliyik: -Yüksək səviyyə şifrələmə -Parollarınız qabaqcıl ucdan-uca şifrələmə (AES-256 bit, salted hashtag və PBKDF2 SHA-256) ilə qorunur, beləcə datanızın güvənli və gizli qalmasını təmin edir. +More reasons to choose Bitwarden: -Daxili parol yaradıcı -Çox istifadə etdiyiniz hər veb sayt üçün təhlükəsizlik tələblərinə görə güclü, unikal və təsadüfi şifrələr yaradın. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Qlobal tərcümələr -Bitwarden tərcümələri 40 dildə mövcuddur və qlobal cəmiyyətimiz sayəsində böyüməyə davam edir. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Çarpaz platform tətbiqləri -Bitwarden anbarındakı həssas verilənləri, istənilən brauzerdən, mobil cihazdan və ya masaüstü əməliyyat sistemindən və daha çoxundan qoruyub paylaşın. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Bütün cihazlarınız üçün güvənli və ödənişsiz bir parol meneceri + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Anbarınıza bir neçə cihazdan eyniləşdirərək müraciət edin diff --git a/apps/browser/store/locales/be/copy.resx b/apps/browser/store/locales/be/copy.resx index f84dd699a74..65c337826bc 100644 --- a/apps/browser/store/locales/be/copy.resx +++ b/apps/browser/store/locales/be/copy.resx @@ -1,17 +1,17 @@  - @@ -118,24 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – бясплатны менеджар пароляў + Bitwarden Password Manager - Бяспечны і бясплатны менеджар пароляў для ўсіх вашых прылад + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden - просты і бяспечны спосаб захоўваць усе вашы імёны карыстальніка і паролі, а таксама лёгка іх сінхранізаваць паміж усімі вашымі прыладамі. Пашырэнне праграмы Bitwarden дазваляе хутка ўвайсці на любы вэб-сайт з дапамогай Safari або Chrome і падтрымліваецца сотнямі іншых папулярных праграм. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -Крадзеж пароляў — сур'ёзная праблема. Сайты і праграмы, якія вы выкарыстоўваеце падвяргаюцца нападам кожны дзень. Праблемы ў іх бяспецы могуць прывесці да крадзяжу вашага пароля. Акрамя таго, калі вы выкарыстоўваеце адны і тыя ж паролі на розных сайтах і праграмах, то хакеры могуць лёгка атрымаць доступ да некалькіх вашых уліковых запісаў адразу (да паштовай скрыні, да банкаўскага рахунку ды г. д.). +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Эксперты па бяспецы рэкамендуюць выкарыстоўваць розныя выпадкова знегерыраваныя паролі для кожнага створанага вамі ўліковага запісу. Але як жа кіраваць усімі гэтымі паролямі? Bitwarden дазваляе вам лёгка атрымаць доступ да вашых пароляў, а гэтак жа ствараць і захоўваць іх. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Bitwarden захоўвае ўсе вашы імёны карыстальніка і паролі ў зашыфраваным сховішчы, якое сінхранізуецца паміж усімі вашымі прыладамі. Да таго, як даныя пакінуць вашу прыладу, яны будуць зашыфраваны і толькі потым адпраўлены. Мы ў Bitwarden не зможам прачытаць вашы даныя, нават калі мы гэтага захочам. Вашы даныя зашыфраваны пры дапамозе алгарытму AES-256 і PBKDF2 SHA-256. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden — гэта праграмнае забеспячэнне з адкрытым на 100% зыходным кодам. Зыходны код Bitwarden размешчаны на GitHub, і кожны можа свабодна праглядаць, правяраць і рабіць унёсак у код Bitwarden. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. + +Use Bitwarden to secure your workforce and share sensitive information with colleagues. + + +More reasons to choose Bitwarden: + +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. + +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. + +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! + - Бяспечны і бясплатны менеджар пароляў для ўсіх вашых прылад + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Сінхранізацыя і доступ да сховішча з некалькіх прылад diff --git a/apps/browser/store/locales/bg/copy.resx b/apps/browser/store/locales/bg/copy.resx index 29c468f0457..bc08f6a107a 100644 --- a/apps/browser/store/locales/bg/copy.resx +++ b/apps/browser/store/locales/bg/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden — безплатен управител на пароли + Bitwarden Password Manager - Сигурен и свободен управител на пароли за всички устройства + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - „Bitwarden, Inc.“ е компанията-майка на „8bit Solutions LLC“. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -ОПРЕДЕЛЕН КАТО НАЙ-ДОБРИЯТ УПРАВИТЕЛ НА ПАРОЛИ ОТ „THE VERGE“, „U.S. NEWS & WORLD REPORT“, „CNET“ И ОЩЕ. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Управлявайте, съхранявайте, защитавайте и споделяйте неограничен брой пароли на неограничен брой устройства от всяка точка на света. Битуорден предоставя решение за управление на паролите с отворен код, от което може да се възползва всеки, било то у дома, на работа или на път. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Създавайте сигурни, уникални и случайни пароли според изискванията за сигурност на всеки уеб сайт, който посещавате. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -С Изпращанията на Битуорден можете незабавно да предавате шифрована информация под формата на файлове и обикновен текст – директно и с всекиго. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Битуорден предлага планове за екипи и големи фирми, така че служителите в компаниите да могат безопасно да споделят пароли помежду си. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Защо да изберете Битуорден: -Шифроване от най-висока класа -Паролите са защитени със сложен шифър „от край до край“ (AES-256 bit, salted hashtag и PBKDF2 SHA-256), така че данните Ви остават да са защитени и неприкосновени. +More reasons to choose Bitwarden: -Вграден генератор на пароли -Създавайте сигурни, уникални и случайни пароли според изискванията за сигурност на всеки уеб сайт, който посещавате. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Глобални преводи -Битуорден е преведен на 40 езика и този брой не спира да расте, благодарение на нашата глобална общност. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Приложения за всяка система -Защитете и споделяйте поверителните данни от своя трезор в Битуорден от всеки браузър, мобилно устройство или компютър. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Сигурен и свободен управител на пароли за всички устройства + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Удобен достъп до трезора, който се синхронизира от всички устройства diff --git a/apps/browser/store/locales/bn/copy.resx b/apps/browser/store/locales/bn/copy.resx index a8eb4f7c756..1bcfb190016 100644 --- a/apps/browser/store/locales/bn/copy.resx +++ b/apps/browser/store/locales/bn/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – বিনামূল্যের পাসওয়ার্ড ব্যবস্থাপক + Bitwarden Password Manager - আপনার সমস্ত ডিভাইসের জন্য একটি সুরক্ষিত এবং বিনামূল্যের পাসওয়ার্ড ব্যবস্থাপক + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - আপনার সমস্ত ডিভাইসের জন্য একটি সুরক্ষিত এবং বিনামূল্যের পাসওয়ার্ড ব্যবস্থাপক + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. একাধিক ডিভাইস থেকে আপনার ভল্ট সিঙ্ক এবং ব্যাবহার করুন diff --git a/apps/browser/store/locales/bs/copy.resx b/apps/browser/store/locales/bs/copy.resx index 191198691d4..82e4eb1d88e 100644 --- a/apps/browser/store/locales/bs/copy.resx +++ b/apps/browser/store/locales/bs/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/locales/ca/copy.resx b/apps/browser/store/locales/ca/copy.resx index 0bd454addb2..27e685841bd 100644 --- a/apps/browser/store/locales/ca/copy.resx +++ b/apps/browser/store/locales/ca/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Administrador de contrasenyes gratuït + Bitwarden Password Manager - Administrador de contrasenyes segur i gratuït per a tots els vostres dispositius + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. és la companyia matriu de solucions de 8 bits LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -Nomenada Millor gestor de contrasenyes per THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Gestioneu, emmagatzemeu, segures i compartiu contrasenyes il·limitades a través de dispositius il·limitats des de qualsevol lloc. Bitwarden lliura solucions de gestió de contrasenyes de codi obert a tothom, ja siga a casa, a la feina o sobre la marxa. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Genereu contrasenyes fortes, úniques i aleatòries basades en els requisits de seguretat per a cada lloc web que freqüenteu. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send transmet ràpidament informació xifrada --- Fitxers i text complet - directament a qualsevol persona. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden ofereix equips i plans empresarials per a empreses perquè pugueu compartir amb seguretat contrasenyes amb els companys. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Per què triar Bitwarden: -Xifratge de classe mundial -Les contrasenyes estan protegides amb un xifratge avançat fi-a-fi (AES-256 bit, salted hashtag, and PBKDF2 SHA-256), de manera que les vostres dades es mantenen segures i privades. +More reasons to choose Bitwarden: -Generador de contrasenyes integrat -Genereu contrasenyes fortes, úniques i aleatòries basades en els requisits de seguretat per a cada lloc web que freqüenteu. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Traduccions globals -Les traduccions de Bitwarden existeixen en 40 idiomes i creixen, gràcies a la nostra comunitat global. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Aplicacions de plataforma creuada -Assegureu-vos i compartiu dades sensibles a la vostra caixa forta de Bitwarden des de qualsevol navegador, dispositiu mòbil o S.O. d'escriptori, i molt més. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Administrador de contrasenyes segur i gratuït per a tots els vostres dispositius + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sincronitzeu i accediu a la vostra caixa forta des de diversos dispositius diff --git a/apps/browser/store/locales/cs/copy.resx b/apps/browser/store/locales/cs/copy.resx index 6b711e28639..59d8c60b404 100644 --- a/apps/browser/store/locales/cs/copy.resx +++ b/apps/browser/store/locales/cs/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Bezplatný správce hesel + Bitwarden Password Manager - Bezpečný a bezplatný správce hesel pro všechna Vaše zařízení + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. je mateřskou společností 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -THE VERGE, U.S. NEWS & WORLD REPORT, CNET A DALŠÍ JI OZNAČILY ZA NEJLEPŠÍHO SPRÁVCE HESEL. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Spravujte, ukládejte, zabezpečujte a sdílejte neomezený počet hesel na neomezeném počtu zařízení odkudkoliv. Bitwarden poskytuje open source řešení pro správu hesel všem, ať už doma, v práci nebo na cestách. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generujte silná, jedinečná a náhodná hesla na základě bezpečnostních požadavků pro každou webovou stránku, kterou navštěvujete. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send rychle přenáší šifrované informace --- soubory a prostý text -- přímo a komukoli. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden nabízí plány Teams a Enterprise pro firmy, takže můžete bezpečně sdílet hesla s kolegy. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Proč si vybrat Bitwarden: -Šifrování na světové úrovni -Hesla jsou chráněna pokročilým koncovým šifrováním (AES-256 bit, salted hashování a PBKDF2 SHA-256), takže Vaše data zůstanou bezpečná a soukromá. +More reasons to choose Bitwarden: -Vestavěný generátor hesel -Generujte silná, jedinečná a náhodná hesla na základě bezpečnostních požadavků pro každou webovou stránku, kterou navštěvujete. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Globální překlady -Překlady Bitwarden existují ve 40 jazycích a díky naší globální komunitě se stále rozšiřují. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Aplikace pro více platforem -Zabezpečte a sdílejte citlivá data v rámci svého trezoru Bitwarden z jakéhokoli prohlížeče, mobilního zařízení nebo operačního systému pro počítač. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Bezpečný a bezplatný správce hesel pro všechna Vaše zařízení + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Synchronizujte a přistupujte ke svému trezoru z různých zařízení diff --git a/apps/browser/store/locales/cy/copy.resx b/apps/browser/store/locales/cy/copy.resx index 82223296307..983a112c071 100644 --- a/apps/browser/store/locales/cy/copy.resx +++ b/apps/browser/store/locales/cy/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Rheolydd cyfineiriau am ddim + Bitwarden Password Manager - Rheolydd diogel a rhad ac am ddim ar gyfer eich holl ddyfeisiau + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. yw rhiant-gwmni 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -Y RHEOLYDD CYFRINEIRIAU GORAU YN ÔL THE VERGE, US NEWS & WORLD REPORT, CNET, A MWY. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Rheolwch, storiwch, diogelwch a rhannwch gyfrineiriau di-ri ar draws dyfeiriau di-ri o unrhyw le. Mae Bitwarden yn cynnig rhaglenni rheoli cyfrineiriau cod-agored i bawb, boed gartref, yn y gwaith, neu ar fynd. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Gallwch gynhyrchu cyfrineiriau cryf, unigryw ac ar hap yn seiliedig ar ofynion diogelwch ar gyfer pob gwefan rydych chi'n ei defnyddio. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Mae Bitwarden Send yn trosglwyddo gwybodaeth wedi'i hamgryptio yn gyflym -- ffeiliau a thestun plaen -- yn uniongyrchol i unrhyw un. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Mae Bitwarden yn cynnig cynlluniau Teams ac Enterprise i gwmnïau er mwyn i chi allu rhannu cyfrineiriau gyda chydweithwyr yn ddiogel. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Pam dewis Bitwarden: -Amgryptio o'r radd flaenaf -Mae cyfrineiriau wedi'u hamddiffyn ag amgryptio datblygedig o un pen i'r llall (AES-256 bit, hashio â halen, a PBKDF2 SHA-256) er mwyn i'ch data aros yn ddiogel ac yn breifat. +More reasons to choose Bitwarden: -Cynhyrchydd cyfrineiriau -Gallwch gynhyrchu cyfrineiriau cryf, unigryw ac ar hap yn seiliedig ar ofynion diogelwch ar gyfer pob gwefan rydych chi'n ei defnyddio. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Ar gael yn eich iaith chi -Mae Bitwarden wedi'i gyfieithu i dros 40 o ieithoedd, diolch i'n cymuned fyd-eang. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Rhaglenni traws-blatfform -Diogelwch a rhannwch ddata sensitif yn eich cell Bitwarden o unrhyw borwr, dyfais symudol, neu system weithredu, a mwy. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Rheolydd diogel a rhad ac am ddim ar gyfer eich holl ddyfeisiau + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Gallwch gael mynediad at, a chysoni, eich cell o sawl dyfais diff --git a/apps/browser/store/locales/da/copy.resx b/apps/browser/store/locales/da/copy.resx index 858c56dea97..775a3edd818 100644 --- a/apps/browser/store/locales/da/copy.resx +++ b/apps/browser/store/locales/da/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Gratis adgangskodemanager + Bitwarden Password Manager - En sikker og gratis adgangskodemanager til alle dine enheder + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. er moderselskab for 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -UDNÆVNT BEDSTE PASSWORD MANAGER AF THE VERGE, U.S. NEWS & WORLD REPORT, CNET OG FLERE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Administrér, gem, sikr og del adgangskoder ubegrænset på tværs af enheder hvor som helst. Bitwarden leverer open source adgangskodeadministrationsløsninger til alle, hvad enten det er hjemme, på arbejdspladsen eller på farten. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generér stærke, unikke og tilfældige adgangskoder baseret på sikkerhedskrav til hvert websted, du besøger. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send overfører hurtigt krypterede oplysninger --- filer og almindelig tekst - direkte til enhver. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden tilbyder Teams og Enterprise-planer for virksomheder, så du sikkert kan dele adgangskoder med kolleger. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Hvorfor vælge Bitwarden: -Kryptering i verdensklasse -Adgangskoder er beskyttet med avanceret end-to-end-kryptering (AES-256 bit, saltet hashing og PBKDF2 SHA-256), så dine data forbliver sikre og private. +More reasons to choose Bitwarden: -Indbygget adgangskodegenerator -Generér stærke, unikke og tilfældige adgangskoder baseret på sikkerhedskrav til hvert websted, du besøger. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Globale oversættelser -Bitwarden findes på 40 sprog, og flere kommer til, takket være vores globale fællesskab. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Applikationer på tværs af platforme -Beskyt og del følsomme data i din Bitwarden boks fra enhver browser, mobilenhed eller desktop OS og mere. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - En sikker og gratis adgangskodemanager til alle dine enheder + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Synkroniser og få adgang til din boks fra flere enheder diff --git a/apps/browser/store/locales/de/copy.resx b/apps/browser/store/locales/de/copy.resx index 139a6026fdf..2267c6c85ed 100644 --- a/apps/browser/store/locales/de/copy.resx +++ b/apps/browser/store/locales/de/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Kostenloser Passwort-Manager + Bitwarden Passwort-Manager - Ein sicherer und kostenloser Passwort-Manager für all deine Geräte + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. ist die Muttergesellschaft von 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -AUSGEZEICHNET ALS BESTER PASSWORTMANAGER VON THE VERGE, U.S. NEWS & WORLD REPORT, CNET UND ANDEREN. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Verwalte, speichere, sichere und teile unbegrenzte Passwörter von überall auf unbegrenzten Geräten. Bitwarden liefert Open-Source-Passwort-Management-Lösungen für alle, sei es zu Hause, am Arbeitsplatz oder unterwegs. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generiere starke, einzigartige und zufällige Passwörter basierend auf Sicherheitsanforderungen für jede Website, die du häufig besuchst. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send überträgt schnell verschlüsselte Informationen - Dateien und Klartext - direkt an jeden. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden bietet Teams und Enterprise Pläne für Unternehmen an, damit du Passwörter sicher mit Kollegen teilen kannst. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Warum Bitwarden: -Weltklasse Verschlüsselung -Passwörter sind durch erweiterte Ende-zu-Ende-Verschlüsselung (AES-256 Bit, salted hashing und PBKDF2 SHA-256) so bleiben deine Daten sicher und privat. +More reasons to choose Bitwarden: -Integrierter Passwortgenerator -Generiere starke, einzigartige und zufällige Passwörter basierend auf Sicherheitsanforderungen für jede Website, die du häufig besuchst. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Globale Übersetzungen -Bitwarden Übersetzungen existieren in 40 Sprachen und wachsen dank unserer globalen Community. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Plattformübergreifende Anwendungen -Sichere und teile vertrauliche Daten in deinem Bitwarden Tresor von jedem Browser, jedem mobilen Gerät oder Desktop-Betriebssystem und mehr. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Ein sicherer und kostenloser Passwort-Manager für all deine Geräte + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Synchronisiere und greife auf deinen Tresor von unterschiedlichen Geräten aus zu diff --git a/apps/browser/store/locales/el/copy.resx b/apps/browser/store/locales/el/copy.resx index 01def6ea5af..fb50f95bdc2 100644 --- a/apps/browser/store/locales/el/copy.resx +++ b/apps/browser/store/locales/el/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Δωρεάν Διαχειριστής Κωδικών + Bitwarden Password Manager - Ένας ασφαλής και δωρεάν διαχειριστής κωδικών για όλες τις συσκευές σας + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Η Bitwarden, Inc. είναι η μητρική εταιρεία της 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -ΟΝΟΜΑΣΘΗΚΕ ΩΣ Ο ΚΑΛΥΤΕΡΟΣ ΔΙΑΧΕΙΡΙΣΤΗΣ ΚΩΔΙΚΩΝ ΠΡΟΣΒΑΣΗΣ ΑΠΟ ΤΟ VERGE, το U.S. NEWS & WORLD REPORT, το CNET και άλλα. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Διαχειριστείτε, αποθηκεύστε, ασφαλίστε και μοιραστείτε απεριόριστους κωδικούς πρόσβασης σε απεριόριστες συσκευές από οπουδήποτε. Το Bitwarden παρέχει λύσεις διαχείρισης κωδικών πρόσβασης ανοιχτού κώδικα σε όλους, στο σπίτι, στη δουλειά ή εν κινήσει. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Δημιουργήστε ισχυρούς, μοναδικούς και τυχαίους κωδικούς πρόσβασης βάσει των απαιτήσεων ασφαλείας, για κάθε ιστότοπο που συχνάζετε. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Το Bitwarden Send αποστέλλει γρήγορα κρυπτογραφημένες πληροφορίες --- αρχεία και απλό κείμενο -- απευθείας σε οποιονδήποτε. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Το Bitwarden προσφέρει προγράμματα Teams και Enterprise για εταιρείες, ώστε να μπορείτε να μοιράζεστε με ασφάλεια τους κωδικούς πρόσβασης με τους συναδέλφους σας. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Γιατί να επιλέξετε το Bitwarden: -Κρυπτογράφηση παγκόσμιας κλάσης -Οι κωδικοί πρόσβασης προστατεύονται με προηγμένη end-to-end κρυπτογράφηση (AES-256 bit, salted hashing και PBKDF2 SHA-256), ώστε τα δεδομένα σας να παραμένουν ασφαλή και ιδιωτικά. +More reasons to choose Bitwarden: -Ενσωματωμένη Γεννήτρια Κωδικών Πρόσβασης -Δημιουργήστε ισχυρούς, μοναδικούς και τυχαίους κωδικούς πρόσβασης βάσει των απαιτήσεων ασφαλείας, για κάθε ιστότοπο που συχνάζετε. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Παγκόσμιες Μεταφράσεις -Υπάρχουν μεταφράσεις για το Bitwarden σε 40 γλώσσες και αυξάνονται συνεχώς, χάρη στην παγκόσμια κοινότητά μας. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Εφαρμογές για όλες τις πλατφόρμες -Ασφαλίστε και μοιραστείτε ευαίσθητα δεδομένα εντός του Bitwarden Vault από οποιοδήποτε πρόγραμμα περιήγησης, κινητή συσκευή ή λειτουργικό σύστημα υπολογιστών, και πολλά άλλα. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Ένας ασφαλής και δωρεάν διαχειριστής κωδικών για όλες τις συσκευές σας + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Συγχρονίστε και αποκτήστε πρόσβαση στο θησαυροφυλάκιό σας από πολλαπλές συσκευές diff --git a/apps/browser/store/locales/en_GB/copy.resx b/apps/browser/store/locales/en_GB/copy.resx index 191198691d4..82e4eb1d88e 100644 --- a/apps/browser/store/locales/en_GB/copy.resx +++ b/apps/browser/store/locales/en_GB/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/locales/en_IN/copy.resx b/apps/browser/store/locales/en_IN/copy.resx index 191198691d4..82e4eb1d88e 100644 --- a/apps/browser/store/locales/en_IN/copy.resx +++ b/apps/browser/store/locales/en_IN/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/locales/es/copy.resx b/apps/browser/store/locales/es/copy.resx index dc7484777ab..472697d825c 100644 --- a/apps/browser/store/locales/es/copy.resx +++ b/apps/browser/store/locales/es/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Gestor de contraseñas gratuito + Bitwarden Password Manager - Un gestor de contraseñas seguro y gratuito para todos tus dispositivos + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. es la empresa matriz de 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NOMBRADO MEJOR ADMINISTRADOR DE CONTRASEÑAS POR THE VERGE, U.S. NEWS & WORLD REPORT, CNET Y MÁS. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Administre, almacene, proteja y comparta contraseñas ilimitadas en dispositivos ilimitados desde cualquier lugar. Bitwarden ofrece soluciones de gestión de contraseñas de código abierto para todos, ya sea en casa, en el trabajo o en mientras estás de viaje. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Genere contraseñas seguras, únicas y aleatorias en función de los requisitos de seguridad de cada sitio web que frecuenta. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send transmite rápidamente información cifrada --- archivos y texto sin formato, directamente a cualquier persona. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden ofrece planes Teams y Enterprise para empresas para que pueda compartir contraseñas de forma segura con colegas. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -¿Por qué elegir Bitwarden? -Cifrado de clase mundial -Las contraseñas están protegidas con cifrado avanzado de extremo a extremo (AES-256 bits, salted hashing y PBKDF2 SHA-256) para que sus datos permanezcan seguros y privados. +More reasons to choose Bitwarden: -Generador de contraseñas incorporado -Genere contraseñas fuertes, únicas y aleatorias en función de los requisitos de seguridad de cada sitio web que frecuenta. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Traducciones Globales -Las traducciones de Bitwarden existen en 40 idiomas y están creciendo, gracias a nuestra comunidad global. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Aplicaciones multiplataforma -Proteja y comparta datos confidenciales dentro de su Caja Fuerte de Bitwarden desde cualquier navegador, dispositivo móvil o sistema operativo de escritorio, y más. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Un gestor de contraseñas seguro y gratuito para todos tus dispositivos + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sincroniza y accede a tu caja fuerte desde múltiples dispositivos diff --git a/apps/browser/store/locales/et/copy.resx b/apps/browser/store/locales/et/copy.resx index 2014ec88a80..eccbeba1edf 100644 --- a/apps/browser/store/locales/et/copy.resx +++ b/apps/browser/store/locales/et/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Tasuta paroolihaldur + Bitwarden Password Manager - Tasuta ja turvaline paroolihaldur kõikidele sinu seadmetele + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Tasuta ja turvaline paroolihaldur kõikidele Sinu seadmetele + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sünkroniseeri ja halda oma kontot erinevates seadmetes diff --git a/apps/browser/store/locales/eu/copy.resx b/apps/browser/store/locales/eu/copy.resx index e5b3d542e38..e4271e8ae37 100644 --- a/apps/browser/store/locales/eu/copy.resx +++ b/apps/browser/store/locales/eu/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden — Doaneko pasahitz kudeatzailea + Bitwarden Password Manager - Bitwarden, zure gailu guztietarako pasahitzen kudeatzaile seguru eta doakoa + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. 8bit Solutions LLC-ren enpresa matrizea da. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -THE VERGE, U.S. NEWS & WORLD REPORT ETA CNET ENPRESEK PASAHITZ-ADMINISTRATZAILE ONENA izendatu dute, besteak beste. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Gailu guztien artean pasahitz mugagabeak kudeatu, biltegiratu, babestu eta partekatzen ditu. Bitwardenek kode irekiko pasahitzak administratzeko irtenbideak eskaintzen ditu, bai etxean, bai lanean, bai bidaiatzen ari zaren bitartean. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Pasahitz sendoak, bakarrak eta ausazkoak sortzen ditu, webgune bakoitzaren segurtasun-baldintzetan oinarrituta. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send-ek azkar transmititzen du zifratutako informazioa --- artxiboak eta testu sinplea -- edozein pertsonari zuzenean. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden-ek Taldeak eta Enpresak planak eskaintzen ditu, enpresa bereko lankideek pasahitzak modu seguruan parteka ditzaten. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Zergatik aukeratu Bitwarden: -Mundu-mailako zifratzea -Pasahitzak muturretik muturrerako zifratze aurreratuarekin babestuta daude (AES-256 bit, salted hashtag eta PBKDF2 SHA-256), zure informazioa seguru eta pribatu egon dadin. +More reasons to choose Bitwarden: -Pasahitzen sortzailea -Pasahitz sendoak, bakarrak eta ausazkoak sortzen ditu, web gune bakoitzaren segurtasun-baldintzetan oinarrituta. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Itzulpenak -Bitwarden 40 hizkuntzatan dago, eta gero eta gehiago dira, gure komunitate globalari esker. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Plataforma anitzeko aplikazioak -Babestu eta partekatu zure Bitwarden kutxa gotorraren informazio konfidentziala edozein nabigatzailetatik, gailu mugikorretatik, mahaigaineko aplikaziotik eta gehiagotatik. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Zure gailu guztietarako pasahitzen kudeatzaile seguru eta doakoa + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sinkronizatu eta sartu zure kutxa gotorrean hainbat gailutatik diff --git a/apps/browser/store/locales/fa/copy.resx b/apps/browser/store/locales/fa/copy.resx index 23cb3f3bf05..67095435acc 100644 --- a/apps/browser/store/locales/fa/copy.resx +++ b/apps/browser/store/locales/fa/copy.resx @@ -1,17 +1,17 @@  - @@ -118,40 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - مدیریت کلمه عبور رایگان + Bitwarden Password Manager - یک مدیریت کننده کلمه عبور رایگان برای تمامی دستگاه‌هایتان + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden، Inc. شرکت مادر 8bit Solutions LLC است. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -به عنوان بهترین مدیر کلمه عبور توسط VERGE، US News & WORLD REPORT، CNET و دیگران انتخاب شد. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -کلمه‌های عبور با تعداد نامحدود را در دستگاه‌های نامحدود از هر کجا مدیریت کنید، ذخیره کنید، ایمن کنید و به اشتراک بگذارید. Bitwarden راه حل های مدیریت رمز عبور منبع باز را به همه ارائه می دهد، چه در خانه، چه در محل کار یا در حال حرکت. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -بر اساس الزامات امنیتی برای هر وب سایتی که بازدید می کنید، کلمه‌های عبور قوی، منحصر به فرد و تصادفی ایجاد کنید. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send به سرعت اطلاعات رمزگذاری شده --- فایل ها و متن ساده - را مستقیماً به هر کسی منتقل می کند. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden برنامه‌های Teams و Enterprise را برای شرکت‌ها ارائه می‌دهد تا بتوانید به‌طور ایمن کلمه‌های را با همکاران خود به اشتراک بگذارید. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -چرا Bitwarden را انتخاب کنید: -رمزگذاری در کلاس جهانی -کلمه‌های عبور با رمزگذاری پیشرفته (AES-256 بیت، هشتگ سالت و PBKDF2 SHA-256) محافظت می‌شوند تا داده‌های شما امن و خصوصی بمانند. +More reasons to choose Bitwarden: -تولیدکننده کلمه عبور داخلی -بر اساس الزامات امنیتی برای هر وب سایتی که بازدید می کنید، کلمه‌های عبور قوی، منحصر به فرد و تصادفی ایجاد کنید. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -ترجمه های جهانی -ترجمه Bitwarden به 40 زبان وجود دارد و به لطف جامعه جهانی ما در حال رشد است. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -برنامه های کاربردی چند پلتفرمی -داده‌های حساس را در Bitwarden Vault خود از هر مرورگر، دستگاه تلفن همراه یا سیستم عامل دسکتاپ و غیره ایمن کنید و به اشتراک بگذارید. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! + - یک مدیریت کننده کلمه عبور رایگان برای تمامی دستگاه‌هایتان + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. همگام‌سازی و دسترسی به گاوصندوق خود از دستگاه های مختلف diff --git a/apps/browser/store/locales/fi/copy.resx b/apps/browser/store/locales/fi/copy.resx index 4440603239b..42e914a13f7 100644 --- a/apps/browser/store/locales/fi/copy.resx +++ b/apps/browser/store/locales/fi/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Ilmainen salasanahallinta + Bitwarden – Salasanahallinta - Turvallinen ja ilmainen salasanahallinta kaikille laitteillesi + Kotona, töissä tai reissussa, Bitwarden suojaa helposti kaikki salasanasi, avainkoodisi ja arkaluonteiset tietosi. - Bitwarden, Inc. on 8bit Solutions LLC:n emoyhtiö. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NIMENNYT PARHAAKSI SALASANOJEN HALLINNAKSI MM. THE VERGE, U.S. NEWS & WORLD REPORT JA CNET. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Hallinnoi, säilytä, suojaa ja jaa salasanoja rajattomalta laitemäärältä mistä tahansa. Bitwarden tarjoaa avoimeen lähdekoodin perustuvan salasanojen hallintaratkaisun kaikille, olitpa sitten kotona, töissä tai liikkeellä. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Luo usein käyttämillesi sivustoille automaattisesti vahvoja, yksilöllisiä ja satunnaisia salasanoja. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send -ominaisuudella lähetät tietoa nopeasti salattuna — tiedostoja ja tekstiä — suoraan kenelle tahansa. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Yritystoimintaan Bitwarden tarjoaa yrityksille Teams- ja Enterprise-tilaukset, jotta salasanojen jakaminen kollegoiden kesken on turvallista. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Miksi Bitwarden?: -Maailmanluokan salaus -Salasanat on suojattu tehokkaalla päästä päähän salauksella (AES-256 bit, suolattu hajautus ja PBKDF2 SHA-256), joten tietosi pysyvät turvassa ja yksityisinä. +More reasons to choose Bitwarden: -Sisäänrakennettu salasanageneraattori -Luo usein käyttämillesi sivustoille vahvoja, yksilöllisiä ja satunnaisia salasanoja. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Monikielinen -Bitwardenin sovelluksia on käännetty yli 40 kielelle ja määrä kasvaa jatkuvasti, kiitos kansainvälisen yhteisömme. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Alustariippumattomaton -Suojaa, käytä ja jaa Bitwarden-holvisi arkaluontoisia tietoja kaikilla selaimilla, mobiililaitteilla, pöytätietokoneilla ja muissa järjestelmissä. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Turvallinen ja ilmainen salasanahallinta kaikille laitteillesi + Kotona, töissä tai reissussa, Bitwarden suojaa helposti kaikki salasanasi, avainkoodisi ja arkaluonteiset tietosi. Synkronoi ja hallitse holviasi useilla laitteilla diff --git a/apps/browser/store/locales/fil/copy.resx b/apps/browser/store/locales/fil/copy.resx index 4947fa69968..0f68a90bfaa 100644 --- a/apps/browser/store/locales/fil/copy.resx +++ b/apps/browser/store/locales/fil/copy.resx @@ -1,17 +1,17 @@  - @@ -118,17 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Libreng Password Manager + Bitwarden Password Manager - Isang ligtas at libreng password manager para sa lahat ng iyong mga aparato. + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Si Bitwarden, Inc. ang parent company ng 8bit Solutions LLC. Tinawag na Pinakamahusay na Password Manager ng The Verge, U.S. News & World Report, CNET at iba pa. I-manage, i-store, i-secure at i-share ang walang limitasyong mga password sa walang limitasyong mga device mula sa kahit saan. Bitwarden nagbibigay ng mga open source na solusyon sa password management sa lahat, kahit sa bahay, sa trabaho o habang nasa daan. Lumikha ng mga matatas, natatanging, at mga random na password na naka-base sa mga pangangailangan ng seguridad para sa bawat website na madalas mong bisitahin. Ang Bitwarden Send ay nagpapadala ng maayos na naka-encrypt na impormasyon - mga file at plaintext - diretso sa sinuman. Ang Bitwarden ay nag-aalok ng mga Teams at Enterprise plans para sa m. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! + +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. + +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. + +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. + +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. + +Use Bitwarden to secure your workforce and share sensitive information with colleagues. + + +More reasons to choose Bitwarden: + +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. + +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. + +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Isang ligtas at libreng password manager para sa lahat ng iyong mga aparato. + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. I-sync at i-access ang iyong kahadeyero mula sa maraming mga aparato diff --git a/apps/browser/store/locales/fr/copy.resx b/apps/browser/store/locales/fr/copy.resx index 9d311fe7cf8..9927f885d3e 100644 --- a/apps/browser/store/locales/fr/copy.resx +++ b/apps/browser/store/locales/fr/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Gestion des mots de passe + Bitwarden Password Manager - Un gestionnaire de mots de passe sécurisé et gratuit pour tous vos appareils + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. est la société mère de 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NOMMÉ MEILLEUR GESTIONNAIRE DE MOTS DE PASSE PAR THE VERGE, U.S. NEWS & WORLD REPORT, CNET, ET PLUS ENCORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Gérez, stockez, sécurisez et partagez un nombre illimité de mots de passe sur un nombre illimité d'appareils, où que vous soyez. Bitwarden fournit des solutions de gestion de mots de passe open source à tout le monde, que ce soit chez soi, au travail ou en déplacement. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Générez des mots de passe robustes, uniques et aléatoires basés sur des exigences de sécurité pour chaque site web que vous fréquentez. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send transmet rapidement des informations chiffrées --- fichiers et texte --- directement à quiconque. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden propose des plans Teams et Enterprise pour les sociétés afin que vous puissiez partager des mots de passe en toute sécurité avec vos collègues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Pourquoi choisir Bitwarden : -Un chiffrement de classe internationale -Les mots de passe sont protégés par un cryptage avancé de bout en bout (AES-256 bit, hachage salé et PBKDF2 SHA-256) afin que vos données restent sécurisées et privées. +More reasons to choose Bitwarden: -Générateur de mots de passe intégré -Générez des mots de passe forts, uniques et aléatoires en fonction des exigences de sécurité pour chaque site web que vous fréquentez. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Traductions internationales -Les traductions de Bitwarden existent dans 40 langues et ne cessent de croître, grâce à notre communauté globale. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Applications multiplateformes -Sécurisez et partagez des données sensibles dans votre coffre Bitwarden à partir de n'importe quel navigateur, appareil mobile ou système d'exploitation de bureau, et plus encore. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Un gestionnaire de mots de passe sécurisé et gratuit pour tous vos appareils + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Synchroniser et accéder à votre coffre depuis plusieurs appareils diff --git a/apps/browser/store/locales/gl/copy.resx b/apps/browser/store/locales/gl/copy.resx index 04e84f6677c..0fdb224988a 100644 --- a/apps/browser/store/locales/gl/copy.resx +++ b/apps/browser/store/locales/gl/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Xestor de contrasinais gratuíto + Bitwarden Password Manager - Un xestor de contrasinais seguro e gratuíto para todos os teus dispositivos + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. é a empresa matriz de 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NOMEADO MELLOR ADMINISTRADOR DE CONTRASINAIS POR THE VERGE, Ou.S. NEWS & WORLD REPORT, CNET E MÁS. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Administre, almacene, protexa e comparta contrasinais ilimitados en dispositivos ilimitados desde calquera lugar. Bitwarden ofrece solucións de xestión de contrasinais de código aberto para todos, xa sexa en casa, no traballo ou en mentres estás de viaxe. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Xere contrasinais seguros, únicas e aleatorias en función dos requisitos de seguridade de cada sitio web que frecuenta. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send transmite rapidamente información cifrada --- arquivos e texto sen formato, directamente a calquera persoa. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden ofrece plans Teams e Enterprise para empresas para que poida compartir contrasinais de forma segura con colegas. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Por que elixir Bitwarden? -Cifrado de clase mundial -Os contrasinais están protexidas con cifrado avanzado de extremo a extremo (AES-256 bits, salted hashing e PBKDF2 XA-256) para que os seus datos permanezan seguros e privados. +More reasons to choose Bitwarden: -Xerador de contrasinais incorporado -Xere contrasinais fortes, únicas e aleatorias en función dos requisitos de seguridade de cada sitio web que frecuenta. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Traducións Globais -As traducións de Bitwarden existen en 40 idiomas e están a crecer, grazas á nosa comunidade global. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Aplicacións multiplataforma -Protexa e comparta datos confidenciais dentro da súa Caixa Forte de Bitwarden desde calquera navegador, dispositivo móbil ou sistema operativo de escritorio, e máis. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Un xestor de contrasinais seguro e gratuíto para todos os teus dispositivos + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sincroniza e accede á túa caixa forte desde múltiples dispositivos diff --git a/apps/browser/store/locales/he/copy.resx b/apps/browser/store/locales/he/copy.resx index cd980970fca..7f366f0e931 100644 --- a/apps/browser/store/locales/he/copy.resx +++ b/apps/browser/store/locales/he/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – מנהל ססמאות חינמי + Bitwarden Password Manager - מנהל ססמאות חינמי ומאובטח עבור כל המכשירים שלך + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - מנהל סיסמאות חינמי ומאובטח עבור כל המכשירים שלך + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. סנכרון וגישה לכספת שלך ממגוון מכשירים diff --git a/apps/browser/store/locales/hi/copy.resx b/apps/browser/store/locales/hi/copy.resx index 8db837a3c3a..1ea7314d529 100644 --- a/apps/browser/store/locales/hi/copy.resx +++ b/apps/browser/store/locales/hi/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - बिटवार्डन - मुक्त कूटशब्द प्रबंधक + Bitwarden Password Manager - आपके सभी उपकरणों के लिए एक सुरक्षित और नि: शुल्क कूटशब्द प्रबंधक + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - आपके सभी उपकरणों के लिए एक सुरक्षित और नि: शुल्क पासवर्ड प्रबंधक + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. अनेक उपकरणों से अपने तिजोरी सिंक और एक्सेस करें diff --git a/apps/browser/store/locales/hr/copy.resx b/apps/browser/store/locales/hr/copy.resx index 5ff2bcbe01a..dff95b37969 100644 --- a/apps/browser/store/locales/hr/copy.resx +++ b/apps/browser/store/locales/hr/copy.resx @@ -1,17 +1,17 @@  - @@ -118,40 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - besplatni upravitelj lozinki + Bitwarden Password Manager - Siguran i besplatan upravitelj lozinki za sve vaše uređaje + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. je vlasnik tvrtke 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -THE VERGE, U.S. NEWS & WORLD REPORT, CNET I DRUGI ODABRALI SU BITWARDEN NAJBOLJIM UPRAVITELJEM LOZINKI. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Upravljajte, spremajte, osigurajte i dijelite neograničen broj lozinki na neograničenom broju uređaja bilo gdje. Bitwarden omogućuje upravljanje lozinkama, bazirano na otvorenom kodu, svima, bilo kod kuće, na poslu ili u pokretu. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generirajte jake, jedinstvene i nasumične lozinke bazirane na sigurnosnim zahtjevima za svaku web stranicu koju često posjećujete. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send omoguzćuje jednostavno i brzo slanje šifriranih podataka --- datoteki ili teksta -- direktno, bilo kome. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden nudi Teams i Enterprise planove za tvrtke kako biste sigurno mogli dijeliti lozinke s kolegama na poslu. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Zašto odabrati Bitwarden? -Svjetski priznata enkripcija -Lozinke su zaštićene naprednim end-to-end šifriranjem (AES-256 bit, salted hashtag i PBKDF2 SHA-256) kako bi vaši osobni podaci ostali sigurni i samo vaši. +More reasons to choose Bitwarden: -Ugrađen generator lozinki -Generirajte jake, jedinstvene i nasumične lozinke bazirane na sigurnosnim zahtjevima za svako web mjesto koje često posjećujete. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Svjetski dostupan -Bitwarden je, zahvaljujući našoj globalnoj zajednici, dostupan na više od 40 jezika. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Podržani svi OS -Osigurajte i sigurno dijelite osjetljive podatke sadržane u vašem Bitwarden trezoru iz bilo kojeg preglednika, mobilnog uređaja ili stolnog računala s bilo kojim OS. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! + - Siguran i besplatan upravitelj lozinki za sve tvoje uređaje + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sinkroniziraj i pristupi svojem trezoru s više uređaja diff --git a/apps/browser/store/locales/hu/copy.resx b/apps/browser/store/locales/hu/copy.resx index 0b3761a8ada..3e6b8e42d46 100644 --- a/apps/browser/store/locales/hu/copy.resx +++ b/apps/browser/store/locales/hu/copy.resx @@ -1,17 +1,17 @@  - @@ -118,36 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Ingyenes jelszókezelő + Bitwarden Password Manager - Egy biztonságos és ingyenes jelszókezelő az összes eszközre. + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - A Bitwarden, Inc. a 8bit Solutions LLC anyavállalata. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -A VERGE, A US NEWS & WORLD REPORT, a CNET ÉS MÁSOK LEGJOBB JELSZÓKEZELŐJE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Korlátlan számú jelszavak kezelése, tárolása, védelme és megosztása korlátlan eszközökön bárhonnan. A Bitwarden nyílt forráskódú jelszókezelési megoldásokat kínál mindenkinek, legyen az otthon, a munkahelyen vagy útközben. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Hozzunk létre erős, egyedi és véletlenszerű jelszavakat a biztonsági követelmények alapján minden webhelyre, amelyet gyakran látogatunk. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -A Bitwarden Send gyorsan továbbítja a titkosított információkat-fájlokat és egyszerű szöveget közvetlenül bárkinek. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -A Bitwarden csapatokat és vállalati terveket kínál a vállalatok számára, így biztonságosan megoszthatja jelszavait kollégáival. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Miért válasszuk a Bitwardent: -Világszínvonalú titkosítási jelszavak fejlett végpontok közötti titkosítással (AES-256 bit, titkosított hashtag és PBKDF2 SHA-256) védettek, így az adatok biztonságban és titokban maradnak. +More reasons to choose Bitwarden: -Beépített jelszógenerátor A biztonsági követelmények alapján erős, egyedi és véletlenszerű jelszavakat hozhat létre minden gyakran látogatott webhelyen. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Globális fordítások +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -A Bitwarden fordítások 40 nyelven léteznek és globális közösségünknek köszönhetően egyre bővülnek. Többplatformos alkalmazások Biztonságos és megoszthatja az érzékeny adatokat a Bitwarden Széfben bármely böngészőből, mobileszközről vagy asztali operációs rendszerből stb. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! + - Egy biztonságos és ingyenes jelszókezelő az összes eszközre + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. A széf szinkronizálása és elérése több eszközön. diff --git a/apps/browser/store/locales/id/copy.resx b/apps/browser/store/locales/id/copy.resx index b52252a342e..b0791fa3b1f 100644 --- a/apps/browser/store/locales/id/copy.resx +++ b/apps/browser/store/locales/id/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Pengelola Sandi Gratis + Bitwarden Password Manager - Pengelola sandi yang aman dan gratis untuk semua perangkat Anda + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Pengelola sandi yang aman dan gratis untuk semua perangkat Anda + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sinkronkan dan akses brankas Anda dari beberapa perangkat diff --git a/apps/browser/store/locales/it/copy.resx b/apps/browser/store/locales/it/copy.resx index 56bf9a907ce..bcbbe105127 100644 --- a/apps/browser/store/locales/it/copy.resx +++ b/apps/browser/store/locales/it/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Password Manager Gratis + Bitwarden Password Manager - Un password manager sicuro e gratis per tutti i tuoi dispositivi + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. è la società madre di 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NOMINATO MIGLIOR PASSWORD MANAGER DA THE VERGE, U.S. NEWS & WORLD REPORT, CNET, E ALTRO. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Gestisci, archivia, proteggi, e condividi password illimitate su dispositivi illimitati da qualsiasi luogo. Bitwarden offre soluzioni di gestione delle password open-source a tutti, a casa, al lavoro, o in viaggio. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Genera password forti, uniche, e casuali in base ai requisiti di sicurezza per ogni sito web che frequenti. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send trasmette rapidamente informazioni crittate - via file e testo in chiaro - direttamente a chiunque. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offre piani Teams ed Enterprise per le aziende così puoi condividere le password in modo sicuro con i tuoi colleghi. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Perché Scegliere Bitwarden: -Crittografia Di Livello Mondiale -Le password sono protette con crittografia end-to-end avanzata (AES-256 bit, salted hashing, e PBKDF2 SHA-256) per tenere i tuoi dati al sicuro e privati. +More reasons to choose Bitwarden: -Generatore Di Password Integrato -Genera password forti, uniche e casuali in base ai requisiti di sicurezza per ogni sito web che frequenti. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Traduzioni Globali -Le traduzioni di Bitwarden esistono in 40 lingue e sono in crescita grazie alla nostra comunità globale. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Applicazioni Multipiattaforma -Proteggi e condividi i dati sensibili all'interno della tua cassaforte di Bitwarden da qualsiasi browser, dispositivo mobile, o sistema operativo desktop, e altro. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Un password manager sicuro e gratis per tutti i tuoi dispositivi + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sincronizza e accedi alla tua cassaforte da più dispositivi diff --git a/apps/browser/store/locales/ja/copy.resx b/apps/browser/store/locales/ja/copy.resx index 13ce1bc4e9e..67c479fcde6 100644 --- a/apps/browser/store/locales/ja/copy.resx +++ b/apps/browser/store/locales/ja/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - 無料パスワードマネージャー + Bitwarden Password Manager - あらゆる端末で使える、安全な無料パスワードマネージャー + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc.は8bit Solutions LLC.の親会社です。 + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -THE VERGEやU.S. NEWS、WORLD REPORT、CNETなどからベストパスワードマネージャーに選ばれました。 +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -端末や場所を問わずパスワードの管理・保存・保護・共有を無制限にできます。Bitwardenは自宅や職場、外出先でもパスワード管理をすべての人に提供し、プログラムコードは公開されています。 +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -よく利用するどのWebサイトでも、セキュリティ条件にそった強力でユニークなパスワードをランダムに生成することができます。 +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Sendは、暗号化した情報(ファイルや平文)をすぐに誰にでも直接送信することができます。 +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwardenは企業向けにTeamsとEnterpriseのプランを提供しており、パスワードを同僚と安全に共有することができます。 +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Bitwardenを選ぶ理由は? -・世界最高レベルの暗号化 -パスワードは高度なエンドツーエンド暗号化(AES-256 bit、salted hashing、PBKDF2 SHA-256)で保護されるので、データは安全に非公開で保たれます。 +More reasons to choose Bitwarden: -・パスワード生成機能 -よく利用するどのWebサイトでも、セキュリティ条件にそった強力でユニークなパスワードをランダムに生成することができます。 +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -・グローバルな翻訳 -Bitwardenは40ヶ国語に翻訳されており、グローバルなコミュニティのおかげで増え続けています。 +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -・クロスプラットフォームアプリケーション -あなたのBitwarden Vaultで、ブラウザ・モバイル機器・デスクトップOSなどの垣根を超えて、機密データを保護・共有することができます。 +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - あらゆる端末で使える、安全な無料パスワードマネージャー + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. 複数の端末で保管庫に同期&アクセス diff --git a/apps/browser/store/locales/ka/copy.resx b/apps/browser/store/locales/ka/copy.resx index 191198691d4..82e4eb1d88e 100644 --- a/apps/browser/store/locales/ka/copy.resx +++ b/apps/browser/store/locales/ka/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/locales/km/copy.resx b/apps/browser/store/locales/km/copy.resx index 191198691d4..82e4eb1d88e 100644 --- a/apps/browser/store/locales/km/copy.resx +++ b/apps/browser/store/locales/km/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/locales/kn/copy.resx b/apps/browser/store/locales/kn/copy.resx index 6928f557e45..f68f2c25dab 100644 --- a/apps/browser/store/locales/kn/copy.resx +++ b/apps/browser/store/locales/kn/copy.resx @@ -1,17 +1,17 @@  - @@ -118,40 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - ಬಿಟ್ವರ್ಡ್ – ಉಚಿತ ಪಾಸ್ವರ್ಡ್ ನಿರ್ವಾಹಕ + Bitwarden Password Manager - ನಿಮ್ಮ ಎಲ್ಲಾ ಸಾಧನಗಳಿಗೆ ಸುರಕ್ಷಿತ ಮತ್ತು ಉಚಿತ ಪಾಸ್‌ವರ್ಡ್ ನಿರ್ವಾಹಕ + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - ಬಿಟ್ವಾರ್ಡೆನ್, ಇಂಕ್. 8 ಬಿಟ್ ಸೊಲ್ಯೂಷನ್ಸ್ ಎಲ್ಎಲ್ ಸಿ ಯ ಮೂಲ ಕಂಪನಿಯಾಗಿದೆ. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -ವರ್ಜ್, ಯು.ಎಸ್. ನ್ಯೂಸ್ & ವರ್ಲ್ಡ್ ರಿಪೋರ್ಟ್, ಸಿನೆಟ್ ಮತ್ತು ಹೆಚ್ಚಿನದರಿಂದ ಉತ್ತಮ ಪಾಸ್‌ವರ್ಡ್ ವ್ಯವಸ್ಥಾಪಕ ಎಂದು ಹೆಸರಿಸಲಾಗಿದೆ. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -ಎಲ್ಲಿಂದಲಾದರೂ ಅನಿಯಮಿತ ಸಾಧನಗಳಲ್ಲಿ ಅನಿಯಮಿತ ಪಾಸ್‌ವರ್ಡ್‌ಗಳನ್ನು ನಿರ್ವಹಿಸಿ, ಸಂಗ್ರಹಿಸಿ, ಸುರಕ್ಷಿತಗೊಳಿಸಿ ಮತ್ತು ಹಂಚಿಕೊಳ್ಳಿ. ಮನೆಯಲ್ಲಿ, ಕೆಲಸದಲ್ಲಿ ಅಥವಾ ಪ್ರಯಾಣದಲ್ಲಿರಲಿ ಪ್ರತಿಯೊಬ್ಬರಿಗೂ ಬಿಟ್‌ವಾರ್ಡೆನ್ ಓಪನ್ ಸೋರ್ಸ್ ಪಾಸ್‌ವರ್ಡ್ ನಿರ್ವಹಣಾ ಪರಿಹಾರಗಳನ್ನು ನೀಡುತ್ತದೆ. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -ನೀವು ಆಗಾಗ್ಗೆ ಪ್ರತಿ ವೆಬ್‌ಸೈಟ್‌ಗೆ ಸುರಕ್ಷತಾ ಅವಶ್ಯಕತೆಗಳನ್ನು ಆಧರಿಸಿ ಬಲವಾದ, ಅನನ್ಯ ಮತ್ತು ಯಾದೃಚ್ pass ಿಕ ಪಾಸ್‌ವರ್ಡ್‌ಗಳನ್ನು ರಚಿಸಿ. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -ಬಿಟ್‌ವಾರ್ಡೆನ್ ಕಳುಹಿಸಿ ಎನ್‌ಕ್ರಿಪ್ಟ್ ಮಾಡಿದ ಮಾಹಿತಿಯನ್ನು ತ್ವರಿತವಾಗಿ ರವಾನಿಸುತ್ತದೆ --- ಫೈಲ್‌ಗಳು ಮತ್ತು ಸರಳ ಪಠ್ಯ - ನೇರವಾಗಿ ಯಾರಿಗಾದರೂ. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -ಬಿಟ್‌ವಾರ್ಡೆನ್ ಕಂಪೆನಿಗಳಿಗೆ ತಂಡಗಳು ಮತ್ತು ಎಂಟರ್‌ಪ್ರೈಸ್ ಯೋಜನೆಗಳನ್ನು ನೀಡುತ್ತದೆ ಆದ್ದರಿಂದ ನೀವು ಪಾಸ್‌ವರ್ಡ್‌ಗಳನ್ನು ಸಹೋದ್ಯೋಗಿಗಳೊಂದಿಗೆ ಸುರಕ್ಷಿತವಾಗಿ ಹಂಚಿಕೊಳ್ಳಬಹುದು. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -ಬಿಟ್‌ವಾರ್ಡೆನ್ ಅನ್ನು ಏಕೆ ಆರಿಸಬೇಕು: -ವಿಶ್ವ ದರ್ಜೆಯ ಗೂ ry ಲಿಪೀಕರಣ -ಪಾಸ್‌ವರ್ಡ್‌ಗಳನ್ನು ಸುಧಾರಿತ ಎಂಡ್-ಟು-ಎಂಡ್ ಎನ್‌ಕ್ರಿಪ್ಶನ್ (ಎಇಎಸ್ -256 ಬಿಟ್, ಉಪ್ಪುಸಹಿತ ಹ್ಯಾಶ್‌ಟ್ಯಾಗ್ ಮತ್ತು ಪಿಬಿಕೆಡಿಎಫ್ 2 ಎಸ್‌ಎಚ್‌ಎ -256) ನೊಂದಿಗೆ ರಕ್ಷಿಸಲಾಗಿದೆ ಆದ್ದರಿಂದ ನಿಮ್ಮ ಡೇಟಾ ಸುರಕ್ಷಿತ ಮತ್ತು ಖಾಸಗಿಯಾಗಿರುತ್ತದೆ. +More reasons to choose Bitwarden: -ಅಂತರ್ನಿರ್ಮಿತ ಪಾಸ್ವರ್ಡ್ ಜನರೇಟರ್ -ನೀವು ಆಗಾಗ್ಗೆ ಪ್ರತಿ ವೆಬ್‌ಸೈಟ್‌ಗೆ ಸುರಕ್ಷತಾ ಅವಶ್ಯಕತೆಗಳನ್ನು ಆಧರಿಸಿ ಬಲವಾದ, ಅನನ್ಯ ಮತ್ತು ಯಾದೃಚ್ pass ಿಕ ಪಾಸ್‌ವರ್ಡ್‌ಗಳನ್ನು ರಚಿಸಿ. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -ಜಾಗತಿಕ ಅನುವಾದಗಳು -ಬಿಟ್ವಾರ್ಡೆನ್ ಅನುವಾದಗಳು 40 ಭಾಷೆಗಳಲ್ಲಿ ಅಸ್ತಿತ್ವದಲ್ಲಿವೆ ಮತ್ತು ಬೆಳೆಯುತ್ತಿವೆ, ನಮ್ಮ ಜಾಗತಿಕ ಸಮುದಾಯಕ್ಕೆ ಧನ್ಯವಾದಗಳು. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -ಕ್ರಾಸ್ ಪ್ಲಾಟ್‌ಫಾರ್ಮ್ ಅಪ್ಲಿಕೇಶನ್‌ಗಳು -ಯಾವುದೇ ಬ್ರೌಸರ್, ಮೊಬೈಲ್ ಸಾಧನ, ಅಥವಾ ಡೆಸ್ಕ್‌ಟಾಪ್ ಓಎಸ್ ಮತ್ತು ಹೆಚ್ಚಿನವುಗಳಿಂದ ನಿಮ್ಮ ಬಿಟ್‌ವಾರ್ಡನ್ ವಾಲ್ಟ್‌ನಲ್ಲಿ ಸೂಕ್ಷ್ಮ ಡೇಟಾವನ್ನು ಸುರಕ್ಷಿತಗೊಳಿಸಿ ಮತ್ತು ಹಂಚಿಕೊಳ್ಳಿ. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! + - ನಿಮ್ಮ ಎಲ್ಲಾ ಸಾಧನಗಳಿಗೆ ಸುರಕ್ಷಿತ ಮತ್ತು ಉಚಿತ ಪಾಸ್‌ವರ್ಡ್ ನಿರ್ವಾಹಕ + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. ಅನೇಕ ಸಾಧನಗಳಿಂದ ನಿಮ್ಮ ವಾಲ್ಟ್ ಅನ್ನು ಸಿಂಕ್ ಮಾಡಿ ಮತ್ತು ಪ್ರವೇಶಿಸಿ diff --git a/apps/browser/store/locales/ko/copy.resx b/apps/browser/store/locales/ko/copy.resx index 0fb5dd713fc..fdfb93ad6ae 100644 --- a/apps/browser/store/locales/ko/copy.resx +++ b/apps/browser/store/locales/ko/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - 무료 비밀번호 관리자 + Bitwarden Password Manager - 당신의 모든 기기에서 사용할 수 있는, 안전한 무료 비밀번호 관리자 + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc.은 8bit Solutions LLC.의 모회사입니다. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -VERGE, U.S. NEWS, WORLD REPORT, CNET 등에서 최고의 비밀번호 관리자라고 평가했습니다! +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -관리하고, 보관하고, 보호하고, 어디에서든 어떤 기기에서나 무제한으로 비밀번호를 공유하세요. Bitwarden은 모두에게 오픈소스 비밀번호 관리 솔루션을 제공합니다. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -강하고, 독특하고, 랜덤한 비밀번호를 모든 웹사이트의 보안 요구사항에 따라 생성할 수 있습니다. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send는 빠르게 암호화된 파일과 텍스트를 모두에게 전송할 수 있습니다. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden은 회사들을 위해 팀과 기업 플랜을 제공해서 동료에게 안전하게 비밀번호를 공유할 수 있습니다. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Bitwarden을 선택하는 이유: -세계 최고의 암호화 -비밀번호는 고급 종단간 암호화 (AES-256 bit, salted hashtag, 그리고 PBKDF2 SHA-256)을 이용하여 보호되기 때문에 데이터를 안전하게 보관할 수 있습니다. +More reasons to choose Bitwarden: -내장 비밀번호 생성기 -강하고, 독특하고, 랜덤한 비밀번호를 모든 웹사이트의 보안 요구사항에 따라 생성할 수 있습니다. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -언어 지원 -Bitwarden 번역은 전 세계의 커뮤니티 덕분에 40개의 언어를 지원하고 더 성장하고 있습니다. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -크로스 플랫폼 애플리케이션 -Bitwarden 보관함에 있는 민감한 정보를 어떠한 브라우저, 모바일 기기, 데스크톱 OS 등을 이용하여 보호하고 공유하세요. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - 당신의 모든 기기에서 사용할 수 있는, 안전한 무료 비밀번호 관리자 + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. 여러 기기에서 보관함에 접근하고 동기화할 수 있습니다. diff --git a/apps/browser/store/locales/lt/copy.resx b/apps/browser/store/locales/lt/copy.resx index 92009c5c6d5..d83c6ca99a5 100644 --- a/apps/browser/store/locales/lt/copy.resx +++ b/apps/browser/store/locales/lt/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – nemokamas slaptažodžių tvarkyklė + Bitwarden Password Manager - Saugi ir nemokama slaptažodžių tvarkyklė visiems įrenginiams + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. yra patronuojančioji 8bit Solutions LLC įmonė. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -GERIAUSIU SLAPTAŽODŽIŲ TVARKYTOJU PRIPAŽINTAS THE VERGE, U.S. NEWS & WORLD REPORT, CNET IR KT. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Tvarkyk, laikyk, saugok ir bendrink neribotą skaičių slaptažodžių neribotuose įrenginiuose iš bet kurios vietos. Bitwarden teikia atvirojo kodo slaptažodžių valdymo sprendimus visiems – tiek namuose, tiek darbe, ar keliaujant. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generuok stiprius, unikalius ir atsitiktinius slaptažodžius pagal saugos reikalavimus kiekvienai lankomai svetainei. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send greitai perduoda užšifruotą informaciją – failus ir paprastą tekstą – tiesiogiai bet kam. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden siūlo komandoms ir verslui planus įmonėms, kad galėtum saugiai dalytis slaptažodžiais su kolegomis. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Kodėl rinktis Bitwarden: -Pasaulinės klasės šifravimas -Slaptažodžiai yra saugomi su pažangiu šifravimu nuo galo iki galo (AES-256 bitų, sūdytu šifravimu ir PBKDF2 SHA-256), todėl tavo duomenys išliks saugūs ir privatūs. +More reasons to choose Bitwarden: -Integruotas slaptažodžių generatorius -Generuok stiprius, unikalius ir atsitiktinius slaptažodžius pagal saugos reikalavimus kiekvienai dažnai lankomai svetainei. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Visuotiniai vertimai -Mūsų pasaulinės bendruomenės dėka Bitwarden vertimai egzistuoja 40 kalbose ir vis daugėja. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Įvairių platformų programos -Apsaugok ir bendrink neskelbtinus duomenis savo Bitwarden Vault iš bet kurios naršyklės, mobiliojo įrenginio ar darbalaukio OS ir kt. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Saugi ir nemokama slaptažodžių tvarkyklė visiems įrenginiams + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Pasiekite savo saugyklą iš kelių įrenginių diff --git a/apps/browser/store/locales/lv/copy.resx b/apps/browser/store/locales/lv/copy.resx index aec5e836c1b..e64cc2eb3a4 100644 --- a/apps/browser/store/locales/lv/copy.resx +++ b/apps/browser/store/locales/lv/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Bezmaksas Paroļu Pārvaldnieks + Bitwarden Password Manager - Drošs un bezmaksas paroļu pārvaldnieks priekš visām jūsu ierīcēm. + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. ir 8bit Solutions LLC mātesuzņēmums. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -THE VERGE, U.S. NEWS & WORLD REPORT, CNET UN CITI ATZINA PAR LABĀKO PAROĻU PĀRVALDNIEKU. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Pārvaldi, uzglabā, aizsargā un kopīgo neierobežotu skaitu paroļu neierobežotā skaitā ierīču no jebkuras vietas. Bitwarden piedāvā atvērtā koda paroļu pārvaldības risinājumus ikvienam - gan mājās, gan darbā, gan ceļā. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Ģenerē spēcīgas, unikālas un nejaušas paroles, pamatojoties uz drošības prasībām, katrai bieži apmeklētai vietnei. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send ātri pārsūta šifrētu informāciju - failus un atklātu tekstu - tieši jebkuram. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden piedāvā Teams un Enterprise plānus uzņēmumiem, lai tu varētu droši kopīgot paroles ar kolēģiem. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Kāpēc izvēlēties Bitwarden: -Pasaules klases šifrēšana -Paroles tiek aizsargātas ar modernu end-to-end šifrēšanu (AES-256 bitu, sālītu šifrēšanu un PBKDF2 SHA-256), lai tavi dati paliktu droši un privāti. +More reasons to choose Bitwarden: -Iebūvēts paroļu ģenerators -Ģenerē spēcīgas, unikālas un nejaušas paroles, pamatojoties uz drošības prasībām katrai bieži apmeklētai vietnei. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Globālie tulkojumi -Bitwarden tulkojumi ir pieejami 40 valodās, un to skaits turpina pieaugt, pateicoties mūsu globālajai kopienai. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Starpplatformu lietojumprogrammas -Nodrošini un kopīgo sensitīvus datus savā Bitwarden Seifā no jebkuras pārlūkprogrammas, mobilās ierīces vai darbvirsmas operētājsistēmas un daudz ko citu. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Drošs un bezmaksas paroļu pārvaldnieks priekš visām jūsu ierīcēm. + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sinhronizē un piekļūsti savai glabātavai no vairākām ierīcēm diff --git a/apps/browser/store/locales/ml/copy.resx b/apps/browser/store/locales/ml/copy.resx index cf9b6312273..e22993d5b75 100644 --- a/apps/browser/store/locales/ml/copy.resx +++ b/apps/browser/store/locales/ml/copy.resx @@ -1,17 +1,17 @@  - @@ -118,27 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - സൗജന്യ പാസ്സ്‌വേഡ് മാനേജർ + Bitwarden Password Manager - നിങ്ങളുടെ എല്ലാ ഉപകരണങ്ങൾക്കും സുരക്ഷിതവും സൗജന്യവുമായ പാസ്‌വേഡ് മാനേജർ + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - നിങ്ങളുടെ എല്ലാ ലോഗിനുകളും പാസ്‌വേഡുകളും സംഭരിക്കുന്നതിനുള്ള ഏറ്റവും എളുപ്പവും സുരക്ഷിതവുമായ മാർഗ്ഗമാണ് Bitwarden, ഒപ്പം നിങ്ങളുടെ എല്ലാ ഉപകരണങ്ങളും തമ്മിൽ സമന്വയിപ്പിക്കുകയും ചെയ്യുന്നു. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -പാസ്‌വേഡ് മോഷണം ഗുരുതരമായ പ്രശ്‌നമാണ്. നിങ്ങൾ ഉപയോഗിക്കുന്ന വെബ്‌സൈറ്റുകളും അപ്ലിക്കേഷനുകളും എല്ലാ ദിവസവും ആക്രമണത്തിലാണ്. സുരക്ഷാ ലംഘനങ്ങൾ സംഭവിക്കുകയും നിങ്ങളുടെ പാസ്‌വേഡുകൾ മോഷ്‌ടിക്കപ്പെടുകയും ചെയ്യുന്നു. അപ്ലിക്കേഷനുകളിലും വെബ്‌സൈറ്റുകളിലും ഉടനീളം സമാന പാസ്‌വേഡുകൾ നിങ്ങൾ വീണ്ടും ഉപയോഗിക്കുമ്പോൾ ഹാക്കർമാർക്ക് നിങ്ങളുടെ ഇമെയിൽ, ബാങ്ക്, മറ്റ് പ്രധാനപ്പെട്ട അക്കൗണ്ടുകൾ എന്നിവ എളുപ്പത്തിൽ ആക്‌സസ്സുചെയ്യാനാകും. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -നിങ്ങളുടെ എല്ലാ ഉപകരണങ്ങളിലും സമന്വയിപ്പിക്കുന്ന ഒരു എൻ‌ക്രിപ്റ്റ് ചെയ്ത വാൾട്ടിൽ Bitwarden നിങ്ങളുടെ എല്ലാ ലോഗിനുകളും സംഭരിക്കുന്നു. നിങ്ങളുടെ ഉപകരണം വിടുന്നതിനുമുമ്പ് ഇത് പൂർണ്ണമായും എൻ‌ക്രിപ്റ്റ് ചെയ്‌തിരിക്കുന്നതിനാൽ, നിങ്ങളുടെ ഡാറ്റ നിങ്ങൾക്ക് മാത്രമേ ആക്‌സസ് ചെയ്യാൻ കഴിയൂ . Bitwarden ടീമിന് പോലും നിങ്ങളുടെ ഡാറ്റ വായിക്കാൻ കഴിയില്ല. നിങ്ങളുടെ ഡാറ്റ AES-256 ബിറ്റ് എൻ‌ക്രിപ്ഷൻ, സാൾട്ടിങ് ഹാഷിംഗ്, PBKDF2 SHA-256 എന്നിവ ഉപയോഗിച്ച് അടച്ചിരിക്കുന്നു. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -100% ഓപ്പൺ സോഴ്‌സ് സോഫ്റ്റ്വെയറാണ് Bitwarden . Bitwarden സോഴ്‌സ് കോഡ് GitHub- ൽ ഹോസ്റ്റുചെയ്‌തിരിക്കുന്നു, മാത്രമല്ല എല്ലാവർക്കും ഇത് അവലോകനം ചെയ്യാനും ഓഡിറ്റുചെയ്യാനും ബിറ്റ് വാർഡൻ കോഡ്ബേസിലേക്ക് സംഭാവന ചെയ്യാനും സ്വാതന്ത്ര്യമുണ്ട്. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. + +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. + +Use Bitwarden to secure your workforce and share sensitive information with colleagues. +More reasons to choose Bitwarden: +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. + +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - നിങ്ങളുടെ എല്ലാ ഉപകരണങ്ങൾക്കും സുരക്ഷിതവും സൗജന്യവുമായ പാസ്‌വേഡ് മാനേജർ. + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. ഒന്നിലധികം ഉപകരണങ്ങളിൽ നിന്ന് നിങ്ങളുടെ വാൾട് സമന്വയിപ്പിച്ച് ആക്‌സസ്സുചെയ്യുക diff --git a/apps/browser/store/locales/mr/copy.resx b/apps/browser/store/locales/mr/copy.resx index 191198691d4..82e4eb1d88e 100644 --- a/apps/browser/store/locales/mr/copy.resx +++ b/apps/browser/store/locales/mr/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/locales/my/copy.resx b/apps/browser/store/locales/my/copy.resx index 191198691d4..82e4eb1d88e 100644 --- a/apps/browser/store/locales/my/copy.resx +++ b/apps/browser/store/locales/my/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/locales/nb/copy.resx b/apps/browser/store/locales/nb/copy.resx index 74a8558db65..26a09cc855d 100644 --- a/apps/browser/store/locales/nb/copy.resx +++ b/apps/browser/store/locales/nb/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden — Fri passordbehandling + Bitwarden Password Manager - En sikker og fri passordbehandler for alle dine PCer og mobiler + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - En sikker og fri passordbehandler for alle dine PCer og mobiler + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Synkroniser og få tilgang til ditt hvelv fra alle dine enheter diff --git a/apps/browser/store/locales/ne/copy.resx b/apps/browser/store/locales/ne/copy.resx index 191198691d4..82e4eb1d88e 100644 --- a/apps/browser/store/locales/ne/copy.resx +++ b/apps/browser/store/locales/ne/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/locales/nl/copy.resx b/apps/browser/store/locales/nl/copy.resx index e0779ba777c..44dd02b439f 100644 --- a/apps/browser/store/locales/nl/copy.resx +++ b/apps/browser/store/locales/nl/copy.resx @@ -1,17 +1,17 @@  - @@ -118,40 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Gratis wachtwoordbeheer + Bitwarden Password Manager - Een veilige en gratis oplossing voor wachtwoordbeheer voor al je apparaten + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is het moederbedrijf van 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -BESTE WACHTWOORDBEHEERDER VOLGENS THE VERGE, U.S. NEWS & WORLD REPORT, CNET EN ANDEREN. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Beheer, bewaar, beveilig en deel een onbeperkt aantal wachtwoorden op een onbeperkt aantal apparaten, waar je ook bent. Bitwarden levert open source wachtwoordbeheeroplossingen voor iedereen, of dat nu thuis, op het werk of onderweg is. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Genereer sterke, unieke en willekeurige wachtwoorden op basis van beveiligingsvereisten voor elke website die je bezoekt. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send verzendt snel versleutelde informatie --- bestanden en platte tekst -- rechtstreeks naar iedereen. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden biedt Teams- en Enterprise-abonnementen voor bedrijven, zodat je veilig wachtwoorden kunt delen met collega's. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Waarom Bitwarden: -Versleuteling van wereldklasse -Wachtwoorden worden beschermd met geavanceerde end-to-end-codering (AES-256 bit, salted hashtag en PBKDF2 SHA-256) zodat jouw gegevens veilig en privé blijven. +More reasons to choose Bitwarden: -Ingebouwde wachtwoordgenerator -Genereer sterke, unieke en willekeurige wachtwoorden op basis van beveiligingsvereisten voor elke website die je bezoekt. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Wereldwijde vertalingen -Bitwarden-vertalingen bestaan ​​in 40 talen en groeien dankzij onze wereldwijde community. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Platformoverschrijdende toepassingen -Beveilig en deel gevoelige gegevens binnen uw Bitwarden Vault vanuit elke browser, mobiel apparaat of desktop-besturingssysteem, en meer. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! + - Een veilige en gratis oplossing voor wachtwoordbeheer voor al uw apparaten + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Synchroniseer en gebruik je kluis op meerdere apparaten diff --git a/apps/browser/store/locales/nn/copy.resx b/apps/browser/store/locales/nn/copy.resx index 191198691d4..82e4eb1d88e 100644 --- a/apps/browser/store/locales/nn/copy.resx +++ b/apps/browser/store/locales/nn/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/locales/or/copy.resx b/apps/browser/store/locales/or/copy.resx index 191198691d4..82e4eb1d88e 100644 --- a/apps/browser/store/locales/or/copy.resx +++ b/apps/browser/store/locales/or/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/locales/pl/copy.resx b/apps/browser/store/locales/pl/copy.resx index 5b3941cb7ee..60709c7d4d1 100644 --- a/apps/browser/store/locales/pl/copy.resx +++ b/apps/browser/store/locales/pl/copy.resx @@ -1,17 +1,17 @@  - @@ -118,40 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - darmowy menedżer haseł + Bitwarden Password Manager - Bezpieczny i darmowy menedżer haseł dla wszystkich Twoich urządzeń + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. jest macierzystą firmą 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAZWANY NAJLEPSZYM MENEDŻEREM HASEŁ PRZEZ THE VERGE, US NEWS & WORLD REPORT, CNET I WIĘCEJ. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Zarządzaj, przechowuj, zabezpieczaj i udostępniaj nieograniczoną liczbę haseł na nieograniczonej liczbie urządzeń z każdego miejsca. Bitwarden dostarcza rozwiązania do zarządzania hasłami z otwartym kodem źródłowym każdemu, niezależnie od tego, czy jest w domu, w pracy, czy w podróży. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generuj silne, unikalne i losowe hasła w oparciu o wymagania bezpieczeństwa dla każdej odwiedzanej strony. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Funkcja Bitwarden Send szybko przesyła zaszyfrowane informacje --- pliki i zwykły tekst -- bezpośrednio do każdego. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden oferuje plany dla zespołów i firm, dzięki czemu możesz bezpiecznie udostępniać hasła współpracownikom. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Dlaczego warto wybrać Bitwarden: -Szyfrowanie światowej klasy -Hasła są chronione za pomocą zaawansowanego szyfrowania typu end-to-end (AES-256 bitów, dodatkowy ciąg zaburzający i PBKDF2 SHA-256), dzięki czemu Twoje dane pozostają bezpieczne i prywatne. +More reasons to choose Bitwarden: -Wbudowany generator haseł -Generuj silne, unikalne i losowe hasła w oparciu o wymagania bezpieczeństwa dla każdej odwiedzanej strony. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Przetłumaczone aplikacje -Tłumaczenia Bitwarden są dostępne w 40 językach i rosną dzięki naszej globalnej społeczności. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Aplikacje wieloplatformowe -Zabezpiecz i udostępniaj poufne dane w swoim sejfie Bitwarden z dowolnej przeglądarki, urządzenia mobilnego, systemu operacyjnego i nie tylko. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! + - Bezpieczny i darmowy menedżer haseł dla wszystkich Twoich urządzeń + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Synchronizacja i dostęp do sejfu z różnych urządzeń diff --git a/apps/browser/store/locales/pt_BR/copy.resx b/apps/browser/store/locales/pt_BR/copy.resx index 48111fa814f..8b99c436d05 100644 --- a/apps/browser/store/locales/pt_BR/copy.resx +++ b/apps/browser/store/locales/pt_BR/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Gerenciador de Senhas Gratuito + Bitwarden Password Manager - Um gerenciador de senhas gratuito e seguro para todos os seus dispositivos + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. é a empresa matriz da 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NOMEADA MELHOR GERENCIADORA DE SENHAS PELA VERGE, U.S. NEWS & WORLD REPORT, CNET, E MUITO MAIS. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Gerenciar, armazenar, proteger e compartilhar senhas ilimitadas através de dispositivos ilimitados de qualquer lugar. Bitwarden fornece soluções de gerenciamento de senhas de código aberto para todos, seja em casa, no trabalho ou em viagem. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Gere senhas fortes, únicas e aleatórias com base nos requisitos de segurança para cada site que você frequenta. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -A Bitwarden Send transmite rapidamente informações criptografadas --- arquivos e texto em formato de placa -- diretamente para qualquer pessoa. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden oferece equipes e planos empresariais para empresas para que você possa compartilhar senhas com colegas com segurança. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Por que escolher Bitwarden: -Criptografia de Classe Mundial -As senhas são protegidas com criptografia avançada de ponta a ponta (AES-256 bit, salted hashing e PBKDF2 SHA-256) para que seus dados permaneçam seguros e privados. +More reasons to choose Bitwarden: -Gerador de senhas embutido -Gerar senhas fortes, únicas e aleatórias com base nos requisitos de segurança para cada site que você freqüenta. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Traduções globais -As traduções Bitwarden existem em 40 idiomas e estão crescendo, graças à nossa comunidade global. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Aplicações multiplataforma -Proteja e compartilhe dados sensíveis dentro de seu Bitwarden Vault a partir de qualquer navegador, dispositivo móvel ou SO desktop, e muito mais. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Um gerenciador de senhas gratuito e seguro para todos os seus dispositivos + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sincronize e acesse o seu cofre através de múltiplos dispositivos diff --git a/apps/browser/store/locales/pt_PT/copy.resx b/apps/browser/store/locales/pt_PT/copy.resx index 845a94a3ca7..d310629612e 100644 --- a/apps/browser/store/locales/pt_PT/copy.resx +++ b/apps/browser/store/locales/pt_PT/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Gestor de Palavras-passe Gratuito + Bitwarden Password Manager - Um gestor de palavras-passe seguro e gratuito para todos os seus dispositivos + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - A Bitwarden, Inc. é a empresa-mãe da 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NOMEADO O MELHOR GESTOR DE PALAVRAS-PASSE PELO THE VERGE, U.S. NEWS & WORLD REPORT, CNET E MUITO MAIS. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Gerir, armazenar, proteger e partilhar palavras-passe ilimitadas em dispositivos ilimitados a partir de qualquer lugar. O Bitwarden fornece soluções de gestão de palavras-passe de código aberto para todos, seja em casa, no trabalho ou onde estiver. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Gera palavras-passe fortes, únicas e aleatórias com base em requisitos de segurança para todos os sites que frequenta. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -O Bitwarden Send transmite rapidamente informações encriptadas - ficheiros e texto simples - diretamente a qualquer pessoa. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -O Bitwarden oferece os planos Equipas e Empresarial destinados a empresas, para que possa partilhar de forma segura as palavras-passe com os seus colegas. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Razões para escolher o Bitwarden: -Encriptação de classe mundial -As palavras-passe são protegidas com encriptação avançada de ponta a ponta (AES-256 bit, salted hashtag e PBKDF2 SHA-256) para que os seus dados permaneçam seguros e privados. +More reasons to choose Bitwarden: -Gerador de palavras-passe incorporado -Gera palavras-passe fortes, únicas e aleatórias com base nos requisitos de segurança para todos os sites que frequenta. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Traduções globais -O Bitwarden está traduzido em 40 idiomas e está a crescer, graças à nossa comunidade global. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Aplicações multiplataforma -Proteja e partilhe dados confidenciais no seu cofre Bitwarden a partir de qualquer navegador, dispositivo móvel ou sistema operativo de computador, e muito mais. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Um gestor de palavras-passe seguro e gratuito para todos os seus dispositivos + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sincronize e aceda ao seu cofre através de vários dispositivos diff --git a/apps/browser/store/locales/ro/copy.resx b/apps/browser/store/locales/ro/copy.resx index 0e12b289af3..7b0070fad22 100644 --- a/apps/browser/store/locales/ro/copy.resx +++ b/apps/browser/store/locales/ro/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Manager de parole gratuit + Bitwarden Password Manager - Un manager de parole sigur și gratuit pentru toate dispozitivele dvs. + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. este compania mamă a 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NUMIT CEL MAI BUN MANAGER DE PAROLE DE CĂTRE THE VERGE, U.S. NEWS & WORLD REPORT, CNET ȘI MULȚI ALȚII. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Gestionați, stocați, securizați și partajați un număr nelimitat de parole pe un număr nelimitat de dispozitive, de oriunde. Bitwarden oferă soluții open source de gestionare a parolelor pentru toată lumea, fie că se află acasă, la serviciu sau în mișcare. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generați parole puternice, unice și aleatorii, bazate pe cerințe de securitate pentru fiecare site web pe care îl frecventați. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send transmite rapid informații criptate --- fișiere și text simple -- direct către oricine. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden oferă planuri Teams și Enterprise pentru companii, astfel încât să puteți partaja în siguranță parolele cu colegii. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -De ce să alegeți Bitwarden: -Criptare de clasă mondială -Parolele sunt protejate cu criptare avansată end-to-end (AES-256 bit, salted hashing și PBKDF2 SHA-256), astfel încât datele dvs. să rămână sigure și private. +More reasons to choose Bitwarden: -Generator de parole încorporat -Generați parole puternice, unice și aleatorii, bazate pe cerințele de securitate pentru fiecare site web pe care îl frecventați. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Traduceri la nivel mondial -Bitwarden este deja tradus în 40 de limbi și numărul lor crește, datorită comunității noastre mondiale. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Aplicații multi-platformă -Protejați și partajați date sensibile în seiful Bitwarden de pe orice browser, dispozitiv mobil sau sistem de operare desktop și multe altele. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Un manager de parole sigur și gratuit, pentru toate dispozitivele dvs. + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sincronizează și accesează seiful dvs. de pe multiple dispozitive diff --git a/apps/browser/store/locales/ru/copy.resx b/apps/browser/store/locales/ru/copy.resx index 4e48ecbc88a..212a899f76e 100644 --- a/apps/browser/store/locales/ru/copy.resx +++ b/apps/browser/store/locales/ru/copy.resx @@ -1,17 +1,17 @@  - @@ -118,40 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – бесплатный менеджер паролей + Bitwarden Password Manager - Защищенный и бесплатный менеджер паролей для всех ваших устройств + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. является материнской компанией 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -НАЗВАН ЛУЧШИМ ДИСПЕТЧЕРОМ ПАРОЛЕЙ VERGE, US NEWS & WORLD REPORT, CNET И МНОГИМИ ДРУГИМИ. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Управляйте, храните, защищайте и делитесь неограниченным количеством паролей на неограниченном количестве устройств из любого места. Bitwarden предоставляет решения с открытым исходным кодом по управлению паролями для всех, дома, на работе или в дороге. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Создавайте надежные, уникальные и случайные пароли на основе требований безопасности для каждого посещаемого вами сайта. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send быстро передает зашифрованную информацию - файлы и простой текст - напрямую кому угодно. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden предлагает для компаний планы Teams и Enterprise, чтобы вы могли безопасно делиться паролями с коллегами. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Почему выбирают Bitwarden: -Шифрование мирового класса -Пароли защищены передовым сквозным шифрованием (AES-256 bit, соленый хэштег и PBKDF2 SHA-256), поэтому ваши данные остаются в безопасности и конфиденциальности. +More reasons to choose Bitwarden: -Встроенный генератор паролей -Создавайте надежные, уникальные и случайные пароли на основе требований безопасности для каждого посещаемого вами сайта. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. - Глобальные переводы - Переводы Bitwarden существуют на 40 языках и постоянно растут благодаря нашему глобальному сообществу. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. - Кросс-платформенные приложения - Защищайте и делитесь конфиденциальными данными в вашем Bitwarden Vault из любого браузера, мобильного устройства, настольной ОС и т. д. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! + - Защищенный и бесплатный менеджер паролей для всех ваших устройств + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Синхронизация и доступ к хранилищу с нескольких устройств diff --git a/apps/browser/store/locales/si/copy.resx b/apps/browser/store/locales/si/copy.resx index 191198691d4..82e4eb1d88e 100644 --- a/apps/browser/store/locales/si/copy.resx +++ b/apps/browser/store/locales/si/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/locales/sk/copy.resx b/apps/browser/store/locales/sk/copy.resx index ba2a2a5a071..de7fa7dee31 100644 --- a/apps/browser/store/locales/sk/copy.resx +++ b/apps/browser/store/locales/sk/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Bezplatný správca hesiel + Bitwarden Password Manager - Bezpečný a bezplatný správca hesiel pre všetky vaše zariadenia + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. je materská spoločnosť spoločnosti 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -OHODNOTENÝ AKO NAJLEPŠÍ SPRÁVCA HESIEL V THE VERGE, U.S. NEWS & WORLD REPORT, CNET A ĎALŠÍMI. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Spravujte, ukladajte, zabezpečte a zdieľajte neobmedzený počet hesiel naprieč neobmedzeným počtom zariadení odkiaľkoľvek. Bitwarden ponúka open source riešenie na správu hesiel komukoľvek, kdekoľvek doma, v práci alebo na ceste. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Vygenerujte si silné, unikátne a náhodné heslá podľa bezpečnostných požiadaviek na každej stránke, ktorú navštevujete. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send rýchlo prenesie šifrované informácie -- súbory a text -- priamo komukoľvek. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden ponúka Teams a Enterprise paušály pre firmy, aby ste mohli bezpečne zdieľať hesla s kolegami. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Prečo si vybrať Bitwarden: -Svetová trieda v šifrovaní -Heslá sú chránené pokročilým end-to-end šifrovaním (AES-256 bit, salted hash a PBKDF2 SHA-256), takže Vaše dáta zostanú bezpečné a súkromné. +More reasons to choose Bitwarden: -Vstavaný generátor hesiel -Vygenerujte si silné, unikátne a náhodné heslá podľa bezpečnostných požiadaviek na každej stránke, ktorú navštevujete. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Svetová lokalizácia -Vďaka našej globálnej komunite má Bitwarden neustále rastúcu lokalizáciu už do 40 jazykov. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Aplikácie pre rôzne platformy -Zabezpečte a zdieľajte súkromné dáta prostredníctvom Bitwarden trezora z ktoréhokoľvek prehliadača, mobilného zariadenia, alebo stolného počítača a ďalších. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Bezpečný a bezplatný správca hesiel pre všetky vaše zariadenia + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Synchronizujte a pristupujte k vášmu trezoru z viacerých zariadení diff --git a/apps/browser/store/locales/sl/copy.resx b/apps/browser/store/locales/sl/copy.resx index 83288e3872a..80886de48ab 100644 --- a/apps/browser/store/locales/sl/copy.resx +++ b/apps/browser/store/locales/sl/copy.resx @@ -1,17 +1,17 @@  - @@ -118,40 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - brezplačni upravljalnik gesel + Bitwarden Password Manager - Varen in brezplačen upravljalnik gesel za vse vaše naprave + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. je matično podjetje podjetja 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAJBOŠJI UPRAVLJALNIK GESEL PO MNEJU THE VERGE, U.S. NEWS & WORLD REPORT, CNET IN DRUGIH. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Upravljajte, shranjujte, varujte in delite neomejeno število gesel na neomejenem številu naprav, kjerkoli. Bitwarden ponuja odprtokodne rešitve za upravljanje gesel vsem, tako doma kot v službi ali na poti. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Ustvarite močna, edinstvena in naključna gesla, skladna z varnostnimi zahtevami za vsako spletno mesto, ki ga obiščete. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Z Bitwarden Send hitro prenesite šifrirane informacije --- datoteke in navadno besedilo -- neposredno komurkoli. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden ponuja storitvi za organizacije Teams in Enterprise, s katerima lahko gesla varno delite s sodelavci. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Zakaj izbrati Bitwarden: -Vrhunsko šifriranje -Gesla so zaščitena z naprednim šifriranjem (AES-256, soljene hash-vrednosti in PBKDF2 SHA-256), tako da vaši podatki ostanejo varni in zasebni. +More reasons to choose Bitwarden: -Vgrajeni generator gesel -Ustvarite močna, edinstvena in naključna gesla v skladu z varnostnimi zahtevami za vsako spletno mesto, ki ga obiščete. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Prevodi za ves svet -Bitwarden je preveden že v 40 jezikov, naša globalna skupnost pa ves čas posodabljan in ustvarja nove prevede. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Deluje na vseh platformah -Varujte in delite svoje občutljive podatke znotraj vašega Bitwarden trezorja v katerem koli brskalniku, mobilni napravi, namiznem računalniku in drugje. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! + - Varen in brezplačen upravljalnik gesel za vse vaše naprave + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sinhronizirajte svoj trezor gesel in dostopajte do njega z več naprav diff --git a/apps/browser/store/locales/sr/copy.resx b/apps/browser/store/locales/sr/copy.resx index 9bfe7990356..9c34d5812a4 100644 --- a/apps/browser/store/locales/sr/copy.resx +++ b/apps/browser/store/locales/sr/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Бесплатни Менаџер Лозинке + Bitwarden Password Manager - Сигурни и бесплатни менаџер лозинке за сва Ваша уређаја + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. је матична компанија фирме 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -Именован као најбољи управљач лозинкама од стране новинских сајтова као што су THE VERGE, U.S. NEWS & WORLD REPORT, CNET, и других. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Управљајте, чувајте, обезбедите, и поделите неограничен број лозинки са неограниченог броја уређаја где год да се налазите. Bitwarden свима доноси решења за управљање лозинкама која су отвореног кода, било да сте код куће, на послу, или на путу. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Генеришите јаке, јединствене, и насумичне лозинке у зависности од безбедносних захтева за сваки сајт који често посећујете. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send брзо преноси шифроване информације--- датотеке и обичан текст-- директно и свима. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden нуди планове за компаније и предузећа како бисте могли безбедно да делите лозинке са вашим колегама. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Зашто изабрати Bitwarden: -Шифровање светске класе -Лозинке су заштићене напредним шифровањем од једног до другог краја (AES-256 bit, salted hashing, и PBKDF2 SHA-256) како би ваши подаци остали безбедни и приватни. +More reasons to choose Bitwarden: -Уграђен генератор лозинки -Генеришите јаке, јединствене, и насумичне лозинке у зависности од безбедносних захтева за сваки сајт који често посећујете. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Глобално преведен -Bitwarden преводи постоје за 40 језика и стално се унапређују, захваљујући нашој глобалној заједници. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Вишеплатформне апликације -Обезбедите и поделите осетљиве податке у вашем Bitwarden сефу из било ког претраживача, мобилног уређаја, или desktop оперативног система, и других. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Сигурни и бесплатни менаџер лозинке за сва Ваша уређаја + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Синхронизујте и приступите сефу са више уређаја diff --git a/apps/browser/store/locales/sv/copy.resx b/apps/browser/store/locales/sv/copy.resx index 8b3cb2a4026..6406ab013e6 100644 --- a/apps/browser/store/locales/sv/copy.resx +++ b/apps/browser/store/locales/sv/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Gratis lösenordshanterare + Bitwarden Password Manager - En säker och gratis lösenordshanterare för alla dina enheter + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. är moderbolag till 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -UTNÄMND TILL DEN BÄSTA LÖSENORDSHANTERAREN AV THE VERGE, U.S. NEWS & WORLD REPORT, CNET MED FLERA. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Hantera, lagra, säkra och dela ett obegränsat antal lösenord mellan ett obegränsat antal enheter var som helst ifrån. Bitwarden levererar lösningar för lösenordshantering med öppen källkod till alla, vare sig det är hemma, på jobbet eller på språng. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generera starka, unika och slumpmässiga lösenord baserat på säkerhetskrav för varje webbplats du besöker. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send överför snabbt krypterad information --- filer och klartext -- direkt till vem som helst. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden erbjuder abonnemang för team och företag så att du säkert kan dela lösenord med kollegor. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Varför välja Bitwarden: -Kryptering i världsklass -Lösenord skyddas med avancerad end-to-end-kryptering (AES-256 bitar, saltad hashtag och PBKDF2 SHA-256) så att dina data förblir säkra och privata. +More reasons to choose Bitwarden: -Inbyggd lösenordsgenerator -Generera starka, unika och slumpmässiga lösenord baserat på säkerhetskrav för varje webbplats du besöker. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Globala översättningar -Översättningar av Bitwarden finns på 40 språk och antalet växer tack vare vår globala gemenskap. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Plattformsoberoende program -Säkra och dela känsliga data i ditt Bitwardenvalv från alla webbläsare, mobiler och datorer. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - En säker och gratis lösenordshanterare för alla dina enheter + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Synkronisera och kom åt ditt valv från flera enheter diff --git a/apps/browser/store/locales/te/copy.resx b/apps/browser/store/locales/te/copy.resx index 191198691d4..82e4eb1d88e 100644 --- a/apps/browser/store/locales/te/copy.resx +++ b/apps/browser/store/locales/te/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/locales/th/copy.resx b/apps/browser/store/locales/th/copy.resx index 9c8965b01f4..f784b1884b4 100644 --- a/apps/browser/store/locales/th/copy.resx +++ b/apps/browser/store/locales/th/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – โปรแกรมจัดการรหัสผ่านฟรี + Bitwarden Password Manager - โปรแกรมจัดการรหัสผ่านที่ปลอดภัยและฟรี สำหรับอุปกรณ์ทั้งหมดของคุณ + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. เป็นบริษัทแม่ของ 8bit Solutions LLC + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -ได้รับการระบุชื่อเป็น โปรแกรมจัดการรหัสผ่านที่ดีที่สุด โดย The Verge, U.S. News & World Report, CNET, และที่อื่นๆ +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -สามารถจัดการ จัดเก็บ ปกป้อง และแชร์รหัสผ่านไม่จำกัดจำนวนระหว่างอุปกรณ์ต่างๆ โดยไม่จำกัดจำนวนจากที่ไหนก็ได้ Bitwarden เสนอโซลูชันจัดการรหัสผ่านโอเพนซอร์สให้กับทุกคน ไม่ว่าจะอยู่ที่บ้าน ที่ทำงาน หรือนอกสถานที่ +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -สามารถส่มสร้างรหัสผ่านที่ปลอดภัยและไม่ซ้ำกัน ตามเงื่อนไขความปลอดภัยที่กำหนดได้ สำหรับเว็บไซต์ทุกแห่งที่คุณใช้งานบ่อย +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send สามารถส่งข้อมูลที่ถูกเข้ารหัส --- ไฟล์ หรือ ข้อความ -- ตรงไปยังใครก็ได้ได้อย่างรวดเร็ว +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden มีแผนแบบ Teams และ Enterprise สำหรับบริษัทต่างๆ ซึางคุณสามารถแชร์รหัสผ่านกับเพื่อนร่วมงานได้อย่างปลอดภัย +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -ทำไมควรเลือก Bitwarden: -การเข้ารหัสมาตรฐานโลก -รหัสผ่านจะได้รับการปกป้องด้วยการเข้ารหัสชั้นสูง (AES-256 บิต, salted hashtag, และ PBKDF2 SHA-256) แบบต้นทางถึงปลายทาง เพื่อให้ข้อมูลของคุณปลอดภัยและเป็นส่วนตัว +More reasons to choose Bitwarden: -มีตัวช่วยส่มสร้างรหัสผ่าน -สามารถสุ่มสร้างรหัสผ่านที่ปลอดภัยและไม่ซ้ำกัน ตามเงื่อนไขความปลอดภัยที่กำหนดได้ สำหรับเว็บไซต์ทุกแห่งที่คุณใช้งานบ่อย +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -แปลเป็นภาษาต่างๆ ทั่วโลก -Bitwarden ได้รับการแปลเป็นภาษาต่างๆ กว่า 40 ภาษา และกำลังเพิ่มขึ้นเรื่อยๆ ด้วยความสนับสนุนจากชุมชนผู้ใช้งานทั่วโลก +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -แอปพลิเคชันข้ามแพลตฟอร์ม -ปกป้องและแชร์ข้อมูลอ่อนไหวในตู้เซฟ Bitwarden จากเว็บเบราว์เซอร์ อุปกรณ์มือถือ หรือเดสท็อป หรือช่องทางอื่นๆ +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - โปรแกรมจัดการรหัสผ่านที่ปลอดภัยและฟรี สำหรับอุปกรณ์ทั้งหมดของคุณ + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. ซิงค์และเข้าถึงตู้นิรภัยของคุณจากหลายอุปกรณ์ diff --git a/apps/browser/store/locales/tr/copy.resx b/apps/browser/store/locales/tr/copy.resx index 1fc3e2a34b3..539aad3aee5 100644 --- a/apps/browser/store/locales/tr/copy.resx +++ b/apps/browser/store/locales/tr/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Ücretsiz Parola Yöneticisi + Bitwarden Password Manager - Tüm aygıtlarınız için güvenli ve ücretsiz bir parola yöneticisi + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc., 8bit Solutions LLC’nin ana şirketidir. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -THE VERGE, U.S. NEWS & WORLD REPORT, CNET VE BİRÇOK MEDYA KURULUŞUNA GÖRE EN İYİ PAROLA YÖNETİCİSİ. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Sınırsız sayıda parolayı istediğiniz kadar cihazda yönetin, saklayın, koruyun ve paylaşın. Bitwarden; herkesin evde, işte veya yolda kullanabileceği açık kaynaklı parola yönetim çözümleri sunuyor. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Sık kullandığınız web siteleri için güvenlik gereksinimlerinize uygun, güçlü, benzersiz ve rastgele parolalar oluşturabilirsiniz. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send, şifrelenmiş bilgileri (dosyalar ve düz metinler) herkese hızlı bir şekilde iletmenizi sağlıyor. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden, parolaları iş arkadaşlarınızla güvenli bir şekilde paylaşabilmeniz için şirketlere yönelik Teams ve Enterprise paketleri de sunuyor. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Neden Bitwarden? -Üst düzey şifreleme -Parolalarınız gelişmiş uçtan uca şifreleme (AES-256 bit, salted hashing ve PBKDF2 SHA-256) ile korunuyor, böylece verileriniz güvende ve gizli kalıyor. +More reasons to choose Bitwarden: -Dahili parola oluşturucu -Sık kullandığınız web siteleri için güvenlik gereksinimlerinize uygun, güçlü, benzersiz ve rastgele parolalar oluşturabilirsiniz. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Çeviriler -Bitwarden 40 dilde kullanılabiliyor ve gönüllü topluluğumuz sayesinde çeviri sayısı giderek artıyor. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Her platformla uyumlu uygulamalar -Bitwarden kasanızdaki hassas verilere her tarayıcıdan, mobil cihazdan veya masaüstü işletim sisteminden ulaşabilir ve onları paylaşabilirsiniz. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Tüm cihazarınız için güvenli ve ücretsiz bir parola yöneticisi + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Hesabınızı senkronize ederek kasanıza tüm cihazlarınızdan ulaşın diff --git a/apps/browser/store/locales/uk/copy.resx b/apps/browser/store/locales/uk/copy.resx index d59cd7f1036..5a7de183636 100644 --- a/apps/browser/store/locales/uk/copy.resx +++ b/apps/browser/store/locales/uk/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Безплатний менеджер паролів + Bitwarden Password Manager - Захищений, безплатний менеджер паролів для всіх ваших пристроїв + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - 8bit Solutions LLC є дочірньою компанією Bitwarden, Inc. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -НАЙКРАЩИЙ МЕНЕДЖЕР ПАРОЛІВ ЗА ВЕРСІЄЮ THE VERGE, U.S. NEWS & WORLD REPORT, CNET, А ТАКОЖ ІНШИХ ВИДАНЬ. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Зберігайте, захищайте, керуйте і надавайте доступ до паролів на різних пристроях де завгодно. Bitwarden пропонує рішення для керування паролями на основі відкритого програмного коду особистим та корпоративним користувачам на всіх пристроях. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Генеруйте надійні, випадкові та унікальні паролі, які відповідають вимогам безпеки, для кожного вебсайту та сервісу. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Швидко відправляйте будь-кому зашифровану інформацію, як-от файли чи звичайний текст, за допомогою функції Bitwarden Send. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden пропонує командні та корпоративні тарифні плани для компаній, щоб ви могли безпечно обмінюватися паролями з колегами. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Чому варто обрати Bitwarden: -Всесвітньо визнані стандарти шифрування -Паролі захищаються з використанням розширеного наскрізного шифрування (AES-256 bit, хешування з сіллю та PBKDF2 SHA-256), тому ваші дані завжди захищені та приватні. +More reasons to choose Bitwarden: -Вбудований генератор паролів -Генеруйте надійні, випадкові та унікальні паролі, які відповідають вимогам безпеки, для кожного вебсайту та сервісу. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Переклад багатьма мовами -Завдяки нашій глобальній спільноті, Bitwarden перекладено 40 мовами, і їх кількість зростає. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Програми для різних платформ -Зберігайте і діліться важливими даними, а також користуйтеся іншими можливостями у вашому сховищі Bitwarden в будь-якому браузері, мобільному пристрої, чи комп'ютерній операційній системі. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Захищений, безплатний менеджер паролів для всіх ваших пристроїв + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Синхронізуйте й отримуйте доступ до свого сховища на різних пристроях diff --git a/apps/browser/store/locales/vi/copy.resx b/apps/browser/store/locales/vi/copy.resx index 220d50bdfae..e0403d1f328 100644 --- a/apps/browser/store/locales/vi/copy.resx +++ b/apps/browser/store/locales/vi/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Trình quản lý mật khẩu miễn phí + Bitwarden Password Manager - Một trình quản lý mật khẩu an toàn và miễn phí cho mọi thiết bị của bạn + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc là công ty mẹ của 8bit Solutions LLC + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -ĐƯỢC ĐÁNH GIÁ LÀ TRÌNH QUẢN LÝ MẬT KHẨU TỐT NHẤT BỞI NHÀ BÁO LỚN NHƯ THE VERGE, CNET, U.S. NEWS & WORLD REPORT VÀ HƠN NỮA +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Quản lý, lưu trữ, bảo mật và chia sẻ mật khẩu không giới hạn trên các thiết bị không giới hạn mọi lúc, mọi nơi. Bitwarden cung cấp các giải pháp quản lý mật khẩu mã nguồn mở cho tất cả mọi người, cho dù ở nhà, tại cơ quan hay khi đang di chuyển. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Tạo mật khẩu mạnh, không bị trùng và ngẫu nhiên dựa trên các yêu cầu bảo mật cho mọi trang web bạn thường xuyên sử dụng. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Tính năng 'Bitwarden Send' nhanh chóng truyền thông tin được mã hóa --- tệp và văn bản - trực tiếp đến bất kỳ ai. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden cung cấp các gói 'Nhóm' và 'Doanh nghiệp' cho các công ty để bạn có thể chia sẻ mật khẩu với đồng nghiệp một cách an toàn. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Tại sao bạn nên chọn Bitwarden: -Mã hóa tốt nhất thế giới -Mật khẩu được bảo vệ bằng mã hóa đầu cuối (end-to-end encryption) tiên tiến như AES-256 bit, salted hashtag, và PBKDF2 SHA-256 nên dữ liệu của bạn luôn an toàn và riêng tư. +More reasons to choose Bitwarden: -Trình tạo mật khẩu tích hợp -Tạo mật khẩu mạnh, không bị trùng lặp, và ngẫu nhiên dựa trên các yêu cầu bảo mật cho mọi trang web mà bạn thường xuyên sử dụng. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Bản dịch ngôn ngữ từ cộng đồng -Bitwarden đã có bản dịch 40 ngôn ngữ và đang phát triển nhờ vào cộng đồng toàn cầu của chúng tôi. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Ứng dụng đa nền tảng -Bảo mật và chia sẻ dữ liệu nhạy cảm trong kho lưu trữ Bitwarden của bạn từ bất kỳ trình duyệt, điện thoại thông minh hoặc hệ điều hành máy tính nào, và hơn thế nữa. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Một trình quản lý mật khẩu an toàn và miễn phí cho mọi thiết bị của bạn + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Đồng bộ hóa và truy cập vào kho lưu trữ của bạn từ nhiều thiết bị diff --git a/apps/browser/store/locales/zh_CN/copy.resx b/apps/browser/store/locales/zh_CN/copy.resx index e424ef743ad..94543f8f6f7 100644 --- a/apps/browser/store/locales/zh_CN/copy.resx +++ b/apps/browser/store/locales/zh_CN/copy.resx @@ -1,17 +1,17 @@  - @@ -118,40 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – 免费密码管理器 + Bitwarden Password Manager - 安全免费的跨平台密码管理器 + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. 是 8bit Solutions LLC 的母公司。 + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -被 THE VERGE、U.S. NEWS & WORLD REPORT、CNET 等评为最佳的密码管理器。 +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -从任何地方,不限制设备,管理、存储、保护和共享无限的密码。Bitwarden 为每个人提供开源的密码管理解决方案,无论是在家里,在工作中,还是在旅途中。 +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -基于安全要求,为您经常访问的每个网站生成强大、唯一和随机的密码。 +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send 快速传输加密的信息---文件和文本---直接给任何人。 +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden 为公司提供团队和企业计划,因此您可以安全地与同事共享密码。 +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -为何选择 Bitwarden: -世界级的加密技术 -密码受到先进的端到端加密(AES-256 位、盐化标签和 PBKDF2 SHA-256)的保护,为您的数据保持安全和隐密。 +More reasons to choose Bitwarden: -内置密码生成器 -基于安全要求,为您经常访问的每个网站生成强大、唯一和随机的密码。 +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -全球翻译 -Bitwarden 的翻译有 40 种语言,而且还在不断增加,感谢我们的全球社区。 +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -跨平台应用程序 -从任何浏览器、移动设备或桌面操作系统,以及更多的地方,在您的 Bitwarden 密码库中保护和分享敏感数据。 +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! + - 安全免费的跨平台密码管理器 + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. 从多台设备同步和访问密码库 diff --git a/apps/browser/store/locales/zh_TW/copy.resx b/apps/browser/store/locales/zh_TW/copy.resx index be39fdca065..ab37ed5f7b5 100644 --- a/apps/browser/store/locales/zh_TW/copy.resx +++ b/apps/browser/store/locales/zh_TW/copy.resx @@ -1,17 +1,17 @@  - @@ -118,40 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – 免費密碼管理工具 + Bitwarden Password Manager - 安全、免費、跨平台的密碼管理工具 + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. 是 8bit Solutions LLC 的母公司。 + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -被 THE VERGE、U.S. NEWS & WORLD REPORT、CNET 等評為最佳的密碼管理器。 +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -從任何地方,不限制設備,管理、存儲、保護和共享無限的密碼。Bitwarden 為每個人提供開源的密碼管理解決方案,無論是在家裡,在工作中,還是在旅途中。 +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -基於安全要求,為您經常訪問的每個網站生成強大、唯一和隨機的密碼。 +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send 快速傳輸加密的信息---文檔和文本---直接給任何人。 +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden 為公司提供團隊和企業計劃,因此您可以安全地與同事共享密碼。 +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -為何選擇 Bitwarden: -世界級的加密技術 -密碼受到先進的端到端加密(AES-256 位、鹽化標籤和 PBKDF2 SHA-256)的保護,為您的資料保持安全和隱密。 +More reasons to choose Bitwarden: -內置密碼生成器 -基於安全要求,為您經常訪問的每個網站生成強大、唯一和隨機的密碼。 +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -全球翻譯 -Bitwarden 的翻譯有 40 種語言,而且還在不斷增加,感謝我們的全球社區。 +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -跨平台應用程式 -從任何瀏覽器、行動裝置或桌面作業系統,以及更多的地方,在您的 Bitwarden 密碼庫中保護和分享敏感資料。 +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! + - 安全、免費、跨平台的密碼管理工具 + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. 在多部裝置上同步和存取密碼庫 From 14cb4bc5aae3adf91d31423528aa6e164c32c806 Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez Date: Fri, 19 Apr 2024 14:55:34 -0500 Subject: [PATCH 023/110] [PM-7581] Validate cache state from external contexts within LocalBackedSessionStorage (#8842) * [PM-7581] Validate cache state from external contexts within LocalBackedSessionStorage * [PM-7581] Continuing with exploring refining the LocalBackedSessionStorage * [PM-7558] Fix Vault Load Times * [PM-7558] Committing before reworking LocalBackedSessionStorage to function without extending the MemoryStorageService * [PM-7558] Working through refinement of LocalBackedSessionStorage * [PM-7558] Reverting some changes * [PM-7558] Refining implementation and removing unnecessary params from localBackedSessionStorage * [PM-7558] Fixing logic for getting the local session state * [PM-7558] Adding a method to avoid calling bypass cache when a key is known to be a null value * [PM-7558] Fixing tests in a temporary manner * [PM-7558] Removing unnecessary chagnes that affect mv2 * [PM-7558] Removing unnecessary chagnes that affect mv2 * [PM-7558] Adding partition for LocalBackedSessionStorageService * [PM-7558] Wrapping duplicate cache save early return within isDev call * [PM-7558] Wrapping duplicate cache save early return within isDev call * [PM-7558] Wrapping duplicate cache save early return within isDev call --- .../browser/src/background/main.background.ts | 20 +- apps/browser/src/platform/background.ts | 13 +- .../storage-service.factory.ts | 11 +- .../platform/listeners/on-install-listener.ts | 5 + ...cal-backed-session-storage.service.spec.ts | 109 ++++++++-- .../local-backed-session-storage.service.ts | 196 ++++++++++++++---- .../foreground-memory-storage.service.ts | 8 +- 7 files changed, 277 insertions(+), 85 deletions(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index c627c0032bd..622a115067c 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -226,6 +226,7 @@ import { BackgroundPlatformUtilsService } from "../platform/services/platform-ut import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service"; import { BackgroundDerivedStateProvider } from "../platform/state/background-derived-state.provider"; import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service"; +import { ForegroundMemoryStorageService } from "../platform/storage/foreground-memory-storage.service"; import { fromChromeRuntimeMessaging } from "../platform/utils/from-chrome-runtime-messaging"; import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service"; import FilelessImporterBackground from "../tools/background/fileless-importer.background"; @@ -394,13 +395,26 @@ export default class MainBackground { ), ); + this.platformUtilsService = new BackgroundPlatformUtilsService( + this.messagingService, + (clipboardValue, clearMs) => this.clearClipboard(clipboardValue, clearMs), + async () => this.biometricUnlock(), + self, + ); + const mv3MemoryStorageCreator = (partitionName: string) => { + if (this.popupOnlyContext) { + return new ForegroundMemoryStorageService(partitionName); + } + // TODO: Consider using multithreaded encrypt service in popup only context return new LocalBackedSessionStorageService( + this.logService, new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, false), this.keyGenerationService, new BrowserLocalStorageService(), new BrowserMemoryStorageService(), + this.platformUtilsService, partitionName, ); }; @@ -469,12 +483,6 @@ export default class MainBackground { this.biometricStateService = new DefaultBiometricStateService(this.stateProvider); this.userNotificationSettingsService = new UserNotificationSettingsService(this.stateProvider); - this.platformUtilsService = new BackgroundPlatformUtilsService( - this.messagingService, - (clipboardValue, clearMs) => this.clearClipboard(clipboardValue, clearMs), - async () => this.biometricUnlock(), - self, - ); this.tokenService = new TokenService( this.singleUserStateProvider, diff --git a/apps/browser/src/platform/background.ts b/apps/browser/src/platform/background.ts index 9c3510178cd..a48c420e777 100644 --- a/apps/browser/src/platform/background.ts +++ b/apps/browser/src/platform/background.ts @@ -5,16 +5,11 @@ import MainBackground from "../background/main.background"; import { BrowserApi } from "./browser/browser-api"; const logService = new ConsoleLogService(false); +if (BrowserApi.isManifestVersion(3)) { + startHeartbeat().catch((error) => logService.error(error)); +} const bitwardenMain = ((self as any).bitwardenMain = new MainBackground()); -bitwardenMain - .bootstrap() - .then(() => { - // Finished bootstrapping - if (BrowserApi.isManifestVersion(3)) { - startHeartbeat().catch((error) => logService.error(error)); - } - }) - .catch((error) => logService.error(error)); +bitwardenMain.bootstrap().catch((error) => logService.error(error)); /** * Tracks when a service worker was last alive and extends the service worker diff --git a/apps/browser/src/platform/background/service-factories/storage-service.factory.ts b/apps/browser/src/platform/background/service-factories/storage-service.factory.ts index 19d5a9c1403..83e8a780a6d 100644 --- a/apps/browser/src/platform/background/service-factories/storage-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/storage-service.factory.ts @@ -17,6 +17,11 @@ import { KeyGenerationServiceInitOptions, keyGenerationServiceFactory, } from "./key-generation-service.factory"; +import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory"; +import { + platformUtilsServiceFactory, + PlatformUtilsServiceInitOptions, +} from "./platform-utils-service.factory"; export type DiskStorageServiceInitOptions = FactoryOptions; export type SecureStorageServiceInitOptions = FactoryOptions; @@ -25,7 +30,9 @@ export type MemoryStorageServiceInitOptions = FactoryOptions & EncryptServiceInitOptions & KeyGenerationServiceInitOptions & DiskStorageServiceInitOptions & - SessionStorageServiceInitOptions; + SessionStorageServiceInitOptions & + LogServiceInitOptions & + PlatformUtilsServiceInitOptions; export function diskStorageServiceFactory( cache: { diskStorageService?: AbstractStorageService } & CachedServices, @@ -63,10 +70,12 @@ export function memoryStorageServiceFactory( return factory(cache, "memoryStorageService", opts, async () => { if (BrowserApi.isManifestVersion(3)) { return new LocalBackedSessionStorageService( + await logServiceFactory(cache, opts), await encryptServiceFactory(cache, opts), await keyGenerationServiceFactory(cache, opts), await diskStorageServiceFactory(cache, opts), await sessionStorageServiceFactory(cache, opts), + await platformUtilsServiceFactory(cache, opts), "serviceFactories", ); } diff --git a/apps/browser/src/platform/listeners/on-install-listener.ts b/apps/browser/src/platform/listeners/on-install-listener.ts index ef206301e3f..adf575a17a9 100644 --- a/apps/browser/src/platform/listeners/on-install-listener.ts +++ b/apps/browser/src/platform/listeners/on-install-listener.ts @@ -23,6 +23,11 @@ export async function onInstallListener(details: chrome.runtime.InstalledDetails stateServiceOptions: { stateFactory: new StateFactory(GlobalState, Account), }, + platformUtilsServiceOptions: { + win: self, + biometricCallback: async () => false, + clipboardWriteCallback: async () => {}, + }, }; const environmentService = await environmentServiceFactory(cache, opts); diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts index 7740a22071d..a4581e6ac1a 100644 --- a/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts +++ b/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts @@ -2,6 +2,8 @@ import { mock, MockProxy } from "jest-mock-extended"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { AbstractMemoryStorageService, AbstractStorageService, @@ -11,16 +13,26 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { BrowserApi } from "../browser/browser-api"; + import { LocalBackedSessionStorageService } from "./local-backed-session-storage.service"; -describe("LocalBackedSessionStorage", () => { +describe.skip("LocalBackedSessionStorage", () => { + const sendMessageWithResponseSpy: jest.SpyInstance = jest.spyOn( + BrowserApi, + "sendMessageWithResponse", + ); + let encryptService: MockProxy; let keyGenerationService: MockProxy; let localStorageService: MockProxy; let sessionStorageService: MockProxy; + let logService: MockProxy; + let platformUtilsService: MockProxy; - let cache: Map; + let cache: Record; const testObj = { a: 1, b: 2 }; + const stringifiedTestObj = JSON.stringify(testObj); const key = new SymmetricCryptoKey(Utils.fromUtf8ToArray("00000000000000000000000000000000")); let getSessionKeySpy: jest.SpyInstance; @@ -40,20 +52,24 @@ describe("LocalBackedSessionStorage", () => { }; beforeEach(() => { + sendMessageWithResponseSpy.mockResolvedValue(null); + logService = mock(); encryptService = mock(); keyGenerationService = mock(); localStorageService = mock(); sessionStorageService = mock(); sut = new LocalBackedSessionStorageService( + logService, encryptService, keyGenerationService, localStorageService, sessionStorageService, + platformUtilsService, "test", ); - cache = sut["cache"]; + cache = sut["cachedSession"]; keyGenerationService.createKeyWithPurpose.mockResolvedValue({ derivedKey: key, @@ -64,19 +80,27 @@ describe("LocalBackedSessionStorage", () => { getSessionKeySpy = jest.spyOn(sut, "getSessionEncKey"); getSessionKeySpy.mockResolvedValue(key); - sendUpdateSpy = jest.spyOn(sut, "sendUpdate"); - sendUpdateSpy.mockReturnValue(); + // sendUpdateSpy = jest.spyOn(sut, "sendUpdate"); + // sendUpdateSpy.mockReturnValue(); }); describe("get", () => { - it("should return from cache", async () => { - cache.set("test", testObj); - const result = await sut.get("test"); - expect(result).toStrictEqual(testObj); + describe("in local cache or external context cache", () => { + it("should return from local cache", async () => { + cache["test"] = stringifiedTestObj; + const result = await sut.get("test"); + expect(result).toStrictEqual(testObj); + }); + + it("should return from external context cache when local cache is not available", async () => { + sendMessageWithResponseSpy.mockResolvedValue(stringifiedTestObj); + const result = await sut.get("test"); + expect(result).toStrictEqual(testObj); + }); }); describe("not in cache", () => { - const session = { test: testObj }; + const session = { test: stringifiedTestObj }; beforeEach(() => { mockExistingSessionKey(key); @@ -117,8 +141,8 @@ describe("LocalBackedSessionStorage", () => { it("should set retrieved values in cache", async () => { await sut.get("test"); - expect(cache.has("test")).toBe(true); - expect(cache.get("test")).toEqual(session.test); + expect(cache["test"]).toBeTruthy(); + expect(cache["test"]).toEqual(session.test); }); it("should use a deserializer if provided", async () => { @@ -148,13 +172,56 @@ describe("LocalBackedSessionStorage", () => { }); describe("remove", () => { + describe("existing cache value is null", () => { + it("should not save null if the local cached value is already null", async () => { + cache["test"] = null; + await sut.remove("test"); + expect(sendUpdateSpy).not.toHaveBeenCalled(); + }); + + it("should not save null if the externally cached value is already null", async () => { + sendMessageWithResponseSpy.mockResolvedValue(null); + await sut.remove("test"); + expect(sendUpdateSpy).not.toHaveBeenCalled(); + }); + }); + it("should save null", async () => { + cache["test"] = stringifiedTestObj; + await sut.remove("test"); expect(sendUpdateSpy).toHaveBeenCalledWith({ key: "test", updateType: "remove" }); }); }); describe("save", () => { + describe("currently cached", () => { + it("does not save the value a local cached value exists which is an exact match", async () => { + cache["test"] = stringifiedTestObj; + await sut.save("test", testObj); + expect(sendUpdateSpy).not.toHaveBeenCalled(); + }); + + it("does not save the value if a local cached value exists, even if the keys not in the same order", async () => { + cache["test"] = JSON.stringify({ b: 2, a: 1 }); + await sut.save("test", testObj); + expect(sendUpdateSpy).not.toHaveBeenCalled(); + }); + + it("does not save the value a externally cached value exists which is an exact match", async () => { + sendMessageWithResponseSpy.mockResolvedValue(stringifiedTestObj); + await sut.save("test", testObj); + expect(sendUpdateSpy).not.toHaveBeenCalled(); + expect(cache["test"]).toBe(stringifiedTestObj); + }); + + it("saves the value if the currently cached string value evaluates to a falsy value", async () => { + cache["test"] = "null"; + await sut.save("test", testObj); + expect(sendUpdateSpy).toHaveBeenCalledWith({ key: "test", updateType: "save" }); + }); + }); + describe("caching", () => { beforeEach(() => { localStorageService.get.mockResolvedValue(null); @@ -167,21 +234,21 @@ describe("LocalBackedSessionStorage", () => { }); it("should remove key from cache if value is null", async () => { - cache.set("test", {}); - const cacheSetSpy = jest.spyOn(cache, "set"); - expect(cache.has("test")).toBe(true); + cache["test"] = {}; + // const cacheSetSpy = jest.spyOn(cache, "set"); + expect(cache["test"]).toBe(true); await sut.save("test", null); // Don't remove from cache, just replace with null - expect(cache.get("test")).toBe(null); - expect(cacheSetSpy).toHaveBeenCalledWith("test", null); + expect(cache["test"]).toBe(null); + // expect(cacheSetSpy).toHaveBeenCalledWith("test", null); }); it("should set cache if value is non-null", async () => { - expect(cache.has("test")).toBe(false); - const setSpy = jest.spyOn(cache, "set"); + expect(cache["test"]).toBe(false); + // const setSpy = jest.spyOn(cache, "set"); await sut.save("test", testObj); - expect(cache.get("test")).toBe(testObj); - expect(setSpy).toHaveBeenCalledWith("test", testObj); + expect(cache["test"]).toBe(stringifiedTestObj); + // expect(setSpy).toHaveBeenCalledWith("test", stringifiedTestObj); }); }); diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.ts index 3f01e4169e9..146eb11b2bd 100644 --- a/apps/browser/src/platform/services/local-backed-session-storage.service.ts +++ b/apps/browser/src/platform/services/local-backed-session-storage.service.ts @@ -1,8 +1,10 @@ -import { Observable, Subject, filter, map, merge, share, tap } from "rxjs"; +import { Subject } from "rxjs"; import { Jsonify } from "type-fest"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { AbstractMemoryStorageService, AbstractStorageService, @@ -13,57 +15,77 @@ import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { MemoryStorageOptions } from "@bitwarden/common/platform/models/domain/storage-options"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { fromChromeEvent } from "../browser/from-chrome-event"; +import { BrowserApi } from "../browser/browser-api"; import { devFlag } from "../decorators/dev-flag.decorator"; import { devFlagEnabled } from "../flags"; +import { MemoryStoragePortMessage } from "../storage/port-messages"; +import { portName } from "../storage/port-name"; export class LocalBackedSessionStorageService extends AbstractMemoryStorageService implements ObservableStorageService { - private cache = new Map(); private updatesSubject = new Subject(); - - private commandName = `localBackedSessionStorage_${this.name}`; - private encKey = `localEncryptionKey_${this.name}`; - private sessionKey = `session_${this.name}`; - - updates$: Observable; + private commandName = `localBackedSessionStorage_${this.partitionName}`; + private encKey = `localEncryptionKey_${this.partitionName}`; + private sessionKey = `session_${this.partitionName}`; + private cachedSession: Record = {}; + private _ports: Set = new Set([]); + private knownNullishCacheKeys: Set = new Set([]); constructor( + private logService: LogService, private encryptService: EncryptService, private keyGenerationService: KeyGenerationService, private localStorage: AbstractStorageService, private sessionStorage: AbstractStorageService, - private name: string, + private platformUtilsService: PlatformUtilsService, + private partitionName: string, ) { super(); - const remoteObservable = fromChromeEvent(chrome.runtime.onMessage).pipe( - filter(([msg]) => msg.command === this.commandName), - map(([msg]) => msg.update as StorageUpdate), - tap((update) => { - if (update.updateType === "remove") { - this.cache.set(update.key, null); - } else { - this.cache.delete(update.key); - } - }), - share(), - ); + BrowserApi.addListener(chrome.runtime.onConnect, (port) => { + if (port.name !== `${portName(chrome.storage.session)}_${partitionName}`) { + return; + } - remoteObservable.subscribe(); + this._ports.add(port); - this.updates$ = merge(this.updatesSubject.asObservable(), remoteObservable); + const listenerCallback = this.onMessageFromForeground.bind(this); + port.onDisconnect.addListener(() => { + this._ports.delete(port); + port.onMessage.removeListener(listenerCallback); + }); + port.onMessage.addListener(listenerCallback); + // Initialize the new memory storage service with existing data + this.sendMessageTo(port, { + action: "initialization", + data: Array.from(Object.keys(this.cachedSession)), + }); + }); + this.updates$.subscribe((update) => { + this.broadcastMessage({ + action: "subject_update", + data: update, + }); + }); } get valuesRequireDeserialization(): boolean { return true; } + get updates$() { + return this.updatesSubject.asObservable(); + } + async get(key: string, options?: MemoryStorageOptions): Promise { - if (this.cache.has(key)) { - return this.cache.get(key) as T; + if (this.cachedSession[key] != null) { + return this.cachedSession[key] as T; + } + + if (this.knownNullishCacheKeys.has(key)) { + return null; } return await this.getBypassCache(key, options); @@ -71,7 +93,8 @@ export class LocalBackedSessionStorageService async getBypassCache(key: string, options?: MemoryStorageOptions): Promise { const session = await this.getLocalSession(await this.getSessionEncKey()); - if (session == null || !Object.keys(session).includes(key)) { + if (session[key] == null) { + this.knownNullishCacheKeys.add(key); return null; } @@ -80,8 +103,8 @@ export class LocalBackedSessionStorageService value = options.deserializer(value as Jsonify); } - this.cache.set(key, value); - return this.cache.get(key) as T; + void this.save(key, value); + return value as T; } async has(key: string): Promise { @@ -89,41 +112,48 @@ export class LocalBackedSessionStorageService } async save(key: string, obj: T): Promise { + // This is for observation purposes only. At some point, we don't want to write to local session storage if the value is the same. + if (this.platformUtilsService.isDev()) { + const existingValue = this.cachedSession[key] as T; + if (this.compareValues(existingValue, obj)) { + this.logService.warning(`Possible unnecessary write to local session storage. Key: ${key}`); + this.logService.warning(obj as any); + } + } + if (obj == null) { return await this.remove(key); } - this.cache.set(key, obj); + this.knownNullishCacheKeys.delete(key); + this.cachedSession[key] = obj; await this.updateLocalSessionValue(key, obj); - this.sendUpdate({ key, updateType: "save" }); + this.updatesSubject.next({ key, updateType: "save" }); } async remove(key: string): Promise { - this.cache.set(key, null); + this.knownNullishCacheKeys.add(key); + delete this.cachedSession[key]; await this.updateLocalSessionValue(key, null); - this.sendUpdate({ key, updateType: "remove" }); - } - - sendUpdate(storageUpdate: StorageUpdate) { - this.updatesSubject.next(storageUpdate); - void chrome.runtime.sendMessage({ - command: this.commandName, - update: storageUpdate, - }); + this.updatesSubject.next({ key, updateType: "remove" }); } private async updateLocalSessionValue(key: string, obj: T) { const sessionEncKey = await this.getSessionEncKey(); const localSession = (await this.getLocalSession(sessionEncKey)) ?? {}; localSession[key] = obj; - await this.setLocalSession(localSession, sessionEncKey); + void this.setLocalSession(localSession, sessionEncKey); } async getLocalSession(encKey: SymmetricCryptoKey): Promise> { - const local = await this.localStorage.get(this.sessionKey); + if (Object.keys(this.cachedSession).length > 0) { + return this.cachedSession; + } + this.cachedSession = {}; + const local = await this.localStorage.get(this.sessionKey); if (local == null) { - return null; + return this.cachedSession; } if (devFlagEnabled("storeSessionDecrypted")) { @@ -135,9 +165,11 @@ export class LocalBackedSessionStorageService // Error with decryption -- session is lost, delete state and key and start over await this.setSessionEncKey(null); await this.localStorage.remove(this.sessionKey); - return null; + return this.cachedSession; } - return JSON.parse(sessionJson); + + this.cachedSession = JSON.parse(sessionJson); + return this.cachedSession; } async setLocalSession(session: Record, key: SymmetricCryptoKey) { @@ -192,4 +224,76 @@ export class LocalBackedSessionStorageService await this.sessionStorage.save(this.encKey, input); } } + + private compareValues(value1: T, value2: T): boolean { + if (value1 == null && value2 == null) { + return true; + } + + if (value1 && value2 == null) { + return false; + } + + if (value1 == null && value2) { + return false; + } + + if (typeof value1 !== "object" || typeof value2 !== "object") { + return value1 === value2; + } + + if (JSON.stringify(value1) === JSON.stringify(value2)) { + return true; + } + + return Object.entries(value1).sort().toString() === Object.entries(value2).sort().toString(); + } + + private async onMessageFromForeground( + message: MemoryStoragePortMessage, + port: chrome.runtime.Port, + ) { + if (message.originator === "background") { + return; + } + + let result: unknown = null; + + switch (message.action) { + case "get": + case "getBypassCache": + case "has": { + result = await this[message.action](message.key); + break; + } + case "save": + await this.save(message.key, JSON.parse((message.data as string) ?? null) as unknown); + break; + case "remove": + await this.remove(message.key); + break; + } + + this.sendMessageTo(port, { + id: message.id, + key: message.key, + data: JSON.stringify(result), + }); + } + + protected broadcastMessage(data: Omit) { + this._ports.forEach((port) => { + this.sendMessageTo(port, data); + }); + } + + private sendMessageTo( + port: chrome.runtime.Port, + data: Omit, + ) { + port.postMessage({ + ...data, + originator: "background", + }); + } } diff --git a/apps/browser/src/platform/storage/foreground-memory-storage.service.ts b/apps/browser/src/platform/storage/foreground-memory-storage.service.ts index 1e5220002a8..b3ac8de55e3 100644 --- a/apps/browser/src/platform/storage/foreground-memory-storage.service.ts +++ b/apps/browser/src/platform/storage/foreground-memory-storage.service.ts @@ -21,12 +21,16 @@ export class ForegroundMemoryStorageService extends AbstractMemoryStorageService } updates$; - constructor() { + constructor(private partitionName?: string) { super(); this.updates$ = this.updatesSubject.asObservable(); - this._port = chrome.runtime.connect({ name: portName(chrome.storage.session) }); + let name = portName(chrome.storage.session); + if (this.partitionName) { + name = `${name}_${this.partitionName}`; + } + this._port = chrome.runtime.connect({ name }); this._backgroundResponses$ = fromChromeEvent(this._port.onMessage).pipe( map(([message]) => message), filter((message) => message.originator === "background"), From 36ea3301ec6d4292830a9bbeddbe606290a732ae Mon Sep 17 00:00:00 2001 From: Victoria League Date: Fri, 19 Apr 2024 16:11:12 -0400 Subject: [PATCH 024/110] [CL-218] Add new icons for extension refresh (#8805) --- .../src/scss/bwicons/fonts/bwi-font.svg | 21 +++++++++++++----- .../src/scss/bwicons/fonts/bwi-font.ttf | Bin 76108 -> 79272 bytes .../src/scss/bwicons/fonts/bwi-font.woff | Bin 76184 -> 79348 bytes .../src/scss/bwicons/fonts/bwi-font.woff2 | Bin 33420 -> 34628 bytes .../src/scss/bwicons/styles/style.scss | 11 +++++++++ libs/components/src/stories/icons.mdx | 11 +++++++++ 6 files changed, 38 insertions(+), 5 deletions(-) diff --git a/libs/angular/src/scss/bwicons/fonts/bwi-font.svg b/libs/angular/src/scss/bwicons/fonts/bwi-font.svg index e815389126c..bc0a348fee4 100644 --- a/libs/angular/src/scss/bwicons/fonts/bwi-font.svg +++ b/libs/angular/src/scss/bwicons/fonts/bwi-font.svg @@ -5,11 +5,11 @@ - + @@ -154,7 +154,7 @@ - + @@ -187,6 +187,17 @@ + + + + + + + + + + + diff --git a/libs/angular/src/scss/bwicons/fonts/bwi-font.ttf b/libs/angular/src/scss/bwicons/fonts/bwi-font.ttf index f9b63283e0465ea0285ac022a2aa1540746d3d60..f70eea7af77f4f057b184abb7f6f85337734d3d6 100644 GIT binary patch delta 7209 zcmb_B3wTpin&-dIjPJ2be!V2qg}1zGKx6927KVEIOw_#pQ|(Y-1UV_lkR`+O?0zN8Pn6HZGfg$TADzJ_5y4JFjV9n*U?=&j^%I`if3m;12N~gug{NzjNif z4KK|fd4|B`EJB!LD^@R=(Vle7eV0J#$H=5}W&4J1@)Bzy5SxnxxvSb&UX$NcmO^+1 z5_r2;uU&_VB5L_|x=&b+z4P$xa4<0!vf)Ug6TG#fJ%q45gdwD|nx2gw@PgKm?`Y4^ z5!eYVP7?Fj7wI_U;l5+kiA~%M&@*&E_gfWVs6*NU?$I8ON^_GKxs+T^R+5e6RyB2GmxY3Pg43bc&zZ-{6E_*J;JZfp*uF%Q*%co-VKiF| z2D904-pB|X$4NXdaV&?AI^(#%%S^`?S?)o^)C;_XYltfGf*wc4oLtB}GRI($yf`y^ zB?EqN&OD;dQ>W~*Y_kPr*`7LL!#4#Vu$gd8Hrc32(Id2j5ABm*VubX(!*Ufh(6iQd zUdDa)Fx!V$G?%PSG#+#f&2;4fuUc!UE9)-Jgp8+0vu&?;G_$$MA7_xu+1%e~TTcZ+ zQ0@Or(Vw-IhbKJ=A{Fp8+cVTr;-W$~O@3ZuSzCG;g?B}-Q@NC#pu=00vPO|aichMV z$5gJL@;1ZK!z4Vl3lMQl?ei)kz0{$ax=)c+BCq~&RsOs6FHp|+Tyz_&!l8yQG?B{e zKWdY+bEZ&f6^+MrDu%RRCZ%XxsPd^;^h5gm(W}1fxK z{ekUCY+G`l|hmPqbRfH5I z{%xr@@#FICGPQpnS&UwK7a@tQ4Xs0;cTJtsnOPsemee!4Tyei{G z{zMC?Udm3SR@~c$ik{lLQa#8#^geace0PxE*QR@{+DUian^f0#-M3iN=F9urwaL5( zC}yJf!FN=p9zvz&9XKVXwMoZxcKK*A+SjRJ;W)K?>&C<$mc%{Opxy(<&@`^_SybrW~D*%LcU6s-tJLc<4Gt&;)_-j{D)sqNL`-t4z-(VPweGK1Vi1?If%9QyZvTCO4OL!^#( zG(TW>p(M`<&6X!{I2gx<%*N&S}svpy&u2R8k4nx5`N|Ap7=@C*XAe@vr2 zhrpUsH-eggeuKJ%$SiVeBGNE(X!^%h8mq?~EzbwSPY3&?%s03oit&}3lCDk^Is7N}5iKs15N52cE_5>=cg2%w=Di*D%R13&B z!@Q6f$fE+B_JW9`bShjEC^6_6hs_@G!11Y3Nw2r_GOH5{jY_erz7^W)JQ2l^$Ll%0 zRnGt?FoNV-H@{$lD3}GExhh;(ZPMvYf|wuIPq^3zK~uvl$(XSG}*207M+yk2v=2w&2Gu)v)S@W?3X#WoSaRLvQmNb@~43%S!;z2w8ISW|0Nj z7ii^v@Qdd%v9{`~7|nzSzFw*M)#vC8$~OVcF=^qb{eGGjpj&=;O>+^tI`7A{haX0N zIdb+#)pb9@jU#_~O-nO-M)%M}M>FT}1ONl8h2TI92S=f2Sm5DGP2y*4&p?L=UupZp z%{n-dj{f1O0ph6L@DoO)!EuWTHmb@DoHYX`w}C|#7{+y*6)qjqZFre2!y1(vKH>l| zec0jGoj{AvaE%MrkEN4=r`*t!$t?pV9$2pedr_@H4?Ls-ClP4QhQ%uI2LztUhO3Z~ z;)Nbnq2YcXyasU^z+2g^q=j^nb=Y3sPkxCxL2Y_mVhIL_AQqkz2Gplr=)M>$+Ahv; z4a20cQuBCiS6dsWPVdri;`2gdv15;)0|eA?!jQ!6LiJ4}H;RE<1x^gIh=OknxFDQE z#O8*pIBuuEsU&|!_DVsl3WqDJ!g{N2wab(1tI=5{>XYFrL73vM&UF<#nnlDZtgbH9 zTXdHf#EUJaJ0+KqE+nU`EtK6JOONnPdh*T8*4dRhi!M~BaIC}zqNroR9wAVqkIm}#6= zyqTszxJya2WR}!rDQ^ z_2>twIe;WpOh$dz)urf{j7%nBk(S8BM~YFsvWoZ=)DeAZBKlMzbwDes8$cDUWflCG z0n_F}VX9^DRR^T0ySr(Wz5$2wIdjT^zBAg}f7RWT?4vjgzU$_cC;KQ4gKw%1MqLUO zF~$it>Us$3A)tpCzA^M;5CrCGxN%YH$<&$Do3K08vj`T03nHnG8&doFnJu@czJNF2 z@zi$MkbF5+13Ob|nZ}*)myw%b8$GGkQDM~MC0tAHC2s%=GBOCMSK^8|Vi->96L%?& zba_&X7md5TvLJ?Y1g|MniPf_r>Ovd2q7fQ{+JiKBn;HSWBcUK-M^qZ5r}Z!D;xTrs zAXbxkwBViq!LcWj7mUR7H0pHS#GyP=hIO%Adr&;mDGsL>w5QY|l;S+kFf+3K0_SoFaJ776dkYR1R&*$YP+27)HCB2M zLWQi6hB~keXB136gCGc2!QeBA29CvL?ZW^s@H$Anf`u7?`zutxEE;%*#r5F{_0~MWuD1#-uc2bO1`egNAiz@$81*_MWx%kkLEw3~4L7M)yg`&v zD=W4xsFh&MXr(dX@#`kbJX||TmgS&a>`RWwva!I(@U_V|b8?wRe{)U_& z_&FRUC{dGZ&gI>aQJ=!D~qk z;*TCav$~KP)R{%m$O}4jDtvUjU=&5O4w}bt;JDFX@S4rp27{a$FnbLKqq+dgF+_9% zFJtR0>P(_2Q#KgO@%C|~)C1{`Q!Qj3GLy6sV#iiLT^lB<4M@5`Oo4LM1=LCwt(b-t zS8Z+5t$KbY}U)E z-7`L9R`X*4MK&QkXQjqO%#8#JkF|ZPr3){%?^2;JRfw= z$O>X2ewh=4Su@;0fpdC8*(1-fA%W}QM2lgD(LC~;1A2wLb zmt0K1QRd5V)0FSzcu5ox+nG(DIW2;$IU0uXZ z2xTX)a|L-2ILRFhx-l-WF4399#RcGlF6OT6(8!1CGJdTy=yVzioiq2_+%}WX(eCi# zkE&irGmbX5+XhL4$fa0LL^~xpF|`v6$fhl?^AiSsiIYj$!V@KZ!d5~uJ;QVfz) zk2GFR7I+TV7}la74Og9dza~si9j>ANsb=)yMLCfED)>Bp&v`unoofHXT!~O^9(3i# zi(3ZT%u~N32JBJsbE=`MBQuV?I@&YvN*=`GL)xZ_p>~WErhk0;FEh`q|NoA0P1yNN zsqpjHE(9K@8y&q6eEbhXM$l;Lf(XX&Gj#vAF+MKz0yX|C1xCNq=to{1=kq^ODE_}P z^Mwjy_l_&W{^R(3oc_PK|8a!>jOf2#Ljw^a5eM;;Phd9l61$6A#g_=2*roGG#|*2C zQ{`S$ulZ@K;>dEwcPU-U8Q00IZSJSD^RplIp7b~P*X16~Jk zvSaGrX{&1YOy5$stZrn+!kNGR$-}cwHFz3cxn$ENk6!ZeoMm%2T*|~R-PRaw{P?o( zn>wy&zT&;+Cl*8&j4XU_QNvZeEh`pZ-MXgr)YV-3$qr^o$&$@WK41E~rC(puea(H# zwqM(N?Gv4?%k!6??t1CEvK7ZxwygYq)my9I>t54S)^ldfp|xAqb*&p*zqfbY^_C3{ z8$Z~z<%VUO{hRl1*&d(QcXDg}wpH8ybaV49muB6Py7kblpZ~Psr(fUZzwPJ!Ew|r& z``bHgJ34pVv*Y(WZ{D?Hchl}Wc7Jq7{vB7``N5v0dv@&kHnCyvnSGIco%{CF&W8-K zCi-;_Kg>XB5!kbB@!%IymH>w(7q82lo&)|9 l@7DwOAgbJ-UXn-|xp-h}7>WmCVYp%7jWA>lJQjxi{{cEen>qjh delta 4106 zcmahMYgkle`g_lJ&YZblxC}55a#2WBlzU){gO({ND2Wg#f+!*Z0wR*D%!sDRDxt@m zt#w`Uc~Z;Pee9HM)GFG>W|kY#f0I{4-uHKADtA`RkSvc*Qu32+_z{DRbNrJGPWz#OCTPDauE${);43Js5)^n z8z+?KclZ4Qx}|v#4n0yeL}a6!{WwB0GFfeEfeimY86`1Fn|ua#0ZmZar2_gEbt{`? z2Y;PI!Yv1gPXV~)gW5lU$}iWqK`4ph2{%j}B#kT}i%28cM0Sv;36Uy_!Xa2%INdS! z76NV2wh2<>*>Fd!ker^D8W$U*2@bLc-bH6fZsQ`2u;!+lx}|xRP}j{XMvKL0uviTL zgW(#byp|WVh3 zbE@<;i@{*E8e$92Df?Bw|$JEbhd~}($lr^Z=Rv}O)HY_q^76^Sj zp-#Cf^l6~wDD#Fjaw^djq4)<|B3&HXeINB2N25{aL?bDu-*Vk@Sz-ta+A{U@*sNqFP3dmXP7{wN;kbl%jL`W zEyk45Dd(26r(fWB&?!44dv*~j)@PpvkylP*#2IqB^9yt(Mb11<0T;r+%r}|meGlxQ zrVdQXjY-9kJn|AIRUo&`*$q^$bjSg@pECmMd3ZXDot*cfR^>HCvjE!V++x*ha>)?K+NDpz?wGB%o1946K<5%v|y|d_sp& zFw`jHbW(-RkoK(If;RSMtkx}+tot`Ii(BSmTS*kX!3|4?*8hN_1Fa^GcBAF74aaeA zFfH8Ja@EG1v9e!m`v`N5Vw+3e=02+ilMC8?QQNk>DgsWYg43C`^(c@2MxGp{tT^W7 zqn%B+7iHd4SSXv0-rc4C1?P~KfEI{yh)$x9N^N_WVWOYzty33m9xhasDEMV82c2@u zvo@eXN{7_(t6Qp{j{QqPO?>p%6@UxFE$96PHQiFvbAw{3(nX)A4RYSW%c6!1WBO52 z`iowFC0R#*t2&oPkFmCl{LTZyWlT9zZamC7dG|*iVsLL%ohyQF`Q*7l5KT%S z{TI!UpFRIPur^6EKRAruM?WY8^BHA?W>W{rAzKIul%5(0mX{B#2GJ*%2mbs7h&ALn zx{q5Xmkr{yH!0U>CpAmss6c7`XqpDiV zU(tS&YBNtx{7m4)hcJ9EjgzxJN42@i`}7pelIt&T0#m+nn!ZNUaktJU+i=dBBTYTre|aWJ|U41V%Lafz=zT5Gd6^8 zyba=2113qgk7+U;WK*&sCE4z?@w*T;%soD?By_3qS;XQ(Wtu6;DOv25oSuP>6Ip!b z^}l0|+2b4UAq$w9v-mhQ+eDV6=ua{@$d~r*m_D_nY=}>F#l^>IL~dy?b%d|X&8=`G z1qKEI<*0@at63BlEl@E;YvOs1<8-1JV9w{?R5#9*=QVRtIW+^ z;RvSznOcBA(I|uk7-ECPAiV+O1?zQYf#(4mCEE1*5JTYj01%^tLxXsoRZ9iX=NFp7 zjCc=F4GnYXw7L+BCE6AcW3@&Y^k%#+HA0|CCxFRRGm}D4U_eqh_kf`11(CXfd7BVs zGsXsK0}X}{gCSU_H}e7uCTcL4-H5?>&^m$wLUn=hX7iMBQTYk>K=et&!gN|~sM#M% zZ%{*NFcc3)^UQpEphFv^e}7`Z|T(0f)Y+prfpr#}!xU#&d@LBKBIo@_g;<3x^ZqF!!` zr#BQDZ!^hZuuiO1o}qi`D$lVncwUSrZaTyr^^`bZsTfBJ=_j1qbJ78&(AVP#heq|B z?CFev_kfy|9X&0R09!c&CehO{=oy^?)mk{JoZ<>;j;ACMYNB4;ow0tI<^Jo$z!UZx z-t#mbEo1oRS*6W$B@t3wN5%%(fcnptARmK}AQ0kaz{dTU(RYEj{ z=##sB*snv!_yo}T26!I^hB06VV&;GWGtzsUQz6{NSY|SKCnrTgz7XN=7!}Az`Q*ch z$sJIPm>nhEh+-<`l&!83|9w82a*)tth z&9jKW9Q#TyesgNh>B7PrfjvD{=PJ#A&(HZYJHE zRGf4yxhmyAYFuhtT0wee#=RLQ@0*;d$;{2XnpHF1I=%D$eLri+-aEtXtaf@`IVH1R ze(=cbD{~^|yfb&>++%Y`9bTWcYu2EI z7uF0lG&iO<-e@|x*4w9~$`U}x9PkzJv?=5%tMqg^pwC0$+EQuf7qgqleJ z>o4IvyVD@hvnB=1CP^J*8u<0)=}3WxCWQJ#di?%;5YI#4b!Zex4*F=oPj#*5dsH>W Y`$T$@N|t#V(je56n+iuf`ZPH3AAw-ljsO4v diff --git a/libs/angular/src/scss/bwicons/fonts/bwi-font.woff b/libs/angular/src/scss/bwicons/fonts/bwi-font.woff index 1e57b1aab32302c3438d584d5f9056158fb1c5b9..52cecc3ead80bbe917f34b4e12d74f4d9583444f 100644 GIT binary patch delta 6946 zcmb6;3wTpymjAiWrOAFLe9=1FxC_@S= zBbA2`Gz>a*WLI?1Va4uH$7K{z83jdgeGR(mu&$2iAiFA}HtC%I-n1}$`|Zy5yZ3*b z|D5xm|NQ55(_R0@efuS++;(kU9RWfJlzvMLYX3Na`1*WMwvV$#*Uewr+J%c9xH6Z% zXU2W0yQOsng}sDxUFx;mvSdv=!d@d#Fp0i14q56t7Pc-RFrMO+(KqLZ%zx}aAi}C~ zom5bx?(x^gwb77)U`M&F||hMrw=>wLs(h=jGxp(A0R{_fJ&)m;RNcOhZ6`W|I1 z%UYK%MA$P(=%#N@V@a~>))g!9KyQ!Yu{dHmHfDr-qfdNsZa6VKj1c7V1^MEvI)mZ= zaB|8K!m7$p7u5y`Ag-B0hr5wUH(>}VucR9LyJ5Kda5wuAyMa`Z<^hGd8(0Ly`q`Ih zKhVy7$Ebs5aT`GQz**hzRfK{4(mF^V?&j#O8Kjt8L#`uB$r`ee>>wm|V5SSi*dr4& zw3fG7eb(IAb}`chk+RZ~V8Aap?27G@adBzv4dV^b&Y@nmcc@o(>h+SO*E?nQ;&|vi zquF9Gn9YXEdPd+lPU3lqV>x`(5y$;%WOxzCa*rUUUf?B!A*#d+dh8iMxbEgA4na+{gQ)p7E)O_tNrWZ9NHLR&g}IL$;srZlQ-v?5Iv1E=MqjF1}dx6Gj` zlhRswRfWr(wZN+y8#tJLzoy0pSGT6$Bktjm#iQORgLHMi-)T!s4hSj(VaykdtnJi8 z^2oJhXKdcg{DItp4Im@RfEO^=R$LEE6O=vq2I6h*!f9;Eu^JiCVVpXAf*&Y%k z4dh7_>iU5b_1iT8+OGbSHmI9Ek?y^r;iOKbZ<<+4>37dkxo(*KeM&d$=B%Lb|DL;; zcFoOi(5`S^siutfwpLB_@7eCyiTT&52S@2*!38%NsBX}HNG2(dy*WNT*09(gduhQU zSu;})sUxjqt-4axDGuDUuomcD0iJD7`y!3MX^{*js?1(AY&WSOBsbRh3wvyG=Y1%~ ziO$EUG(AgJY1n6%6={d_cjc>tSVQ+8)zghxK7jYh4x?nJSKNs-?JMbVWBXS9gp<8J zS81pC5!cV(_C12mu9h{P$=eTH)cL`+Q#I(1>)us`r5EvT@2lFbdp4wYJ#pu6RYl-- z1ATWr!6Co0O=s8QNM`f6r+#B~5JOzT}8t zeNX&GWgeUG&r~gCPsK+>dg}nypK5n~1E*B!={!@~GLz%3g^h{KP9Cr*?e$)n8vUvbN(h z5NtrjJbV0t<{SrKqt0P^{T&VKTjYJ_#G{&8cD(r*u1z?3qh>}`Xhu`XorKt8AHU@o zc>1kH8V$k+{BPf_O&-9>{&%!%XSd=`?eA%-dk+_C-p^;%Bhbn64=|)s^Q4m<9~NAk z9R2qcJzZJ%F|Qr_S)BC!DTVetPM$mcGobFu{tg8TqDO2*cdd>6>N;Uy`==3&-2ucq z_Mhv)Lh%Mi@bC!nLIyGhR(?K5!+R0&-u?4QZ4ySY_n)~fswNaQEzBejkv)Vs{lW3T zmXwuOL9wq)$pX;?PA?QGDkP#fOc3w~^9n%>3!4hg}72yj64wC3r=JiEupONL8|gH%Bnb0R<358iwyli(dVZdIBdCLd(k9V=qohn z8GD*7?1Gb%BA9t>yv*vvJfl+JtZ9apYFAh>_<232x9S<-1V)gYD`(`65e2iLGgpN2 zDor}QNf2|Q`Y~5}AYiJmlkCkkHO*!u;cI4?8Tu#L44fmwUGB2_jd>MN5z_fgnUlS? zbU|h;I)^zKk~1W|#4vNKjYb{Q?+T}-*}-73DEVzu(j+EbXU=G@xngd*ET)0U8EMG{ zp3!GGIIG@fHH#*jHDj($O1Fn9DnjNA$>>S5;VJY`!jL+4-*fPvMVGQN?6MxMozg!Kc$kSfg-*CI^VA!w$L>pxJ0}qZ3w*#*;YX zg6@&v62ILAtJKL(6zjYT9#to&aIz;8ZdNCM#EIaBImoEZ4c)3hgYSCab%;_=ThFe? zI<|wXB)#O9)NaG;Zg6>QPr0&octn;oj)xGmns!knp@O9ina z6e_O>>8-k3ovv)p1f5l)HW{iAgozoI+0FuclZZHZm6dsVi|)GIXo1DFU2+;JATdR) zuxz&CR1e>z2H(^+UtO-V=z`S>#|kV+QcEqYPc5?56(Px{U+wWb?7xsQWMqZ=vFvWo zm7#)$tm!OE_pb=a=~AQFqSvoCWSHo7h{LRjdS{npRiv-w1qzsg1s*N(Y>DcT(MbRM zmf8ukjKKIZIlw{_z#6pIw!n)c>zbda;m=2MNj_d8MjD3|ugB-ms@`IsdQoGcln$&V zK?syai*tQeAA5MH*A)!9pgNoG57o5IhH8W*+k1Nf3uCsIIUIB)i7PlNT6NIH;5<=X zU5({GQ8Sb(KqeVW#$#ztAfk8xFU%jD45HxA3zkfwAs6p=A|8N>7S$vPpsY@x!^dN} z&U~WjV4vP4Nz%R*baptA9dxm(j8xKUTB_3*4&xd{Sg{XrUHW}g9Y7K)CZo31)k*qw zG7?Exq$M)(kOCC1q%1lSWkj1Ai#C-<4bY0>`cOn`Nf|$?z?8ahGgUL#R0C=3>S`RO zEnrtVZCWYNc0_vHZ@L;2y%dMRwt8A=qL<<@*iv!O>yjvlF-owG*F!)LK0Op;i>_Y` z0bu6A&t@loo4k;G6SgJ0XT#0lgmAL$j^qP<%({D%e}OmPK=K|~o%lm?0&GdHU>df- zPeXUY26|Ghq(ZO9OSpwRNZtSzWMmLjuf!R)7o$6=Pt>W{Q|U=fUexdWB>{BjFkVxz z9J6Ox#ECj`M#9twwFhbNHZ=ll!@&SzhgBM+r};19B5bt>_g2q@$D$7gu&_{s z8Qw1hOn~P@D=ZbA_cy3sSv2qri}2JRqKW`ZG5Z1VtYDFB`Ie{-^R0P| zO>Y%gUPHxn4IJ*uf&d2@FzR(i%79^6gTV8!2^XnUyg`&vDl3*QD3xFwkxFC2!r9onB-s|nV| zhc<>&Z5pFiZW$`uG|{E zh?IOdRdA|#%tdNR3x0iIrJqU-W7Ya2l^-TTscHdgB8!ww#)PX@HmU5YnUH1>)?5r@ z7_w;cD+e_bV!+YRozWv95A!dcP{{MMkS8%+9kl9YSCszMTRW=#R%T$vM7q>I6KyzpYKj|ctUIFfZ2tEmyqrey~(7XOG%Mw3!Nk@ zRJOi4r}$DY+(A$3}P=O;Ct%4F0geVz}gr1?*+M1Q$&?4vv>Z z0kIu96uU%_5lbjW3#GppB1r_XSy_ls>|BN8Eg`Hl^D8U!*)hS)#A0WF2Z56^0)Y(l z3rtINBymvz_<)nSKQlP=kqXAII)hH9q0lk&%e0I%lgHj_cf%HN+ncaY%g9KBghAwz zEGMF#5}cUaf+y1nO@a=iboWUdw&IY1I~Zsc^?I?A6M$1JEW>zMju&Lk&FDD2LuxT7 z5~o9;j7s9X48xio90>eGQIf=oJQ6v~=*k`rl_;|wB$5<^72fN>Q%?_{|bOBJ{$n!|K2c!{~u6!BW0#B9v+UL z3qa;58<81K{w?JH*n&q!moqU)!{2G_hhF`EF&h=bXeI2<5g9QU{wpDW2csw+3;!fy zCtmUyT+JM1w{pw)LV*)IbuQ^O!!qL}xyRIFe#WZU(;ZutPUV90RQiUDXEJj#pLCz{ z)_YfGHDyoF-s`(3(2!G?TM=v;b9ijqxTbM$6>KfMV*I#pefYSDu;BG0`+}^Q4`VmsRbUvaY(ldT8pb+Nb_uZ{6v7SN$tj z-G0@RSA9CIefsKam}@pPL>fN5_IzX84NW(^-}Lm%@XVoE@6WEE(=&I;&GVXRiJO8T%FD*E?uxsH%?f2Z$e9O}v&5Lpto#{NfxMay|OXn^8=9bt^kp?pw99XXR~{)%9yWynWpr?Q6Yj``6tQy}tL<`kD>PHvIXnrn}Sc zPHsH3@vEQL|NPu0@1}q1n|trBd*9xiwz*^T?#;)x+_iPdw#IGuZTsZDocnIr{^5=V zJ2vn5Hnw`_g$Ke9bUe^clPnovP4vqF|I}}r_}%$nj<@82Nmi3E!U4Ym9*^Ii2Uq6a zp_0(+ap7kV{xgTe$c`6fPK|+B$W!sp5LNC=fh1Bwu8hAPf`WK71b4*e#}M@Y E6*9^cGynhq delta 4085 zcma)9dstOf7T;^{bMCqC2agLE2zewVD#|<5@T&P@RI(8@9|%5B2@n;POyP=#$taOW zy^YgH=zOU`Iq4~xs6o{7OH(VyM>&q6rdv@Iwp zuRz@xLTDq$_DLawixtE|Q}y;YpTzSIst?*<`bs{)a**V}f1~^+*K9^dbt3 zy(mvy9u)B+W6Yo}A;XspNTBV0J9WF|QhR8hPMgVOx0_71t_R$Dzlt}FpBQaR$|cKD zs`K$p_UU}>RAtOL+7YK7J`$M+^%ST;y*kY(h7Lb_Q$nLXzB{6d{7L6gON2wirh6-R%H^{TIrl4VK)*+At^$^A% zs#Lqb#!zz6@O=#MAxDll!XsaKasy+vSXSwne*xB4tK?;0<_zv(++d!qa>D0&3(6Q| zIf)|^$U=F-xQX&xQ?5g`vTdpsv~t={;=j?rrmUR4l!v`N<5^Co<$cOYLVg)Fd|15HDjXVMZ0;vCB@4qD?pQ*rAfe|kclko1Kdf@Ui>BNKtBP0uf$TQQx0!;01HxU z<#T_yuf;j{>P*lIPj4syeDoe=`)g?Fk=u5hm8Prp>~&VAY~OiB(vc1hkZT zrui)`xSakc-j?LIy&zt}k|UK<`>=m#&!UtXD z5F61jU)lCSFWT)Rx3+z$O_Xg1hf(ZI2kXK>mk#Y_SXa$QIhgC*NBmq!EKEM|$=l?PPV#D$hQD zR_dd^!OpWOO4b*B7z+DP7_XH6%>@ru>Mr&NDM4-4%ntt@{rhS@Le@R#d+=OUcx($?0C(AYto(FmfQx#lq^U|cKUq$AtW6S zk5^R^hSd2yV(~n3%ar7lLHw4So`HcAd43-C*EzfW@s1x08(4X;_&BZFL|&xiFETjD zSN4OPKC`4Oh)=DGi;vSubY?IM3!j&rT@aQO7#IYMGM(Tmn&gTWnG~Y82m+-%szhovn|nFtWM|I}3ughDdVoO5EV=?rvB6T1(S-SejRvbI z2!M@}97bb^De%z%kfMV_g9L+J&qOfhL{aq^9Q-21wrrhHFfgCt{RMXQ71ZZYoU^q8j|!(j49Nlk8pq_;-sJ=b)`oHL}OPC zMiWq-Bn5YO)$Vx2tTzG}nA3sN7B4<~Zyof{#J1ZheAIV-TSBEE^+R@x4N(d^qwzTr z9lK9kmAX4m0Q*8+r=0rnBp=vusJ)=<{COcrZX62j%q<7p{g0H0uTTfRLIJ;=?4F}n zx;FD~4bE!v?n*E*ZRQYfErUQQfnd&ibwheH2dz);~0&7}k+hIKalJCGTxx3|ngNJrw=yuxP z5)cHEZ?7$p!N5PKEwxTqMkPvCvo5;Ut8>BP2STL_mPy6x@7WeM-+R{uuS@a7!!FQf z@2)VIDaDao_9gXrjo~mI+FJ_4VX-z2TP{SvNnjRrL(Ad5fUO(~ec2&6>rGFD1$t;! z57JyV%)2WQilUlc&R8+aHslU53HXzVkdDn;akO;Pd&ksD@4x|&QnjahkuTt{Uj)iA zhy;-k4+mfOQ_lB*+sEf46eOQyKqMVE9nWF&ZD4)IqDR~IB3weLKM&x-d{xm!ehgJqszn^bC5Bv%@ zE^=@=2*C{+5q`(%$n!N0cMl$Atzd2^_RxEf2foLUD%_uXM<9UD;odP+TM+&bhmq$f zLM%b_>45~~`yxgo#v*cj{4k`Mh!cn;1eWjsymY?*FMJg96p=&}{0aTW!Km}|gl*Ay z9+a^6HJ%6Q=|fTGC`QC1;t<@Q+wcty#SJ!l2PWfwbyBfD!g$J5X3nxySSxI;j?kd6 z;Cg4V^KQt+&~>iX@VM~35f>w~BbWD@8Z{wmPjqeUQ;&}6Jt%H!!r{Ji`%Ue4rhk3n zr~!!svj^TwdMs&b(%$64l!nx})XKD{)9W%G%V>MNU#2cIJM-3{qQUmTbwjp2QI@r3 zsK>p)-8F1l)rkEg_l&waCSuI-v8%@J9eeFJ3&yP+FN|L|A$7vFoS&bXJ89~q3zOfO zl02nr>V;|9(<^3_&YYcBo_950S8#E*FlXSL$~m{^9-Z4cZ^^ud!rJ-y^WRyJzc6m$ zt)h>gO)oxGl2`I`>6t|r%E}j~FTT5^ZK7FiRx3jZg0&!xcV-l0B~5&r{@#Bm3L0qrBnP9G1%PW0a4 isW93S?^78`Dw*YdDGfrsjx^}+J(3E~dnczs!+!yd1mw2> diff --git a/libs/angular/src/scss/bwicons/fonts/bwi-font.woff2 b/libs/angular/src/scss/bwicons/fonts/bwi-font.woff2 index 88036b7b3e8eda7d55afaceea4beb95b02821c88..4c8cfd6e047aeb1b7f7c6430b7252e34e995f1b6 100644 GIT binary patch literal 34628 zcmV(?K-a%_Pew8T0RR910Ea{X3jhEB0X3We0EYDd0RR9100000000000000000000 z00006U;tbZ2nvMSFoVf#0X7081BFZrf@lB)AO(aR2Ot|U>!YG>^JbVRf{g>?F#Oc$ z2+J4;20Yw)_MZ-@$XNT43>0lJYZ(I65;PX#St5ahltnSQYY&WM`0T{d)1PMcZ320Z zKkL8bz2*#T8~Kx*yvOr%dh*`?5#&n*?@xkO10+y9XrYFdiUBnR+KEBinNXb=GILlb zMrYbF^UjQ}6K8mSZoREx8*5_Z1|y`l{f#i-Z~Ge{y%8z75e6|}ASk0sNr_Vg1p^CE z1i`Zs75l=-^SnFp($hPYA+pV8>lN-`%N^WNWXl}~{VH%EvR5hX3SCf+D%dE^PE%2% zL9tc*W~)* zSeL|+6-?^-`B7(m$(6hNyJPr>P&7v%wNfin>^K892L|i_4g}#zYR@!T*i36HonayH zuy7!KM^L9fcwq3Kj(O96q)SRo%(4b4u#I%YqZhjW-&A|s2MFMyes&J@aZ`$psocs< zIdkuO^Jd<=0}yY95I6uBhNM0PN*h2bVH4hfd(I39X+ixQo#m7zNSpXGC8u1vY1Zyi zTYS6Pb@h8Ujhn*ruWGt}+{re0l~fv)R;83SY^_b%oGrta@dN)uLiw@*5Ryrdmq${v zAH{1RrTBroRvGs`rS+`=piTjK4J2JiUX$o5rONI~EmP@Rwzhf8%an}XavrA4S;m}_ zzQt3=%$8*>|1&_;yjCsX)(R3f{p+0Pw*SeT&r8I)_#RM*Awq;OYUzZ^n1AeJ zOta+m8-I2UlkLUQ-dBJh7x%)%*I}sVq8(e524F^qP#&JgwM%p}%&$8&?!L;xl)Qy8 z0<^y(fb^F!*uG7G1P+8{1Z1fT6dB;4Z3m4+ZBPOceiZ{MVg(h4Bst@O*@cTp<0o`Y zD{Lj3lDv={*klPqAxtLCdU zEXiQHf%D6&+cxv1zT;d-`f<3hsvwV# zk1lY-_ax#31EhRPXNsL{XCQ~cVl#BT*_w-ABH5>0`-vzEc}kZ1MJ381Ru&a{e718AI=Tuj>~oLn zu5eKx5ZGMKy9J;ypbT(aK}dU*j<2>sb`CTpNJJ)Wm9#crNK=zXDJBp^K?&CHrNKHX zmsNGV95w`R-gbTmS_u68xB=5kI?N|C#bi zL_@b)){neyI7TEIM|vcJ6v#KRk;xT9)inJsR>Df3?%E{DoauCYONjO8f zC*0-I*0QK$>j;q^g3VsbA_NhyBaW4i;pq z2xmk(s1HS&W@y3S0l|2)f{@4dI1w5^8GT*kEwPJtG%qo#Wq=@U05Z4jjs0FSNEkV9 z7|QLUA^Rx;(+vk)z|*t4GR={$Pij3LS9z~UXO=Yv5{@G`YDe9iRX(bNag9g_=t!j}#E3_r7l#3P5k4X@7Zf%?ao73K6ft1S za3TOsK+wFJ9t7j(V9g{(=$0y=uT-IqB0FZwU*!8zBU8(xy1K1}FnQ-28gq+p7bs-& zgK<1m#@2hsHd}Q9GdC%tW@zqHh$5BQ4*7ZtFju6D>>{3af+goa$-4brOZr-=nea&m zed%p}<#9?YZe7e#FTMNhn`kJIejAf5p8&&tx7XG(DjbH{y{ZJ1_`j?)RmHqwK$nDE z3k`CaXl94pD6EBOnL+T7gryhdu`HBB13SyoX7&Ir-{=eRxR0BWt0Qs@>yR;C5-f%% zMa#t;w!DA%?WkJ|>9FzcWLiNU{MyE;+D(W83SqJo6uP>~p=19-r{(BJ)X+ocQ#KokG>+IZWmFV zu8idOdAu)VRQ@#g+N<<$jq%+hnK)>ktoxn+6Hc1>xj(vSKZZ0yvU`68#)T^+;XB^y_DR3QD}t*ygHWBnTsRQ_Lg$(vz$z=g;VwwJ>0%y=1UV-ay1ef1fXEkG6!;pGBOr zuM%Q{YU<(_L*~#@2tJ~xrT$&4d~9?CRti-tU|&W zNZiclt*+r zWsdy8b{yy;l{F!aeuVNOjfAlCr)UGbsZ6eX&Yi?O%cCj}-Z`mDQ7Ar%spfXD0=q** zsdb7gmJqqBx1-V`M@uv=II>s+zfTMTj5pMDn+{TNqhyVZVsl~io)~Ja`}{kVuG)=K zn#)Lr{LFc$Z4M8~lSiSZ#0-ZAksV{lOSO6-Zh~?~Z6`V;vK}+zX0b^5O3IC?*LF|f znx`xs%o$Gj8EV?(G9kK;8So~KC+hx9rM#}U6C#xDVYwU{yd6tZj7?-l*QTNf9+r{D zL!b#LAR8@;hjpmc=29pqa@!$!Lo<(GxG8J9r_7i&(wJz0g~(uXGG?gKjLEIBPqOj^nK^$3f#=Yjr%LG=m)e&>8$hzAl1m41wHR)o;zLrzf6d3 z?nWo9+$~*!Ig&Kugsq{yPgYIvK*38n$g@5R2_Dhj;*a zp}xiMU3+-fRiKsw)mhbE&LcB~&x>ccAuBL0`Ui!uw1Qi-x#T!xFc^=b4*i@L@639w zaU_-jdy7ODY}#yVZ3|3J=gZ6tb0IU^g%56m7Tw2j&`J>21nqw+dI8T`Q5mkqyP6qo zfsF!OvZ|hu&lRPP|HrZsUzRa;@WQ@8Sk&4Up2+e8;1^}|J=r-%y0Dy=ML{kKo{ZQ$ z6_r>R#bE;9Fp%OYuMv&2z7<;hB@84C5l#` z@C4!lFL)&++MFO~swf#w*kq7=Lt{vYJm7CpR{MAbdV)D2(q5ycTOkVntFg4(`J%w1 zv7$>eq2$D9l-qKsTSnLUZy9ob6Xp63SgauNsQpaPmU^g(-UwnccIP85K83dfB3+P- zNUY^=BN^axIE*&fjlTMR8LIo=qH}sG6WHY=nn+%hJoY=C-ECQ7thp z=x@Eh0&WKI&4`aDcd6b}U9=C#7{$bI>T_> zhvG?rE6tO7;&Qd91>MNl+$0-1*T>IjXb|+izUSB;`1S$Vj6=)hl9{|# zK-H#c z$!sViH%5e8IU zM!6|=S1UcNZc#e*MwRpM(ZL#P&^uD)Up}ewF>&EIQ=qYBopF*4@1*|*yO7c^N5g)y zx|Tunhs0ozODULS?bfXpF$$p7W}&O`He0El?q9$`Q=(;5M^_z?5Z8=tkh~(PxO&k$ z?dl-sVnk@F32uQ3PAcTYD9=}$Cxccj|Y^p@1r#aYzby@hWgxn3Auu~BYn{2uDZC&pajgsca=v(h=nyf zqNJ2*b=7yNd=NE;%JURKu4!q{!dX*&XavfP78^ZI*qjO^yeSB4RD9xmt*u=D-W_4q zzo_Q&WTUN}SoGKy9=pIGN#!aV)3fX^ipCs1rZEfndqN3bszo5yx^+Cfm7tz6HstH* z=+wkVhv>1AGR#ZI%bUzjr#iPYB^OZ_!UO3$`=gXD;Tg+sD{1hLBYD znV++(VVwG=+^mMqSz!wNh;l0Xvd-gHYqX+>Nxx}h>%LV4MEG6=s868H*7=cp+QF56 zNY0h@LCij1<2ThxJ_4-}Jmq!?Bsh24DSz9Qfj#WgeYUkTVQW)+?*I zp9?C01$~NIBkWxu)ix3v(+o}q< zL72UfSdNhP_hG`hP4eqFeeH|S2t$I*9vKX4B6#VqkOdp$EuRHEnGKtsBil*@8^>*Y zw$M8SiG9rczJC#gf!ROCOGo<;{cnV*HF{o=#Ufmo)GJvu!sQziZ~EG$iZ2q38-Y;JiL{j`69bM@7Jz@K@8%$B!^mcohH-Lo zh%a}5%zl-Rk9NnrY3hwJp(m*Qm6?^^*%jwbyLt#XeHxfEum201aiQfm;$W;QZY8Yf z&Bm;Q~-lT)U^ z%63}{g|Uzc;l!@Pj`kQNhDl)l9t5(h_A+u7&MM|bCROq}Dy&S&7zPsJ|5LacE@3#2 zbqS=3*`=|TEB^O*Yd%L|&OX}RmJOc2|2^}kGjF~YupaBOcA-P)b8s1f=KRN9|9@na z-^Br)_gG1R5jfcPP!Wh>U4pZlvv!|(ldAF%DfD}->%=dRwBF;sQW z{zR;*BF5VyqI2I@mtL7i+V=^0Wch6tsO6oiF}}d|z6YUJG!Uh?eWh9D%e06#tmqkm zRlqNZ*ob8E3bco2F;j*xCqI9O+J06Pu9uCe{ zopOyLj##Vae3+uEk`e(GDvRz?4_aZ6PTe$PGIBR@a=ItBF?9v0x{2S_mNS2~b?}(x zETipa&IW!csVfC^LK-G;g5{MF9I#*Ag-oRI=RNxvrs=)a;9pzhBcNq>dxF$fp$YV( zafa+ju&X$a^0?6TfoaYtN$gJzqmKf}-|Vp$x;~<}3evE%bf_utP^}ggE(SAa2v^U= z90rM>55jtVXw?Os55T5*=Vdf^pVO-DD+(fg`L=S*Kln-S4o^tPI)R1g0Ec z`g%9jO$U$mA~je_goHx1%N2`}#-_+pbS;r32Ww!q%_@L+5261nNT+ zL^o)fDk=`)^oV~OrqiyR`OVxTe1lm>1k>SwxTbh!rL&ydaI5oC8*?|y$3Xq9Z7_2Rt6XH`qM(B4xn8_A(e;jyTSo@P=d+tqNLs8-J;mB(7#m4_+wy_Q z9+I4<=C&Lo>%tmBtRZuKTB@>j41|x03qh4d2njN!kTJSGxYn3cRB0FgY7?>G5Lg{{qoT;wKpynJv`&%!g?Zo+$> z?1M8-i?jF({%&&-b{vFp&k=eL9fIG5TPUaB=!n|f{*L-Wf)cE}MBO8)2&hCheD2k3 zcLlWlDF(yw!O;%=J!zS$`glEqJaj5UPG4zN{37K0Nxvkm$(SklY#=)ZSaF<1&85zL z6l0lU>scs^++}b-H9dVr1+>q;fYM~`XE4p)=CUZMAV#Q0`7L6$-ZB0*)NGD?bB1g)e97+l;N4lQ;?3r)Pl0G4mlfFWsuovP zT^$H1L8fTyxrFdNx-EKy$WiKrm9?8GTAl2M(L7&AwH`jVS>L+}FlgZUkLItO54$pC zB0+qA>o+E6*A?+d1OinFgw9uPQJ;`LvhL+w-CamrI56D5cvOUlgrO;=`uo!)$dFQn zC@KaP!n4I5-(*7Pww2D{*2mcLY?Upf!&oJPOu9hw4FxAOT9x_$%et@QbE5ZHhBHJ# z$qQ_8PIJyR?D6t)B-+bw>%$gZ48^$KhP=Z+N z`OYm=1(`U?DCmuCZ>{zk+~Ble&7yJmy^#?Ov#wVKD4z@kjKeskTk9L5mZ5UO5V1{I zTj)xaHNYFNGn3F|y!loI2gWGMh+(dXiMbF;%sH+Fh)O=PS>c$J+;C# zwOi*^3Lur*TkarpS^+W%o6U^!Q#$h-jR!84bdDdJ36r&RF>eU990P1h{OqKsISuG2 zJN;V&Jta>2@9v(wuyTP*i;9a`B486ug166pJf{=77_IiR))z#m2c@{@=8nCRHK|c~ zmGWT|grjfps=o~1^&)$9ErdHxXghyM(8sINik2;J(Cgd*)i z6g55&aXJTw*Sb5*0!&e#KZpQ30Q_RCr=%(N9>VigwM!lHWE(~`ZN|V)5pGBJIj1$Y z%p!EMBQQ=3{%Ctl`^raeM^PFAF=PXi9|e=BcIj4gDmuXhMCELx6l^qBFBR)0K2)i~ z%2D$l;Grm*1NF_-DSFu>rU2=d1uS^fmjMy}kj27)-c{OkOSh-l=1HTebIQW;&zrQT z+X!!>3(xT`(Z=R-b3`}#Ebuzy^+=_dFVZ-+F}F@Vr81ufqsR1_YLB|A07c~LRQ(V5 z_-IcEfF-8BS|NRBwh#s>+(9wb?UU47&#(~nvgn?=>p*u;Rw(;ZSCJA$J4On?wI6i~ z@7?71XJeS135QWk+Y~%whmtAocE$@ev3f5p$p<}SRM3{ZM(E-M?Wpo*vU7vYmxES4 zSM%_1BgVY+T=1^p7-i`x#{|D5?*3?cL9|5j^|=ZXug^T<25pauTc#Od7Urnv#*### z2qDFKA{6r~XbHZ=px!IeJu9+`&~cZ>U{h9k50if;B}B(K`0OSy7XB7NHjQcjYVS7b zrMo;amyJQQ-Rmo>}r-@u3TKzO4)!^pVwhOqcLj4(LlffhuFc%Ppo z0-b@+rjK*(5ygi&7*1@mE{_Ui!^>?jLUf0J>L0+^pBBDxcVTW>-~Wz5obNx_20kT4 zh>~qyqv%BPz)e_1CsE19QbdtduMgcvOiP;LGP^Wkt%?|H+l{<;rsnP$xf%LG%aEf~EV4rxX_ee;@ zh8MHMeTOLaW$N4_4z2a24jI()W`1Aj7;r(~9BcJU(SorffWSocn_Rcey!B{iw1C?^ zU{u#{mQJz;7hPxgJ>lN!16A%7uL@j$9rJicVbE@Er6r6CZ>8ZxNT1|{h_yk|oie8Y zSlifZp1c&%_-iY4oZ_>hX>DRR z=xX8#8&Mr;z{DU!vnsKs^gRX`E7OIE5v+7>Ullw41T4K92#%un1(KKgd^o|4qrH~X zd3VrXyh0&S+m1qe-HI*;1L!y9##BVtNZ;DCo%)+)@Hk14iy0?_e!OGdckh)poci- zh9KwfT%_XA5*d#aoMf0GvBIk{O^aC+3!|a9oXTze(%!K{)YYaAX3f}H48z-gl}kr^ zBE{hf>v=){VJ_au|2q9fC1_uds?Yp>z5J6O_ebq|;QecfYtMdF^mInL%?3a8ZdFjS zIMUajOXqggqw089{r@N#xe!mJ5U$pom5|T>oUonWAFt+QBBLscO|NfhamPMfGpbxZb;|vpd4lI=K}UL<_=`q z5>c0KPD~7!K@NrkD=CVd^C4)0?AJvUuT@zV*Gw^qa0|cvKYhSm{QU+kbPVyjMdkVH zg}49rH{!l4pe_sigS-ZW7iQZsDBs5`7#e0ri29!hYlta9()b$JB;z$04A*mOhJr{t z;!zE{qPzBMc2>VqfJ&X$W|SV;P4(`>4Uzv#lkhL;ss2%_$Zqrp+NtDbV%H=42qAah zUiXn5nl~`_Hq(8qohOiRkMKxO1TvGa#Q%z;!i~;0uDzIBlT{ZJwd~y}ZO|Q$=|9f6oMe?}u`R;rpblYNO{W zTIu85!WLqITsXCY7a{%kuRFvWNZ#HL6-eXvNu|Zg&Rumo8Lr@xxSAJ3F4~ll#aS!1 z{fx|507kqc|7e;zl%z+Yw`|p7{(n_@Sqf(DV~kZA_7JZ0;QNHm`g|vGQ#1Hm=$c8j zHfKDRTU`VbkaMdloHdiWsyS1j{<_oWFRN}e2&}Pd$n_L-`BOfNq0YBcp5Q*lJ*yDLaJYF`sr~frd%A~`3 zyt{o7*U;+?3nc09nju%@;pam&0!bA;(*S4$hsIEL+;6o@21*6s?z^uni}q7(7SqER z+7FsJvC*_Iz-74lV{fQMf8f-y7W5}FUcEGm2QJl)$uU)P>&5cM=hkCwIZFRdJwVuu zNQJb`(_x`V0Pm??{jOb&&+tufIMcvaUs>J6%IlWXRa2TVHQRXNLK}wz_6p=|A=zDt@ zDL5my=hA`j^6?>PEyccOr`-GYwEc}zAtL<~&vaKx)tI#qWF7DSaZsOm{~^e-!m5>K z%K(-BXEvA6G{j1Z^GtuART$kYsH)YbbTv=>sf_jamJ%xf+?Vn6UYBWHK&Khs`Qs)! za$G{F>ghHoy53OUiF>AXv&Okhm-I`K^$bn>9VaId5%Kp3#u&<}@Bsr9k-rW_9Yj?S z8SlvEWQ@n`%t>#UqDklTZaVlpYYWaPt}(Is*04AV&CCNHJBUpXR0~UV44zcLOElKE z4-XX5QHbhjO!;EwPHc@4D1E4RB`-E>_q6;C1IEsPXKQ=nt+u>TAsb;6 z&xYj{@<#fY4mHk^;z_Vs$UP-<*?KrJoxNs9**O0pQ}rco0>k4m_zqU~ErxbX)ndq?ItJ$n z#_x7ioco@~d~5UiQ?ntYcOa4uk9qfV)+lrn769>TDpJ|^;^}w#k}IcV`Y7;rwmr_h z7*-5_)RbI>v?gu7?PEU@!v8=WqDw5=3C2i+wzprrZf@&?%bd5NRPZlrN&8zib__7* zAiA?p6s3%{hGTb`y!z`&6fK|4iaxtd;H1D`^{Vai>TOd+a=Wd&7u5sKx9iZp~Ng8!u>VzCR*cN8pF^Q8Bc#h1mmNk1=j zWA2;VAM-{n?06gbDhG*2wu7je10t$`sc>d+FN4?L`~HUCZ?A#S-q;hh10R4Ma7@6i zI}&iQCib6(COP7sy$7lkD@-u=yf@7j`3_!wqPLU*WUJJm-B7`LY6d+1vY4n~_ftBe zH4Ziq^9}=U{~(c7Kt+dXBNf1wy)x;2bN+J}2w&|kFKBe49Y@4H{%0t!qW9U1cjtNR zoT7V6!XSX)fVuv&D%d=R_Y|Rrzk;tjjVkj@cU{P}gm~&f<7%l#?6`{avx_%8^;#?h zZyG0jlNnUVSb^7I^7Iv`ca5`>pSpll(8xz?yM)7ZoMj8&936}>Sd2zXEWTWBYZ~$C zlt9z->0?$1KIgmGfS@1{CTakt9G-LA`?6!TZ-I1VougZf+FK0j{TVSEjg|D5CFLf* zyab+Kb|m=rPu0mq&{R&Y7MqDoBuF=p;}JJPW7D+vibNt^rS$<${tm0c6dT2>)wy;) zMPNyXz)d{21LsHIH7rdtyE8%IAU2Nl59km2k*>-z1}^n;iS|KQxSJkHr7?Z=J%4!F1(j;AjKw4<6iDw5AXcfyS12K(|m+^Cu9dc4{m+D`(4*1d=w0- zb|t8&-#4L~H{pGi;bV>cKFnc&CU~212Nev0A-sg1;Irl@bd2YG0&Cs{_@|@xHshrU zjz7^NA~2?1wneB-9EI6n06b4snbU5j4n8-wIf%k^_35uQ#b&o3SwUbJ1}8pu)OCEt ze7wt*ApPNp@W9h2qG{+QYCn?qo4K-zlGI8@ff5?}Ide&~In&oTwC8|$*ps8gmeh^f zhCwbLM%rT4mIG%q4FNyz;0`7SvfGD>$eZ3)Pl zzG(07c<&MCuMX(N$OZYL7cVUTquQ~cWoXRiDNH3=$IuTbMb~^csQiq$Img$T7P(7O zjh`+__~E^$>Q&F>-G2z>FY`1j} zr>M5uB?EO7Xx=AldZ+dd*MF0@@8n^_@;$k`u;yYJ_~PQN ztzY`s#8KEXNdq^AaBgJEqQD%rp^_8Q0t8`=5In;O!-S$ygQT`euUt0q=VV6JCB}nB z^X}XH%EQlp_!De9@5b~E9#HE()mzvz5qo3|p@4fDChx#3fwDrU`p91x2^j*+eeyRF z)F5)>vsMGlDH-i8q$|;=)x9Y$>&i#BpT%5!?OvDU6(_dGF%6eSCi8(&l%t?pwkMaP*nMw?LqZ zd|qs3ebHwymft$q`bFLgaUpI<_aJq`7JS+W3;n{5S-{Hqkk;I%RG zaU8T*h|O@HILRT}KWq6EghcHcrMg|K5}pMambgGZfxX&Ev(jeA7x)mAEGvOI=tZX~ z4!s+6hUeB-kHSVYt4b#Fx+t>(gA~W7J_5v%xmTq9Q4#X(DFrlXHux+ZHT%9#iiRdn zw}?Np8XVgCQ+Z^LF~tcTNV9|_QisxqtuqV9dYH%vfzklAz29mVAAWrCi?jNr^`C`q@gxynx=b6MHK^L@@MK{7qKYV+mkAw*S$QV)8dpJCBa| zXRjD_byY=x!wc-@gomp?ee~hCe+IRhw|OZk-`*S?9=^jSltkZs$1O<&8!ofYG#CbP zUyEHrVI$AypooKGY6{BAE9yN1bL%}TRt1-txDhFfSv-J}HqniOjwcSUMb6c)RECG+ z%cf6?LJNcAtiSN6j4CXEil$HEYQm)@?!L3@ki(~a3vO7^#ypP2V>RTDqSmx|{F5FD zpd!GQnJ&-&Xvcgye;q2j;ha0UH>wXUB1+?0mdaP-$w`dJMDkd!!JrohKjjO%ASfSm zqG=r0B{<+Wh~@c&YD7U&UifJ0h&a2AZbul*GLNwAE?>Vca^LXmwZWc!H^4M}7F*Y# z;|UX}EjihrEYy-ktu1tWZLQIyt>q{FbV|#`B5e{usO9A@j{Ye>0FU1Y$@2*|ES)oT zVG8_4IaGjGBQX@~M>=Sj2`mHyh)A7}=9`xHK)EPtcY&zOO?#UV!=$HJG*?7>&8>n$>%c9@pSD`vPuPMC*F`l4Mw35{Liqt_4<5B6)B=%`#=fa7z_j-!McW!;@8AKZz`-=4fn zWhZ|y)r$&D&eM^jX;>6IgBOihW#Si0k}V5%>O*bxINcI9DP?YDzfW~W71_X7pF%4U zR)|tb%E3IYR-K8AY;@pffp&6oprTUyGX8MG@~n(S6QJZ+@NdiHtP;5?%E7W;J)&BS zk9D-+6T?D5mYr1MG`=gz)+2fkI(=9RMJdrK^q+(gL>@xG=Q@#Ngu7ECe#bvug#;5s zbDYQp{EwNlc%sGpTqGJsbx00XV{wu=-9NZ#X3L|gCsOi*`?XF`zWZJR+Q^YHNUq|y$BQZK&FqxBi~qQX*#9g( z@izN%5##-I(W_NR2w|8ujc~!v+6XbC0l=pLL51IR-HpW$?(eW^6zfCdEyU*N!b60@ zUBkm5Y~TTOkG*{yCbM{M7|Ubb9B;*B0nc}#2lyAn1DtfQ03D0Z=Xjdue9Fx4aI49r zVV@6}(@SaF-1H4~lKN0nb1r3PCE9L7J{BE)oaFp$NgQ*O$T0E?$$1Wc=Dgk^F&F<_ z#C!&uS6y>2(ZdZ3gTH@ktW%P2-wTdNVF>!f%ibAZEI7N+AnD4}n0jr}U3iHyD3z8# znmh&O6xF)-Zu_Q#+N+E$-Rp0qrz_Oz?OLtH_6})H&F9!k8A*O+UsFE?>`5}J&S~`U zc-LJ0u1E4{fMPrPAg+Pqbg0PSxCZPXwu`LQg?{_?8$mt)x)}X_fbY}~J;&29F>?_* zd9d3bi%M5+7z<9SHBeed8@Eve`R>Qrip;h&^V3#`AsYX}3 zxjA`N+S)$64h1JHXuI?Pq;Tp~j`s(bJs}OkcIsxJw}*Rc##2y54Spc6NDJg9PHIr~ z9jWw#_$gKQQdeS%>jsHo(U>a(Mprsrz0sU`m)m1%)y`@~=9MM~QdDY&=)?%{Ft zq0C8m7yK7%=J%UbtFgIhr;xE*K{Acv)~jev+OLHGrsYV2# zu^fE5Z5o=Am-o5_1mM(PdWTT!g!X{Pk4@-^KG6hR>NZ+0r5iB`JvdO$3lXqE;Ua zu+QN^U=jb+%JGiZecate8D`^-Il*|sX7O5$A`GTDEt*dwX>{F-Z1)Sn3@wai48jT_ z`Pf&7P$W=~s{N52Ri@gBk4bqo-eAsw=^JTjrUmccUyesBFq30f1T#y`XKd;8HH=A~(w6FP(6c9{sG{8*6k`Kp7hGC7YD_Mr?w z(;;L+(oP)(*@Qxa`~Zmn3XPLdgx3_30|_5{W!{5cN+*|?IM2NE*mE^Aklh^8eTr^A z;^SwJN-X9HRm_z(I^q!m3`YpY2y_!7=2qW+iQ`-mm`kEyi-2_k(qSZu7J;VNv|3a7 z#XTpkB>DZeFU_8?VS@!R5#&E7ufNNX=u_9{zR-8jzsy~qqD$;nnr>^f=KDe&S`7Zl zssp1vKP5*$TT~2+YGLQ4Yt3>n8fHWt0v~D&d_G2i2^$IdDTvu+-GWQwxgkL(TF($v z4Wb}>CxcKn!HD}9vWs-g4xxR%*02R@ENJ7xD7omu==@phuV^uKcYd#lZgcWxBrWX= zKFgMcAm<^3kvO1+d2bz|Qfi_{A#vOm@ zlw+TbI0Xt{4@^5JQ*XrGDqHmZ!vfFOE|!=T6{_NTQ5M0KPVohr_-HrEZkR(PXJ}=+6n+cLqtFg+*+2Jo?dQmleJF;;D>!SEDPR9 zbGZ)|W`^}4plmAJe(Sk+iG<~J9z+0bjjF<4kPV!1<42lbuB$iFU5JR;O;S#h z@##Gm)ao45PvPu`Z}L5_AJ>}|6{+I3p)4Mk%EagG=cA=y$%cld?D`G#`qNnIQh|v| zHM}{OKGR>gD-=8MYV_^Gz^>73_nt93V=EAO@_;mC4WI!KARqvaJ2VSYlHHbtRRjeL z&5C9lY;0?FV~hl;fk+U$ATmkPP7od#i4fbdJTygc_ckOmf59$_DakhYpFk@hoo7fJ zj7U!YzjRDWvy)IxOM%(Vie5o#Y-me-1|X6bm~M1TB}gN|2nZZu#_S-dNN_jwCNqEe zJ3c16#NdAdS7Eeohv7O0do}iUVPGfqkIW53o$w*j8u()m$L)JYy_7G8H88NSwcH;5=edqH!^ESENW!C@WNr)wsj-Y2Bt$i=5&Ot>4!?PjK{km<8@NU zaF=mk^MsE}ZD6upi>>Bk5wl*l8;_xTomcx1+}UYf+W0xrZ};siC5xoGK9>glb66Z(!QdX0%Rk@N;A;g?LbB zY;IQ>m%2E`Z_F%wap_V`#vcWibWMhGI0hw!DV?WEnUTw`bxBE0TCSJ8uFKeNicy$7 z(p*8gSk|xCi;bb49l!MYGhvvnIY!$R8C<_NXY-^Hw2G+&X?-{PxIm&H*bxcdq&FOl1sT6z_lcUuV$?LmM?Re+^<-@ zx}698IWn5r+~4`rZ@*ewx9@F|K_g(Ia&Eip+fOTdtXz?<;N2$q!Flew=;%6S&Rz0#M0*I*%a?2*R2x$85%~9&FE9e)YUee(rA0^0A>4Cv0a=oICkm zJ1>`==Y1!`)xEwsRgSv#Bgt{u&@}56D7K|0DV|@YZEcHpsMHK4#Kb_e1U1lHUXu{V zuhO)xcfGzW{<3T0cca@s{*@Je^Q`7Cyt=^PbL9?p{@uk7?w#xpYM_!`BMzjHb)gcM z`BiY%cM@uB4YJRjX@Wr&0}fN1pJD4zw2fu_9A$yYgII-|OJ#!{TW}plLm5Zw5NR{h zH~EZbb$2f=XDb&r!7J4McXT!;9qsU-dEnsLsE5@%*zq2&1WvT)ni?7?uy040UoqWc zH15Cwh}v&}*P~z`0TFG4ATS}&f9@XPJ-Q?1UGuOh4~OL{31PSe7$${q6AYuq!Vy3i z>qwtQkaEY+B+({OvNC3s3z<$&zfFifdGtGKubv`1dGC+(#!^br!nBzQ+8BJYzyAPr zDFjL?(hAW5p|FwLB*A(S)6H>>27L!wgboTAmviUJQizXWRj`@EaShL7wWB?A+NGk? z$hm36i?-uJ#Aq$HY~^Y+s*!qlmN*Q|t*40u{P8)o1T7ME2o3aXcvia^RO&5TVg5LxVyd z8a^CFA0p(lvSN&fW$YZOpnk9PTi6K{!R&gVP@OQ z{1v*$n44&YC`2CH(Ra+%Rh^l6y|*vt>}y-b%aRVb`SNP|?$Htoh&s1t52_lF0bNM| zGeFG0vR)>ax{%Y?d#^I3PDnV@cW<_%JHR9<{ian|LH^5ay`^W))Q;Y0wF@9fJ2Rcr z9Yq3K^8hx$vQDb`a^ON;z4y>-2JK)u3(N|0n0$1wWYWb&86NXFMm69t%hsdlcJ}R@ zZiEz6wev(Vd}|%_uCH6j)m~>fSvG(I zD8<|H_y~mL6Ob{2vj#J8?$pjnX8{hI4l>SGpr^4W0FOX?`T&v+hVdeqGc;)U^Rev6^tUSl|CoEIRu=?-Hl*+4G*u5e zWNPL}6Q|EfRqbGiC}3a!l;UQg*eLQi992!YhNE=GM)1ckX^?5WhSb|Kd@xR zJm;=%Y@sX0JVzhvSeqQoETSJ~DPo?E&oUDL7EuRap~iq!`SzzH;cX6R7lM*871oXH zvs&3WY-cA}o@#vxt$^oL-7bE+PV8H_ms~!A&hr zN$R5P@9m$6xj6?|0jERDaYyb5v_N+y#-` zo)(dsrx!oCmA@?lD`s3yJTT`LHaioRc=p=-xo?79?=HF(9-jkC&fT+SsPmIvno9t4 z`-0v6TXZcr;f1<*p7S$?Is|PqFn;c-3Az|Yh1msDE_rM06hp)kUIb|%7(4m&1d>b? zEq4O&`I<3OWxu|Tbdo$$s*fX`Am{cq7LiYqdN!5@l8^ZB9Nx)#)}rfCe7F)MPe#gy z@pt)3Yv-@m%Y(yOW%)nT4aISC_ozNn`~>c4nY0Ijsx-=QoFwB&;D+k-;gQ3+nX-qU zBK2%ckt+N2b)F+GLZPWLnRZL16F9;T>HcfkS7M~~dir_}5P|tCU;lhRIiktOkJ<*hP%s()HA^qkLUsufE98oQgnq)QjbPb+b91Gb@C zXamFgsLASv@li38=Z0}uw~ zHMoZ-iUk?4=A8q;;$AEI|4tbvf==ffg8gE20`O0ZDVlrW$c5XEsfE#d8iri!dqb^L zEw@4P&*pEhOuSxK7pC-TouMAg%zPCWx9ZyYdGqL%P0_u-gVN`h2`knFTD&`X^Bqj( zgZVKdUH|adUw3!M7%DN2cTP<{&Exb19PJ0abO*N5DklciX=>F*hZz`@2fX!BDP#MY zl@D%#ipPLQuni(w9Awe*7O7E6+LH0EC10z=*>BnKxSi7-b4IE~dg!yUdoKs~?>A4b zO)|kY*;Zd8g6E^a$-M)-6^{zY8j74cmNCE3$smFWW5j6h*5xP0$bN ztO+CFGlD6(2?R&`|LqDa>A_&A&0M>B;y+I`ouW!vf)!AtfjE*jRP|40<6Hng9}Y!r zpca4A#}!jlD4K;xG&Jl>pJ~kUt?3nouU^?+=hmT29s6ZQ3`V-Ds-pTe?Pb5XI&J889_vn#4OxH^+*94`n%Qn*KiQOUM4HRq+G;bO<_ zLxRp-LW5M$%=d+cs5{Sl2O1^O4qt5g0ZkS!4dpo6MK=q=F*yBi-tUn+e{G4aa<)Kc z8H__%=t#l^K_dsJA@=UvxG&w$N7(J{qH1Zo6fU-}r{hpjYivI6EXC(<8E~>@0F3P)m@2Lg5U#q{u)o)6gLXlfLo6m3n1V#dm-!JRGSR3* zCkiFSp=9pY(0B+6AV+#{ z&$xvtYy#Z}?qE`xBW7K@ZGt`ugw)bqT7*tP$P`0_Xn>Y8OcT@vQI-F6)xe#FG?X+w zNYV&kZ*<38(WRrsuxBmtT1T#WMk1~aMbcR+fD8yy8NH8#1<69QrqAx?cr`V3AuY`g zw-^&86%J9kiO-%Zicu4_$~GrLSHLXsJ;TH>A|7d<+MeEvNalxzA|aCSBA}PbXN)9d z!0KF!<+v(XeV)*8CDN{r(L1Q`7#MRZ`@4!mY%K zk1v1cQ@g-3Ipx;a?5P*`kG;NM)7sWX^E5&Jf7@@BC>py@Ir)$()q^pbk5vYGweE*l zrPN4%u(TnEJoOhf+xeJDUA`3+htkotvqCJ@1SkuvG>l`hy`mKtdB!GMp+aRQ&o>!n z?Df_3bu8nfSWEciM5cx#{md>Y%?K5sNgpTlfwMM=P&OejNlBMp^KXBpk^~463V12J z5EOJ>Q3`C}XDiUeO0xYFj}MbV!VMc{vh-2};aQ|~Iwryh`b5d%`3MSe<3u4U5oU!Ez%z0Xd|_!PXgjA^m39JxcCz|1s$sokZ)&88 z@8f8@Iy8A1C%1lQpP-;snAq*Llsd6-pOY4FA2yUQ%<$UyzJ(~$vjqI9P>vmpgn?J^ za~#NS0zNF37vR9_K|hC%j$Stp8A&Cak<%?Odh%TzvS&?cAStM(uk5N1Q~cTEKV2H9 z7(3bDj7GH7sXmtKQ+GyA26C9xtgDkO)Uf+;;DbzY{Zs7+_;NXmxiK%suPq*6YG7Y!rO zd=Xycv&)gigU8{|9;BA{hdTZ9dNaukF#- zkb=7V0q{lW>A zYB*7eZ}#_9+&MRS?4oDw#*GyE%KRf60(|QceEIYqc5tCV`{S~**O{G3>~pxog4A+UpCyc1u~KXzYFOu8}ua`a;s(^Nn#39*J3!gL~Rgr-3u zL?WA^`d#L&oRkNZ%y_F`dvwE(1|iz~D&7J^9^rYR}v!wV_AUJCFd5LAU?2?|Z0 zZDRWV`_$OCo;ft7(z7(-m{@FbjN^-f@ucV!eD08v;{Ruvi!Yl}?uQUOVA+`na5F6v zIaz^-6q*y$2N>!_GsJ-}a@|@AMtk)HCQ0?^=lILqD%nY0LVe0gZ zpQS7mtO!=mX5&_7B?f2;*rHPb(^nT(?bXzy-@bp|o4XRVZjR`j~N7bCz?^ zAxxc~^>L&HHgOgTiU~NNuY8xqc>pD;Up8|MfqPUSVd#&|{aiC_D1QV}wGg6=7sWtd zpK{wlk8WXA7RC@1ha|(VrC{$^!otNTC$ z`-U(AA&AvC;WV8ge4i05iim~s*8pd-{wbw5PKfIT-jR?p!psXH^zZ$M6i$@Cd`F64 zzYmqPG5Q(3$RsK871O(Hm}jcAVkdrM;Rshd)!}B-$uxb`cFvBnwXr*kxblRCCwcSK z8p3tXrQd}*em4Y#_dcVHdRfi8XH?7QW@V~_o08a%Y|sAn1BSlzeiM?3CHQdKsXQO8qj3^_~uQ%qGkStmf9OL&?TTw+p<% zyD>L2U8;Xe(5!mwpWyjHCc&HHKRNdEgp$yqvwn-XPvY}77tq1h?QkA2yR@$7Vp2_& z&5Rt?y;pENrLu~Te5$&u-xRxksHUBKV~u0WZ*eiTlV(XZ>r99>Yt}2TIQvwGhty1m zcw*<)E+#lT5Geu5#a-QZB3D=2fSRhjxn|-c7bjlr?%HmKyQrpSk^*2(8NUp2jWFl) zPbPT0pB@I1@wJ2b7{PN|2DRbX6my(IU^rODtA}C#B5bGenIj;8$n>uwKMT?$6eQpr z;^2<*cZ@DQ*2x*t5aH!FqX>>91Q>RHGwUv8u)WR2y?`d(mb$F0FX-4Yq*#68h-jm& z3KbtdK*N=NWSp)hn7SZMWgL?QAX<20+>=M(=IF6w3>dh_qJbw7$@gr?(PLaW!+4(D zMlgPoU1tA50;P?egh%=OpfCV<6ooG0&HYM(MT_6$=ewiLFeNf1zx?6X*i!_&|1g53 z%ie>Ylc2fVaa^Zf??z44^T(x9U>_6U`DjjVyNyWjzz?(B*gBj@0UvPQxCrKzrB&X0 z;t7iTdjqyF#K)yS6cI7yt#p5-q6?DKlHGvZ97aV z6z6+<^UWa95Dwcf5>+S=C%Y<)FR#22CeOuTQ!HTS=)0mvv9Sf_Dq1aWtH`AtohqgT zAD%AyEr2|zDM?&i4j95rnqDcmqTjp&T^S?S}ourl)yXy))84kaoDBc5w-b<#~JXy`MsYnGJ%V z-Dq$lh|O%jBr+6}?E_&9oAFvcLy54|;arxnAppwuwtLU(GMSRCu#JRFcscR*+<{+7 z0_h^T7&Ka0&izPgh+A{BH;D+c0kbvl1NslM_F0;sh2%k4h{2tZybsG0CN&LFvZ^!N%oOTZQkr;(lje(h+*aPT(W zWMBueW71~$1{D(0e!;;o*+(Juy2_1AnDV%jU>&aN9giqdL4a9E;=f_o*jWIu+CD7-d5l9#KD3HM&3sSk9z%#Rrv{K_Eyu4E@|syll6> zWV4gGuSoWj+Zbfr({W+VWc)+@z|K z3Cj`GZbAWqG!SqQ(e%U?rECa}s1u7$=XEo|ycJYM?*NN~%39toKstIof zGO(Yx3>}StCuL!_W0p1xY`fryZm6DDgB93?)o~{GA{2z;35p$kY?Jx5fPMCcqtrnf z9+oyJOpq=tg7Z)nY)Y704QmBilPO!4HhC`rU#cDYmS8~u;GM8;{0Jn^!_9ZSwX51y z;wfggC`IK6Eq2jOdO>2C2vaQ(8eUcZT_rG=-%&~9T($js!n zw9vWC9S1O0hC(zr8`4BGG(rwJ=g~9?{5qkR(})@t#?EE}Nz8c#V_~@f3g*>fJ2@^D zWiQ41_2(gUkU)YY!2z97s3zScoD!id7+_*a&k3)IeR#WAe0J6$d8h-E9=fc7wKq~q z@!1;Ld|BFDI$(qt=c1jwF>s}fro5VE9qcAvp$j?~#?i~Yb5`lq)fsJi(aUG$*6sqy zJFI5acOuYKEH4cGqa51F(Y~bK1;J=NK^UNWXPHAiHk%I5hkjWtkffBSDo0hWN()#Y zNogpDa;XT}TkVki(yYYc)b=Fj@Nhk#qX0qQp-R)cI3ojIr+1Eaq`XF(KoS%hc~JSd z*i1#4(a?zjC}{d(nR5Am??1SIG7ed-Pi2*m#=GRk@zTmQPU>wLKcCes-Pk%X=5%We z#lmP2kPZRS02&7LK^j6}!S^44L_$P@Ejt^#RZMC8g=qgZYB`mlK%JHang!=BU;beO z$g^OJ1o(75k`F+*@%=;lu@YGxq5&{If$CN0SVdYqd`a&8V7)>p6PJvtTE4&sMl+@MU@ zOz)dvI$<)CQZ(B{+i{jfSiz}U#mn;}vKGIT!P)foW@%%3pq%XJoA zu!YoS;+vCF*H|7oL4;luQ25a(IA6Rk^mv{ZbPzn%WkNa8teNw%?cP_Xt)ZYLkY4q`hwlMXonx?ppFjW~&(|-sPSym8#SSPTzS$Cq&o5qOPusG6;X`%Ih7Fr-b4&sH+RJb;q?t?RWzhkXi4J7VGnWuh zz4`0AX-t#h!z*grMU{fl>XHn{x$#8`i+P1Z&MHYrP?R{3Qi-`p5g(P-*PHR{ulr9O zCKuLbTSh$&CZJA324GNp|I$ErQgJDEGPgX8LcT_g|E_@6$A8GdvqmvFOWjA1!lf%ix2{?=FsvkPlRMHn0V636y)Qs)xux z`@>(-R;K-6yj6m?Hbod^zXh_!x4yr3OC^QNzl?>4A9(T_hQ$v*Ek2dQ(v~lojZbHo z*zmuLi%a<)U^j+zZ#5^w!{@afzAmUeRde_K`^73#BcrSHQ*@K&=KcLFp~WPO3?m>C zbxqD{NqUMVlQ%9P*Y%g}7OB9CN-SrW=yH~+V~}?5pCaI{$wK}&4;N=;$h97qqzl-TSBt0zTaZK+dos#;vZd5sy#Z!nkri zy&<<%2RlAt;K9H@wkK_ubI#iO2lc*Bo!Ixs{sGbZ_`mu?;xTk1SzQ&y1>+Sk0|c2A z2oNJTvu329@&Ex4D-M~Col0P}I@~~s*hJ5`*7kx7f5mcJa7O3keQMRhSa}U$rO(A{&Q$OH}r6bYp?8b?17CUB& zqOR)%IV_uP?iP7$r90~%WA%&G0jr6!$~?3kY5bL?AfrBG*Fn?>IFtWm{JI(-nv0`DgGc3!R$jFXa&|Mgon6vJ8Pqu<$FS>dd#GOQ8~kktEV~S z-9qVO8l}ON7bEsVz_f=cpOCZO6n9<2nQg5BksL<@R(=IXi=>Oaz_AMYME4i{@WXPE z{?1_sW5xT@(e(~n1B14R`TI1P4-1ALXiOQHNGF+24+<5w7^)~wA)?7DRj_n4S0$w2 z)Ur2VNfx=juFhl>O~C$Ua&ONbs;nX>w%3pU!WpSt(x|wILqx30-Eqjq%b)NMIo2=N za@+xX3D|xa1H^ii!kmpk0XqVeYm_c}`KA}v2KNSCGE~w-PHOcRRpfcsb>mSRZa35R zNZcuJ9x(ymc`>3m=<%DhZ1LX4M`n0eYU(*SVpn=ZkK;B=t0twPC^9>{GMtps?$_4C~6*mcWR<{&Oij9p9CF2tk zo-ZpCe)Ec@5PjM~)LiQ3l@QH#Ldb+^jW4^Alh}}fWpqF_fn)5-Hd8U%FBR!xTyt=0Qn)vCJ;2t zfi#_&Fe91({G{pOHfdOMbAhq>3(+D-4{Iz02BM&;BM6jx70nv#BY~fgXdR|fC~}N^ zw@tVmXOORhVM2p|EP_DjsREOE+^wOQgjp2FuW_=Y4%R~fje^IT(#f0EXHcj{Fw)&c z+R>~vlfotA?2SR;2MTq8Vw3siJ2_+;cXX2Wk599$>?Ted3BF(0J3Y-#s&P$9db7X3 z?e^7)i?2qOmv1zoug>3arvVJDqih&G&dZ&jLrIlyn4imgZnT~vKeT6hV2ZJ}nt{q# zy_WIp^uY0MpTQ_@mq^&G;8j>27N?<`ZM;Do)@giUK&YiPDhvdJ$KGSqzhayx^56X8 z1K|I0ks&fTvHxtj4S&+>@Ib}e0_6e0V>{#F{(T78WsJ<#t%oFf2~V7`i2$Wz`_^xe@519ZK;k?cD>Ms2#*mBq5HtMo=;szJ7!JsDtJFGi+}p-MUwx$I|VND8E_BcoQBU z{CZtuFZsk!hz?&o`T5-3XS#9;M<>1g=q0m_{;GLtvP6+wq_5gaXz%T=h*}a%-M&6| zi;QPop|&jf$`WcT5`MYS@#3vz(XWkSn&&WN^4Vei3dxs`)&^p{P+4Wds;xomI6ppH z;P>FRJY8BMue6Esy)VNr5nad-J$fKFjfpj&ks|R*{v!OBv>0T9&Ts_t`u`wFRtu2_ z+1P6LZy^Gnb*(P%6c_WQj{x@b-t*M;b-@B-*RdF zp-h8NVqMwce7dfP$*Jv~cAi5EAoDDs)$C~_9GtBv7nge24k&oZk)Q}(OlUE%La#k7 z+|zy9CkDcr+VAPY%AweI$!|*Q8JS;#F}DVboY>7!8 zRehbHK3)CwGo!ST31~W3Q=dQ ztOqdeB018n2TLA>v0lKme;30`M}>n+j}eN~Na(|tR#9Og4@^(RQuY z&M#UJd*$Kxv}-^&m*Tc0)Y|n=m`v`SW+knnDO!xi!?>`9Sx1IMJcE@V@K_NRk+41| z+w*8o5&3zH=;;=Q3!y{;mV}f|3jm1FD<{v~r+mJc_)IIcFxn_rxlV0+yJ*Z3DriuPpSBRF(qyCwndrG~| z`26}wG6lTJSf-I1#uZ{U52R=13&Wwbc0n@-o&jdx$FA?~>KZfS9nd%Y!gGJ=`;PF8 zjIjUte_}>S{A!`U^nZKiuhM=CfeSm)xqBfY`Q?gMu6*3gL{*l#B$F!D9l}>d(*{!$ z#ZzlBPpZ?;5gsO7Y*OtLD{4&f7*EH|72oP_^Lr&o4L&r))6F^7wBASoOXI@7p;IYp zc|k$u^`^$KW5;SK^0SW}3u|n;P_QCTrjDk_Uo60yxv0~Jk6a4HnrIq&`4t*xu(a1z zeEMZMJ@%3!Q^LU)SBWzy@*57Pu-HCh34Pseo|eFdK2VGBlS0K8ZhN^e&&o{3?;?uu zoaoy9G-1%No@z_`WO1 z>o;IS-hSpm)j&)+uiGXV6T}sQfL5i=cN+1zZ%gp02A9qB1K;OyXXsmgk*VQVGzj$%>I_+9$oztTUEyjvXq{i4^^;PRqxOMlO zvr0T=w(UX1M#T?>I*i0-bas9pt!eXiX2kYjnr^?QG``_pMrK&hC*C!oVB`AOH~B&q zs#7;**N7!ivKF%=ZR$+ATHy~gBm&d{BHDJSvV|Kw zpbx`SQ%f2Zdt)>=$*CrGbF*_&vWbqZffB){@s2{wzG!8mA2g~@n|wj-O-b_LWR&e{P# z9TBWiTp~hE%{~(c#AmCApR}Z~`L@n%%B<@OC|nC9)k{1E4fEo- zVl8*A`y#hl>7$Y&S7SydIYRLB2}z>!@!KRDi?9$EL5C~|c>=;T;esIr+63PP5fcso z;H|(|NuOm?^&l%#(Wja$`x=d$_ztuYhTB||(I_Z5_WRfMs)@*mdOlr=kYq}wpi1Uw z%1oK#N*ywi*_*lTEk4^JK7t3zuo%k6gXh(nR!PHK`i`Bq!3n{2jk`zUd7sU{P2YEM zZN#Z0Wnx$_svi_`o9Kc1d`GA>!b)`wzM-veduys=rC4B(l!Z_46=yK># zpV8i?q5e~+UNo`OTPqwYHN%P7Sur;qrx_-*kp9Tv!zYjQTQpUEYFK{e%|_`jdl}Z<)C6M?Tx>zTXqw6^_WAh%;{paW8jTmv zQwO4g%3ku?2SkIAWlk`o=bjG!ldj zMx@3VNRo$i^kBQKfYFMbp#n zo=;CtL!%n-X{AYoieinr57JOt{Ysu+lag@GXX!KuA7eoCfs(2-^6v*Ki&{Apkpx`HpF2Am2Me~YVw49GRFO_V=BHEH0JWcC z%A)>ag9d|gB@l&=kwg84Vhx6bA(RSyflv(n8QBoEpxfmHtxFxtbao9<1Ti)R4>|bN4!r0aSjd8JFaNb%wWp z*6=G@mzftt?fcbYE}8S#UU$4RQj76wy_){jJg*AYraJg4yAlOoHW@RW^FOw zYEH2>$G5IfI{f)|E9hj?!6VMGuU+!1PS|5Nd8IblyYi2ZuwdZ^595`|UfYED?2PPO zUa=GuH$mWy`tu6N1!3`tq@*k5x5C3dgSykXLXhX6x{#Mut8amup~*N=iP0lnA}U;( zer4EnXti<4LRRr(dvjOjuQbg)U@Xa8wq{caZ1l-{SzfJeUO80p`=FuIq!&g35v5Zg zAW34D9CENL2!JbB9N7k-a4hS}{Pnz5LWmAJ%YrRGqgFJyIN}{d3H!evS38{;b1&#g zM$On1C5L=H#@K5ly0~s_J_bsDaLZuh+aG_nmixSeUmK3hT6j35BfgKk3#-*|x_7xE zPxIP`cRntbO7q+J15;i~4_(}&DV}2sJgvLU+pB$c61~@ zsAJfdvS@xr_zywMebR*O%%4#;G^1X!I9mAJ9yTCC#wqOsfBy&XTg!6*ZhA&P#e-|A z=CqP`Nn4iXp;j$+UOf%>$p%nHs_9*V?W+y;ZYpi7w{lHD+^(Qq(G#5T{x#8Z(BQa{ z{_xc0`@5W@?qp|sFF=2F3NBrmw5lll677%EE=YAE(^KZ|UTIxACu>gXnpBW?Mrt$! zn6}1NsOhBHK~J4@p>!(ju~I3i{uv#{sv@?nI0+>`66aphqV7#Ggzq;rA+of1x`4PX<2isZ)@Gs@K=O&y62P@ho9u-mn}CEUBM#S*{F^J6y80k!}y8LcR+Y zN~?9Mt}b9`NZvh}Nb?{_gn2r#hG`5%#|<+6YL`;0x^kt=3d+x-?zwm0`E|W}nmpLr zs?OA9Zsyw)U)UFJmy@%&Sfk^XtY~&qTA8IG|HbDjq6>{TH)|!M4U)_k_8NCIMg_)F zXO~$S#SbpmG%ep63O)_2*J$|?hqt_GtvC)-YE7nbJa$0j?*M2htQ`P^8%$ve?EfD# z7l#HMYr*sKP8@!~9Z$8nj`sZ^M`OSwi)k-hz zTD#k-&3574y7!CTxy5#qeQrwYQuf|b;NqxgSC$TM-KS|I{vxO*4{oHm%h_J8+(12(sd`z+}&cJQ%zI$OA|eE7MNgAyj=X*v>D5 zP@PfeKXmrM;*h%=C9e#ow&&E)SUG>}>+0Wu0m8lm#*Nsos?XXgr*EO(UI<UnS!J|)m<@@LuB8eP;bdCX#WJcy@1Wk@uj=uGKYlK&E?BZ|;4i-tsG#ikw&KF>ZcChRTLlJKO7@ArlOCE1r6(9`Pf4U(=CXDgk+O_lEWSTh z%!r_aX&21L+KVm%0&UAK$pCl%DP=O&S(1bjq} z)>?Zn=Uq`YC-~ahNCzr`x1Oq6(jW-AEvi4q@(Gfh*z_m0 z(Qj+2nMNT7GBe7%dwCR=dV@D!p!KdAW4&?fyK_u7LJ-_DFiQBzviEILY<_YZBq+5d z6c-3BOw!x)FBv4h%zU$Dko}Ya3dn(!rw>a^LdA9BtLoNGHij}yaMcy{xY}K7wa9XF zb09jj_2oiz!Vl^*@)vUVnCI_DB}bQ>N2ul!^F>ux25ADhoV9(T4-ZOAsdPoih}vD7pGzLk&&fSFO7c!K*|9WI?yi0GsFi^8-&0Ur#i`dRebq!C)a`0NCRC}CpZqQ22p}tc=p1=A1t&%_wmJ7IBTTm?@{OcM7_Nk zf-}Qts$PKgK<(o?y$zpX#fko!bjYBfnL|>Lg zdsXJf4)7MDqhLoFqALguAGn-waoN`@Wn$aD5X`-dUPd1iGxFDO=^21NG=NRL9axgBNeUs(o1HCt8y$%l20;i9~7K&-KY}7)}W1O z6DDy^$n3J&AW8Prq}$vD3cFqkqlG+rasu@m>fZHisD!S{Y|l|hoPirymWkVmE5~;7 zpg7FvRd?9Yh%iYpPTAEqEY`%2!!Z#x_E0RgO=<>39Py}jnoX~OhEPKXX>yp>1h&pH~Q&s3E4lI^Kk|(xQ>4W?s}{hweZbf`Iams8U;tqg@br(J5#L5My$>9J7oSU_Ef3^DWYoDYc4w1J&ztV(nE#rY z?4-ky$iUEB0wcQi!})|!LR~_>E%;zU0MK_ZHJd|eV?03+B4aR37tJU!jNvv~m>d|2Yf4oB9eF>)|_ zn$uaX6JIA97dbUxltC-p>`TIJ4Ob0~FUX-4<-Cwl`KI#h;xca~;JXz$Bv+5275b-4 zPF8AHIMtU63xz?^P2-W<o zUp%Fn=>~H7K86qww2@I#RRm{^_E9`T-jU%Bc>8v%9j)A|>P}nR9o1Fb)*V*C z_a(SvbX7xZ@xupiXvFsq9u~JYtQuvS_AgAy$)PS2afrTh{gL%_eX*kGv;%jvx$DTvrCOL4`v>9hAxdRI@T-Qs!sb<8 z>wXcuyJel%8<%IF;h)_)Gc8%S_Ou^e^+M`CR_BrejBM~F6emz z25?#Q&FO1nqN;aP8OvI?4mQA&{CvsOO;oHY_oe(doA@^Mh5|!D{Tk7(%dT7kDnD@w zqjP_rtUc9}l+(V>;+8|-f#2e1&8H)`v7D@mS7^6wrS(Od+vDAQ2G8=mh_{fC5f143 zyOF2IT-zRhz}5RI;P_7}vwi+;fM-sdOX$Rx zJw0jGZnwnJ!$)e@o-h5T5r&63PsaB3{dEI9D{7=ujfsg3Q`3n%{e#$&X^k5mHl7KP zl5OUdUv3j{eWCB)7wGNPQJFFN-Ov97>j`*XVu|)Qeo3MdPD!1TPQj_fZ`827R2~k2 zjFg3PPQFj#N#R!Axt;Aib6p4A0F&@iie4|h4nf-)dmo#`*^mcE;GvVmwb%=t+uond z`Q0;hfVU4&^F&qJ(iTB%dg=AsVPPuP`>%zDZ|`sLkZ<1$3t~yPfHWet#Wb7{L|!1B z*~KX-uf-Hb<%+?Q#+CgU^-m6^lGHMt8?jm}{q1L5}^jLf^ zpsl(0qu^q)TjKlYF1fL#T-kMMAEm1zdn7k#@1E|B#S_UT6S>`c_6Fsy9V)Fe8Kb_s zhF8kI?soCMRpQc%&xN8ck1HGDMfX5+rz1e{pPSK>v%4#&Cu17}1%Ri6zzxdubmhd= zT7%xaM{m~KXWBQpcjZ@3n46~DS17RC1t|dZlxICbzgvp7`PI)<$&xyY?>)6Vj}YNV z&ax%G;D#q8pF90}xkP43tr36Ogi+lybChMuxi-1Y&26-#E(x7gg-Joo_qJmlT-D6OqY0TW+E-azA*dGc-$lxOf-B{|KatA>W|2$+uJXlh@Gt{ zFMs)Rzx>~SXUoegDqd|{H4@Ql-l;a~FRz`sbK}+8(p}mI_M)C28(V^)*%M>s)&Oql z;x~wm3o13#F zwk7Mq!Q8+o(pSG^BiZ*~M3d5UPk}9)CAJo=8dxSFP1*X*s}m9s_ot5hquEc1QwX`S zjrJ~|`<>bzcFRRG8W6}5;^Z5#xHvw>Yj`kst1vBwkBh@L0{^`|fDiy;mn0S@7Ol^Z zvMY^VurSKDlv$teR+Lm2TnzvqqIZrc`mq_=9u*1z0K{LycF9kPa(=05)-HQPVcNJV zA(^dQx+o`x80?cr`+~LOcfWPjYUlm%pw|L))O^?o8DW-y`^nE9J|Rax_|1&Z<@AVG!ZY@SR|i<)+_~)<`j7$c8W* z1WmTbRXi+_oCw?mNE*2)xd`LOhA05f_iGaOvYDTW~OmS?R}qu$?d295;Tf%B3^m>Q^rqHz<*F56ef5e?U#WF{=V=VM zQP=P|Ew+T6obdosc>06;-2B+I8(KQuo$1*HiDoIoVFoo_O%rk3CeyM>rq2F z-zbp;%u9q}2?8)wVSNfV7!2P2gg}5R*Sg(sOW4w`i3Sinx)25?c~A&k1ON<(!n|^<)M+%?KvMNVg)7b`4M74ctl1- z>;=|ImJ*p0*W+QHgB_1m>Xh^pwdzJkRku<7xb-aq!e*ZU8h`7Qrk$a7H0XAKN!;Ye zM2B2gn2`>C=clE9{0Hc}Apto4)-i7azV5<7YKVCQLKkptmrg}h(>`tl<+Y;ih{-flhayIdI`f4HGrV{#z1=6R}Lsdq~(E^8A%HZH4G&M0TT z@nfv=tCH8T>SSm4TV^kq{px;k*x>={%OO?Q50pD@GH>)ZLS5Ey#A*A%mN3aqxOQio z%DL4ID_xhZ5tjjc+dRty2O3Rpm@04fH!%Z3@34=^q|yRJGB3;Jy8BXM`TDLxv1*>h zDHKHB4)^-G>D!Jq7;FG(GifN7W6nEhais_fz}OGprwHKc;^8_;vJF zJJ71VxKrz3spR0w5ME$%<95njX z-921;g&<*m=7zSwfaUMm<=7>FC6vl|s4R7PY-XlXK}q``0&CYzr&u?cop4V*u^Pt}QC*{d&~~OrJ?G0l+|&1_1W; zu;dH)GaJq^=7M4x?d3#=1G_{J>=MIVT2>PTqT|GA18k`D9{|1JlWUSu+1!16e-o#_ zpVPMyok1V{WXYlRcfy~y0zM#eHt1wE5;L6 z>j{1OZ9ZErRwuQZwhDHHZfwo@ui5G4rWQsO2p+kOY32YT_;vG3F^}f=N7Kv7K=f_% zpJ~U$T&_P~7BjoE3l?=gDG}u7q0N+W`VrPw--y(S;GgJY^HW=I6L| zC%L|>XTS@uYzQ~?@kOqc}#;MbiewDa?0(e$z=dT<^TGY6rys=>-b#6l17St7i# z@zL3N&EK5F>tNuqT1x-*^(W^A-!hh1v0G&b-+nE7 z05&Z7-yfZ+^6$?-IY2O!66IIc^WvjdND=3Wu{L~SzoNGU!AE0gX9va(P(yA*!%139 zEA=F4*F%gRTg!=Gccf1%qADDf$s*gd(3sWE=;`(!a4J(AZo^Gn%%ql)P;}!)nV?`|a8ODDx6h2CZb6raLv@Qx^NsZm)>^$o1V&Fc~1zfnXF`yAF`vNv$!1QtIjF5?BGyPKN zu0t)~gt_X4ze#7-kFyO1FTJU+fhcxM(?2>rjzy*xzc|B5_^{k&%zwj{`0}oJ@{cJM z`9We?l}Q%zc)kt~LzG;H+;M0)Mrd@%9A2i;Sat&;o0;T|rMzvY87y-yS(oAP1gX-x zvg*oDfA-l-ZyLS66GcEWx|eSG$q$~<`EhM4D=%%iLY1;Zs?dHkbVZ2DhjwyXeqQqL z-ri+F_y0I_Xo60R5l8^aO3m6P_eeWnEU39)2wDA+OQcj#?WXeg=j-xu_J&PjXGgke+ZDIWR;oLz zpG@-cOX@ujVg}>FoFLG+k7=P4-t{i`;P4Olir+A&g%{#PPzDJKU^nw`(w5ItsB?cM zIt7f+9Sh7&M-XpbBL?snha_OeU(j*|NXW2%>_rKthfxh&5Q<$zO7+zu;MtXSeu5^J zkhT2vzbJZpPb{%l{(1esSO5EfX+EI73LbIK0LJERBp8hGi1nHQAIEIQ>ozNPua;fWqE&Qy>;CAZT#U0znMM1stn> zyC6Wr=?fA(@KQ;s;({Gg;KBh1gkV-hPSH=08f!NEe6C+GsDK#g zP`4!+;PGp*Tfa(hlGl*;|Aw*wY9KEk6xHcHnkAO(aR2Ot}h-bDv;*f;?8AlyO}!Nvik z0K7305o{a)ko-fl|BnfD$dIy|0=rXlY$t<@@kx9^- z3>KLcFYS1Fl`zS{89`=!N z1I#2(!%lB_er>*MuHgO!k^f&1d3XPgQs7?_eRpIXp(s%%(IzS26hX1jsVEAfQ;eDz zy*Vr9oal5@w;9lXf6w-vy?qUnAe5k)#Dh?>!c_An369n8w|zS|SNRn@6f&ie+%idY zf2Nuq>P%@pbxhdAGK35q2HOSRx_w8o(%)y@=}b8r5ZFGF^r-4Gg81@X{{x0WomR45 z*Jn6l_-O!=R%w+nj#IEX4%K#miVzU2U;Y2Xbmg}tuaHm9k~`o+6!Pvej1{xB)6ikj zY3xt$X8)XWlUG94PW*AWPX$1nTY}IwuvJZ#9G#c1&c2I*IUw5s#>u& z>mUs*Pa5LT6wUvcsQI{{&K36l$erk0{ifszAAB|2di z^1WRUl1b-$*)dX%v@gf1@?DqO!lmyj_t!Kx|4S|DAD3jjRk|vtbmiG84WJFHD5#(d zV1a-BpRwl8e0w!xSu2g~oFwb^$+GXBWILO&wpH!*j?y}7qmg%`wz=`>isWs!w%V!A zraHBt(pgwz#{v_0EPDV2hoEcd5_X3ISpW-zVOiD`GJ&Y+vVf}*xZ&LlFW$_aV=W(Ea`f{iUDMT|4YMhPO1 zt{^cfi@8=IMTP9ps$A4OGo4(>1sr6{#i}LyDBQ6?Zjfw;6A39kcE~k$NbD#Y?wuD4wT#!!&oHm4@xGMEr?_(f3SY36DHszPrE`rq zzx;H~%(C+fGAr70{GOtm-5s*I`R@Z`|9C?*+ZI9to>5sfqAGcrU`WakT+FPkU@Mj# zvoO>8dp^YDHEsa~w&c*`D2Pjn16snuU}fvXSK-$WhVk_sV_e{medp5bOg1zy&5Y0z z*AOHm?qfWj=cGE_X_U-#17|A@8SpKki@0k);f1k?*>Ed2|NCJpyq)0-LAtWKvJ5KA z_%p4U@_)hdaw{uM*KzaRADMZ+?-y_$gFeR)Az@k>au`Ji3C1+%=Z0dCi|Q=3t*nFr zqy=D56-_z${f)an<4F(C=7KTbr;s$D-rg6K*rcbYBsa%n6D^aZxi$OPKJr-1hQae1WGd@PB9@3(9ofkmK;k<10W?$cX~7SnahN& zreMd80f5>OG&?K{A!t#1ShWnb5JX`fROP#}QY_$A)#Y^sdJ(gi7wh~OjA>0^w2J`I ztWS}c=?%?`T9SZ)6U{PqI}O-P1?2L}DJ3UH8=SnNq{bI;e9inxro8%{Lc9{%V7C(GRo&U8gAy@-%udeF z2^zFxn|0-7)oLDV42fp$)~wD0iei!&3?#^>pa3o{UsF)_At?b3$xMZmPYDddkS0eE z1D5onpaO+$C%!e&Aj5Dh0AX;@r4~VT(>OavlxR#AFflbz4yEm5CtpJWM%h)x|PS)8%Jk%2i1}1HaguL}>SKv0 zYivqiI+5j$g|acAv@gHVFgW{-Lm?e1Q8t1tT=M@zrU1O5-lNXUhn)*)q1|h+ytR%# zyu_Eb5Rs)fCeT(9(*Mh0_O*cdg%r!L@u+f$xo3>8N3wAuS(($TyM&)M_|65FgDXfO z#D~we>8!qoc+$1ZZG>VHPA4nM^=*g$aY5K3FEpC#6yb{58wkY%TMzC28qh-mqQ~&x zTrLU^9;Xt?HQ%lJ_M-&76Hmg(dXj$9mE0@w9X9OeYQM``O9%GoEr4C{XJ;k;{EhKR z2BpIZ=*ShF?G`Cmg?tARTBBYk0rQqA zJDp=}T3te4U|pt@xfcN&)(GfuZ*y~Y%asIf#A%WwVbfKOs^yQ!#|LqMy>{av5qSk3 zqxn8nbT6-SX(5X+m9Ok1A-TNxZCncYLrLAEuv7H(bPUBplR+X3LZ+onC>}asyx%CM z8^tqZq7ZzgEM;K8rdG5d<9RiAs+h86Evi5OJJEu$E=vNNVX~<2HrRt2omXtN*&`W^ zzM`!V{usy`WL85h_IUum#MWL?%r(R96m=3nRis*%@};0$U_DY$v~0 zU{JaNgEI?C>TilpAnpuh?4IS<>spbs4{^SVN(AWql}?^_QIrF*yTHRGjtPlh11F1& zwOCIhO3J>-VBIOtm(ZkBY{|pmvDZa1YecDS5oFvnd7tP33OoWEE0Zce@^guJg~c5f zUU1qIJXhR`v1ZL;g?2?Toz^(2SX|`m?q@`aDlXBi?#}#|Lz-}1mF&re}gvkm! z!SS^4l^CeeeQKxN4YP52YuWJ-pK)hk?y{1ohV>Z(8$EaJK zpjZ393&%Rn zXs(g3Rdjv2;#JVq`%Q_v)%|{KxzyYPLnLU55?e)8g>^y;v~zk^1lLKbDH_07XK4IW z4wk^ZOKa?KDfqz^mxBWe3_p@hFDJSiwVq~gR(d^Bbed0SA(bm?{{1+ZsKt-T)WD^m zj8?;qG(Z{>9%CGVLmXonQp?AK+^1GhlP>-k*VStCkO4L^EEp~adBOKqVQeCjbQ-l8 zu2g8syT66VE- zdzF*7LI?m%itr>_1y&4ILYfifLMpPumNnfoI?8{$;MGRumLtl2LCQhZreGTE*){Nb z1|!z($p`SvZAS=pK|P?h?B77(>#{NuOIn0d?vPV0%FDsuT~%S6Btm2p2WYA$7!MmW zM2xU>=i%ze^>})k&+6uVXWP^+1H>7z#rim%QwYJio>10+=-^Rt$K z4ETz2O5Ta>Khjo0xpr?fOI>JtR54yhe3sfzF0LS>BJ|s z88RIVTgE=iYwp|dtgL0`%P?6|CN$yHLI3~D!aR;K=MB|&f?GzgVKWKZvd?s z?i{1~@SCYaRHr2^i0NvaY~utELZaL-mLc6GsB-&4Prv$Yj44!P1H`Ksb(W@oW+aesNDPEL$p2J2sdt?b^D^}3 zw98p38*-e_L?bO{CM)_`yd$jwv(IV5HweDso!d@*T3PkDCaru%I&XZax1eM)VlsOV zNIjZS-o)OJ9ti|CiC0SySg0`%tv*9y;$q&Z%DzUuacf0AeoVdN75F-O0bxzBs(M@F zdelzi^Uug4*CbE@qZD9xJkm*4L?6g1hDiig^a(f2*=v21Ce`ABPV{6Wt`AG zy3x6?GB=>9F++@7hn-h2;sLE(@t4H{=Osk(p0%ed7rCUn zQ|hAg?bwTB1UdjD+Lm9U6blFT(3MJf?2V7ed>$1CInQ9%!kHq*+a&9K48W~=(dn_R z^Hdoe-lUCFEc?jW&9+kG6IZ}32 z`8C4%Z<;}*_uE){%RxP3Qp(p-Q5}ewqj*+v5oVnu@*EsN_qkr>3n9?kC&q*4gb+@1 z5C6zfkj#9-T})WKf>*CtM3OmYcFgX_;WYV>nWoNJX$oG;ln<|8)^WVv8Z9AX(rf0L zanLOSBCMhW^-R3ezK#U59bD-JW<72UM)@jAdfq7K5zvajQ}9cmM%8JB?Bh@dDyY)0 zl=g;%#HQZl4gpo4_50Q=k}MSiP3t&2|1*sM?4${>LEIVP4Q?ve2A2 zz1Wb*q(}=9IMOt8)KpKcE#VPd9F@xQx?2JZS-&j8poxBn#Y0Jsj5{EuiL$DTFoJTBq-f0Gc)}wz}>Umm;s!g z2R7KJH?hB%cXTWASygt+LD|;YnA@WcUcO=i>GNWk_x`O+BsUK$0&UUMhRD2vids@T zgJ-X+VVUEy>6JC5f3S7>B?>HWL0h447Q9xtwui8bWfq7fl9*pgAU!+Sj+_Vwx>TBZ zl5w3phY~C6T0@X&a8#Hcc2RG{x(KOqn)>}ps<`FTZSy>GJNMJPN&e#)aT$b0?qlK5550IqYO(+UEb*>dB$LGTc&5?$*Al>J^RvQC@gIYHRC&1 zJY9YB*L}dYL$4D2t>}e#IrUH>O8T7I(ZP9!!%eZLz$CHBBexvqckA-QaPv_ zQT(=Nh3_obqmIr^B;6+u4Bz1Z4)zf#Vu#pP1w2pkCUS(`=Xui8`eLRmwB!1!RAqYi zk#b6{vWL`}=N#j572JaV%QiA2L9T!$1lhHwzChBK!COBg%;3bT@M_scQJi2#c%omD z3r(^JzjSwr=I!pspXP=r1_K5SHutDe&|a6?<-Nk;V3tVpw%ITSpxSmK>w!Nd+x;3v z20gsvV!rPXi7N3x&@C0tlR7*&Tep;TC{$v#nz3O@ZI_e~R45F(OB||&VKQ~n3CYN% zQtdpT)G@IOsXCt^QkOHA?JjsoJIkoMnc1pKf(C*^HPSGFS>{+qkjH*#1QC%!4}0!( zj5BIA)2}Sr1`xP+%QBo%Xq0|5{)#*j%qk|N+{$-rS$8%m%lI3by@Uer7nXV3sx}2zFIZMUJQ?VeYkoq#VfSRH5G$!y3FPY+pSmLd@}b5KOk!hKOYVh*A&;T zv==kQ-eovsZx<$ghlaETdd{UKd;bUpQE%QTycCAJEpie)+B;W}K$IGZ#YJ ziyMp8SD#C4aq3W)^qQ|5qKfLA4f8sEx^ZtY0H-}!iS>-Pr`uaJ$3Zk_7Aw$^^jZCV z<5t)!nzW?sqW)zivB&Tq5+cXzthi{7fSH|J4HwB}s!Z%C*ty?!r6!ALN2RV+t4g?l z1N~fP%(@t{_TJ+%s!`7e*!II&BQ690Z!N_X+jOR}-SVqu6ild95cO4Mcj$@R3RBz4 z-RNJzrtWXT`@1xN$%c}!H%RrmU*pv?9d>Ol7PSLruEVkz*|;dEP;^}P9avYbE}L8C z2gRo{n_fs>sZT4x+PNPaNqyP;it!ebtfAJn90TiQ3?bH#xjv1mW|V>OQE|kpvIrp} zQwk}g>)N%#jIx65cyN`J@G7L_a0s8BAjq)`tDDi_1I>{Dwkj913LIInY*_$5>b99z zv3|F#xIFH@`WY7Qch4efU+f<&Q(^vlk&V@cRKryHaG4>Ly^Vch6(iDfGhO2+6~f`7 zmC%q975?tgzoY+7QH+FBmn#eKr?$%5!~Jzug}ex!-+?~#5YYzC6No5-A)rD@Vu7NN zNzru#uIi*>14` z05UE&@SSvWqi|1g9<2SK@ycvXq2Sv8r}HnR{~PHvhH#k^G&|6~wSU}gWdvP6?qo`Z;h|cB_Xq5u2-H<1@77F{FcKvdxgl?78+f zUfu{dn{}E3k(68(V84rQSXsDTAS4Bua?s5sgim#C(IqBMsT;Kib~&GRD#L_iF*~At z^_j_fZSiMzGCY{u>j@P zP{7cS6RNSkQ&BQouh2(qA=U=EFk}tz49v{LcMWg8t%5xhC1u1oHig8lfG{I=F-Tf4 zsECeMB3{Di7YTKZG_Hx)`leR zbtPLXLJ0Ms6nFRh%!`ak35tt^51SwuZG%+}RxCQObjl&w=j2s4KGS6{i>ke;wu9vL z<8*iUXH}1u6u_3ytsZkI(#|Qb@`}*mJnCQT?~sL%qyFq50_*^Ai}5yfnr_nG@J!TC zwa<}lXw|kv4GZ$OBX6G58XIO1dc^@4CwhPC(~$PTFCPq|xdOzH4NTq$B4KUQ&E~_% z3N9chXCtLxqB#w>Sc~{jZrLeCt$%=rBHtf~4_Bw?Cy$sqO}H#z!K2&>h_I_na|6O_ zxk*<)xKV7@8cm&37EWceadWx_a2H(!mbXhAo68Lm-RQEwW1rK*wPL=A7>&*9Hv&+JV&jAG&@@r*rAW~kQ~_jdV~XA81H&lu;m zC1(-YIAKQ=t|p87JhnO4i#sJL%6=bY&MDzWzm(+gA+m9NiLka+E= zL)>69qUPptMi_}XD7aCVFeV?8)+o-QPYTVwP^`qaN);e#?347ZT@cos>I2?@#6DBpw? z3Rz^$2OLUQ3&HW=&$VTd^7__tNm*oywR15EhnzX5G!9d}&9Y`0{u?Gq9TMJ1Wf&Oe z&JgBr#{kVW4t0VE5wA!|NT4_H+4OPFE|ETsLw{tGb$Yr$HYl~h(BK{Z(iiD0wa5ee zWPQtUKe~x=k)37=l#NLtQL@czq(;Q|+=SieBx>1M@FxdF&|-$gABGnScY4aXT~NUX4GOc(jm4~40rxxB^&{rO1M zBI=kM7G~{8n~4PXsj5YXw=TtD8tW-Rc(@pS;$in}v+cFd#hqK{)&q-dJh;&5dER#< z>E7NN9ybZ;HXD4+hf$$2gCl((#Mb4CBh_(P_5X*Wkqgmi3VPM%tcZN}al}e`f4rQL zi4GQ79B=6_PH#!TaDn3Q$2~`y(TvU1#V9I_s1olJ2hst4BWq6`*NU8v_6&9+jSSle zQlvdMP>ymx%SP;L#9f%}NJu@p88I>Z1adGum`PCR?eRhD72h6Eyf?;@I4s8~a_jJ| z|LFtv;)Bh&;K&ly1bO~k{pIj)>3x?$>=gKiH3JAQWD6o}o#7Sq4YLD8{bz>b2q{6* z1RICN<1DJ-=;k~<7d@dB@V^`AV`t~o&8%Lf0Hyj&oe^qaH#LVZ_C@~BPCWR}O7xLs zWH$a1&0Mm(G;WbS4ApOiK^s}*iH1QipTU(unn1#Q#v%Qws3_(uGg2bZ{r)KF-shDZ zt1f1`$-7b3r&)UUc`WXnV}7wdIq5KRVx}C-ej3A!W1=TM1?*s`ntde7g=%ZHQ1O1? zauOJyvG)j#Od{?>U*Z21+TLh{_hk_ww}m%Wi{QpV1MSg60s}Fl0a$?VDXp&*VRc0v zBZ~izgE_vEaU?bmg3D4Qu_Ydr&D;cX#*|GE^CI?f36#qS!mnZM+i(n#?0jJ0u_CPb zUok=Nd_pb~d{WY^v1+QK6+g}lY!VC9aseprNNMhjVtWYO(F>5|aQJ2}=F~iu#-+|D|rr)-C4W&|a2?nf(}rIt^P0X+8Kk zqO(5Vi`yy8^Y#XNR&{7kWZo zjOlpZq~opiN(JFM661z3` z@xfNRcaAij6X}@kaCd7V>7v&=T`H)5*9@s5r@TL}PCBZhYbFI6!J)B|eXVpmrA%uA zaQB>PiE?nG&SHF+gI0pTj5NWtFTiCu^m6~a)@O%igQ{M8(%K`%QT*Id9h>}LsG-fp za?ZEjm4TQp{%A}iY(}KP(o5}=Xxur1JOrm-o%BdJFT%(xQEQEXvTEgrkyjPwJe_gq zckoNI7a?|W0owI~*8OvyuJWSGqhwU(4(Q|Mtg$eT?9VR>pkeIayaR=Rk=K1uIe}_z z>K&>uwwQ?k%PZqh;OIN6guKWnEBtsTL5JK&SU4y=G+6L1D@b{TL4KyG+b~_I3IvYWA32Bgq-Y~ z#v7TxAj?yN9E-HB8cqzdv^*`0)Bf0V#wG3ultp7`SenW7>3YE%Dkm2;(RguJMp9X85@ z3iD?z4oVM!nOzv-*xAUC4nTh!VI_Mva1c}frbeakSA@N?+2bh`3d0gGy2)vJXIjE< zn9jM*wrXrbnF2N3LDW8J`lqKyTDCOKBg23`rn?<_3wCKR`;;N*scYwHH6xraVuV5B zC<;Mb&k~C8s10Gkl`;SkJ{W08FcC9T4US2_Yg)M8=z-VKK zb7O{|g4m>D+!VOe*B+s@0}>g6B^ni=5D0v^kHny>tI+gGw!~vN6nl=kYiJvFG3#VY zE!(#6q2h|Ln{Cy_^k}9YlGz4qqpDI^wnN}J1wrDnJh-0-nw6FziY`h}e~i7-hxmJC zI*s?&aM&gp@_3lyF(R2d8HQ%7`*)K%AEiJgXJGzUW2|L~lBb~p0g}LNSdNY-w@K2N zK@+@*DYCY=Z5`v7@DOPLG{Q>zz35*~OK2fT^|Q?)enUbgnlmt_;7)FL+I}q>JI=3xOC2hRCeCSp#&YPb zrI_u-j>nEIMznbY3TZMFEn#Qzm3W4j$EU|8b!> zFmWto?{FJETKzkOU80M9EkX}d|6>9M^Ct9v1Z#sa?8YZb^>Vlnih;|R-m8-j-?KU< z>@;R{|j5&db*;7H-X>Haq6^h;Hdu6Id14il5-A^!lx1r*!;slOsZJv8u91@meR7&Qi z(EFwVa?{2cfDfCBL{6Xr>V9|0v{SM%pfaEZFEXDJi{Vj6$#lrfdY=egH%dIv8>$yN zqj@XF5W0{;4~qAjTjtne6kT6x`RjY?)GiZShQKpa-rGltLdG57yGvqT_2#6EY0bHY z9q zo%vV6@~+8Ua$7i*rX+xDZ{=waRtaNFv5_DSnB3uLm;idT25{U^xNDLe!rL&VpbGav zvG`Gu(5n7+ZcRKaTnZD8g2iec^|mtkZt{~+KQDA_Zkt;l%R{}e=xxbYElNBxO-|7r z5K#xL3;I3o0rd04g(17!a!Vs`XijwwYykQ>J4CCg{{)+AWLr!y*@>r)oa+Q_Va&Rx zJn&?gKLl5bNp9f~w^=S7S5z>bn(2`LO=BM{{bC-H2Ef{?h{qUk^at7;15~t5l$4M% zdu7zy=KSZl5#$a*K$B2WIb8T`+&~PBpNM z-@(;G&8o57vYqO!#D_Czt+}oNwrs_T<>}YlIAa#4T)(FtMD4pO>)-v(h#g@zjdH&j z^;6`1)Y-U*vqxvq7VOTe9JK%d#ZpdSgbD@^IK7*EG^uPf!C9M(dI%II&!2&Kw|5ru zO9v2hB-!L|`^k`wvj}~e%H9iu!D!6b;LByErs2O134wn$BFzjzrz$Qrz^@365;X~Z zIo#)#_lsk+?*i$_TaI=ycHT6o_ohVbUQ|w}sVg`0h6voV=t$7+m+ECOQuWDYlYVtM}WU1ZhID|pL=naV@!l+*wOqI^|Kmx6@j$#v#YKRL}j)K$V`o9t9M!0uA z*==w1GIG^b?=G=wE(Q?x_SGW5)rqu3$tZy40iy`10uK(*pGqGA+p5@OqG@efUV|VW zCv+BPXzdLB*rZNVF*o_~iOV%}i~W-qk%eOY3q7&B5XuGTVs4#dd3NBdSW-!wBu%5a zv=?H3`*qXI9(guueFCv#>KB^r@t4!Ki$3BFig+c6pxrki5s49e=l)}rysUX#p$U_l z@fE@w1T{S9koQ?lg@}yjd;w$L2l&&|EAM%DY|_!Q;4te@)I5Gen`2R7d>J6w>#8P^ zH`8*f8=Lc_+;rpapVV~Im)@{4NHYvhIP>+TbRoVHQs&s>Czn;lPWd(lcf}c38>G zKI@nmB%{x*u#tD_TQYUsBw%*l;-d4rUMZ_%iciK&3dw8ujNrF)fbV)alCR2NSg9k_ zvW%-~#JMxaWdcX`7BJzMe44nK4|Gpd69LvmIpwx%G+Ep7KVMn?pa0nZ zrnkIa<-rKmkaa`QH}axtltC}l`Wb8#jkc`PTM@kdg4}P~$zP{^E*=n}7DRp8&{V-( zpxDh!0q%fDTS;BgJgAxYE}g6om_i1cuN)f%&ENVs8q`etn~veb1G|e(AKrXaOfU)F z6!=-AgdF>|wW?1E!$YtrEea)lL5^N@=L|n_6jSCL$4t@Y=cj)x`Zsf0{@=fs|F^yb z*W@X$xXuQuLD&p-1e0pUeoS~Ur|kHiV)eQW(8x$7<8wyk$VgDZ zqOERj-a@!H9kwqh@V$TU<3Ill8uMN)%*goZa{ch|b;NDD!|WR*+8r!iE69~dHj$nZ z>m`y!0zo~I*;!s)_@X6CTf9O-TD+Dn314JdN3b(qF90yMmGYrq>7G6F)F)f!YOSs1 zxid$lkrCmEc8@$0?IOa_$eE+0RBPJUy6O}2)q9TnmEN~a8}NKe9^)NGUX$m)=tTh4 z0%QhyappG|et+g78h770WXEv1d7H+8st?|W4#Ft6(SUMCjt+8G(+V0ufoJ5htMwE71@lw>hD7Wd7Te=bX~gJUoYgJ21NE z@ssN}Z)LAqy2_0E#hVA|T(F<|cW&-(Cm*nQ&y9H$AB#*~%xlY&VrggEcBK>YW zk1?4jpr~p1N4;)3HnvSDDuUq56rzy^L7ZT7tADf%nhs zgJ6rlQ=U?*BPd(a=A*PD;|gSmDu5w?Q{|7Gd4KjJ?*&n$8X00)TwG*ab2B9Z zO?nHED?bZ}oB4temqMnwg(#4E`20$VSNhFv(zf|e;g@h;!fXcJXRYGA%3Lai7F9~g ztA4`jFkdk1nT2g_>zEjG_i!17OkE%8f6thu(|=@G$k+OT#&j&(@+AgVEcw|ZT8wYV z2>9a10d-X+5sZMDtR-~;;QPEd)BH~0DMXX05v17*yeQuxg!U@DS z_Ja_g82*JxFYzpP{YOT+PgY3E{ybcJX(lv8o~A&J>}&Cr&4VTuLnP-YLT8mWyVa6; zStj14Iup*~@m9>=uJaH;hyeaA|N+Zf;AW zmTQJmzFW7QPmdgGL4T_nNu+h>4-kR{{Z?@pn@4-K;X~ufN)& zm#&5}r1FU!Nbj{TK2ZKBzntM+*#-b$Z5$q$7Ti@gwB-ZVfSqhUfmd)p{BZ{BmWa-) zFa0G+lwoZ@_{Nnh#gPyjX}>83O2+m2-NAxj0q~zD*f)1y=ksc0T}6&Xw_~BHTyT+W zo{;My@&z~|J^MjtS_XX28N zPjvpRTmEE+3{|WbY?@7V<9|y&p^NQP@$9#?FtcvT)eBXRP}bulAQVJKNaEH5Ktjj`nt((Aqtsgrl&AMeCJKFe zX~vVYlH(a-b9l6g|E9}vlhS>&wT2tiq^QL$XdP^@~ZTf(l;VDfK#Yrph8R zLYLTwlP5LV%uH6h2=C&|MJ%S6)o`m%?6fYnPZZe8<}of2#Z@H@OWKIiW{I?u;|G8s z5D`$oO>vpo*-Hcp#+vD1@)q9(3&Iz(G!+Pgm~LbUFn*}9=F*F9l1f@Qp*R5&1tkwa z7Tr<-#^{~alDBq$TEiAY8iIu}m!j4{a7G++La#40{|zg+^RmS2=Amkf$Vgq{I-I;g zqchFK4v6p)XA)BbhFUFdNv|Vls5b~?Yxl!j!uo2Tyn$5;1wQ_ke^&3-0*}fLn5MVc z?%Jh@NCzwc0t5uW0ih97(5n^sL}!ja(#bU{Fj}h69j*{c0ErWGB58_J&0;m+S(4RB z6kvH$+t-@L`J0&N7#$>O+Jon!;@6cFo=I%bPl^r&O9|n88gh(ua;q=}Ewar0;lwF^ z!uW_|KqwVLPZB5zcfcr)PHJ2G(l~$ro#YraU83JZN{}i$F|1iA@JVm^XZ5813DGvY z(H2Bjzym$}B&bpV{{wR?l=)ntU5aqKGq70MZC$5Vg>*_1hE0U=t(HxSE;6}Kxh>_O zqFNv*?dzuHR}K66#q2p!fj_W8QEsyKkg}UhKBz3Kly`P0 z8r0MslK7AuYpT;%x+x+5&!#w*GGD|k;1Sv9bS_plS=FiZAzhOAPO2=p)v8gsnT%~# zuA4uiXypjYq#S!JQFWjypVrrn(iYRwjoq|rMzc*SppMS+N-My^`$%2Wku(%!J*Rmxu9qnc2Qk5E* zmBa~xXEgJRBTUQl=FOikKi7yD*!*H*7RHO!NQ?J{ED43{ZAkgKGiHw_&sAD?pUqji zbcvabyBXC%{3Z#!hS*7L6-%1<&`6cJc}{+*T+Z2U>&kPJS09}{+nDoLsVZGjqy=Bh zNoG{+SDWFcbDH_7sYx0Z%g>hQY%nLtjJcJ5CR>9bO=7XtCbc8oXU{&uq={zh#@l+G zWtAD7%NmQD1{CNbEGBfp;%0;&XPVrBOFo12x509EzBA?KHtEZsYO?Hptfu<^tXNX| z!IhDd`xR{d@MiuyU^aV#FDGpSS1)kcu~QQk*1<>r8rhn=Vxa5A$G>evh3SC-<$BM7 zP2^-O^=Vb_EVT)mpKi6IPZJt8$mf4E(qmY(qVxHSCpN-@>i%{m)B@Sshz_?`|7KL$ zYHT&8yG_GPCu*DH;+nOgU+5D}YN{(UHVwLUM4%m=qLjmjQS|B`Qbbm^DD*399dmQ^ z#*M2OjSP&D3aFT!tq4#chtq!kSEJrr>sYJwSg{z1DQ|lFEyO1V*}lUA%-BLI<9DnE z0zyWNA^D-AhZv_;mg5veVF~Or8|uaJg@}+ICOra*iKr3dNPZl9EyUMFgdubcjM|T0 z80skWzkW)oI(ehezhmg|VTY;1=Qe&fPN|fqM87HNLcd>OlB21AQ(9sHI>SANCgeA! zCW(?ng+)oiL}5d6d_4MHnE5;78$KBj#$?ji*5)%Rgoi7L*=E1<1 z+Adzn?CeKg5AUPyujQqh97|#4LN!UfV+ChzCnZ(Ypk39ON$6ZwDXyALFw#ootq9!2 zXUB=$Obvz}wR}hPGeW z{ngCe4gJ4yT5Fh9i?b%DX_@IQ0|UFvig7Tu+9!h8E0HYXx66qhrq%KJizMPM;#A@$ zaU##rlW`d)*AXpAtLO6i&!O`_oW_f|7yX>PHM)@Pd&DmG$1jc@5VJU5vNIE!e@fUN- z3AT%=+@A88V6LD;s%RkbEXVHt*KeE>$baG3-q+vX&h&EZ?eCvt5H)24YlGn?h#ezU zW?WJ!t|+CS=vD%h26i-^$mI!6e@8iOP^XaVP7*1MjWo?>v80XIP3V${oB8ZIJhSk` z6XaBb`m_nybCe(!x+&DWA}dK%rhhYu)Q$41q?Hxn$N5oI_#s^t<3(C4{T1D>gpXFz zgT_Mi1LAXhj}_(AAVMSx%S`gTU2%D4>$Wm_%w& zF{%-`fzbY1YBZdDgdUN&5Ekz^o&f&NFui|r-P5o+PDRBh@noWya%5xwxSN}A^yoMG z`U_7ycYqraD2?yD7zVA|TE&Fi^Lw}9k^v{-RS?n$m*OW1`}=M-#(m}G$NH~5)ImZb ziHc9!T1*amx~{Kgdb(-rr9wE1z}n!M!X5)1L{l9x3qfkw#)~0Wn_GN_p2Ng;gdZm+ zGZt5_AFSH%;^HbO{)j+PDQ+0;M7{>ORM?}Y=hXCEDnv>Myc>aP@g(!mNo!td;_j-g zgOHp^W)wG#cET6%kWWkV)ez%Fwi-d&hydg87Bcx5st%(xjZv~}cnLO6mxk2gBx$lr z&?v_06tI$w*0qfTRl`UtVGQgdugUyIEl3kfRy{)um|!CTQsu9zq*&&FCEGD^qX3H0 zQb7{s5(7xXoD*)>SeYtrnXHl>IB;_xza&!65MyCD4aq11EoemR@F=>RK8R2nh8qc^ z2h!jpA2+?KIpGeY>;JfGIMzG_0CA|g-@3hFP?#&6gYDM+Uc7!ITdD#FF%XC=){0Fv zJ&xoSjq3R9L&hc=Aiu5jXkR0}Du2k&FKTaw#4?S++U+2xFl5!TLfK&0X>JCcW05AS z&~Zw-JFxk1DO{+qqA@ZWSCpnZkDr&UL5!p!BuJ`Ex6zxb@>c6p6ov7l^TEiF_Hen$*HM^@?hE3tHwM#T}ldR=IE4D)aC<9zuYR`{0m_( zkNtzqMUol1kfI{JIVPq@*tj70`xMSbuG*1(VEJ;sy0?cIp^3K)7CWbxr-gGOIUBed z$FZb*3k(ndDL~f05fuO(g$@gSlw+~jIw9W4P+}M$rNX$!sN9FaN*3Z*m+r%JX_u_+L;o;uNuGqWFgyx(VmABK*n>^kBe z^?hKP`;nhhj{FerHaqJcvGWK>p7-VT&MeAf>PRv;vOe7X+pJsos5kDC8NMEtv=Vr$ z1a9K*8-rwoGGk7nsB&ub7+Y$HMgnVK!PU^%m^#hGGEWV_XX&H##G2}6`YuLOO?4vu zW=2SLTLt45dSz=(2;)%Drr}N8M?GE~B3rxa$)mB#VX~)|nx=)#)!xBjqjH1#Y-?qh zQPEZ{c&DA~bR(fS?oSs-+-5i*bY@C5vD7ssr z7$bo=Wc)t|E$E{+SEsLo!T`&+y!db@!(bUVHMfFR8DQm*uVP=paB?t^hd?N_kWj+~ zTqq3Aq0xPU2TwT5e};3&)&XZ$CCr=F+Dyy22d*AaTj|t4tXw9_HRy9!Y0#j%5>Wo& zO?kkV9&CQ$^SOGBRX8bn zxQGTN$-n|g1G*Ze{3=(azhVmx4iIvqXDH?3)^N4#w5yRXEde3e@~#1(vtA?m|4ymD zgdQlV3lDS*B|%WIc;W?o$1JJ5XI6uL$<%g6RV z47IM#a#Rj^mv36jsvTIb&(iA_3*TX2i6A!HkTJTGQ@eHzTG;^{g6%M4`7VT(*U6GA z2Um1=e^1yB&3=Jcm+qYCJVLcxBF24k{pQobojWbl(o#(&nysEB;N1d#CFcd{m2ZSj z#p|$+IrHafCf?8$h~zRq-28x#)eDv@yeJNfN#in6F?13EJKI!sSZAKY{I@HZT}zNq z4E%iI@PA&!VpVNMSVoe_8dzJhhU)*x%{>w<3d6g(6)a;u??+^+>gJ_PLN#DK_=N?p zUY)(P?AbGin@A>6Z$%#G#1rfbla@9-=fon9E=(T&9O%LVs*;AYB|})&v%j9-s#qqU zpK9;->azXDePWk!~j99=ARr&gJ8S%+?c<__gP8 z6R)E~+MwuAV3mSwjtl1#q{idEzG+&iz1c71YBMBMP-1!tQ4&}KT=w1s9K&0E9~t?4 zU5g^vcQJ&!x1xzySg`QM&4*UVwZ`i6e7;)x%^&kJ7s@0$9H04%&56qLy@P|B*;YdV zAGi872+y#UUb((yvo+p{SRt0PV4|?-dR!7SqLj7?yKqF3?Q+Src`N~<#tMlcs$#ZiO@9Ef(Ed>_dME zL9OhYjB1FD$S98pQHAEE=5D^pc8OoQ8?ml37Li04ii7}Lqc;A0c{bh(Tb7%=o?WD$ zl*{Is6N(MBjnFI3gvA*?o|7~x7TBP4SDntxyqc8-lXX3++PR&l+eN%FmOM*g(6vmh zhPcEee3y;tx`iF?uLRLL?$F26|^u0KVIxfTm&!s0ah z1)IcJKy6wq;tjJDT8G7pFEX&$fQ;rqVxX`{4U-m=#k=Qz@l8wj@*Ht$bn4iX_fMYR zYFxE?HOtWiCI4-|iOOA6am>kA@p>97tqo1Cq}AA zGAYMYketx{w%enuTUHZ7 |kbio&%cJKasuFxvxt?VBqV40)Vk7xR3j_cJa6n*_? zqVD|p{=Fy|jQuRVzxecl&DUxk{$S@@f_y-!)y3KO<(1Wqbb3z>07jDI`N5?p9E|hi(mhYk zXl>ky``0-uJPh8|5tcIN&|Vb#BjbW9lP}o)z6n$|tYcgK`Ep>*r?ax5!j|ZDz)f^G(IyMIu~1hCSOp zuNhB9GpFL>L3vat7P5MisL)x9X&1QN z0ZLGJzavj)?t84~czihX^zQe{qx~MSoSgf}n4eCQL}(a{f@BK_)NXKIzmf4|Jh@=a z8}}{+av(-4{*q3CA&(0vYNk6$uuOTBr1O;W#H9Ob9XJNE#6RGd7D_xEUi=dYyzEMu ztyD!4(j4XnVl529IFXJzr6^?sdCCbMl5p}N6rDws3Y>urim{1Xx6I7mJoDp0T{Tx& zAn{qKs+%0?jYlb`lgC_B8YO6Hl!vCt2T?U1Ff<+mE@u^@8#IWdqB*_dEcoQ57{tME z6fSEJ`unOWl_Y21CqfAvYV@E)z!Fr14NBPHu20AqXoT*&&t8V$H1)NH7|!j_m8e0qnIz%OFA<=7KNuwH#Xi@Na{(XE=la| z#-@jJ!rMJEI;FZIbMjs5M) zHn_jzqcJ{)c7kZ7IcG}wYZZz*lBu*^qNtGaXL30{{s^tynD0#AO~@1@89o#zO=et1 zF@Xuh_IFvG0=OdIBNlG?aHl0GIP}-@8bn5_>kvrSLI}Z)L=5x|*;*y&b-Egq$s1khxfT8ep)Wj%A4I^4Q)Pz6-zr>DN%2>Co+K--Fc|HKI3|H6og1J$37|Aj^ zR;>lerLUkKFb9KA>Kk0>6M&4lZ(1l6Wm7#8LpiEiR$1Lcr!T4ot;kEtII2dheuNry zobuAi!ICsd#yGr{_#w5MKm`1XzkAkzs6Fa3;KJPW zB?$cQdc>B7bL*~5>-^`A9qr4@cp&fC>&(_Nf41>Ilb@<}H_hd%Ousw0Xk`y_L&*Neh>kLSR)1wh&lc*4%r>BQ?qXoeI^xTS)KI z*}4|p+0Z>;PF&K^*ul7;=a~O7(J^g@MQZANQ*q9+V(ocno#o~sJx67-chlN#4%nI% zD+lr=-96WBmnPeTy0Wa*zDu&5=YGAXdxHh(;>O1POaPOE(nF|QVQkXv``x0%jH7H8 z`b;Q0LWwz`M2pB$d8{;zu~i7%D8@ry0kK=Ue=iUqGUAH-J}As6ET}^4-?-!ryIf4_ z;!7$S_xLZuu}noI@s#s*tGj{Wwjvw%YTEm1uS4UvhmMaU#p<5Ds*R}%m3(vsU$5bN z_d$9V-0G#NaJsPo#63^1dB4LnKex4nS+z&N6U;f80nvfw5N^|SvZ z!MMgU%Jm{qP8gt%7gFagAH>Rk;-L|Ot#0(2+Lf|$6uvQjQ%~qw5Eu&*6Siy5lhuoOAbMMV( z&mKLxbNhDbF73uyp}?k8Ro;Ng7u6jl3j=zjoc_p zs2P62VWZuKn)>B10OJpSfMmMS2oq#oZ$us>?_Js)6z_Fu!>Y=2`~G|W(O|u5x0k0r ze)xy0*y^3-^EU5ymR_@PU9q?SQ>Qkp`6X}F#-l+w$c7KEw$gw6=l`KQd8;?zjH0EP zAD3Sal{Sz_eNyR6E+L0GXG(vXdnrm)M3TzmP}TpY;$A{Rsimdnv$iG_u|^J7GV|`8 zsQ5FOtF%&W-qv6+L_!1WX|SipvNSm1UInca6-bW-XDYAeMK&S$KeBU^W{h=Pw|W%? zmza1pAJEw9U8%ZUY+)J2vB zv23KWc?3o(51J;*vFWt!2BU%2i>$1htx3L& zwN<;-Eh4vCEwUzAvCdfSnCmo+<10}FLk*PEakE}|a1oNKw9vF6ZEh$C)h0u~*O(lq zQjeQZv&Ks@_1Osq{O{~My=-})E;S+(-Z^K^eYpDnsQMNgy{$nPQBxBh{^b2tkm%ts zc&Y4QT?OKvI@I4sFGta9ub!x^@>TuvdwfG#j-Tqk|G~H&_+qQ{xRc7?*ePPTTo>x< zm6CV@w`u4XN<>hYiLkg>s~o~&nq|_X3wt!Vn)c-{!XF;%m1 z)*@LO3L79c%~^yqxB9g_g)|FQZ6Q}249l50IOg!(hj(}GP&Z}gP!>YH2V~7$b`AIa z3$|~C&GYB?w+-eX|26}u|Np&1!)HF=@E|QnE+h`90`Hx`uz0e0?b@B6O%fs_+~xzv zOVzx6e++hXu6xSjBD#c%Bp2OM!Gb`55};%bO1b!W$2A zbKgXY#p6%@|E2GfM|G**L+U~s`B_Tl4MSWWNtRcH(*KDhMQxh1MJm}$0`^n9lG$(H zbSzJ|dXJ$&NzK_z%Im7UJXwHjSz-A4t#1@Um9XHuM?8{UZt<&&#K)v8Uf)zWpU;wy-w)4KUm2E3;2+}e=R7AeHka59$Thp9^8TR75{WzWtLS~*Dq5L6y@u&k`TNe7j@dWX4_e!!5GRVRUie$ZYMr_Q z_;fmw0T933igrU1SrNG(2C8gTxU47+{NVs0Arwxv5;_+XUOkzseUy~(<`5e zCBhM5w7EcCh48^rKEA_R2J<~@0RN>0G)6b#WE(B1?nw`-wWWz#EQ7>5(Fx|w+d}K=W!+2EF1(1Q_*c6wf~07ppr#{zG(6VWrY6 z=J{<7d+>M!0|X}s;ui#Z9;gbd80mJ;}KaJkh%!XY^t7 zPF}}}zJu&ouA&JAH`$0A_Q!U8`1CA&ccg!_AG@AC^y8nhyKLpca;tKDTnUbrS9o}z zBOg3$f_pQ-;Kw8JSmln$LkqoeyTGY#Q+5q;?7*pn4xbAXb{1pv?SogvNO9u8nuRAa zD*kP(=?^+@12gf0H;mkk7ObW{7KB@3p5Ny#FJCnj3BUzO z_*WC4K6_X0=d8C|cIS>he!+s}4#8*^=f=}$8BWBe2L@HPlz;znZ*x*fUS_iJbP}0;h8jPtff4>edk)#!*3l}`#54V`U6U+RyFnSA zC{|6q;X_JFqUjONfn$6A=Dt@EoS*MGwuQZonV6D7xRNSM@VB?38 zk+-v^N_=IgGchE`D~D+1+Y3F~7P%Dp3-VI4vm+ynO$}YGJn{2l-k$n~AqEhC1W{Y# zj2~$WsB%kQ40ZhTAYfqa+qYMA3T(;O(dg)156%z>{JYuNhbm05@@sDH!$}S?{@?8E z_uuBgrfx2Om3K5cddAr4=fZ7)zz=WV&eoe#)t<6{os-xU=Cx59)r=bs_qswl4fKZ8}~<{GY-WS-T`&Q{l3 z;q4gTi9DAdqwP~d)U2FWMrVxjyGZm|HE-S;mQv*5wE$Ect&4Zh%xoi*hi(C~yp4C} zzG?ys%6o6u{o}C9;i=)FtU)=wZ@mY0{Izqv^sW3ze?$^NTLdL184xhA6t4p(O$9$O zWKD>S?Pso{0AjLTDwfV*ktR5iRZfhlMwCGXkUUTq58|nrp1Cxkey@AYyCa%0#wh-{4^2 zNj6}|vX^J3vzbc+>pVu4ea%(%`!DR_cO>pJmVX}bh#K%i4&MIP+6P-JaYvHfJr znJQI}M6o?jTj$0vwwefxws2GfD(-n%`3(|hSz&OjwJL|`*kf%ZR9QV>Ux<4T(nsFC z>rYW$+9RaNo(B&U3s;APu91nZ%FDe|I(%D*j*@t>qIbvOIJf3eb$o^iALRAsN%p3s zHooFm;ZVhXm`d=yOIzjl_LWyh zXi~L+J)SGRpESa(_5q9Ycj~gbie(I1*$@>s8E&*1Foei z0IEKHWNWegdzPwwU_s4(@32^@ULI?irFA$m<)C#W6e~uCa5qs0AweD-T(fJv8qpKF z2s_AD$Vo~D)~nzIe1$~>m!W!y_Y^W3Qaid!1M9@GYzV$OR}h1W+6aS6xIQ1xfJ}r4 z0jHzZXrOxzo*`-gCW$b$(mb8UQd;a|X#p-w!az_FISt*aMnkUS34#m-OhTf?B(Kw1 zFBPm+NIFRp$6`DYsseQ(7(MBS#`@eN#hhq}-LJ7BslDo41%=u&=BmhzoN+ENkV`)ke|IpSJCUf_;iS-BB^O6%# zCnwKiADmc!XxMAePpX$nI=KR`G*a>gv`z)ToQme>OreAW@ZiMqoG8HqI)S^E`~`XqgAe$(x`nPZb@W4g4JmuQUy zdUDM1?yUP`Vd!A`JWF^XzGM@YYUO=F0kg^qr|4@UO$ttr4)$U?Dk~lTg?JrBMI;;l zDv$#&lUeH^)_T@S*zI6}aj)$tUZC1cCLcy+6Uox3SQ5ik^%@8WLSQLvnz79u5SHvY z!t23mwJe+@PeRedl$b55@`^S?e)TpGa4@=ff$G2>8zwE2T1v`Bz5>0$Zim?LMw7pT zOg{Ly^0q#PZ7>S$Mw#-_kt1Iq-U>cv*o*g`a*8-l;!Cv^T+ficR7B8@@4+3k!c_h4 z;*l$mt8o-s1j#q8phY3!Z+1wTbyZy4bE;{ReSuAu6E~W;)GzLV1tw)EjirR8qKMX= z1(p+o-@}VAT{?drTt=A`ILY%VL;jS9cNN)~SOAT;cs>Th3dlFlE6E&?N9_e;{U1yS z2_m`Kvcg@hQoy~q(B*PvrHEz^o>zIy$-|7 zE`=}2C>_S?uWocc-ds@#wkD1{AHidp2eUv)vn>@w9%^W)WY#eoUj<(w6iJi?k^u%x z<5OENzOLf9Z#+(lwI9(#YQdGfyx9?4*TjA&Z0~C&6=r+-cwJk|jk?cdKKLigHB#V8 zl-`wJ?b_8H+bMDX>GG&4vvaSrZ`5XwR+Zdg1mC_I|7&bOsSwcQ_F5W(m_Wr@d4)6^ zW#-o2q)?vHeEY&C8vzH}Ay)Dn(ZiU;9SPwcN5=Yf+n$DnS=tOeR@0U;Hda4A;#pLf z&#>ynp=;G~zp%&Zb{6@NR9VC2*(E*+RrO%8V+uu!P*x^e=BwjwnE)8y8uI^VS@`dt z=Gun!zj}BMof=^=F}c&MQ)hA}=jBV=sN%aOmUowzk0N1>7QaT>bi8Kopz5gkS1nnY zb@TM~C)Wo@mp4EeZp+xyW_`$^hwkQ9-i3w5a5{SE4sq7*vlUTi5 zs~Cq`06o%b_Vp06<~G7};xCJ}2c~>)%aVG9Oy0&zg8q zWO~B+JHN7S0H-Nz{>8~n@W7bqj_niN)FmwLVyW~X$s#iA!6AjLOQ}tsb3BFR=M@%s zjr3M9?jum1GO#rS)xu%6jMtG4KobAx(dqa2|HmIIt-K$J7I;LD^!f(>v}Xvz_g^jc z{rGVq8lM1##l|>wd=0}*NUshz?RZ}iP!y}i*;2|VURPs}C^ja>V%t{tu2P>8pMm|t zg`x$-RnN820Aw$wK$Vg5>j;{c%cBE<3zQ*aZai4=ZmA%|CoA&>LZ4(^kQeunDk?or zER`TFcy686VsMyop$#=!HRxsLwu0nWCK3>2}C8spjN7ch`eC&sR_Q?Q(T0*`4oR%U3O1b;%3Nv9$giVvq1S z5|iiLY;TJiA8%qZw~miTwY8rPpA*XP4N;X}tw2Wv?&zMqXU$0&4hK(MV(}yKHJMBQ z{llNL{V5w7Q2?J`F!55A&#g-uX|;_|)XzHBCv32^1BeKxkH$QH&0BkMN@?16gFxo) zo4dC>bZfsTWs)wyK)Ck;4iXr~2UQqN!1krcviP?3`zW8hm*Kb&{rnt2fHwR2=J6Vv zM_amfnGIn(B=JjQk8-%vxk?jM?b6xB+q^lf zwe^6Ti@U7!ka1Uam$h{uG4Ww@la}`J&r=sUnhd+(%Dl)*qE%yc(wd}iMvcb3qVlAz z`>hu1ZYM0(+$}37*sN-I*{^YVt9_hxt1Vm^x;C#Ssr9D+98b080zf%`}yM+WAz8dBNhP=%Y5#R$F<-d zY@7Do&}bZ!NHwI|oE-D*aZJ=|ZY(;rmQ-W$G(X-j3SgE@i{Zx#YH9?r{Ft;f2RC@Y z5XKiIjWsUm_35Hir&Q+^E1Xl+Ce}F>Dh!*~cV(sx-`2M4dV9ae6v+b;PfUAr7d&|I ziRbG2L%%+QE2F@d{n28Folgo4k!AAJKr_pbg=v;62`UcTD@i_8t0r88S8Ct`UgI?W zu9)yV%~=yrr`=)0z~mXt^S3RJFkUOSn6@at1|p0=UOV5jzHY|Ea`AEt4-x z%$0w`uG)5uZMB_8(RjDE9%(aq8^y!2jl4$7Vijdp%i`qEFuNTBny7^V4L(Y@Lr^0> z0D%#_R@HCQ-Z03`<&N=ooBlSc7v6}M5}a`D)HdMv>AROLy0O@p77^rPBryu8n1)9( zCKov_b|M_h?aN*FiX5hrZ6O2ss0^J%&YPNMoEN_J!|~|3q*zj-V9X(P{Uv~!bP69Ry}rKBt6rvbQ8|D5oU_g@ z!#)#KGZlw5fXzrgTrht`Jy)~AZn@fhqjyZbFWryf^?m7sojh{tSd@+J=VJf>BsLi@ zbaWK@ojj#<(lW}k1@kN(CFo5(&kyTjXN?hGr|d?7WzH!#=DLxUE0glCGE5$HW26tZ zw}mD!vz9-bPF?62D)75;L**307@EXvR#^niPBitrGZqtr+2uttt9sQ49uKV^bouee zerkXF(7>@{Px5%aOA~~N!p4+>{P@d`Gi)@L^c@_$vv==+Rr|f4YF158U(4qCECC6E zL_vLGyfGf|q2G2E_=4^*9sQ0;GM5cJ;au&!f2-vudy_BXcJDW&n_OiI?_9oU)Ospsv!CS1YRLOsH&L z`U$9)wJ+T@<9-u?LQ|u0p4MPeCutG@a7z;siAxh;7a%sVk;!iWc)X5v><0$}Bo!is zXOhq(isDCwALw~2pg3W5B*i*K!#6+zLWz-t7*&*rE^tyQm=#5T^AE=$;1aj*axma3 zgdSle=^_=u>qd@@6ZsKv00%DMe}NPWmWZWTPf^U~U3=bS5dEY!EqMHw0;SIOPs_?Y zm7Sf1=K>6IHi^=y*kr{(!D-PudMZ65c~Oh5u)tdwI)woR!>!vhGcu>MvvToafrdz; z+cJe?9an5iVOo@(eS)NaI$^XJiA9kGUMc`t8Q=&=Gd1K9+abP-bC;>K6FWdgI+z0K zAOjQbz@@9jo=O^XX(lxvvX>?p!yc z^!BnZw``})+9^tOOlodUa!eE%{qqjlyFKJ)y@$(2bw-BT%nJMt)Wtc3IfO1;7|Kdq z+*foU;y@Q9j+HXNBGwRNPU!CD=e3N9-`+9~IWXt&a2czdMK0qq6W-eIyCZGvjcjuR z`KL)7JSf*fh`dVz@=K;Uc!)&YYTFH%^WYyfUgtrX*g@o45|BS=l!J#X3yJMQgD|eP zHm*~$vmc_6h)`XiY*yGhl^x^d z1&az@W%KRUzn06U+i&0QLC#605gJ9Y!kVT0SnW#76zY#p?b0}*L6D6fc4J?Zoji7G zMTVWMZ0RDc@ZVQ!K`9}+f3I`GnX`e(Zhi3n^J!^5iJw1Xn1dTUlF#q&b4Yd#DtMja z8wSDL3=R&|-%vnK#4$Igrk-DNB|7R0XkCI}^oAjQ89!j2I3M4MyD?E3EsdeApiC*w zK0jU+QDdlwdY2eYGYft*s)^>_}5DCKYj zB#9W4SjoX5BMb5(CdoAbjCw8q{LICL0hpO_)x?F{d{r&VaZ$OdSkcJOzlAxCj(U_< zrr`$cBR$Q&y3X#WQZ8wpS3Moyz@DUX0Kc(*mTfa8v~ha6h_%AvWh-DU1J|io~3nWy+8Ftih6JL?Ru@Go2ikJ zq|nY`KjvvEInj4RIqMb48#uq?v>e#CVqTo&&^8_*LWVuofQSF#yXWy$04W}II$HqO zBporzuh5GZEX18P&w1$#)~{TTld2t8C?;PP3bbsWOMSG_}Wk!O25 zAYkwuMOj`K3&tm_PS+fay06vBlRx0)QjN`;IY-g7!9A@Mh*=||5T?GE!Aj^y7TAHSwZzRw07?7o51ob#~Jmj zR_SxgbC-*h?vp#B;o#ue*~W4tdeI7ZtslJ7?EjGayzXw*AS9m zmR$h*Ph0Dsm*>Aa68yWqIL|1e+C43K^JFfPJOM@f$fTXppfte4$=ZHEI6N2TK;QqE z6UcbLu?d`4b&<$B1!zK&6H&+pDwa7Ka&*R3tLTk%jFAkec<`V+;LDeK#`(j!dabwU zT&4Sx6>xM#`P*5akSKUZzq{727cV*iY8lwrO;V1vLF zx+h9A{f|xnbT+L9VC%moR>e|5tDuvq(aFI$B0vy=kikr`vIC2$65yOf5}}4lg*cET?Z9A( zBBD5NJwzaqkoi$Db$}wRXo5eztZ+5)`H%57vD~ef7ahFXENik_H2(Jww;xp3n>RP* zy;5ZAZ59%q|MVZ23wW%cup=xi%bH*s;X+80O0__Xw5gEL#A-6Z+_H13Px|}23ktRt z6xwa+$=Np)zpnXRQfM4Q(-e$~8wOO8iC>KDT*o(WQWw-RrxnT@*!&JY?({@E$ot33 zzKv`40bFX~!jxa{e#PTW>9^~<+qwa`+Jl)goz`3C7kr})`R@!4gB~6^@+XuN7M5#m z!_-Zcc)~Jy!QqfP&K4(4{@m^@S!2T?>sqdb`FZEqI}!`ccJ}7dwXg!6l)5U^yVoqS z)HRrPWt5G9HQh1}Nx^7F+Inr-y|rr!e}6>-itG1}<3DbE2*c(A%>5#04F$O8P8RYM zEIx?!NDP6G%S`Oon9Vq}Uwwc=KBiJP1d0d!k}UPaI3go*Gwvdw_+xB3Tc3iFee$5e ztmg1R^*Dw6l&oE5k6?kXfPrQBZ6qX9NoM@!J5$ZTvmeh(j14GE6U6dk7cH72g0!?) z4{E|+FHjmQ1|micFAiDS%~nxTb%!zTRbvB(8q-m$cNbnVlB^73m!9CEXMXIhODukN z$mk**!Vw6k$qx=41;>=SvB+r#ytQ&|tS%(G(qr9VgEpRm4%H##90_9;ETPAQ|GaoljQ zk6#dR*GiPrD3E)OI#qG%rdjwOX1i8^#+l&{z2OKEb`osKgrLSj%1tl7I`<=rs@6 z^+=TRhDbIx6M%$RBA%NQEoC8NsGm(9d`;<&^zyI98DLkVEPE(9w_QeET&{YG%k-}P zO5yBb_p$pqpy4$)iTkH{N6eqx9Nri6fNrxZ*#qpUK4KopB6Se2B*kqLopEk4PQk_k z(k#-F7yw}p{!JVUH@X@c3djq|OJXFFvW%_KcC*DPgE*dO_eFix9kKmLUV%;AP$X&y$NKyxJ5wU}0%gHht7+HEsq`%uSKM#!Ro{D0bJ$!Tw z_YpU?UWOTqvN^tC1eJ}FRjD*;?j*&NcJk3uNqvdl5r<=<7k#(Mk$1#F+KPAC+c~Z6`ITyg*z+wCSSP5ktB{RZYZMr{0 z0Q3xH)|x1-a6mAWV3%&VWs=inm)o$;vvYQdNt%rLuKQEO=91_BK>i$QAlz%h2NB)ds`MvNAp`N9%4lXHYK^ zaZ4Bv85P|mO0ifbCibb- z?3PqJrFiN(~QyZSq@DdMml-dx)#d*LqHc=>F3Njc2 z4Eg{gg8?~~YXKw6;8u_y%+K)^7Sb88q7NXu`b?a)&+ge;yy(F>v_!5Y!|Qch(*6dk z6zGksw;YZ?YgaIb3a%be2Ic753(5!!p{-?z!e zaOoi#j%676_;4q-VT0|)RY-k9mxIF<4fV*XjkeJ*B&>65K415tWc zOQojbcnF_wV>UvG#YT+f{3qqmkIcmI@Mo>tB`X5FH~uZYvSz;bxpPyG$d7kUPRQpk zKMs>NwoA6PJr7R~Wj=E56UA3VQx59~>=nYfu<y&Ul%DU!2n;yBmBgtJSiCMTX<`B{?#TRr#Jo@;k+v@wjxJBXBBb9HSG!1#a zx=!oH?+Yl-Vt&Fg#TG%60ZrKGeToJiRe6!m_0Hzr|W*?6#mk&-QbyiSZ{%x~Qii{0(fS-|1l zkN*XUU$8*;>7tLKXm=9u8riSROatO~Gr41&Bc2Z_gBLJO#}$h2W?o8N(QNmEyV~mx z7;%s4TptT%5_lV|tmZ*7bEQ!dzxk-ia?*}2SobFOpHfo=cy${WNtq=wyS+3AkR|jF$RV(yq7NJyx?!3hceN{E)`1EP*;v!-$rj}dfb?*PmMr~tVq2tyL z4SWwe8HN;T0eA%)pCsEn>#9r*ddBJVhF5(*+i@el8xYNX`WSW@l@=dF_P!hHcdrRn_LzJGRgLW*v{s z((Wh|E0tm|0Aj|gn$UMPl+}9uBVC$2Q?9$JS1qIzrac2v?vw7c!t?0W)XrzbN6t0* zL`U%cXrYSBRUryyN{i&my;qoj_eRkm@07`tY=kzzY+}sP?VnhN&E_GK_gVH91LpH% z8z*CJEIzjQMtf(9Lbmm@{%d;iJX$aHZgLlW8qE*}N%-Vk-gMYt_Egp0W_ zmyt?K{l$%vwMAq;DwrzK#CWw1y%tlodgwFEW!-MhIut&i6mBnZZkbaS4%2GcIK zQYx2NwPKS-%_wdr{^4b#+u|j2z~uIgK5Mbj?efETaH+ZVzK`>CWXGf@&;rBn-{Ge_ zSAGa)(DpgeU2*!RM^Ci0vQwPoarXjjIMJ@La|(&=Pk2$AnQ41iVr6ZhaAVhTI)Ni> ztaV}7^Hc|uY^(p%2}u@65i9!0l&3Y|hhqXiSx)%J;nV6vCQQ=-k&Ab)98Kvga}6NT zkV-j+19;gk10jc$XE?0K)VO`e1CYSJuI3Dd+^6Jjt2=8rPSTp>5gw@^30yY(_F;8( zbv{8N$pU+qyWe+rvsgFxBoKb(YIp=$9sAgQdhWo$sR+Lk+MwZ+!@cb68~q|q;qK?2 z?*6pznrB3~=e2#G_@I^)(@Mcr)6K$y2QuYlgJx#h1qkin+VXOpnOgboW2&q!&D?9+omF+f#nnBkw7;_1n^w7o zm@{%W-sk7zn6d(>5b$;2ejQ4;#LW9SiSh@{-AzjOb2+*%m4unuoB3jO@mrr)jZ zeYq+>retBXvV^c0eH>2X!ZRQ_e)ju9@#*3_FtkDIJfs0xDL?_R)JzQ+3K1=I1wSsN zZ!)^yo+l6OtfzAy;##`4qQk?Em)e)S)xo?%3*4F7V>(&U*k?Cvy#7qBjtZ15ED8zs zzX~hU(v&y?Y+#%UM=CS@rE`lmr4C)K{-tIL4tz|@V3qR@*E)L*kA3>YF|#hy>B(c6 z@?CITh3EvALl?ES1-Ds-{lUu!FnqwwD?jVm%{?!uc6j9vQEuYo|MX?q`*?MA-qmPz zj)~P}FMj9vJ|bc0w8N9u$#<66cLXh1nY5%kiSdnnLRhGbQG+sK4Kd$_hf!uU%}fsu z$4jJXMmk8XsF-nI&4?wU_rJd_yqd?1n)&l{gnQ_Tev6aNzj)taKe24<#U30BB}Xr0 z^WI%Ahe<9)i)+uWInOpA4Z6qyJZ?{f&X-feU-|p|Puu%kL*M`P8EwT|%c1Zf3KH zP#b%}E*?JpkY|0Fl<@TtIdJyMIy7(E;d5zGPUL(60L(7;qz#m{!P_p^Tbw%U0MG~M zO>4H!A!`X^ocX{yV6S-q+sM_*1~LT8kYCL#Y}xiqS&ipJXwk0-7Y%kK9=DiWE#_hfYbduj{ql0 z_LrEPHvuyO%>B;H&yE=csq9%Nlt7~Yc}g;z$Uaca!Vu|qujB)aT>Y0R-pMxU%+3y( zV(D&CSr$*xQ(Yw4seA))8M8u%?2(PL-XpigGVE6|Y8cA?PcA%wXz7W0M%`S?W z!QRcPz)_kx((XqWLx4@2@BH7ZZ#L#PE1g4U<;Pd-`asHldH(Y0-AVyi5fH{-mn;a9 z0{Ca+Ap@7ZaU948H?iihWjliC#lQ3?(Cq7>94}xtAT4wN?(M= z?1Tnm*Y4VKZ)S0O_WQ6INxE%XXN`YMqWN#hyZiGjIXwdx5piAtWucQW=FIO8veq2A zo`!xe52gut+l6 zDGU7$ag*iYHWXZR#Lh@2>}{`MOk5LoSGb{kv0Pk-Gd{&9xPdNc3TkJ-By_gQ$xXU1 zU!UzpR^TR05$i((@vXEGMmg&vjlRUAc1SP$r*HU>HY~19&JU29r%aX#MRMa%wnY(; zoCXr | bwi-sticky-note | secure note item type | | | bwi-users | user group | | | bwi-vault | general vault | +| | bwi-vault-f | general vault | ## Actions @@ -68,8 +69,10 @@ or an options menu icon. | | bwi-minus-square | unselect all action | | | bwi-paste | paste from clipboard action | | | bwi-pencil-square | edit action | +| | bwi-popout | popout action | | | bwi-play | start or play action | | | bwi-plus | new or add option in contained buttons/links | +| | bwi-plus-f | new or add option in contained buttons/links | | | bwi-plus-circle | new or add option in text buttons/links | | | bwi-plus-square | - | | | bwi-refresh | "re"-action; such as refresh or regenerate | @@ -101,6 +104,7 @@ or an options menu icon. | | bwi-arrow-circle-left | - | | | bwi-arrow-circle-right | - | | | bwi-arrow-circle-up | - | +| | bwi-back | back arrow | | | bwi-caret-down | table sort order | | | bwi-caret-right | - | | | bwi-caret-up | table sort order | @@ -128,6 +132,7 @@ or an options menu icon. | | bwi-bolt | deprecated "danger" icon | | | bwi-bookmark | bookmark or save related actions | | | bwi-browser | web browser | +| | bwi-browser-alt | web browser | | | bwi-bug | test or debug action | | | bwi-camera | actions related to camera use | | | bwi-chain-broken | unlink action | @@ -138,6 +143,7 @@ or an options menu icon. | | bwi-cut | cut or omit actions | | | bwi-dashboard | statuses or dashboard views | | | bwi-desktop | desktop client | +| | bwi-desktop-alt | desktop client | | | bwi-dollar | account credit | | | bwi-file | file related objects or actions | | | bwi-file-pdf | PDF related object or actions | @@ -153,7 +159,9 @@ or an options menu icon. | | bwi-lightbulb | - | | | bwi-link | link action | | | bwi-mobile | mobile client | +| | bwi-mobile-alt | mobile client | | | bwi-money | - | +| | bwi-msp | - | | | bwi-paperclip | attachments | | | bwi-passkey | passkey | | | bwi-pencil | editing | @@ -176,6 +184,8 @@ or an options menu icon. | | bwi-user | relates to current user or organization member | | | bwi-user-circle | - | | | bwi-user-f | - | +| | bwi-user-monitor | - | +| | bwi-wand | - | | | bwi-wireless | - | | | bwi-wrench | tools or additional configuration options | @@ -203,4 +213,5 @@ or an options menu icon. | | bwi-twitch | link to our Twitch page | | | bwi-twitter | link to our twitter page | | | bwi-windows | support for windows | +| | bwi-x-twitter | x version of twitter | | | bwi-youtube | link to our youtube page | From 91f1d9fb86142805d0774182c3ee6234e13946e3 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Fri, 19 Apr 2024 16:44:24 -0400 Subject: [PATCH 025/110] Auth/PM-6689 - Migrate Security Stamp to Token Service and State Provider (#8792) * PM-6689 - Add security stamp to Token state * PM-6689 - Remove Security Stamp from account and state service * PM-6689 - Add security stamp get and set to token service + abstraction + tests * PM-6689 - Add migration for security stamp, test it, and register it with migrator * PM-6689 - Update sync service + deps to use token service. * PM-6689 - Cleanup missed usages of account tokens which has been removed. * PM-6689 - Per PR feedback, remove unnecessary data migration as the security stamp is only in memory and doesn't need to be migrated. --- .../browser/src/background/main.background.ts | 1 + apps/cli/src/bw.ts | 1 + .../src/services/jslib-services.module.ts | 1 + .../login-strategies/login.strategy.spec.ts | 4 - .../common/login-strategies/login.strategy.ts | 9 +-- .../src/auth/abstractions/token.service.ts | 6 ++ .../src/auth/services/token.service.spec.ts | 79 +++++++++++++++++++ .../common/src/auth/services/token.service.ts | 25 ++++++ .../src/auth/services/token.state.spec.ts | 2 + libs/common/src/auth/services/token.state.ts | 5 ++ .../platform/abstractions/state.service.ts | 2 - .../models/domain/account-tokens.spec.ts | 9 --- .../platform/models/domain/account.spec.ts | 4 +- .../src/platform/models/domain/account.ts | 18 ----- .../src/platform/services/state.service.ts | 17 ---- .../src/vault/services/sync/sync.service.ts | 6 +- 16 files changed, 126 insertions(+), 63 deletions(-) delete mode 100644 libs/common/src/platform/models/domain/account-tokens.spec.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 622a115067c..7d3471e598b 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -788,6 +788,7 @@ export default class MainBackground { this.avatarService, logoutCallback, this.billingAccountProfileStateService, + this.tokenService, ); this.eventUploadService = new EventUploadService( this.apiService, diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index e784997d824..c3c4042adff 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -633,6 +633,7 @@ export class Main { this.avatarService, async (expired: boolean) => await this.logout(), this.billingAccountProfileStateService, + this.tokenService, ); this.totpService = new TotpService(this.cryptoFunctionService, this.logService); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 204ff5a294b..9d311d34af5 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -628,6 +628,7 @@ const safeProviders: SafeProvider[] = [ AvatarServiceAbstraction, LOGOUT_CALLBACK, BillingAccountProfileStateService, + TokenServiceAbstraction, ], }), safeProvider({ diff --git a/libs/auth/src/common/login-strategies/login.strategy.spec.ts b/libs/auth/src/common/login-strategies/login.strategy.spec.ts index 431f736e949..e0833342ce3 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -27,7 +27,6 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Account, AccountProfile, - AccountTokens, AccountKeys, } from "@bitwarden/common/platform/models/domain/account"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; @@ -213,9 +212,6 @@ describe("LoginStrategy", () => { kdfType: kdf, }, }, - tokens: { - ...new AccountTokens(), - }, keys: new AccountKeys(), }), ); diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index a6dc1931839..a73c32e1208 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -27,11 +27,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { - Account, - AccountProfile, - AccountTokens, -} from "@bitwarden/common/platform/models/domain/account"; +import { Account, AccountProfile } from "@bitwarden/common/platform/models/domain/account"; import { UserId } from "@bitwarden/common/types/guid"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; @@ -192,9 +188,6 @@ export abstract class LoginStrategy { kdfType: tokenResponse.kdf, }, }, - tokens: { - ...new AccountTokens(), - }, }), ); diff --git a/libs/common/src/auth/abstractions/token.service.ts b/libs/common/src/auth/abstractions/token.service.ts index 75bb3838828..fc3bd317f47 100644 --- a/libs/common/src/auth/abstractions/token.service.ts +++ b/libs/common/src/auth/abstractions/token.service.ts @@ -213,4 +213,10 @@ export abstract class TokenService { * @returns A promise that resolves with a boolean representing the user's external authN status. */ getIsExternal: () => Promise; + + /** Gets the active or passed in user's security stamp */ + getSecurityStamp: (userId?: UserId) => Promise; + + /** Sets the security stamp for the active or passed in user */ + setSecurityStamp: (securityStamp: string, userId?: UserId) => Promise; } diff --git a/libs/common/src/auth/services/token.service.spec.ts b/libs/common/src/auth/services/token.service.spec.ts index d32c4d8e1cd..3e92053d2f7 100644 --- a/libs/common/src/auth/services/token.service.spec.ts +++ b/libs/common/src/auth/services/token.service.spec.ts @@ -23,6 +23,7 @@ import { EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, REFRESH_TOKEN_DISK, REFRESH_TOKEN_MEMORY, + SECURITY_STAMP_MEMORY, } from "./token.state"; describe("TokenService", () => { @@ -2191,6 +2192,84 @@ describe("TokenService", () => { }); }); + describe("Security Stamp methods", () => { + const mockSecurityStamp = "securityStamp"; + + describe("setSecurityStamp", () => { + it("should throw an error if no user id is provided and there is no active user in global state", async () => { + // Act + // note: don't await here because we want to test the error + const result = tokenService.setSecurityStamp(mockSecurityStamp); + // Assert + await expect(result).rejects.toThrow("User id not found. Cannot set security stamp."); + }); + + it("should set the security stamp in memory when there is an active user in global state", async () => { + // Arrange + globalStateProvider + .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) + .stateSubject.next(userIdFromAccessToken); + + // Act + await tokenService.setSecurityStamp(mockSecurityStamp); + + // Assert + expect( + singleUserStateProvider.getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY).nextMock, + ).toHaveBeenCalledWith(mockSecurityStamp); + }); + + it("should set the security stamp in memory for the specified user id", async () => { + // Act + await tokenService.setSecurityStamp(mockSecurityStamp, userIdFromAccessToken); + + // Assert + expect( + singleUserStateProvider.getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY).nextMock, + ).toHaveBeenCalledWith(mockSecurityStamp); + }); + }); + + describe("getSecurityStamp", () => { + it("should throw an error if no user id is provided and there is no active user in global state", async () => { + // Act + // note: don't await here because we want to test the error + const result = tokenService.getSecurityStamp(); + // Assert + await expect(result).rejects.toThrow("User id not found. Cannot get security stamp."); + }); + + it("should return the security stamp from memory with no user id specified (uses global active user)", async () => { + // Arrange + globalStateProvider + .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) + .stateSubject.next(userIdFromAccessToken); + + singleUserStateProvider + .getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY) + .stateSubject.next([userIdFromAccessToken, mockSecurityStamp]); + + // Act + const result = await tokenService.getSecurityStamp(); + + // Assert + expect(result).toEqual(mockSecurityStamp); + }); + + it("should return the security stamp from memory for the specified user id", async () => { + // Arrange + singleUserStateProvider + .getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY) + .stateSubject.next([userIdFromAccessToken, mockSecurityStamp]); + + // Act + const result = await tokenService.getSecurityStamp(userIdFromAccessToken); + // Assert + expect(result).toEqual(mockSecurityStamp); + }); + }); + }); + // Helpers function createTokenService(supportsSecureStorage: boolean) { return new TokenService( diff --git a/libs/common/src/auth/services/token.service.ts b/libs/common/src/auth/services/token.service.ts index c24a2c186b8..40036a8453c 100644 --- a/libs/common/src/auth/services/token.service.ts +++ b/libs/common/src/auth/services/token.service.ts @@ -32,6 +32,7 @@ import { EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, REFRESH_TOKEN_DISK, REFRESH_TOKEN_MEMORY, + SECURITY_STAMP_MEMORY, } from "./token.state"; export enum TokenStorageLocation { @@ -850,6 +851,30 @@ export class TokenService implements TokenServiceAbstraction { return Array.isArray(decoded.amr) && decoded.amr.includes("external"); } + async getSecurityStamp(userId?: UserId): Promise { + userId ??= await firstValueFrom(this.activeUserIdGlobalState.state$); + + if (!userId) { + throw new Error("User id not found. Cannot get security stamp."); + } + + const securityStamp = await this.getStateValueByUserIdAndKeyDef(userId, SECURITY_STAMP_MEMORY); + + return securityStamp; + } + + async setSecurityStamp(securityStamp: string, userId?: UserId): Promise { + userId ??= await firstValueFrom(this.activeUserIdGlobalState.state$); + + if (!userId) { + throw new Error("User id not found. Cannot set security stamp."); + } + + await this.singleUserStateProvider + .get(userId, SECURITY_STAMP_MEMORY) + .update((_) => securityStamp); + } + private async getStateValueByUserIdAndKeyDef( userId: UserId, storageLocation: UserKeyDefinition, diff --git a/libs/common/src/auth/services/token.state.spec.ts b/libs/common/src/auth/services/token.state.spec.ts index dc00fec383c..bb82410fac1 100644 --- a/libs/common/src/auth/services/token.state.spec.ts +++ b/libs/common/src/auth/services/token.state.spec.ts @@ -10,6 +10,7 @@ import { EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, REFRESH_TOKEN_DISK, REFRESH_TOKEN_MEMORY, + SECURITY_STAMP_MEMORY, } from "./token.state"; describe.each([ @@ -22,6 +23,7 @@ describe.each([ [API_KEY_CLIENT_ID_MEMORY, "apiKeyClientIdMemory"], [API_KEY_CLIENT_SECRET_DISK, "apiKeyClientSecretDisk"], [API_KEY_CLIENT_SECRET_MEMORY, "apiKeyClientSecretMemory"], + [SECURITY_STAMP_MEMORY, "securityStamp"], ])( "deserializes state key definitions", ( diff --git a/libs/common/src/auth/services/token.state.ts b/libs/common/src/auth/services/token.state.ts index 458d6846c17..57d85f2a559 100644 --- a/libs/common/src/auth/services/token.state.ts +++ b/libs/common/src/auth/services/token.state.ts @@ -69,3 +69,8 @@ export const API_KEY_CLIENT_SECRET_MEMORY = new UserKeyDefinition( clearOn: [], // Manually handled }, ); + +export const SECURITY_STAMP_MEMORY = new UserKeyDefinition(TOKEN_MEMORY, "securityStamp", { + deserializer: (securityStamp) => securityStamp, + clearOn: ["logout"], +}); diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index 051604f0ae5..f1d4b3848ef 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -181,8 +181,6 @@ export abstract class StateService { * Sets the user's Pin, encrypted by the user key */ setProtectedPin: (value: string, options?: StorageOptions) => Promise; - getSecurityStamp: (options?: StorageOptions) => Promise; - setSecurityStamp: (value: string, options?: StorageOptions) => Promise; getUserId: (options?: StorageOptions) => Promise; getVaultTimeout: (options?: StorageOptions) => Promise; setVaultTimeout: (value: number, options?: StorageOptions) => Promise; diff --git a/libs/common/src/platform/models/domain/account-tokens.spec.ts b/libs/common/src/platform/models/domain/account-tokens.spec.ts deleted file mode 100644 index 733b3908e9a..00000000000 --- a/libs/common/src/platform/models/domain/account-tokens.spec.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { AccountTokens } from "./account"; - -describe("AccountTokens", () => { - describe("fromJSON", () => { - it("should deserialize to an instance of itself", () => { - expect(AccountTokens.fromJSON({})).toBeInstanceOf(AccountTokens); - }); - }); -}); diff --git a/libs/common/src/platform/models/domain/account.spec.ts b/libs/common/src/platform/models/domain/account.spec.ts index 0c76c16cc2d..77c242b6ff5 100644 --- a/libs/common/src/platform/models/domain/account.spec.ts +++ b/libs/common/src/platform/models/domain/account.spec.ts @@ -1,4 +1,4 @@ -import { Account, AccountKeys, AccountProfile, AccountSettings, AccountTokens } from "./account"; +import { Account, AccountKeys, AccountProfile, AccountSettings } from "./account"; describe("Account", () => { describe("fromJSON", () => { @@ -10,14 +10,12 @@ describe("Account", () => { const keysSpy = jest.spyOn(AccountKeys, "fromJSON"); const profileSpy = jest.spyOn(AccountProfile, "fromJSON"); const settingsSpy = jest.spyOn(AccountSettings, "fromJSON"); - const tokensSpy = jest.spyOn(AccountTokens, "fromJSON"); Account.fromJSON({}); expect(keysSpy).toHaveBeenCalled(); expect(profileSpy).toHaveBeenCalled(); expect(settingsSpy).toHaveBeenCalled(); - expect(tokensSpy).toHaveBeenCalled(); }); }); }); diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index 5a9a7646962..cd416ec1f94 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -171,24 +171,11 @@ export class AccountSettings { } } -export class AccountTokens { - securityStamp?: string; - - static fromJSON(obj: Jsonify): AccountTokens { - if (obj == null) { - return null; - } - - return Object.assign(new AccountTokens(), obj); - } -} - export class Account { data?: AccountData = new AccountData(); keys?: AccountKeys = new AccountKeys(); profile?: AccountProfile = new AccountProfile(); settings?: AccountSettings = new AccountSettings(); - tokens?: AccountTokens = new AccountTokens(); constructor(init: Partial) { Object.assign(this, { @@ -208,10 +195,6 @@ export class Account { ...new AccountSettings(), ...init?.settings, }, - tokens: { - ...new AccountTokens(), - ...init?.tokens, - }, }); } @@ -225,7 +208,6 @@ export class Account { data: AccountData.fromJSON(json?.data), profile: AccountProfile.fromJSON(json?.profile), settings: AccountSettings.fromJSON(json?.settings), - tokens: AccountTokens.fromJSON(json?.tokens), }); } } diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index f660cd7a342..d0a55d7a47c 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -839,23 +839,6 @@ export class StateService< ); } - async getSecurityStamp(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) - )?.tokens?.securityStamp; - } - - async setSecurityStamp(value: string, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - account.tokens.securityStamp = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - } - async getUserId(options?: StorageOptions): Promise { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) diff --git a/libs/common/src/vault/services/sync/sync.service.ts b/libs/common/src/vault/services/sync/sync.service.ts index ff8e9f1f4f5..73869ff488e 100644 --- a/libs/common/src/vault/services/sync/sync.service.ts +++ b/libs/common/src/vault/services/sync/sync.service.ts @@ -15,6 +15,7 @@ import { AccountService } from "../../../auth/abstractions/account.service"; import { AvatarService } from "../../../auth/abstractions/avatar.service"; import { KeyConnectorService } from "../../../auth/abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "../../../auth/abstractions/master-password.service.abstraction"; +import { TokenService } from "../../../auth/abstractions/token.service"; import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason"; import { DomainSettingsService } from "../../../autofill/services/domain-settings.service"; import { BillingAccountProfileStateService } from "../../../billing/abstractions/account/billing-account-profile-state.service"; @@ -73,6 +74,7 @@ export class SyncService implements SyncServiceAbstraction { private avatarService: AvatarService, private logoutCallback: (expired: boolean) => Promise, private billingAccountProfileStateService: BillingAccountProfileStateService, + private tokenService: TokenService, ) {} async getLastSync(): Promise { @@ -309,7 +311,7 @@ export class SyncService implements SyncServiceAbstraction { } private async syncProfile(response: ProfileResponse) { - const stamp = await this.stateService.getSecurityStamp(); + const stamp = await this.tokenService.getSecurityStamp(response.id as UserId); if (stamp != null && stamp !== response.securityStamp) { if (this.logoutCallback != null) { await this.logoutCallback(true); @@ -323,7 +325,7 @@ export class SyncService implements SyncServiceAbstraction { await this.cryptoService.setProviderKeys(response.providers); await this.cryptoService.setOrgKeys(response.organizations, response.providerOrganizations); await this.avatarService.setSyncAvatarColor(response.id as UserId, response.avatarColor); - await this.stateService.setSecurityStamp(response.securityStamp); + await this.tokenService.setSecurityStamp(response.securityStamp, response.id as UserId); await this.stateService.setEmailVerified(response.emailVerified); await this.billingAccountProfileStateService.setHasPremium( From f829cdd8a724dae42ab93aaadf930d433dd2cc4d Mon Sep 17 00:00:00 2001 From: aj-rosado <109146700+aj-rosado@users.noreply.github.com> Date: Mon, 22 Apr 2024 10:18:11 +0100 Subject: [PATCH 026/110] [PM-7603] Fix individual vault export not appearing on Event Logs (#8829) * Added validation to update User_ClientExportedVault on events even with no organization id or cipher id * Fixed missing data and validation --- .../common/src/services/event/event-collection.service.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/libs/common/src/services/event/event-collection.service.ts b/libs/common/src/services/event/event-collection.service.ts index 641c1b4d44b..1482bb8b61e 100644 --- a/libs/common/src/services/event/event-collection.service.ts +++ b/libs/common/src/services/event/event-collection.service.ts @@ -36,7 +36,7 @@ export class EventCollectionService implements EventCollectionServiceAbstraction const userId = await firstValueFrom(this.stateProvider.activeUserId$); const eventStore = this.stateProvider.getUser(userId, EVENT_COLLECTION); - if (!(await this.shouldUpdate(cipherId, organizationId))) { + if (!(await this.shouldUpdate(cipherId, organizationId, eventType))) { return; } @@ -64,6 +64,7 @@ export class EventCollectionService implements EventCollectionServiceAbstraction private async shouldUpdate( cipherId: string = null, organizationId: string = null, + eventType: EventType = null, ): Promise { const orgIds$ = this.organizationService.organizations$.pipe( map((orgs) => orgs?.filter((o) => o.useEvents)?.map((x) => x.id) ?? []), @@ -85,6 +86,11 @@ export class EventCollectionService implements EventCollectionServiceAbstraction return false; } + // Individual vault export doesn't need cipher id or organization id. + if (eventType == EventType.User_ClientExportedVault) { + return true; + } + // If the cipher is null there must be an organization id provided if (cipher == null && organizationId == null) { return false; From b5362ca1ce6f4260b3ae52eecc6b0e9ace325945 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Mon, 22 Apr 2024 08:55:19 -0400 Subject: [PATCH 027/110] Browser MV3: Default store values to session storage (#8844) * Introduce browser large object storage location. This location is encrypted and serialized to disk in order to allow for storage of uncountable things like vault items that take a significant amount of time to prepare, but are not guaranteed to fit within session storage. however, limit the need to write to disk is a big benefit, so _most_ things are written to storage.session instead, where things specifically flagged as large will be moved to disk-backed memory * Store derived values in large object store for browser * Fix AbstractMemoryStorageService implementation --- .../browser/src/background/main.background.ts | 18 ++++++---- .../derived-state-provider.factory.ts | 10 +++--- .../browser-memory-storage.service.ts | 11 +++++- .../background-derived-state.provider.ts | 9 ++++- .../state/background-derived-state.ts | 2 +- .../state/derived-state-interactions.spec.ts | 28 +++++++++------ .../foreground-derived-state.provider.ts | 9 +++-- .../state/foreground-derived-state.spec.ts | 3 +- .../state/foreground-derived-state.ts | 3 +- .../browser-storage-service.provider.ts | 35 +++++++++++++++++++ .../src/popup/services/services.module.ts | 32 ++++++++++++++++- apps/cli/src/bw.ts | 4 +-- apps/desktop/src/main.ts | 2 +- .../src/services/jslib-services.module.ts | 2 +- libs/common/spec/fake-state-provider.ts | 4 +-- .../src/platform/state/derive-definition.ts | 4 +-- .../default-derived-state.provider.ts | 17 ++++++--- .../default-derived-state.spec.ts | 16 ++++----- .../src/platform/state/state-definition.ts | 4 ++- .../src/platform/state/state-definitions.ts | 16 ++++++--- 20 files changed, 171 insertions(+), 58 deletions(-) create mode 100644 apps/browser/src/platform/storage/browser-storage-service.provider.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 7d3471e598b..0a9ad44962c 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -111,7 +111,6 @@ import { KeyGenerationService } from "@bitwarden/common/platform/services/key-ge import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; -import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; import { SystemService } from "@bitwarden/common/platform/services/system.service"; import { UserKeyInitService } from "@bitwarden/common/platform/services/user-key-init.service"; import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service"; @@ -226,6 +225,7 @@ import { BackgroundPlatformUtilsService } from "../platform/services/platform-ut import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service"; import { BackgroundDerivedStateProvider } from "../platform/state/background-derived-state.provider"; import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service"; +import { BrowserStorageServiceProvider } from "../platform/storage/browser-storage-service.provider"; import { ForegroundMemoryStorageService } from "../platform/storage/foreground-memory-storage.service"; import { fromChromeRuntimeMessaging } from "../platform/utils/from-chrome-runtime-messaging"; import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service"; @@ -246,6 +246,8 @@ export default class MainBackground { secureStorageService: AbstractStorageService; memoryStorageService: AbstractMemoryStorageService; memoryStorageForStateProviders: AbstractMemoryStorageService & ObservableStorageService; + largeObjectMemoryStorageForStateProviders: AbstractMemoryStorageService & + ObservableStorageService; i18nService: I18nServiceAbstraction; platformUtilsService: PlatformUtilsServiceAbstraction; logService: LogServiceAbstraction; @@ -424,12 +426,16 @@ export default class MainBackground { ? mv3MemoryStorageCreator("stateService") : new MemoryStorageService(); this.memoryStorageForStateProviders = BrowserApi.isManifestVersion(3) - ? mv3MemoryStorageCreator("stateProviders") - : new BackgroundMemoryStorageService(); + ? new BrowserMemoryStorageService() // mv3 stores to storage.session + : new BackgroundMemoryStorageService(); // mv2 stores to memory + this.largeObjectMemoryStorageForStateProviders = BrowserApi.isManifestVersion(3) + ? mv3MemoryStorageCreator("stateProviders") // mv3 stores to local-backed session storage + : this.memoryStorageForStateProviders; // mv2 stores to the same location - const storageServiceProvider = new StorageServiceProvider( + const storageServiceProvider = new BrowserStorageServiceProvider( this.storageService, this.memoryStorageForStateProviders, + this.largeObjectMemoryStorageForStateProviders, ); this.globalStateProvider = new DefaultGlobalStateProvider(storageServiceProvider); @@ -466,9 +472,7 @@ export default class MainBackground { this.accountService, this.singleUserStateProvider, ); - this.derivedStateProvider = new BackgroundDerivedStateProvider( - this.memoryStorageForStateProviders, - ); + this.derivedStateProvider = new BackgroundDerivedStateProvider(storageServiceProvider); this.stateProvider = new DefaultStateProvider( this.activeUserStateProvider, this.singleUserStateProvider, diff --git a/apps/browser/src/platform/background/service-factories/derived-state-provider.factory.ts b/apps/browser/src/platform/background/service-factories/derived-state-provider.factory.ts index 4f329c93d5e..4025d01950f 100644 --- a/apps/browser/src/platform/background/service-factories/derived-state-provider.factory.ts +++ b/apps/browser/src/platform/background/service-factories/derived-state-provider.factory.ts @@ -4,14 +4,14 @@ import { BackgroundDerivedStateProvider } from "../../state/background-derived-s import { CachedServices, FactoryOptions, factory } from "./factory-options"; import { - MemoryStorageServiceInitOptions, - observableMemoryStorageServiceFactory, -} from "./storage-service.factory"; + StorageServiceProviderInitOptions, + storageServiceProviderFactory, +} from "./storage-service-provider.factory"; type DerivedStateProviderFactoryOptions = FactoryOptions; export type DerivedStateProviderInitOptions = DerivedStateProviderFactoryOptions & - MemoryStorageServiceInitOptions; + StorageServiceProviderInitOptions; export async function derivedStateProviderFactory( cache: { derivedStateProvider?: DerivedStateProvider } & CachedServices, @@ -22,6 +22,6 @@ export async function derivedStateProviderFactory( "derivedStateProvider", opts, async () => - new BackgroundDerivedStateProvider(await observableMemoryStorageServiceFactory(cache, opts)), + new BackgroundDerivedStateProvider(await storageServiceProviderFactory(cache, opts)), ); } diff --git a/apps/browser/src/platform/services/browser-memory-storage.service.ts b/apps/browser/src/platform/services/browser-memory-storage.service.ts index f824a1df0de..b067dc5a128 100644 --- a/apps/browser/src/platform/services/browser-memory-storage.service.ts +++ b/apps/browser/src/platform/services/browser-memory-storage.service.ts @@ -1,7 +1,16 @@ +import { AbstractMemoryStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; + import AbstractChromeStorageService from "./abstractions/abstract-chrome-storage-api.service"; -export default class BrowserMemoryStorageService extends AbstractChromeStorageService { +export default class BrowserMemoryStorageService + extends AbstractChromeStorageService + implements AbstractMemoryStorageService +{ constructor() { super(chrome.storage.session); } + type = "MemoryStorageService" as const; + getBypassCache(key: string): Promise { + return this.get(key); + } } diff --git a/apps/browser/src/platform/state/background-derived-state.provider.ts b/apps/browser/src/platform/state/background-derived-state.provider.ts index 95eec711132..f3d217789ed 100644 --- a/apps/browser/src/platform/state/background-derived-state.provider.ts +++ b/apps/browser/src/platform/state/background-derived-state.provider.ts @@ -1,5 +1,9 @@ import { Observable } from "rxjs"; +import { + AbstractStorageService, + ObservableStorageService, +} from "@bitwarden/common/platform/abstractions/storage.service"; import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state"; // eslint-disable-next-line import/no-restricted-paths -- extending this class for this client import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/default-derived-state.provider"; @@ -12,11 +16,14 @@ export class BackgroundDerivedStateProvider extends DefaultDerivedStateProvider parentState$: Observable, deriveDefinition: DeriveDefinition, dependencies: TDeps, + storageLocation: [string, AbstractStorageService & ObservableStorageService], ): DerivedState { + const [cacheKey, storageService] = storageLocation; return new BackgroundDerivedState( parentState$, deriveDefinition, - this.memoryStorage, + storageService, + cacheKey, dependencies, ); } diff --git a/apps/browser/src/platform/state/background-derived-state.ts b/apps/browser/src/platform/state/background-derived-state.ts index 7a7146aa886..c62795acdcd 100644 --- a/apps/browser/src/platform/state/background-derived-state.ts +++ b/apps/browser/src/platform/state/background-derived-state.ts @@ -23,10 +23,10 @@ export class BackgroundDerivedState< parentState$: Observable, deriveDefinition: DeriveDefinition, memoryStorage: AbstractStorageService & ObservableStorageService, + portName: string, dependencies: TDeps, ) { super(parentState$, deriveDefinition, memoryStorage, dependencies); - const portName = deriveDefinition.buildCacheKey(); // listen for foreground derived states to connect BrowserApi.addListener(chrome.runtime.onConnect, (port) => { diff --git a/apps/browser/src/platform/state/derived-state-interactions.spec.ts b/apps/browser/src/platform/state/derived-state-interactions.spec.ts index d709c401af0..a5df01bc989 100644 --- a/apps/browser/src/platform/state/derived-state-interactions.spec.ts +++ b/apps/browser/src/platform/state/derived-state-interactions.spec.ts @@ -38,14 +38,21 @@ describe("foreground background derived state interactions", () => { let memoryStorage: FakeStorageService; const initialParent = "2020-01-01"; const ngZone = mock(); + const portName = "testPort"; beforeEach(() => { mockPorts(); parentState$ = new Subject(); memoryStorage = new FakeStorageService(); - background = new BackgroundDerivedState(parentState$, deriveDefinition, memoryStorage, {}); - foreground = new ForegroundDerivedState(deriveDefinition, memoryStorage, ngZone); + background = new BackgroundDerivedState( + parentState$, + deriveDefinition, + memoryStorage, + portName, + {}, + ); + foreground = new ForegroundDerivedState(deriveDefinition, memoryStorage, portName, ngZone); }); afterEach(() => { @@ -65,7 +72,12 @@ describe("foreground background derived state interactions", () => { }); it("should initialize a late-connected foreground", async () => { - const newForeground = new ForegroundDerivedState(deriveDefinition, memoryStorage, ngZone); + const newForeground = new ForegroundDerivedState( + deriveDefinition, + memoryStorage, + portName, + ngZone, + ); const backgroundEmissions = trackEmissions(background.state$); parentState$.next(initialParent); await awaitAsync(); @@ -82,8 +94,6 @@ describe("foreground background derived state interactions", () => { const dateString = "2020-12-12"; const emissions = trackEmissions(background.state$); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises await foreground.forceValue(new Date(dateString)); await awaitAsync(); @@ -99,9 +109,7 @@ describe("foreground background derived state interactions", () => { expect(foreground["port"]).toBeDefined(); const newDate = new Date(); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - foreground.forceValue(newDate); + await foreground.forceValue(newDate); await awaitAsync(); expect(connectMock.mock.calls.length).toBe(initialConnectCalls); @@ -114,9 +122,7 @@ describe("foreground background derived state interactions", () => { expect(foreground["port"]).toBeUndefined(); const newDate = new Date(); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - foreground.forceValue(newDate); + await foreground.forceValue(newDate); await awaitAsync(); expect(connectMock.mock.calls.length).toBe(initialConnectCalls + 1); diff --git a/apps/browser/src/platform/state/foreground-derived-state.provider.ts b/apps/browser/src/platform/state/foreground-derived-state.provider.ts index ccefb1157c1..d9262e3b6e7 100644 --- a/apps/browser/src/platform/state/foreground-derived-state.provider.ts +++ b/apps/browser/src/platform/state/foreground-derived-state.provider.ts @@ -5,6 +5,7 @@ import { AbstractStorageService, ObservableStorageService, } from "@bitwarden/common/platform/abstractions/storage.service"; +import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state"; // eslint-disable-next-line import/no-restricted-paths -- extending this class for this client import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/default-derived-state.provider"; @@ -14,16 +15,18 @@ import { ForegroundDerivedState } from "./foreground-derived-state"; export class ForegroundDerivedStateProvider extends DefaultDerivedStateProvider { constructor( - memoryStorage: AbstractStorageService & ObservableStorageService, + storageServiceProvider: StorageServiceProvider, private ngZone: NgZone, ) { - super(memoryStorage); + super(storageServiceProvider); } override buildDerivedState( _parentState$: Observable, deriveDefinition: DeriveDefinition, _dependencies: TDeps, + storageLocation: [string, AbstractStorageService & ObservableStorageService], ): DerivedState { - return new ForegroundDerivedState(deriveDefinition, this.memoryStorage, this.ngZone); + const [cacheKey, storageService] = storageLocation; + return new ForegroundDerivedState(deriveDefinition, storageService, cacheKey, this.ngZone); } } diff --git a/apps/browser/src/platform/state/foreground-derived-state.spec.ts b/apps/browser/src/platform/state/foreground-derived-state.spec.ts index fce672a5ef5..2c29f39bc12 100644 --- a/apps/browser/src/platform/state/foreground-derived-state.spec.ts +++ b/apps/browser/src/platform/state/foreground-derived-state.spec.ts @@ -33,13 +33,14 @@ jest.mock("../browser/run-inside-angular.operator", () => { describe("ForegroundDerivedState", () => { let sut: ForegroundDerivedState; let memoryStorage: FakeStorageService; + const portName = "testPort"; const ngZone = mock(); beforeEach(() => { memoryStorage = new FakeStorageService(); memoryStorage.internalUpdateValuesRequireDeserialization(true); mockPorts(); - sut = new ForegroundDerivedState(deriveDefinition, memoryStorage, ngZone); + sut = new ForegroundDerivedState(deriveDefinition, memoryStorage, portName, ngZone); }); afterEach(() => { diff --git a/apps/browser/src/platform/state/foreground-derived-state.ts b/apps/browser/src/platform/state/foreground-derived-state.ts index b005697be89..b9dda763dfd 100644 --- a/apps/browser/src/platform/state/foreground-derived-state.ts +++ b/apps/browser/src/platform/state/foreground-derived-state.ts @@ -35,6 +35,7 @@ export class ForegroundDerivedState implements DerivedState { constructor( private deriveDefinition: DeriveDefinition, private memoryStorage: AbstractStorageService & ObservableStorageService, + private portName: string, private ngZone: NgZone, ) { this.storageKey = deriveDefinition.storageKey; @@ -88,7 +89,7 @@ export class ForegroundDerivedState implements DerivedState { return; } - this.port = chrome.runtime.connect({ name: this.deriveDefinition.buildCacheKey() }); + this.port = chrome.runtime.connect({ name: this.portName }); this.backgroundResponses$ = fromChromeEvent(this.port.onMessage).pipe( map(([message]) => message as DerivedStateMessage), diff --git a/apps/browser/src/platform/storage/browser-storage-service.provider.ts b/apps/browser/src/platform/storage/browser-storage-service.provider.ts new file mode 100644 index 00000000000..e0214baef44 --- /dev/null +++ b/apps/browser/src/platform/storage/browser-storage-service.provider.ts @@ -0,0 +1,35 @@ +import { + AbstractStorageService, + ObservableStorageService, +} from "@bitwarden/common/platform/abstractions/storage.service"; +import { + PossibleLocation, + StorageServiceProvider, +} from "@bitwarden/common/platform/services/storage-service.provider"; +// eslint-disable-next-line import/no-restricted-paths +import { ClientLocations } from "@bitwarden/common/platform/state/state-definition"; + +export class BrowserStorageServiceProvider extends StorageServiceProvider { + constructor( + diskStorageService: AbstractStorageService & ObservableStorageService, + limitedMemoryStorageService: AbstractStorageService & ObservableStorageService, + private largeObjectMemoryStorageService: AbstractStorageService & ObservableStorageService, + ) { + super(diskStorageService, limitedMemoryStorageService); + } + + override get( + defaultLocation: PossibleLocation, + overrides: Partial, + ): [location: PossibleLocation, service: AbstractStorageService & ObservableStorageService] { + const location = overrides["browser"] ?? defaultLocation; + switch (location) { + case "memory-large-object": + return ["memory-large-object", this.largeObjectMemoryStorageService]; + default: + // Pass in computed location to super because they could have + // override default "disk" with web "memory". + return super.get(location, overrides); + } + } +} diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 123e901e4e3..a7da6b76127 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -71,6 +71,7 @@ import { GlobalState } from "@bitwarden/common/platform/models/domain/global-sta import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; +import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service"; import { DerivedStateProvider, @@ -108,6 +109,7 @@ import { DefaultBrowserStateService } from "../../platform/services/default-brow import I18nService from "../../platform/services/i18n.service"; import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service"; import { ForegroundDerivedStateProvider } from "../../platform/state/foreground-derived-state.provider"; +import { BrowserStorageServiceProvider } from "../../platform/storage/browser-storage-service.provider"; import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service"; import { fromChromeRuntimeMessaging } from "../../platform/utils/from-chrome-runtime-messaging"; import { BrowserSendStateService } from "../../tools/popup/services/browser-send-state.service"; @@ -120,6 +122,10 @@ import { InitService } from "./init.service"; import { PopupCloseWarningService } from "./popup-close-warning.service"; import { PopupSearchService } from "./popup-search.service"; +const OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE = new SafeInjectionToken< + AbstractStorageService & ObservableStorageService +>("OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE"); + const needsBackgroundInit = BrowserPopupUtils.backgroundInitializationRequired(); const isPrivateMode = BrowserPopupUtils.inPrivateMode(); const mainBackground: MainBackground = needsBackgroundInit @@ -380,6 +386,21 @@ const safeProviders: SafeProvider[] = [ }, deps: [], }), + safeProvider({ + provide: OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE, + useFactory: ( + regularMemoryStorageService: AbstractMemoryStorageService & ObservableStorageService, + ) => { + if (BrowserApi.isManifestVersion(2)) { + return regularMemoryStorageService; + } + + return getBgService( + "largeObjectMemoryStorageForStateProviders", + )(); + }, + deps: [OBSERVABLE_MEMORY_STORAGE], + }), safeProvider({ provide: OBSERVABLE_DISK_STORAGE, useExisting: AbstractStorageService, @@ -466,7 +487,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: DerivedStateProvider, useClass: ForegroundDerivedStateProvider, - deps: [OBSERVABLE_MEMORY_STORAGE, NgZone], + deps: [StorageServiceProvider, NgZone], }), safeProvider({ provide: AutofillSettingsServiceAbstraction, @@ -542,6 +563,15 @@ const safeProviders: SafeProvider[] = [ }, deps: [], }), + safeProvider({ + provide: StorageServiceProvider, + useClass: BrowserStorageServiceProvider, + deps: [ + OBSERVABLE_DISK_STORAGE, + OBSERVABLE_MEMORY_STORAGE, + OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE, + ], + }), ]; @NgModule({ diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index c3c4042adff..437f807bc61 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -309,9 +309,7 @@ export class Main { this.singleUserStateProvider, ); - this.derivedStateProvider = new DefaultDerivedStateProvider( - this.memoryStorageForStateProviders, - ); + this.derivedStateProvider = new DefaultDerivedStateProvider(storageServiceProvider); this.stateProvider = new DefaultStateProvider( this.activeUserStateProvider, diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 0655e5600d2..bffd2002ff1 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -157,7 +157,7 @@ export class Main { activeUserStateProvider, singleUserStateProvider, globalStateProvider, - new DefaultDerivedStateProvider(this.memoryStorageForStateProviders), + new DefaultDerivedStateProvider(storageServiceProvider), ); this.environmentService = new DefaultEnvironmentService(stateProvider, accountService); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 9d311d34af5..27b182de5dc 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1047,7 +1047,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: DerivedStateProvider, useClass: DefaultDerivedStateProvider, - deps: [OBSERVABLE_MEMORY_STORAGE], + deps: [StorageServiceProvider], }), safeProvider({ provide: StateProvider, diff --git a/libs/common/spec/fake-state-provider.ts b/libs/common/spec/fake-state-provider.ts index 2078fe3abde..306ae00c215 100644 --- a/libs/common/spec/fake-state-provider.ts +++ b/libs/common/spec/fake-state-provider.ts @@ -249,11 +249,11 @@ export class FakeDerivedStateProvider implements DerivedStateProvider { deriveDefinition: DeriveDefinition, dependencies: TDeps, ): DerivedState { - let result = this.states.get(deriveDefinition.buildCacheKey()) as DerivedState; + let result = this.states.get(deriveDefinition.buildCacheKey("memory")) as DerivedState; if (result == null) { result = new FakeDerivedState(parentState$, deriveDefinition, dependencies); - this.states.set(deriveDefinition.buildCacheKey(), result); + this.states.set(deriveDefinition.buildCacheKey("memory"), result); } return result; } diff --git a/libs/common/src/platform/state/derive-definition.ts b/libs/common/src/platform/state/derive-definition.ts index 8f62d3a342c..9cb5eff3e8c 100644 --- a/libs/common/src/platform/state/derive-definition.ts +++ b/libs/common/src/platform/state/derive-definition.ts @@ -171,8 +171,8 @@ export class DeriveDefinition> = {}; - constructor(protected memoryStorage: AbstractStorageService & ObservableStorageService) {} + constructor(protected storageServiceProvider: StorageServiceProvider) {} get( parentState$: Observable, deriveDefinition: DeriveDefinition, dependencies: TDeps, ): DerivedState { - const cacheKey = deriveDefinition.buildCacheKey(); + // TODO: we probably want to support optional normal memory storage for browser + const [location, storageService] = this.storageServiceProvider.get("memory", { + browser: "memory-large-object", + }); + const cacheKey = deriveDefinition.buildCacheKey(location); const existingDerivedState = this.cache[cacheKey]; if (existingDerivedState != null) { // I have to cast out of the unknown generic but this should be safe if rules @@ -29,7 +34,10 @@ export class DefaultDerivedStateProvider implements DerivedStateProvider { return existingDerivedState as DefaultDerivedState; } - const newDerivedState = this.buildDerivedState(parentState$, deriveDefinition, dependencies); + const newDerivedState = this.buildDerivedState(parentState$, deriveDefinition, dependencies, [ + location, + storageService, + ]); this.cache[cacheKey] = newDerivedState; return newDerivedState; } @@ -38,11 +46,12 @@ export class DefaultDerivedStateProvider implements DerivedStateProvider { parentState$: Observable, deriveDefinition: DeriveDefinition, dependencies: TDeps, + storageLocation: [string, AbstractStorageService & ObservableStorageService], ): DerivedState { return new DefaultDerivedState( parentState$, deriveDefinition, - this.memoryStorage, + storageLocation[1], dependencies, ); } diff --git a/libs/common/src/platform/state/implementations/default-derived-state.spec.ts b/libs/common/src/platform/state/implementations/default-derived-state.spec.ts index 958a9386114..e3b1587e3a1 100644 --- a/libs/common/src/platform/state/implementations/default-derived-state.spec.ts +++ b/libs/common/src/platform/state/implementations/default-derived-state.spec.ts @@ -72,12 +72,12 @@ describe("DefaultDerivedState", () => { parentState$.next(dateString); await awaitAsync(); - expect(memoryStorage.internalStore[deriveDefinition.buildCacheKey()]).toEqual( + expect(memoryStorage.internalStore[deriveDefinition.storageKey]).toEqual( derivedValue(new Date(dateString)), ); const calls = memoryStorage.mock.save.mock.calls; expect(calls.length).toBe(1); - expect(calls[0][0]).toBe(deriveDefinition.buildCacheKey()); + expect(calls[0][0]).toBe(deriveDefinition.storageKey); expect(calls[0][1]).toEqual(derivedValue(new Date(dateString))); }); @@ -94,7 +94,7 @@ describe("DefaultDerivedState", () => { it("should store the forced value", async () => { await sut.forceValue(forced); - expect(memoryStorage.internalStore[deriveDefinition.buildCacheKey()]).toEqual( + expect(memoryStorage.internalStore[deriveDefinition.storageKey]).toEqual( derivedValue(forced), ); }); @@ -109,7 +109,7 @@ describe("DefaultDerivedState", () => { it("should store the forced value", async () => { await sut.forceValue(forced); - expect(memoryStorage.internalStore[deriveDefinition.buildCacheKey()]).toEqual( + expect(memoryStorage.internalStore[deriveDefinition.storageKey]).toEqual( derivedValue(forced), ); }); @@ -153,7 +153,7 @@ describe("DefaultDerivedState", () => { parentState$.next(newDate); await awaitAsync(); - expect(memoryStorage.internalStore[deriveDefinition.buildCacheKey()]).toEqual( + expect(memoryStorage.internalStore[deriveDefinition.storageKey]).toEqual( derivedValue(new Date(newDate)), ); @@ -161,7 +161,7 @@ describe("DefaultDerivedState", () => { // Wait for cleanup await awaitAsync(cleanupDelayMs * 2); - expect(memoryStorage.internalStore[deriveDefinition.buildCacheKey()]).toBeUndefined(); + expect(memoryStorage.internalStore[deriveDefinition.storageKey]).toBeUndefined(); }); it("should not clear state after cleanup if clearOnCleanup is false", async () => { @@ -171,7 +171,7 @@ describe("DefaultDerivedState", () => { parentState$.next(newDate); await awaitAsync(); - expect(memoryStorage.internalStore[deriveDefinition.buildCacheKey()]).toEqual( + expect(memoryStorage.internalStore[deriveDefinition.storageKey]).toEqual( derivedValue(new Date(newDate)), ); @@ -179,7 +179,7 @@ describe("DefaultDerivedState", () => { // Wait for cleanup await awaitAsync(cleanupDelayMs * 2); - expect(memoryStorage.internalStore[deriveDefinition.buildCacheKey()]).toEqual( + expect(memoryStorage.internalStore[deriveDefinition.storageKey]).toEqual( derivedValue(new Date(newDate)), ); }); diff --git a/libs/common/src/platform/state/state-definition.ts b/libs/common/src/platform/state/state-definition.ts index 15dc9ff7574..f1e7dc80abd 100644 --- a/libs/common/src/platform/state/state-definition.ts +++ b/libs/common/src/platform/state/state-definition.ts @@ -24,8 +24,10 @@ export type ClientLocations = { web: StorageLocation | "disk-local"; /** * Overriding storage location for browser clients. + * + * "memory-large-object" is used to store non-countable objects in memory. This exists due to limited persistent memory available to browser extensions. */ - //browser: StorageLocation; + browser: StorageLocation | "memory-large-object"; /** * Overriding storage location for desktop clients. */ diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 18df252062f..e04110f28b2 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -116,7 +116,9 @@ export const EVENT_COLLECTION_DISK = new StateDefinition("eventCollection", "dis export const SEND_DISK = new StateDefinition("encryptedSend", "disk", { web: "memory", }); -export const SEND_MEMORY = new StateDefinition("decryptedSend", "memory"); +export const SEND_MEMORY = new StateDefinition("decryptedSend", "memory", { + browser: "memory-large-object", +}); // Vault @@ -133,10 +135,16 @@ export const VAULT_ONBOARDING = new StateDefinition("vaultOnboarding", "disk", { export const VAULT_SETTINGS_DISK = new StateDefinition("vaultSettings", "disk", { web: "disk-local", }); -export const VAULT_BROWSER_MEMORY = new StateDefinition("vaultBrowser", "memory"); -export const VAULT_SEARCH_MEMORY = new StateDefinition("vaultSearch", "memory"); +export const VAULT_BROWSER_MEMORY = new StateDefinition("vaultBrowser", "memory", { + browser: "memory-large-object", +}); +export const VAULT_SEARCH_MEMORY = new StateDefinition("vaultSearch", "memory", { + browser: "memory-large-object", +}); export const CIPHERS_DISK = new StateDefinition("ciphers", "disk", { web: "memory" }); export const CIPHERS_DISK_LOCAL = new StateDefinition("ciphersLocal", "disk", { web: "disk-local", }); -export const CIPHERS_MEMORY = new StateDefinition("ciphersMemory", "memory"); +export const CIPHERS_MEMORY = new StateDefinition("ciphersMemory", "memory", { + browser: "memory-large-object", +}); From 300b17aaeb327d6593d8e90e4943ecd703e9fcdb Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Mon, 22 Apr 2024 10:14:38 -0400 Subject: [PATCH 028/110] [PM-7653] Do not store disk-backed sessions as single blobs (#8852) * Implement a lazy value class This will be used as a source for composing key-protected storage from a single key source. * Simplify local-backed-session-storage The new implementation stores each value to a unique location, prefixed with `session_` to help indicate the purpose. I've also removed the complexity around session keys, favoring passing in a pre-defined value that is determined lazily once for the service worker. This is more in line with how I expect a key-protected storage would work. * Remove decrypted session flag This has been nothing but an annoyance. If it's ever added back, it needs to have some way to determine if the session key matches the one it was written with * Remove unnecessary string interpolation * Remove sync Lazy This is better done as a separate class. * Handle async through type * prefer two factory calls to incorrect value on races. * Fix type * Remove log * Update libs/common/src/platform/misc/lazy.ts Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> --------- Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> --- apps/browser/config/development.json | 1 - .../browser/src/background/main.background.ts | 47 +- .../storage-service.factory.ts | 26 +- .../decorators/dev-flag.decorator.spec.ts | 2 +- apps/browser/src/platform/flags.ts | 1 - ...cal-backed-session-storage.service.spec.ts | 508 +++++------------- .../local-backed-session-storage.service.ts | 225 +++----- libs/common/spec/index.ts | 1 + libs/common/src/platform/misc/lazy.spec.ts | 85 +++ libs/common/src/platform/misc/lazy.ts | 20 + 10 files changed, 380 insertions(+), 536 deletions(-) create mode 100644 libs/common/src/platform/misc/lazy.spec.ts create mode 100644 libs/common/src/platform/misc/lazy.ts diff --git a/apps/browser/config/development.json b/apps/browser/config/development.json index 1b628c173ce..aba10eb25b2 100644 --- a/apps/browser/config/development.json +++ b/apps/browser/config/development.json @@ -1,6 +1,5 @@ { "devFlags": { - "storeSessionDecrypted": false, "managedEnvironment": { "base": "https://localhost:8080" } diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 0a9ad44962c..fa1add06028 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -98,7 +98,9 @@ import { StateFactory } from "@bitwarden/common/platform/factories/state-factory import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; // eslint-disable-next-line no-restricted-imports -- Used for dependency creation import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; +import { Lazy } from "@bitwarden/common/platform/misc/lazy"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { AppIdService } from "@bitwarden/common/platform/services/app-id.service"; import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service"; import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service"; @@ -404,32 +406,51 @@ export default class MainBackground { self, ); - const mv3MemoryStorageCreator = (partitionName: string) => { - if (this.popupOnlyContext) { - return new ForegroundMemoryStorageService(partitionName); + // Creates a session key for mv3 storage of large memory items + const sessionKey = new Lazy(async () => { + // Key already in session storage + const sessionStorage = new BrowserMemoryStorageService(); + const existingKey = await sessionStorage.get("session-key"); + if (existingKey) { + if (sessionStorage.valuesRequireDeserialization) { + return SymmetricCryptoKey.fromJSON(existingKey); + } + return existingKey; + } + + // New key + const { derivedKey } = await this.keyGenerationService.createKeyWithPurpose( + 128, + "ephemeral", + "bitwarden-ephemeral", + ); + await sessionStorage.save("session-key", derivedKey); + return derivedKey; + }); + + const mv3MemoryStorageCreator = () => { + if (this.popupOnlyContext) { + return new ForegroundMemoryStorageService(); } - // TODO: Consider using multithreaded encrypt service in popup only context return new LocalBackedSessionStorageService( - this.logService, + sessionKey, + this.storageService, new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, false), - this.keyGenerationService, - new BrowserLocalStorageService(), - new BrowserMemoryStorageService(), this.platformUtilsService, - partitionName, + this.logService, ); }; this.secureStorageService = this.storageService; // secure storage is not supported in browsers, so we use local storage and warn users when it is used - this.memoryStorageService = BrowserApi.isManifestVersion(3) - ? mv3MemoryStorageCreator("stateService") - : new MemoryStorageService(); this.memoryStorageForStateProviders = BrowserApi.isManifestVersion(3) ? new BrowserMemoryStorageService() // mv3 stores to storage.session : new BackgroundMemoryStorageService(); // mv2 stores to memory + this.memoryStorageService = BrowserApi.isManifestVersion(3) + ? this.memoryStorageForStateProviders // manifest v3 can reuse the same storage. They are split for v2 due to lacking a good sync mechanism, which isn't true for v3 + : new MemoryStorageService(); this.largeObjectMemoryStorageForStateProviders = BrowserApi.isManifestVersion(3) - ? mv3MemoryStorageCreator("stateProviders") // mv3 stores to local-backed session storage + ? mv3MemoryStorageCreator() // mv3 stores to local-backed session storage : this.memoryStorageForStateProviders; // mv2 stores to the same location const storageServiceProvider = new BrowserStorageServiceProvider( diff --git a/apps/browser/src/platform/background/service-factories/storage-service.factory.ts b/apps/browser/src/platform/background/service-factories/storage-service.factory.ts index 83e8a780a6d..e63e39944d3 100644 --- a/apps/browser/src/platform/background/service-factories/storage-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/storage-service.factory.ts @@ -3,6 +3,8 @@ import { AbstractStorageService, ObservableStorageService, } from "@bitwarden/common/platform/abstractions/storage.service"; +import { Lazy } from "@bitwarden/common/platform/misc/lazy"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { BrowserApi } from "../../browser/browser-api"; @@ -17,10 +19,10 @@ import { KeyGenerationServiceInitOptions, keyGenerationServiceFactory, } from "./key-generation-service.factory"; -import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory"; +import { LogServiceInitOptions, logServiceFactory } from "./log-service.factory"; import { - platformUtilsServiceFactory, PlatformUtilsServiceInitOptions, + platformUtilsServiceFactory, } from "./platform-utils-service.factory"; export type DiskStorageServiceInitOptions = FactoryOptions; @@ -70,13 +72,23 @@ export function memoryStorageServiceFactory( return factory(cache, "memoryStorageService", opts, async () => { if (BrowserApi.isManifestVersion(3)) { return new LocalBackedSessionStorageService( - await logServiceFactory(cache, opts), - await encryptServiceFactory(cache, opts), - await keyGenerationServiceFactory(cache, opts), + new Lazy(async () => { + const existingKey = await ( + await sessionStorageServiceFactory(cache, opts) + ).get("session-key"); + if (existingKey) { + return existingKey; + } + const { derivedKey } = await ( + await keyGenerationServiceFactory(cache, opts) + ).createKeyWithPurpose(128, "ephemeral", "bitwarden-ephemeral"); + await (await sessionStorageServiceFactory(cache, opts)).save("session-key", derivedKey); + return derivedKey; + }), await diskStorageServiceFactory(cache, opts), - await sessionStorageServiceFactory(cache, opts), + await encryptServiceFactory(cache, opts), await platformUtilsServiceFactory(cache, opts), - "serviceFactories", + await logServiceFactory(cache, opts), ); } return new MemoryStorageService(); diff --git a/apps/browser/src/platform/decorators/dev-flag.decorator.spec.ts b/apps/browser/src/platform/decorators/dev-flag.decorator.spec.ts index c5401f8a097..da00bc6fe30 100644 --- a/apps/browser/src/platform/decorators/dev-flag.decorator.spec.ts +++ b/apps/browser/src/platform/decorators/dev-flag.decorator.spec.ts @@ -9,7 +9,7 @@ jest.mock("../flags", () => ({ })); class TestClass { - @devFlag("storeSessionDecrypted") test() { + @devFlag("managedEnvironment") test() { return "test"; } } diff --git a/apps/browser/src/platform/flags.ts b/apps/browser/src/platform/flags.ts index 36aa698a7bc..383e982f065 100644 --- a/apps/browser/src/platform/flags.ts +++ b/apps/browser/src/platform/flags.ts @@ -19,7 +19,6 @@ export type Flags = { // required to avoid linting errors when there are no flags // eslint-disable-next-line @typescript-eslint/ban-types export type DevFlags = { - storeSessionDecrypted?: boolean; managedEnvironment?: GroupPolicyEnvironment; } & SharedDevFlags; diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts index a4581e6ac1a..7114bda06e3 100644 --- a/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts +++ b/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts @@ -1,412 +1,200 @@ import { mock, MockProxy } from "jest-mock-extended"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; -import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { - AbstractMemoryStorageService, - AbstractStorageService, - StorageUpdate, -} from "@bitwarden/common/platform/abstractions/storage.service"; +import { Lazy } from "@bitwarden/common/platform/misc/lazy"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; - -import { BrowserApi } from "../browser/browser-api"; +import { FakeStorageService, makeEncString } from "@bitwarden/common/spec"; import { LocalBackedSessionStorageService } from "./local-backed-session-storage.service"; -describe.skip("LocalBackedSessionStorage", () => { - const sendMessageWithResponseSpy: jest.SpyInstance = jest.spyOn( - BrowserApi, - "sendMessageWithResponse", +describe("LocalBackedSessionStorage", () => { + const sessionKey = new SymmetricCryptoKey( + Utils.fromUtf8ToArray("00000000000000000000000000000000"), ); - + let localStorage: FakeStorageService; let encryptService: MockProxy; - let keyGenerationService: MockProxy; - let localStorageService: MockProxy; - let sessionStorageService: MockProxy; - let logService: MockProxy; let platformUtilsService: MockProxy; - - let cache: Record; - const testObj = { a: 1, b: 2 }; - const stringifiedTestObj = JSON.stringify(testObj); - - const key = new SymmetricCryptoKey(Utils.fromUtf8ToArray("00000000000000000000000000000000")); - let getSessionKeySpy: jest.SpyInstance; - let sendUpdateSpy: jest.SpyInstance; - const mockEnc = (input: string) => Promise.resolve(new EncString("ENCRYPTED" + input)); + let logService: MockProxy; let sut: LocalBackedSessionStorageService; - const mockExistingSessionKey = (key: SymmetricCryptoKey) => { - sessionStorageService.get.mockImplementation((storageKey) => { - if (storageKey === "localEncryptionKey_test") { - return Promise.resolve(key?.toJSON()); - } - - return Promise.reject("No implementation for " + storageKey); - }); - }; - beforeEach(() => { - sendMessageWithResponseSpy.mockResolvedValue(null); - logService = mock(); + localStorage = new FakeStorageService(); encryptService = mock(); - keyGenerationService = mock(); - localStorageService = mock(); - sessionStorageService = mock(); + platformUtilsService = mock(); + logService = mock(); sut = new LocalBackedSessionStorageService( - logService, + new Lazy(async () => sessionKey), + localStorage, encryptService, - keyGenerationService, - localStorageService, - sessionStorageService, platformUtilsService, - "test", + logService, ); - - cache = sut["cachedSession"]; - - keyGenerationService.createKeyWithPurpose.mockResolvedValue({ - derivedKey: key, - salt: "bitwarden-ephemeral", - material: null, // Not used - }); - - getSessionKeySpy = jest.spyOn(sut, "getSessionEncKey"); - getSessionKeySpy.mockResolvedValue(key); - - // sendUpdateSpy = jest.spyOn(sut, "sendUpdate"); - // sendUpdateSpy.mockReturnValue(); }); describe("get", () => { - describe("in local cache or external context cache", () => { - it("should return from local cache", async () => { - cache["test"] = stringifiedTestObj; - const result = await sut.get("test"); - expect(result).toStrictEqual(testObj); - }); - - it("should return from external context cache when local cache is not available", async () => { - sendMessageWithResponseSpy.mockResolvedValue(stringifiedTestObj); - const result = await sut.get("test"); - expect(result).toStrictEqual(testObj); - }); + it("return the cached value when one is cached", async () => { + sut["cache"]["test"] = "cached"; + const result = await sut.get("test"); + expect(result).toEqual("cached"); }); - describe("not in cache", () => { - const session = { test: stringifiedTestObj }; + it("returns a decrypted value when one is stored in local storage", async () => { + const encrypted = makeEncString("encrypted"); + localStorage.internalStore["session_test"] = encrypted.encryptedString; + encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted")); + const result = await sut.get("test"); + expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encrypted, sessionKey); + expect(result).toEqual("decrypted"); + }); - beforeEach(() => { - mockExistingSessionKey(key); - }); + it("caches the decrypted value when one is stored in local storage", async () => { + const encrypted = makeEncString("encrypted"); + localStorage.internalStore["session_test"] = encrypted.encryptedString; + encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted")); + await sut.get("test"); + expect(sut["cache"]["test"]).toEqual("decrypted"); + }); + }); - describe("no session retrieved", () => { - let result: any; - let spy: jest.SpyInstance; - beforeEach(async () => { - spy = jest.spyOn(sut, "getLocalSession").mockResolvedValue(null); - localStorageService.get.mockResolvedValue(null); - result = await sut.get("test"); - }); + describe("getBypassCache", () => { + it("ignores cached values", async () => { + sut["cache"]["test"] = "cached"; + const encrypted = makeEncString("encrypted"); + localStorage.internalStore["session_test"] = encrypted.encryptedString; + encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted")); + const result = await sut.getBypassCache("test"); + expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encrypted, sessionKey); + expect(result).toEqual("decrypted"); + }); - it("should grab from session if not in cache", async () => { - expect(spy).toHaveBeenCalledWith(key); - }); + it("returns a decrypted value when one is stored in local storage", async () => { + const encrypted = makeEncString("encrypted"); + localStorage.internalStore["session_test"] = encrypted.encryptedString; + encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted")); + const result = await sut.getBypassCache("test"); + expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encrypted, sessionKey); + expect(result).toEqual("decrypted"); + }); - it("should return null if session is null", async () => { - expect(result).toBeNull(); - }); - }); + it("caches the decrypted value when one is stored in local storage", async () => { + const encrypted = makeEncString("encrypted"); + localStorage.internalStore["session_test"] = encrypted.encryptedString; + encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted")); + await sut.getBypassCache("test"); + expect(sut["cache"]["test"]).toEqual("decrypted"); + }); - describe("session retrieved from storage", () => { - beforeEach(() => { - jest.spyOn(sut, "getLocalSession").mockResolvedValue(session); - }); - - it("should return null if session does not have the key", async () => { - const result = await sut.get("DNE"); - expect(result).toBeNull(); - }); - - it("should return the value retrieved from session", async () => { - const result = await sut.get("test"); - expect(result).toEqual(session.test); - }); - - it("should set retrieved values in cache", async () => { - await sut.get("test"); - expect(cache["test"]).toBeTruthy(); - expect(cache["test"]).toEqual(session.test); - }); - - it("should use a deserializer if provided", async () => { - const deserializer = jest.fn().mockReturnValue(testObj); - const result = await sut.get("test", { deserializer: deserializer }); - expect(deserializer).toHaveBeenCalledWith(session.test); - expect(result).toEqual(testObj); - }); - }); + it("deserializes when a deserializer is provided", async () => { + const encrypted = makeEncString("encrypted"); + localStorage.internalStore["session_test"] = encrypted.encryptedString; + encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted")); + const deserializer = jest.fn().mockReturnValue("deserialized"); + const result = await sut.getBypassCache("test", { deserializer }); + expect(deserializer).toHaveBeenCalledWith("decrypted"); + expect(result).toEqual("deserialized"); }); }); describe("has", () => { - it("should be false if `get` returns null", async () => { - const spy = jest.spyOn(sut, "get"); - spy.mockResolvedValue(null); - expect(await sut.has("test")).toBe(false); + it("returns false when the key is not in cache", async () => { + const result = await sut.has("test"); + expect(result).toBe(false); + }); + + it("returns true when the key is in cache", async () => { + sut["cache"]["test"] = "cached"; + const result = await sut.has("test"); + expect(result).toBe(true); + }); + + it("returns true when the key is in local storage", async () => { + localStorage.internalStore["session_test"] = makeEncString("encrypted").encryptedString; + encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted")); + const result = await sut.has("test"); + expect(result).toBe(true); + }); + + it.each([null, undefined])("returns false when %s is cached", async (nullish) => { + sut["cache"]["test"] = nullish; + await expect(sut.has("test")).resolves.toBe(false); + }); + + it.each([null, undefined])( + "returns false when null is stored in local storage", + async (nullish) => { + localStorage.internalStore["session_test"] = nullish; + await expect(sut.has("test")).resolves.toBe(false); + expect(encryptService.decryptToUtf8).not.toHaveBeenCalled(); + }, + ); + }); + + describe("save", () => { + const encString = makeEncString("encrypted"); + beforeEach(() => { + encryptService.encrypt.mockResolvedValue(encString); + }); + + it("logs a warning when saving the same value twice and in a dev environment", async () => { + platformUtilsService.isDev.mockReturnValue(true); + sut["cache"]["test"] = "cached"; + await sut.save("test", "cached"); + expect(logService.warning).toHaveBeenCalled(); + }); + + it("does not log when saving the same value twice and not in a dev environment", async () => { + platformUtilsService.isDev.mockReturnValue(false); + sut["cache"]["test"] = "cached"; + await sut.save("test", "cached"); + expect(logService.warning).not.toHaveBeenCalled(); + }); + + it("removes the key when saving a null value", async () => { + const spy = jest.spyOn(sut, "remove"); + await sut.save("test", null); expect(spy).toHaveBeenCalledWith("test"); }); - it("should be true if `get` returns non-null", async () => { - const spy = jest.spyOn(sut, "get"); - spy.mockResolvedValue({}); - expect(await sut.has("test")).toBe(true); - expect(spy).toHaveBeenCalledWith("test"); + it("saves the value to cache", async () => { + await sut.save("test", "value"); + expect(sut["cache"]["test"]).toEqual("value"); + }); + + it("encrypts and saves the value to local storage", async () => { + await sut.save("test", "value"); + expect(encryptService.encrypt).toHaveBeenCalledWith(JSON.stringify("value"), sessionKey); + expect(localStorage.internalStore["session_test"]).toEqual(encString.encryptedString); + }); + + it("emits an update", async () => { + const spy = jest.spyOn(sut["updatesSubject"], "next"); + await sut.save("test", "value"); + expect(spy).toHaveBeenCalledWith({ key: "test", updateType: "save" }); }); }); describe("remove", () => { - describe("existing cache value is null", () => { - it("should not save null if the local cached value is already null", async () => { - cache["test"] = null; - await sut.remove("test"); - expect(sendUpdateSpy).not.toHaveBeenCalled(); - }); - - it("should not save null if the externally cached value is already null", async () => { - sendMessageWithResponseSpy.mockResolvedValue(null); - await sut.remove("test"); - expect(sendUpdateSpy).not.toHaveBeenCalled(); - }); - }); - - it("should save null", async () => { - cache["test"] = stringifiedTestObj; - + it("nulls the value in cache", async () => { + sut["cache"]["test"] = "cached"; await sut.remove("test"); - expect(sendUpdateSpy).toHaveBeenCalledWith({ key: "test", updateType: "remove" }); - }); - }); - - describe("save", () => { - describe("currently cached", () => { - it("does not save the value a local cached value exists which is an exact match", async () => { - cache["test"] = stringifiedTestObj; - await sut.save("test", testObj); - expect(sendUpdateSpy).not.toHaveBeenCalled(); - }); - - it("does not save the value if a local cached value exists, even if the keys not in the same order", async () => { - cache["test"] = JSON.stringify({ b: 2, a: 1 }); - await sut.save("test", testObj); - expect(sendUpdateSpy).not.toHaveBeenCalled(); - }); - - it("does not save the value a externally cached value exists which is an exact match", async () => { - sendMessageWithResponseSpy.mockResolvedValue(stringifiedTestObj); - await sut.save("test", testObj); - expect(sendUpdateSpy).not.toHaveBeenCalled(); - expect(cache["test"]).toBe(stringifiedTestObj); - }); - - it("saves the value if the currently cached string value evaluates to a falsy value", async () => { - cache["test"] = "null"; - await sut.save("test", testObj); - expect(sendUpdateSpy).toHaveBeenCalledWith({ key: "test", updateType: "save" }); - }); + expect(sut["cache"]["test"]).toBeNull(); }); - describe("caching", () => { - beforeEach(() => { - localStorageService.get.mockResolvedValue(null); - sessionStorageService.get.mockResolvedValue(null); - - localStorageService.save.mockResolvedValue(); - sessionStorageService.save.mockResolvedValue(); - - encryptService.encrypt.mockResolvedValue(mockEnc("{}")); - }); - - it("should remove key from cache if value is null", async () => { - cache["test"] = {}; - // const cacheSetSpy = jest.spyOn(cache, "set"); - expect(cache["test"]).toBe(true); - await sut.save("test", null); - // Don't remove from cache, just replace with null - expect(cache["test"]).toBe(null); - // expect(cacheSetSpy).toHaveBeenCalledWith("test", null); - }); - - it("should set cache if value is non-null", async () => { - expect(cache["test"]).toBe(false); - // const setSpy = jest.spyOn(cache, "set"); - await sut.save("test", testObj); - expect(cache["test"]).toBe(stringifiedTestObj); - // expect(setSpy).toHaveBeenCalledWith("test", stringifiedTestObj); - }); + it("removes the key from local storage", async () => { + localStorage.internalStore["session_test"] = makeEncString("encrypted").encryptedString; + await sut.remove("test"); + expect(localStorage.internalStore["session_test"]).toBeUndefined(); }); - describe("local storing", () => { - let setSpy: jest.SpyInstance; - - beforeEach(() => { - setSpy = jest.spyOn(sut, "setLocalSession").mockResolvedValue(); - }); - - it("should store a new session", async () => { - jest.spyOn(sut, "getLocalSession").mockResolvedValue(null); - await sut.save("test", testObj); - - expect(setSpy).toHaveBeenCalledWith({ test: testObj }, key); - }); - - it("should update an existing session", async () => { - const existingObj = { test: testObj }; - jest.spyOn(sut, "getLocalSession").mockResolvedValue(existingObj); - await sut.save("test2", testObj); - - expect(setSpy).toHaveBeenCalledWith({ test2: testObj, ...existingObj }, key); - }); - - it("should overwrite an existing item in session", async () => { - const existingObj = { test: {} }; - jest.spyOn(sut, "getLocalSession").mockResolvedValue(existingObj); - await sut.save("test", testObj); - - expect(setSpy).toHaveBeenCalledWith({ test: testObj }, key); - }); - }); - }); - - describe("getSessionKey", () => { - beforeEach(() => { - getSessionKeySpy.mockRestore(); - }); - - it("should return the stored symmetric crypto key", async () => { - sessionStorageService.get.mockResolvedValue({ ...key }); - const result = await sut.getSessionEncKey(); - - expect(result).toStrictEqual(key); - }); - - describe("new key creation", () => { - beforeEach(() => { - keyGenerationService.createKeyWithPurpose.mockResolvedValue({ - salt: "salt", - material: null, - derivedKey: key, - }); - jest.spyOn(sut, "setSessionEncKey").mockResolvedValue(); - }); - - it("should create a symmetric crypto key", async () => { - const result = await sut.getSessionEncKey(); - - expect(result).toStrictEqual(key); - expect(keyGenerationService.createKeyWithPurpose).toHaveBeenCalledTimes(1); - }); - - it("should store a symmetric crypto key if it makes one", async () => { - const spy = jest.spyOn(sut, "setSessionEncKey").mockResolvedValue(); - await sut.getSessionEncKey(); - - expect(spy).toHaveBeenCalledWith(key); - }); - }); - }); - - describe("getLocalSession", () => { - it("should return null if session is null", async () => { - const result = await sut.getLocalSession(key); - - expect(result).toBeNull(); - expect(localStorageService.get).toHaveBeenCalledWith("session_test"); - }); - - describe("non-null sessions", () => { - const session = { test: "test" }; - const encSession = new EncString(JSON.stringify(session)); - const decryptedSession = JSON.stringify(session); - - beforeEach(() => { - localStorageService.get.mockResolvedValue(encSession.encryptedString); - }); - - it("should decrypt returned sessions", async () => { - encryptService.decryptToUtf8 - .calledWith(expect.anything(), key) - .mockResolvedValue(decryptedSession); - await sut.getLocalSession(key); - expect(encryptService.decryptToUtf8).toHaveBeenNthCalledWith(1, encSession, key); - }); - - it("should parse session", async () => { - encryptService.decryptToUtf8 - .calledWith(expect.anything(), key) - .mockResolvedValue(decryptedSession); - const result = await sut.getLocalSession(key); - expect(result).toEqual(session); - }); - - it("should remove state if decryption fails", async () => { - encryptService.decryptToUtf8.mockResolvedValue(null); - const setSessionEncKeySpy = jest.spyOn(sut, "setSessionEncKey").mockResolvedValue(); - - const result = await sut.getLocalSession(key); - - expect(result).toBeNull(); - expect(setSessionEncKeySpy).toHaveBeenCalledWith(null); - expect(localStorageService.remove).toHaveBeenCalledWith("session_test"); - }); - }); - }); - - describe("setLocalSession", () => { - const testSession = { test: "a" }; - const testJSON = JSON.stringify(testSession); - - it("should encrypt a stringified session", async () => { - encryptService.encrypt.mockImplementation(mockEnc); - localStorageService.save.mockResolvedValue(); - await sut.setLocalSession(testSession, key); - - expect(encryptService.encrypt).toHaveBeenNthCalledWith(1, testJSON, key); - }); - - it("should remove local session if null", async () => { - encryptService.encrypt.mockResolvedValue(null); - await sut.setLocalSession(null, key); - - expect(localStorageService.remove).toHaveBeenCalledWith("session_test"); - }); - - it("should save encrypted string", async () => { - encryptService.encrypt.mockImplementation(mockEnc); - await sut.setLocalSession(testSession, key); - - expect(localStorageService.save).toHaveBeenCalledWith( - "session_test", - (await mockEnc(testJSON)).encryptedString, - ); - }); - }); - - describe("setSessionKey", () => { - it("should remove if null", async () => { - await sut.setSessionEncKey(null); - expect(sessionStorageService.remove).toHaveBeenCalledWith("localEncryptionKey_test"); - }); - - it("should save key when not null", async () => { - await sut.setSessionEncKey(key); - expect(sessionStorageService.save).toHaveBeenCalledWith("localEncryptionKey_test", key); + it("emits an update", async () => { + const spy = jest.spyOn(sut["updatesSubject"], "next"); + await sut.remove("test"); + expect(spy).toHaveBeenCalledWith({ key: "test", updateType: "remove" }); }); }); }); diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.ts index 146eb11b2bd..5432e8d918b 100644 --- a/apps/browser/src/platform/services/local-backed-session-storage.service.ts +++ b/apps/browser/src/platform/services/local-backed-session-storage.service.ts @@ -2,7 +2,6 @@ import { Subject } from "rxjs"; import { Jsonify } from "type-fest"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; -import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { @@ -11,13 +10,12 @@ import { ObservableStorageService, StorageUpdate, } from "@bitwarden/common/platform/abstractions/storage.service"; +import { Lazy } from "@bitwarden/common/platform/misc/lazy"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { MemoryStorageOptions } from "@bitwarden/common/platform/models/domain/storage-options"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { BrowserApi } from "../browser/browser-api"; -import { devFlag } from "../decorators/dev-flag.decorator"; -import { devFlagEnabled } from "../flags"; import { MemoryStoragePortMessage } from "../storage/port-messages"; import { portName } from "../storage/port-name"; @@ -25,85 +23,64 @@ export class LocalBackedSessionStorageService extends AbstractMemoryStorageService implements ObservableStorageService { + private ports: Set = new Set([]); + private cache: Record = {}; private updatesSubject = new Subject(); - private commandName = `localBackedSessionStorage_${this.partitionName}`; - private encKey = `localEncryptionKey_${this.partitionName}`; - private sessionKey = `session_${this.partitionName}`; - private cachedSession: Record = {}; - private _ports: Set = new Set([]); - private knownNullishCacheKeys: Set = new Set([]); + readonly valuesRequireDeserialization = true; + updates$ = this.updatesSubject.asObservable(); constructor( - private logService: LogService, - private encryptService: EncryptService, - private keyGenerationService: KeyGenerationService, - private localStorage: AbstractStorageService, - private sessionStorage: AbstractStorageService, - private platformUtilsService: PlatformUtilsService, - private partitionName: string, + private readonly sessionKey: Lazy>, + private readonly localStorage: AbstractStorageService, + private readonly encryptService: EncryptService, + private readonly platformUtilsService: PlatformUtilsService, + private readonly logService: LogService, ) { super(); BrowserApi.addListener(chrome.runtime.onConnect, (port) => { - if (port.name !== `${portName(chrome.storage.session)}_${partitionName}`) { + if (port.name !== portName(chrome.storage.session)) { return; } - this._ports.add(port); + this.ports.add(port); const listenerCallback = this.onMessageFromForeground.bind(this); port.onDisconnect.addListener(() => { - this._ports.delete(port); + this.ports.delete(port); port.onMessage.removeListener(listenerCallback); }); port.onMessage.addListener(listenerCallback); // Initialize the new memory storage service with existing data this.sendMessageTo(port, { action: "initialization", - data: Array.from(Object.keys(this.cachedSession)), + data: Array.from(Object.keys(this.cache)), + }); + this.updates$.subscribe((update) => { + this.broadcastMessage({ + action: "subject_update", + data: update, + }); }); }); - this.updates$.subscribe((update) => { - this.broadcastMessage({ - action: "subject_update", - data: update, - }); - }); - } - - get valuesRequireDeserialization(): boolean { - return true; - } - - get updates$() { - return this.updatesSubject.asObservable(); } async get(key: string, options?: MemoryStorageOptions): Promise { - if (this.cachedSession[key] != null) { - return this.cachedSession[key] as T; - } - - if (this.knownNullishCacheKeys.has(key)) { - return null; + if (this.cache[key] !== undefined) { + return this.cache[key] as T; } return await this.getBypassCache(key, options); } async getBypassCache(key: string, options?: MemoryStorageOptions): Promise { - const session = await this.getLocalSession(await this.getSessionEncKey()); - if (session[key] == null) { - this.knownNullishCacheKeys.add(key); - return null; - } + let value = await this.getLocalSessionValue(await this.sessionKey.get(), key); - let value = session[key]; if (options?.deserializer != null) { value = options.deserializer(value as Jsonify); } - void this.save(key, value); + this.cache[key] = value; return value as T; } @@ -114,7 +91,7 @@ export class LocalBackedSessionStorageService async save(key: string, obj: T): Promise { // This is for observation purposes only. At some point, we don't want to write to local session storage if the value is the same. if (this.platformUtilsService.isDev()) { - const existingValue = this.cachedSession[key] as T; + const existingValue = this.cache[key] as T; if (this.compareValues(existingValue, obj)) { this.logService.warning(`Possible unnecessary write to local session storage. Key: ${key}`); this.logService.warning(obj as any); @@ -125,128 +102,42 @@ export class LocalBackedSessionStorageService return await this.remove(key); } - this.knownNullishCacheKeys.delete(key); - this.cachedSession[key] = obj; + this.cache[key] = obj; await this.updateLocalSessionValue(key, obj); this.updatesSubject.next({ key, updateType: "save" }); } async remove(key: string): Promise { - this.knownNullishCacheKeys.add(key); - delete this.cachedSession[key]; + this.cache[key] = null; await this.updateLocalSessionValue(key, null); this.updatesSubject.next({ key, updateType: "remove" }); } - private async updateLocalSessionValue(key: string, obj: T) { - const sessionEncKey = await this.getSessionEncKey(); - const localSession = (await this.getLocalSession(sessionEncKey)) ?? {}; - localSession[key] = obj; - void this.setLocalSession(localSession, sessionEncKey); - } - - async getLocalSession(encKey: SymmetricCryptoKey): Promise> { - if (Object.keys(this.cachedSession).length > 0) { - return this.cachedSession; - } - - this.cachedSession = {}; - const local = await this.localStorage.get(this.sessionKey); + private async getLocalSessionValue(encKey: SymmetricCryptoKey, key: string): Promise { + const local = await this.localStorage.get(this.sessionStorageKey(key)); if (local == null) { - return this.cachedSession; + return null; } - if (devFlagEnabled("storeSessionDecrypted")) { - return local as any as Record; + const valueJson = await this.encryptService.decryptToUtf8(new EncString(local), encKey); + if (valueJson == null) { + // error with decryption, value is lost, delete state and start over + await this.localStorage.remove(this.sessionStorageKey(key)); + return null; } - const sessionJson = await this.encryptService.decryptToUtf8(new EncString(local), encKey); - if (sessionJson == null) { - // Error with decryption -- session is lost, delete state and key and start over - await this.setSessionEncKey(null); - await this.localStorage.remove(this.sessionKey); - return this.cachedSession; - } - - this.cachedSession = JSON.parse(sessionJson); - return this.cachedSession; + return JSON.parse(valueJson); } - async setLocalSession(session: Record, key: SymmetricCryptoKey) { - if (devFlagEnabled("storeSessionDecrypted")) { - await this.setDecryptedLocalSession(session); - } else { - await this.setEncryptedLocalSession(session, key); - } - } - - @devFlag("storeSessionDecrypted") - async setDecryptedLocalSession(session: Record): Promise { - // Make sure we're storing the jsonified version of the session - const jsonSession = JSON.parse(JSON.stringify(session)); - if (session == null) { - await this.localStorage.remove(this.sessionKey); - } else { - await this.localStorage.save(this.sessionKey, jsonSession); - } - } - - async setEncryptedLocalSession(session: Record, key: SymmetricCryptoKey) { - const jsonSession = JSON.stringify(session); - const encSession = await this.encryptService.encrypt(jsonSession, key); - - if (encSession == null) { - return await this.localStorage.remove(this.sessionKey); - } - await this.localStorage.save(this.sessionKey, encSession.encryptedString); - } - - async getSessionEncKey(): Promise { - let storedKey = await this.sessionStorage.get(this.encKey); - if (storedKey == null || Object.keys(storedKey).length == 0) { - const generatedKey = await this.keyGenerationService.createKeyWithPurpose( - 128, - "ephemeral", - "bitwarden-ephemeral", - ); - storedKey = generatedKey.derivedKey; - await this.setSessionEncKey(storedKey); - return storedKey; - } else { - return SymmetricCryptoKey.fromJSON(storedKey); - } - } - - async setSessionEncKey(input: SymmetricCryptoKey): Promise { - if (input == null) { - await this.sessionStorage.remove(this.encKey); - } else { - await this.sessionStorage.save(this.encKey, input); - } - } - - private compareValues(value1: T, value2: T): boolean { - if (value1 == null && value2 == null) { - return true; + private async updateLocalSessionValue(key: string, value: unknown): Promise { + if (value == null) { + await this.localStorage.remove(this.sessionStorageKey(key)); + return; } - if (value1 && value2 == null) { - return false; - } - - if (value1 == null && value2) { - return false; - } - - if (typeof value1 !== "object" || typeof value2 !== "object") { - return value1 === value2; - } - - if (JSON.stringify(value1) === JSON.stringify(value2)) { - return true; - } - - return Object.entries(value1).sort().toString() === Object.entries(value2).sort().toString(); + const valueJson = JSON.stringify(value); + const encValue = await this.encryptService.encrypt(valueJson, await this.sessionKey.get()); + await this.localStorage.save(this.sessionStorageKey(key), encValue.encryptedString); } private async onMessageFromForeground( @@ -282,7 +173,7 @@ export class LocalBackedSessionStorageService } protected broadcastMessage(data: Omit) { - this._ports.forEach((port) => { + this.ports.forEach((port) => { this.sendMessageTo(port, data); }); } @@ -296,4 +187,32 @@ export class LocalBackedSessionStorageService originator: "background", }); } + + private sessionStorageKey(key: string) { + return `session_${key}`; + } + + private compareValues(value1: T, value2: T): boolean { + if (value1 == null && value2 == null) { + return true; + } + + if (value1 && value2 == null) { + return false; + } + + if (value1 == null && value2) { + return false; + } + + if (typeof value1 !== "object" || typeof value2 !== "object") { + return value1 === value2; + } + + if (JSON.stringify(value1) === JSON.stringify(value2)) { + return true; + } + + return Object.entries(value1).sort().toString() === Object.entries(value2).sort().toString(); + } } diff --git a/libs/common/spec/index.ts b/libs/common/spec/index.ts index 72bd28aca4f..6e9af8400ee 100644 --- a/libs/common/spec/index.ts +++ b/libs/common/spec/index.ts @@ -4,3 +4,4 @@ export * from "./matchers"; export * from "./fake-state-provider"; export * from "./fake-state"; export * from "./fake-account-service"; +export * from "./fake-storage.service"; diff --git a/libs/common/src/platform/misc/lazy.spec.ts b/libs/common/src/platform/misc/lazy.spec.ts new file mode 100644 index 00000000000..76ee085d3da --- /dev/null +++ b/libs/common/src/platform/misc/lazy.spec.ts @@ -0,0 +1,85 @@ +import { Lazy } from "./lazy"; + +describe("Lazy", () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + describe("async", () => { + let factory: jest.Mock>; + let lazy: Lazy>; + + beforeEach(() => { + factory = jest.fn(); + lazy = new Lazy(factory); + }); + + describe("get", () => { + it("should call the factory once", async () => { + await lazy.get(); + await lazy.get(); + + expect(factory).toHaveBeenCalledTimes(1); + }); + + it("should return the value from the factory", async () => { + factory.mockResolvedValue(42); + + const value = await lazy.get(); + + expect(value).toBe(42); + }); + }); + + describe("factory throws", () => { + it("should throw the error", async () => { + factory.mockRejectedValue(new Error("factory error")); + + await expect(lazy.get()).rejects.toThrow("factory error"); + }); + }); + + describe("factory returns undefined", () => { + it("should return undefined", async () => { + factory.mockResolvedValue(undefined); + + const value = await lazy.get(); + + expect(value).toBeUndefined(); + }); + }); + + describe("factory returns null", () => { + it("should return null", async () => { + factory.mockResolvedValue(null); + + const value = await lazy.get(); + + expect(value).toBeNull(); + }); + }); + }); + + describe("sync", () => { + const syncFactory = jest.fn(); + let lazy: Lazy; + + beforeEach(() => { + syncFactory.mockReturnValue(42); + lazy = new Lazy(syncFactory); + }); + + it("should return the value from the factory", () => { + const value = lazy.get(); + + expect(value).toBe(42); + }); + + it("should call the factory once", () => { + lazy.get(); + lazy.get(); + + expect(syncFactory).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/libs/common/src/platform/misc/lazy.ts b/libs/common/src/platform/misc/lazy.ts new file mode 100644 index 00000000000..fb85b93678d --- /dev/null +++ b/libs/common/src/platform/misc/lazy.ts @@ -0,0 +1,20 @@ +export class Lazy { + private _value: T | undefined = undefined; + private _isCreated = false; + + constructor(private readonly factory: () => T) {} + + /** + * Resolves the factory and returns the result. Guaranteed to resolve the value only once. + * + * @returns The value produced by your factory. + */ + get(): T { + if (!this._isCreated) { + this._value = this.factory(); + this._isCreated = true; + } + + return this._value as T; + } +} From 100b43dd8f7ac23cb888b0f031353aa68beffb82 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Mon, 22 Apr 2024 12:06:43 -0400 Subject: [PATCH 029/110] =?UTF-8?q?Revert=20"Auth/PM-6689=20-=20Migrate=20?= =?UTF-8?q?Security=20Stamp=20to=20Token=20Service=20and=20State=20Prov?= =?UTF-8?q?=E2=80=A6"=20(#8860)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 91f1d9fb86142805d0774182c3ee6234e13946e3. --- .../browser/src/background/main.background.ts | 1 - apps/cli/src/bw.ts | 1 - .../src/services/jslib-services.module.ts | 1 - .../login-strategies/login.strategy.spec.ts | 4 + .../common/login-strategies/login.strategy.ts | 9 ++- .../src/auth/abstractions/token.service.ts | 6 -- .../src/auth/services/token.service.spec.ts | 79 ------------------- .../common/src/auth/services/token.service.ts | 25 ------ .../src/auth/services/token.state.spec.ts | 2 - libs/common/src/auth/services/token.state.ts | 5 -- .../platform/abstractions/state.service.ts | 2 + .../models/domain/account-tokens.spec.ts | 9 +++ .../platform/models/domain/account.spec.ts | 4 +- .../src/platform/models/domain/account.ts | 18 +++++ .../src/platform/services/state.service.ts | 17 ++++ .../src/vault/services/sync/sync.service.ts | 6 +- 16 files changed, 63 insertions(+), 126 deletions(-) create mode 100644 libs/common/src/platform/models/domain/account-tokens.spec.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index fa1add06028..15f21b25014 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -813,7 +813,6 @@ export default class MainBackground { this.avatarService, logoutCallback, this.billingAccountProfileStateService, - this.tokenService, ); this.eventUploadService = new EventUploadService( this.apiService, diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 437f807bc61..58329128b8a 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -631,7 +631,6 @@ export class Main { this.avatarService, async (expired: boolean) => await this.logout(), this.billingAccountProfileStateService, - this.tokenService, ); this.totpService = new TotpService(this.cryptoFunctionService, this.logService); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 27b182de5dc..f31bcb1c513 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -628,7 +628,6 @@ const safeProviders: SafeProvider[] = [ AvatarServiceAbstraction, LOGOUT_CALLBACK, BillingAccountProfileStateService, - TokenServiceAbstraction, ], }), safeProvider({ diff --git a/libs/auth/src/common/login-strategies/login.strategy.spec.ts b/libs/auth/src/common/login-strategies/login.strategy.spec.ts index e0833342ce3..431f736e949 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -27,6 +27,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Account, AccountProfile, + AccountTokens, AccountKeys, } from "@bitwarden/common/platform/models/domain/account"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; @@ -212,6 +213,9 @@ describe("LoginStrategy", () => { kdfType: kdf, }, }, + tokens: { + ...new AccountTokens(), + }, keys: new AccountKeys(), }), ); diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index a73c32e1208..a6dc1931839 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -27,7 +27,11 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { Account, AccountProfile } from "@bitwarden/common/platform/models/domain/account"; +import { + Account, + AccountProfile, + AccountTokens, +} from "@bitwarden/common/platform/models/domain/account"; import { UserId } from "@bitwarden/common/types/guid"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; @@ -188,6 +192,9 @@ export abstract class LoginStrategy { kdfType: tokenResponse.kdf, }, }, + tokens: { + ...new AccountTokens(), + }, }), ); diff --git a/libs/common/src/auth/abstractions/token.service.ts b/libs/common/src/auth/abstractions/token.service.ts index fc3bd317f47..75bb3838828 100644 --- a/libs/common/src/auth/abstractions/token.service.ts +++ b/libs/common/src/auth/abstractions/token.service.ts @@ -213,10 +213,4 @@ export abstract class TokenService { * @returns A promise that resolves with a boolean representing the user's external authN status. */ getIsExternal: () => Promise; - - /** Gets the active or passed in user's security stamp */ - getSecurityStamp: (userId?: UserId) => Promise; - - /** Sets the security stamp for the active or passed in user */ - setSecurityStamp: (securityStamp: string, userId?: UserId) => Promise; } diff --git a/libs/common/src/auth/services/token.service.spec.ts b/libs/common/src/auth/services/token.service.spec.ts index 3e92053d2f7..d32c4d8e1cd 100644 --- a/libs/common/src/auth/services/token.service.spec.ts +++ b/libs/common/src/auth/services/token.service.spec.ts @@ -23,7 +23,6 @@ import { EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, REFRESH_TOKEN_DISK, REFRESH_TOKEN_MEMORY, - SECURITY_STAMP_MEMORY, } from "./token.state"; describe("TokenService", () => { @@ -2192,84 +2191,6 @@ describe("TokenService", () => { }); }); - describe("Security Stamp methods", () => { - const mockSecurityStamp = "securityStamp"; - - describe("setSecurityStamp", () => { - it("should throw an error if no user id is provided and there is no active user in global state", async () => { - // Act - // note: don't await here because we want to test the error - const result = tokenService.setSecurityStamp(mockSecurityStamp); - // Assert - await expect(result).rejects.toThrow("User id not found. Cannot set security stamp."); - }); - - it("should set the security stamp in memory when there is an active user in global state", async () => { - // Arrange - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); - - // Act - await tokenService.setSecurityStamp(mockSecurityStamp); - - // Assert - expect( - singleUserStateProvider.getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY).nextMock, - ).toHaveBeenCalledWith(mockSecurityStamp); - }); - - it("should set the security stamp in memory for the specified user id", async () => { - // Act - await tokenService.setSecurityStamp(mockSecurityStamp, userIdFromAccessToken); - - // Assert - expect( - singleUserStateProvider.getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY).nextMock, - ).toHaveBeenCalledWith(mockSecurityStamp); - }); - }); - - describe("getSecurityStamp", () => { - it("should throw an error if no user id is provided and there is no active user in global state", async () => { - // Act - // note: don't await here because we want to test the error - const result = tokenService.getSecurityStamp(); - // Assert - await expect(result).rejects.toThrow("User id not found. Cannot get security stamp."); - }); - - it("should return the security stamp from memory with no user id specified (uses global active user)", async () => { - // Arrange - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); - - singleUserStateProvider - .getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY) - .stateSubject.next([userIdFromAccessToken, mockSecurityStamp]); - - // Act - const result = await tokenService.getSecurityStamp(); - - // Assert - expect(result).toEqual(mockSecurityStamp); - }); - - it("should return the security stamp from memory for the specified user id", async () => { - // Arrange - singleUserStateProvider - .getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY) - .stateSubject.next([userIdFromAccessToken, mockSecurityStamp]); - - // Act - const result = await tokenService.getSecurityStamp(userIdFromAccessToken); - // Assert - expect(result).toEqual(mockSecurityStamp); - }); - }); - }); - // Helpers function createTokenService(supportsSecureStorage: boolean) { return new TokenService( diff --git a/libs/common/src/auth/services/token.service.ts b/libs/common/src/auth/services/token.service.ts index 40036a8453c..c24a2c186b8 100644 --- a/libs/common/src/auth/services/token.service.ts +++ b/libs/common/src/auth/services/token.service.ts @@ -32,7 +32,6 @@ import { EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, REFRESH_TOKEN_DISK, REFRESH_TOKEN_MEMORY, - SECURITY_STAMP_MEMORY, } from "./token.state"; export enum TokenStorageLocation { @@ -851,30 +850,6 @@ export class TokenService implements TokenServiceAbstraction { return Array.isArray(decoded.amr) && decoded.amr.includes("external"); } - async getSecurityStamp(userId?: UserId): Promise { - userId ??= await firstValueFrom(this.activeUserIdGlobalState.state$); - - if (!userId) { - throw new Error("User id not found. Cannot get security stamp."); - } - - const securityStamp = await this.getStateValueByUserIdAndKeyDef(userId, SECURITY_STAMP_MEMORY); - - return securityStamp; - } - - async setSecurityStamp(securityStamp: string, userId?: UserId): Promise { - userId ??= await firstValueFrom(this.activeUserIdGlobalState.state$); - - if (!userId) { - throw new Error("User id not found. Cannot set security stamp."); - } - - await this.singleUserStateProvider - .get(userId, SECURITY_STAMP_MEMORY) - .update((_) => securityStamp); - } - private async getStateValueByUserIdAndKeyDef( userId: UserId, storageLocation: UserKeyDefinition, diff --git a/libs/common/src/auth/services/token.state.spec.ts b/libs/common/src/auth/services/token.state.spec.ts index bb82410fac1..dc00fec383c 100644 --- a/libs/common/src/auth/services/token.state.spec.ts +++ b/libs/common/src/auth/services/token.state.spec.ts @@ -10,7 +10,6 @@ import { EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, REFRESH_TOKEN_DISK, REFRESH_TOKEN_MEMORY, - SECURITY_STAMP_MEMORY, } from "./token.state"; describe.each([ @@ -23,7 +22,6 @@ describe.each([ [API_KEY_CLIENT_ID_MEMORY, "apiKeyClientIdMemory"], [API_KEY_CLIENT_SECRET_DISK, "apiKeyClientSecretDisk"], [API_KEY_CLIENT_SECRET_MEMORY, "apiKeyClientSecretMemory"], - [SECURITY_STAMP_MEMORY, "securityStamp"], ])( "deserializes state key definitions", ( diff --git a/libs/common/src/auth/services/token.state.ts b/libs/common/src/auth/services/token.state.ts index 57d85f2a559..458d6846c17 100644 --- a/libs/common/src/auth/services/token.state.ts +++ b/libs/common/src/auth/services/token.state.ts @@ -69,8 +69,3 @@ export const API_KEY_CLIENT_SECRET_MEMORY = new UserKeyDefinition( clearOn: [], // Manually handled }, ); - -export const SECURITY_STAMP_MEMORY = new UserKeyDefinition(TOKEN_MEMORY, "securityStamp", { - deserializer: (securityStamp) => securityStamp, - clearOn: ["logout"], -}); diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index f1d4b3848ef..051604f0ae5 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -181,6 +181,8 @@ export abstract class StateService { * Sets the user's Pin, encrypted by the user key */ setProtectedPin: (value: string, options?: StorageOptions) => Promise; + getSecurityStamp: (options?: StorageOptions) => Promise; + setSecurityStamp: (value: string, options?: StorageOptions) => Promise; getUserId: (options?: StorageOptions) => Promise; getVaultTimeout: (options?: StorageOptions) => Promise; setVaultTimeout: (value: number, options?: StorageOptions) => Promise; diff --git a/libs/common/src/platform/models/domain/account-tokens.spec.ts b/libs/common/src/platform/models/domain/account-tokens.spec.ts new file mode 100644 index 00000000000..733b3908e9a --- /dev/null +++ b/libs/common/src/platform/models/domain/account-tokens.spec.ts @@ -0,0 +1,9 @@ +import { AccountTokens } from "./account"; + +describe("AccountTokens", () => { + describe("fromJSON", () => { + it("should deserialize to an instance of itself", () => { + expect(AccountTokens.fromJSON({})).toBeInstanceOf(AccountTokens); + }); + }); +}); diff --git a/libs/common/src/platform/models/domain/account.spec.ts b/libs/common/src/platform/models/domain/account.spec.ts index 77c242b6ff5..0c76c16cc2d 100644 --- a/libs/common/src/platform/models/domain/account.spec.ts +++ b/libs/common/src/platform/models/domain/account.spec.ts @@ -1,4 +1,4 @@ -import { Account, AccountKeys, AccountProfile, AccountSettings } from "./account"; +import { Account, AccountKeys, AccountProfile, AccountSettings, AccountTokens } from "./account"; describe("Account", () => { describe("fromJSON", () => { @@ -10,12 +10,14 @@ describe("Account", () => { const keysSpy = jest.spyOn(AccountKeys, "fromJSON"); const profileSpy = jest.spyOn(AccountProfile, "fromJSON"); const settingsSpy = jest.spyOn(AccountSettings, "fromJSON"); + const tokensSpy = jest.spyOn(AccountTokens, "fromJSON"); Account.fromJSON({}); expect(keysSpy).toHaveBeenCalled(); expect(profileSpy).toHaveBeenCalled(); expect(settingsSpy).toHaveBeenCalled(); + expect(tokensSpy).toHaveBeenCalled(); }); }); }); diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index cd416ec1f94..5a9a7646962 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -171,11 +171,24 @@ export class AccountSettings { } } +export class AccountTokens { + securityStamp?: string; + + static fromJSON(obj: Jsonify): AccountTokens { + if (obj == null) { + return null; + } + + return Object.assign(new AccountTokens(), obj); + } +} + export class Account { data?: AccountData = new AccountData(); keys?: AccountKeys = new AccountKeys(); profile?: AccountProfile = new AccountProfile(); settings?: AccountSettings = new AccountSettings(); + tokens?: AccountTokens = new AccountTokens(); constructor(init: Partial) { Object.assign(this, { @@ -195,6 +208,10 @@ export class Account { ...new AccountSettings(), ...init?.settings, }, + tokens: { + ...new AccountTokens(), + ...init?.tokens, + }, }); } @@ -208,6 +225,7 @@ export class Account { data: AccountData.fromJSON(json?.data), profile: AccountProfile.fromJSON(json?.profile), settings: AccountSettings.fromJSON(json?.settings), + tokens: AccountTokens.fromJSON(json?.tokens), }); } } diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index d0a55d7a47c..f660cd7a342 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -839,6 +839,23 @@ export class StateService< ); } + async getSecurityStamp(options?: StorageOptions): Promise { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) + )?.tokens?.securityStamp; + } + + async setSecurityStamp(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultInMemoryOptions()), + ); + account.tokens.securityStamp = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultInMemoryOptions()), + ); + } + async getUserId(options?: StorageOptions): Promise { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) diff --git a/libs/common/src/vault/services/sync/sync.service.ts b/libs/common/src/vault/services/sync/sync.service.ts index 73869ff488e..ff8e9f1f4f5 100644 --- a/libs/common/src/vault/services/sync/sync.service.ts +++ b/libs/common/src/vault/services/sync/sync.service.ts @@ -15,7 +15,6 @@ import { AccountService } from "../../../auth/abstractions/account.service"; import { AvatarService } from "../../../auth/abstractions/avatar.service"; import { KeyConnectorService } from "../../../auth/abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "../../../auth/abstractions/master-password.service.abstraction"; -import { TokenService } from "../../../auth/abstractions/token.service"; import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason"; import { DomainSettingsService } from "../../../autofill/services/domain-settings.service"; import { BillingAccountProfileStateService } from "../../../billing/abstractions/account/billing-account-profile-state.service"; @@ -74,7 +73,6 @@ export class SyncService implements SyncServiceAbstraction { private avatarService: AvatarService, private logoutCallback: (expired: boolean) => Promise, private billingAccountProfileStateService: BillingAccountProfileStateService, - private tokenService: TokenService, ) {} async getLastSync(): Promise { @@ -311,7 +309,7 @@ export class SyncService implements SyncServiceAbstraction { } private async syncProfile(response: ProfileResponse) { - const stamp = await this.tokenService.getSecurityStamp(response.id as UserId); + const stamp = await this.stateService.getSecurityStamp(); if (stamp != null && stamp !== response.securityStamp) { if (this.logoutCallback != null) { await this.logoutCallback(true); @@ -325,7 +323,7 @@ export class SyncService implements SyncServiceAbstraction { await this.cryptoService.setProviderKeys(response.providers); await this.cryptoService.setOrgKeys(response.organizations, response.providerOrganizations); await this.avatarService.setSyncAvatarColor(response.id as UserId, response.avatarColor); - await this.tokenService.setSecurityStamp(response.securityStamp, response.id as UserId); + await this.stateService.setSecurityStamp(response.securityStamp); await this.stateService.setEmailVerified(response.emailVerified); await this.billingAccountProfileStateService.setHasPremium( From b395cb40a7a0e35d836009028f4f32242858d0f7 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Mon, 22 Apr 2024 09:32:44 -0700 Subject: [PATCH 030/110] [AC-1999] Fix deleting collections from collection dialog (#8647) * [AC-1999] Fix null check this.collection can be both null or unassigned and `!= null` will handle both cases. * [AC-1999] Navigate away when selected collection is deleted --------- Co-authored-by: bnagawiecki <107435978+bnagawiecki@users.noreply.github.com> --- .../vault/individual-vault/vault.component.ts | 12 +++++++++--- .../vault-header/vault-header.component.ts | 2 +- .../src/app/vault/org-vault/vault.component.ts | 18 ++++++++++++++---- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index a25ba6edbcf..2c20328336b 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -679,6 +679,14 @@ export class VaultComponent implements OnInit, OnDestroy { } else if (result.action === CollectionDialogAction.Deleted) { await this.collectionService.delete(result.collection?.id); this.refresh(); + // Navigate away if we deleted the collection we were viewing + if (this.selectedCollection?.node.id === c?.id) { + void this.router.navigate([], { + queryParams: { collectionId: this.selectedCollection.parent?.node.id ?? null }, + queryParamsHandling: "merge", + replaceUrl: true, + }); + } } } @@ -710,9 +718,7 @@ export class VaultComponent implements OnInit, OnDestroy { ); // Navigate away if we deleted the collection we were viewing if (this.selectedCollection?.node.id === collection.id) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate([], { + void this.router.navigate([], { queryParams: { collectionId: this.selectedCollection.parent?.node.id ?? null }, queryParamsHandling: "merge", replaceUrl: true, diff --git a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts index c4c67759c7f..a5cd4680081 100644 --- a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts +++ b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts @@ -80,7 +80,7 @@ export class VaultHeaderComponent implements OnInit { ? this.i18nService.t("collections").toLowerCase() : this.i18nService.t("vault").toLowerCase(); - if (this.collection !== undefined) { + if (this.collection != null) { return this.collection.node.name; } diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index 587758dda13..9de404e9698 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -958,11 +958,9 @@ export class VaultComponent implements OnInit, OnDestroy { this.i18nService.t("deletedCollectionId", collection.name), ); - // Navigate away if we deleted the colletion we were viewing + // Navigate away if we deleted the collection we were viewing if (this.selectedCollection?.node.id === collection.id) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate([], { + void this.router.navigate([], { queryParams: { collectionId: this.selectedCollection.parent?.node.id ?? null }, queryParamsHandling: "merge", replaceUrl: true, @@ -1095,6 +1093,18 @@ export class VaultComponent implements OnInit, OnDestroy { result.action === CollectionDialogAction.Deleted ) { this.refresh(); + + // If we deleted the selected collection, navigate up/away + if ( + result.action === CollectionDialogAction.Deleted && + this.selectedCollection?.node.id === c?.id + ) { + void this.router.navigate([], { + queryParams: { collectionId: this.selectedCollection.parent?.node.id ?? null }, + queryParamsHandling: "merge", + replaceUrl: true, + }); + } } } From fb211c5feea0594ccdd992c2729154424376bc57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Mon, 22 Apr 2024 18:58:29 +0200 Subject: [PATCH 031/110] Add rust analyzer support for desktop-native (#8830) --- .vscode/settings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 27e3a9b293a..3a70af3481d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,5 +5,6 @@ "**/locales/*[^n]/messages.json": true, "**/_locales/[^e]*/messages.json": true, "**/_locales/*[^n]/messages.json": true - } + }, + "rust-analyzer.linkedProjects": ["apps/desktop/desktop_native/Cargo.toml"] } From 29d4f1aad5fade6c85749e002a4d0b1a05f2fdbb Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez Date: Mon, 22 Apr 2024 12:58:20 -0500 Subject: [PATCH 032/110] [PM-7660] Master Password Re-Prompt from Autofill Not Working (#8862) --- .../browser/src/background/main.background.ts | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 15f21b25014..a64ee2b8a00 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1105,20 +1105,22 @@ export default class MainBackground { await (this.eventUploadService as EventUploadService).init(true); this.twoFactorService.init(); - if (!this.popupOnlyContext) { - await this.vaultTimeoutService.init(true); - this.fido2Background.init(); - await this.runtimeBackground.init(); - await this.notificationBackground.init(); - this.filelessImporterBackground.init(); - await this.commandsBackground.init(); - await this.overlayBackground.init(); - await this.tabsBackground.init(); - this.contextMenusBackground?.init(); - await this.idleBackground.init(); - if (BrowserApi.isManifestVersion(2)) { - await this.webRequestBackground.init(); - } + if (this.popupOnlyContext) { + return; + } + + await this.vaultTimeoutService.init(true); + this.fido2Background.init(); + await this.runtimeBackground.init(); + await this.notificationBackground.init(); + this.filelessImporterBackground.init(); + await this.commandsBackground.init(); + await this.overlayBackground.init(); + await this.tabsBackground.init(); + this.contextMenusBackground?.init(); + await this.idleBackground.init(); + if (BrowserApi.isManifestVersion(2)) { + await this.webRequestBackground.init(); } if (this.platformUtilsService.isFirefox() && !this.isPrivateMode) { From e29779875797cb8a508d3857aa25ecefca5aa5c1 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Mon, 22 Apr 2024 16:54:41 -0400 Subject: [PATCH 033/110] Stop CryptoService from using `getBgService` (#8843) --- .../src/popup/services/services.module.ts | 43 +++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index a7da6b76127..b344a18492e 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -31,6 +31,7 @@ import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/ab import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; @@ -55,6 +56,7 @@ import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt. import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; @@ -63,6 +65,7 @@ import { AbstractStorageService, ObservableStorageService, } from "@bitwarden/common/platform/abstractions/storage.service"; +import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; // eslint-disable-next-line no-restricted-imports -- Used for dependency injection @@ -102,6 +105,7 @@ import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; import { BrowserFileDownloadService } from "../../platform/popup/services/browser-file-download.service"; import { BrowserStateService as StateServiceAbstraction } from "../../platform/services/abstractions/browser-state.service"; import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service"; +import { BrowserCryptoService } from "../../platform/services/browser-crypto.service"; import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service"; import BrowserLocalStorageService from "../../platform/services/browser-local-storage.service"; import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service"; @@ -232,12 +236,45 @@ const safeProviders: SafeProvider[] = [ }), safeProvider({ provide: CryptoService, - useFactory: (encryptService: EncryptService) => { - const cryptoService = getBgService("cryptoService")(); + useFactory: ( + masterPasswordService: InternalMasterPasswordServiceAbstraction, + keyGenerationService: KeyGenerationService, + cryptoFunctionService: CryptoFunctionService, + encryptService: EncryptService, + platformUtilsService: PlatformUtilsService, + logService: LogService, + stateService: StateServiceAbstraction, + accountService: AccountServiceAbstraction, + stateProvider: StateProvider, + biometricStateService: BiometricStateService, + ) => { + const cryptoService = new BrowserCryptoService( + masterPasswordService, + keyGenerationService, + cryptoFunctionService, + encryptService, + platformUtilsService, + logService, + stateService, + accountService, + stateProvider, + biometricStateService, + ); new ContainerService(cryptoService, encryptService).attachToGlobal(self); return cryptoService; }, - deps: [EncryptService], + deps: [ + InternalMasterPasswordServiceAbstraction, + KeyGenerationService, + CryptoFunctionService, + EncryptService, + PlatformUtilsService, + LogService, + StateServiceAbstraction, + AccountServiceAbstraction, + StateProvider, + BiometricStateService, + ], }), safeProvider({ provide: TotpServiceAbstraction, From 33dae77a4d9caab3a0ea88ec89d10efb38097a84 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Mon, 22 Apr 2024 17:11:30 -0400 Subject: [PATCH 034/110] Revert "Stop CryptoService from using `getBgService` (#8843)" (#8867) This reverts commit e29779875797cb8a508d3857aa25ecefca5aa5c1. --- .../src/popup/services/services.module.ts | 43 ++----------------- 1 file changed, 3 insertions(+), 40 deletions(-) diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index b344a18492e..a7da6b76127 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -31,7 +31,6 @@ import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/ab import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; -import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; @@ -56,7 +55,6 @@ import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt. import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; @@ -65,7 +63,6 @@ import { AbstractStorageService, ObservableStorageService, } from "@bitwarden/common/platform/abstractions/storage.service"; -import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; // eslint-disable-next-line no-restricted-imports -- Used for dependency injection @@ -105,7 +102,6 @@ import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; import { BrowserFileDownloadService } from "../../platform/popup/services/browser-file-download.service"; import { BrowserStateService as StateServiceAbstraction } from "../../platform/services/abstractions/browser-state.service"; import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service"; -import { BrowserCryptoService } from "../../platform/services/browser-crypto.service"; import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service"; import BrowserLocalStorageService from "../../platform/services/browser-local-storage.service"; import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service"; @@ -236,45 +232,12 @@ const safeProviders: SafeProvider[] = [ }), safeProvider({ provide: CryptoService, - useFactory: ( - masterPasswordService: InternalMasterPasswordServiceAbstraction, - keyGenerationService: KeyGenerationService, - cryptoFunctionService: CryptoFunctionService, - encryptService: EncryptService, - platformUtilsService: PlatformUtilsService, - logService: LogService, - stateService: StateServiceAbstraction, - accountService: AccountServiceAbstraction, - stateProvider: StateProvider, - biometricStateService: BiometricStateService, - ) => { - const cryptoService = new BrowserCryptoService( - masterPasswordService, - keyGenerationService, - cryptoFunctionService, - encryptService, - platformUtilsService, - logService, - stateService, - accountService, - stateProvider, - biometricStateService, - ); + useFactory: (encryptService: EncryptService) => { + const cryptoService = getBgService("cryptoService")(); new ContainerService(cryptoService, encryptService).attachToGlobal(self); return cryptoService; }, - deps: [ - InternalMasterPasswordServiceAbstraction, - KeyGenerationService, - CryptoFunctionService, - EncryptService, - PlatformUtilsService, - LogService, - StateServiceAbstraction, - AccountServiceAbstraction, - StateProvider, - BiometricStateService, - ], + deps: [EncryptService], }), safeProvider({ provide: TotpServiceAbstraction, From 4afb5d04f00c4a4a883d3dfc20a8752f929880d6 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Mon, 22 Apr 2024 17:14:14 -0400 Subject: [PATCH 035/110] Remove `alarms` Permission (#8866) --- apps/browser/src/manifest.v3.json | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index cdd0869fc52..26efbbbf9ba 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -59,7 +59,6 @@ "clipboardRead", "clipboardWrite", "idle", - "alarms", "scripting", "offscreen" ], From 714ca66f331ec4ffde3a3e40fc767b27a4967903 Mon Sep 17 00:00:00 2001 From: Bitwarden DevOps <106330231+bitwarden-devops-bot@users.noreply.github.com> Date: Tue, 23 Apr 2024 07:32:09 -0400 Subject: [PATCH 036/110] Bumped browser,cli,desktop,web version to (#8875) --- apps/browser/package.json | 2 +- apps/browser/src/manifest.json | 2 +- apps/browser/src/manifest.v3.json | 2 +- apps/cli/package.json | 2 +- apps/desktop/package.json | 2 +- apps/desktop/src/package-lock.json | 4 ++-- apps/desktop/src/package.json | 2 +- apps/web/package.json | 2 +- package-lock.json | 8 ++++---- 9 files changed, 13 insertions(+), 13 deletions(-) diff --git a/apps/browser/package.json b/apps/browser/package.json index ee6d100572d..506f19f279b 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2024.4.1", + "version": "2024.4.2", "scripts": { "build": "webpack", "build:mv3": "cross-env MANIFEST_VERSION=3 webpack", diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index 78f1e2cc419..18bfaf8acbd 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.4.1", + "version": "2024.4.2", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index 26efbbbf9ba..c0c88706b80 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -3,7 +3,7 @@ "minimum_chrome_version": "102.0", "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.4.1", + "version": "2024.4.2", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/cli/package.json b/apps/cli/package.json index 690842d831d..b06caacfd48 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/cli", "description": "A secure and free password manager for all of your devices.", - "version": "2024.3.1", + "version": "2024.4.0", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 4bb0ab2d931..5e098eb2135 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/desktop", "description": "A secure and free password manager for all of your devices.", - "version": "2024.4.2", + "version": "2024.4.3", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index 11b38bd2738..d6945fd16e1 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2024.4.2", + "version": "2024.4.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2024.4.2", + "version": "2024.4.3", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-native": "file:../desktop_native", diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index a65dab016ca..fa190af9a69 100644 --- a/apps/desktop/src/package.json +++ b/apps/desktop/src/package.json @@ -2,7 +2,7 @@ "name": "@bitwarden/desktop", "productName": "Bitwarden", "description": "A secure and free password manager for all of your devices.", - "version": "2024.4.2", + "version": "2024.4.3", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/apps/web/package.json b/apps/web/package.json index 55fe0987d72..434712cdf4a 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/web-vault", - "version": "2024.4.1", + "version": "2024.4.2", "scripts": { "build:oss": "webpack", "build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js", diff --git a/package-lock.json b/package-lock.json index d72ba9cb19b..f5932a25b80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -193,11 +193,11 @@ }, "apps/browser": { "name": "@bitwarden/browser", - "version": "2024.4.1" + "version": "2024.4.2" }, "apps/cli": { "name": "@bitwarden/cli", - "version": "2024.3.1", + "version": "2024.4.0", "license": "GPL-3.0-only", "dependencies": { "@koa/multer": "3.0.2", @@ -233,7 +233,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2024.4.2", + "version": "2024.4.3", "hasInstallScript": true, "license": "GPL-3.0" }, @@ -247,7 +247,7 @@ }, "apps/web": { "name": "@bitwarden/web-vault", - "version": "2024.4.1" + "version": "2024.4.2" }, "libs/admin-console": { "name": "@bitwarden/admin-console", From e4ebf4aeccb4c214e0b483113d9b9b53cc6f7438 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Tue, 23 Apr 2024 09:18:49 -0400 Subject: [PATCH 037/110] [PM-7349] Update snap description with new URL to help docs (#8703) * Updated snap summary with new URL to help docs. * Updated to use summary and description. --- apps/desktop/electron-builder.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index 4f0d05581c0..960d56b0362 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -228,7 +228,8 @@ "artifactName": "${productName}-${version}-${arch}.${ext}" }, "snap": { - "summary": "After installation enable required `password-manager-service` by running `sudo snap connect bitwarden:password-manager-service`.", + "summary": "Bitwarden is a secure and free password manager for all of your devices.", + "description": "**Installation**\nBitwarden requires access to the `password-manager-service`. Please enable it through permissions or by running `sudo snap connect bitwarden:password-manager-service` after installation. See https://btwrdn.com/install-snap for details.", "autoStart": true, "base": "core22", "confinement": "strict", From 73d0782b6ce4a0be1cf0480d0a9658420b5ef438 Mon Sep 17 00:00:00 2001 From: Will Martin Date: Tue, 23 Apr 2024 09:45:11 -0400 Subject: [PATCH 038/110] [CL-110] fix code block text color in Storybook (#8868) --- libs/components/src/styles.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libs/components/src/styles.scss b/libs/components/src/styles.scss index f03d2dd3409..ae97838e09f 100644 --- a/libs/components/src/styles.scss +++ b/libs/components/src/styles.scss @@ -47,3 +47,8 @@ $card-icons-base: "../../src/billing/images/cards/"; @import "bootstrap/scss/_print"; @import "multi-select/scss/bw.theme.scss"; + +// Workaround for https://bitwarden.atlassian.net/browse/CL-110 +#storybook-docs pre.prismjs { + color: white; +} From 7d58b21856c5551dadd7127c725be2882076cf20 Mon Sep 17 00:00:00 2001 From: vinith-kovan <156108204+vinith-kovan@users.noreply.github.com> Date: Tue, 23 Apr 2024 19:20:15 +0530 Subject: [PATCH 039/110] migrating activate autofill component (#8782) --- .../policies/activate-autofill.component.html | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/admin-console/policies/activate-autofill.component.html b/bitwarden_license/bit-web/src/app/admin-console/policies/activate-autofill.component.html index 9f08e98daea..94f2e8a422d 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/policies/activate-autofill.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/policies/activate-autofill.component.html @@ -5,15 +5,7 @@ }} -
-
- - -
-
+ + + {{ "turnOn" | i18n }} + From 38ea110755a4256f96ba7946b680070b5c32cdc6 Mon Sep 17 00:00:00 2001 From: vinith-kovan <156108204+vinith-kovan@users.noreply.github.com> Date: Tue, 23 Apr 2024 19:22:26 +0530 Subject: [PATCH 040/110] migrating two factor authentication component (#8760) --- .../two-factor-authentication.component.html | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/policies/two-factor-authentication.component.html b/apps/web/src/app/admin-console/organizations/policies/two-factor-authentication.component.html index 3286c086894..d0be72a52e6 100644 --- a/apps/web/src/app/admin-console/organizations/policies/two-factor-authentication.component.html +++ b/apps/web/src/app/admin-console/organizations/policies/two-factor-authentication.component.html @@ -2,15 +2,7 @@ {{ "twoStepLoginPolicyWarning" | i18n }} -
-
- - -
-
+ + + {{ "turnOn" | i18n }} + From 5f3844aa38de27dff8986b399a5db327a6101c04 Mon Sep 17 00:00:00 2001 From: Tom <144813356+ttalty@users.noreply.github.com> Date: Tue, 23 Apr 2024 11:26:31 -0400 Subject: [PATCH 041/110] Getting the user's access token for file upload (#8877) --- libs/common/src/abstractions/api.service.ts | 9 ++++++++- libs/common/src/services/api.service.ts | 5 +++-- libs/common/src/services/event/event-upload.service.ts | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index 9b3160ee19d..c1a0e1f9cd9 100644 --- a/libs/common/src/abstractions/api.service.ts +++ b/libs/common/src/abstractions/api.service.ts @@ -103,6 +103,7 @@ import { EventResponse } from "../models/response/event.response"; import { ListResponse } from "../models/response/list.response"; import { ProfileResponse } from "../models/response/profile.response"; import { UserKeyResponse } from "../models/response/user-key.response"; +import { UserId } from "../types/guid"; import { AttachmentRequest } from "../vault/models/request/attachment.request"; import { CipherBulkDeleteRequest } from "../vault/models/request/cipher-bulk-delete.request"; import { CipherBulkMoveRequest } from "../vault/models/request/cipher-bulk-move.request"; @@ -451,7 +452,13 @@ export abstract class ApiService { end: string, token: string, ) => Promise>; - postEventsCollect: (request: EventRequest[]) => Promise; + + /** + * Posts events for a user + * @param request The array of events to upload + * @param userId The optional user id the events belong to. If no user id is provided the active user id is used. + */ + postEventsCollect: (request: EventRequest[], userId?: UserId) => Promise; deleteSsoUser: (organizationId: string) => Promise; getSsoUserIdentifier: () => Promise; diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index e8135f3d6c2..84fa7bd0773 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -118,6 +118,7 @@ import { EnvironmentService } from "../platform/abstractions/environment.service import { PlatformUtilsService } from "../platform/abstractions/platform-utils.service"; import { StateService } from "../platform/abstractions/state.service"; import { Utils } from "../platform/misc/utils"; +import { UserId } from "../types/guid"; import { AttachmentRequest } from "../vault/models/request/attachment.request"; import { CipherBulkDeleteRequest } from "../vault/models/request/cipher-bulk-delete.request"; import { CipherBulkMoveRequest } from "../vault/models/request/cipher-bulk-move.request"; @@ -1423,8 +1424,8 @@ export class ApiService implements ApiServiceAbstraction { return new ListResponse(r, EventResponse); } - async postEventsCollect(request: EventRequest[]): Promise { - const authHeader = await this.getActiveBearerToken(); + async postEventsCollect(request: EventRequest[], userId?: UserId): Promise { + const authHeader = await this.tokenService.getAccessToken(userId); const headers = new Headers({ "Device-Type": this.deviceType, Authorization: "Bearer " + authHeader, diff --git a/libs/common/src/services/event/event-upload.service.ts b/libs/common/src/services/event/event-upload.service.ts index 6f229751bf1..c87d3b2024d 100644 --- a/libs/common/src/services/event/event-upload.service.ts +++ b/libs/common/src/services/event/event-upload.service.ts @@ -70,7 +70,7 @@ export class EventUploadService implements EventUploadServiceAbstraction { return req; }); try { - await this.apiService.postEventsCollect(request); + await this.apiService.postEventsCollect(request, userId); } catch (e) { this.logService.error(e); // Add the events back to state if there was an error and they were not uploaded. From ca38a5bc1f76f816de4f5c049d69b2fba8c33b54 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 23 Apr 2024 15:29:47 +0000 Subject: [PATCH 042/110] Autosync the updated translations (#8878) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/src/_locales/cy/messages.json | 8 +- apps/browser/src/_locales/de/messages.json | 20 ++-- apps/browser/src/_locales/ko/messages.json | 44 ++++----- apps/browser/src/_locales/nl/messages.json | 6 +- apps/browser/src/_locales/pl/messages.json | 4 +- apps/browser/src/_locales/pt_BR/messages.json | 96 +++++++++---------- apps/browser/src/_locales/sr/messages.json | 16 ++-- apps/browser/src/_locales/zh_CN/messages.json | 12 +-- apps/browser/store/locales/de/copy.resx | 56 +++++------ apps/browser/store/locales/ko/copy.resx | 4 +- apps/browser/store/locales/pl/copy.resx | 60 ++++++------ apps/browser/store/locales/pt_BR/copy.resx | 6 +- apps/browser/store/locales/zh_CN/copy.resx | 60 ++++++------ 13 files changed, 195 insertions(+), 197 deletions(-) diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index c718c1d8765..2be868872c6 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -23,7 +23,7 @@ "message": "Enterprise single sign-on" }, "cancel": { - "message": "Cancel" + "message": "Canslo" }, "close": { "message": "Cau" @@ -318,7 +318,7 @@ "message": "Golygu" }, "view": { - "message": "View" + "message": "Gweld" }, "noItemsInList": { "message": "Does dim eitemau i'w rhestru." @@ -549,10 +549,10 @@ "message": "Ydych chi'n siŵr eich bod am allgofnodi?" }, "yes": { - "message": "Yes" + "message": "Ydw" }, "no": { - "message": "No" + "message": "Na" }, "unexpectedError": { "message": "An unexpected error has occurred." diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 8f2a59af1e9..d55d499b3c1 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Bitwarden Passwortmanager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", + "message": "Zu Hause, am Arbeitsplatz oder unterwegs schützt Bitwarden einfach alle deine Passwörter, Passkeys und vertraulichen Informationen.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -173,10 +173,10 @@ "message": "Master-Passwort ändern" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "Weiter zur Web-App?" }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "Du kannst dein Master-Passwort in der Bitwarden Web-App ändern." }, "fingerprintPhrase": { "message": "Fingerabdruck-Phrase", @@ -3001,7 +3001,7 @@ "description": "Notification message for when saving credentials has failed." }, "success": { - "message": "Success" + "message": "Erfolg" }, "removePasskey": { "message": "Passkey entfernen" @@ -3010,21 +3010,21 @@ "message": "Passkey entfernt" }, "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + "message": "Hinweis: Nicht zugeordnete Organisationseinträge sind nicht mehr in der Ansicht aller Tresore sichtbar und nur über die Administrator-Konsole zugänglich." }, "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + "message": "Hinweis: Ab dem 16. Mai 2024 sind nicht zugewiesene Organisationselemente nicht mehr in der Ansicht aller Tresore sichtbar und nur über die Administrator-Konsole zugänglich." }, "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", + "message": "Weise diese Einträge einer Sammlung aus der", "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." }, "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", + "message": "zu, um sie sichtbar zu machen.", "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." }, "adminConsole": { - "message": "Admin Console" + "message": "Administrator-Konsole" }, "errorAssigningTargetCollection": { "message": "Fehler beim Zuweisen der Ziel-Sammlung." diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index 4bc4302f8b7..1724225b0e2 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Bitwarden 비밀번호 관리자", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", + "message": "집에서도, 직장에서도, 이동 중에도 Bitwarden은 비밀번호, 패스키, 민감 정보를 쉽게 보호합니다.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -173,7 +173,7 @@ "message": "마스터 비밀번호 변경" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "웹 앱에서 계속하시겠용?" }, "changeMasterPasswordOnWebConfirmation": { "message": "You can change your master password on the Bitwarden web app." @@ -688,10 +688,10 @@ "message": "Ask to update a login's password when a change is detected on a website. Applies to all logged in accounts." }, "enableUsePasskeys": { - "message": "Ask to save and use passkeys" + "message": "패스키를 저장 및 사용할지 묻기" }, "usePasskeysDesc": { - "message": "Ask to save new passkeys or log in with passkeys stored in your vault. Applies to all logged in accounts." + "message": "보관함에 새 패스키를 저장하거나 로그인할지 물어봅니다. 모든 로그인된 계정에 적용됩니다." }, "notificationChangeDesc": { "message": "Bitwarden에 저장되어 있는 비밀번호를 이 비밀번호로 변경하시겠습니까?" @@ -2786,55 +2786,55 @@ "message": "Confirm file password" }, "typePasskey": { - "message": "Passkey" + "message": "패스키" }, "passkeyNotCopied": { - "message": "Passkey will not be copied" + "message": "패스키가 복사되지 않습니다" }, "passkeyNotCopiedAlert": { - "message": "The passkey will not be copied to the cloned item. Do you want to continue cloning this item?" + "message": "패스키는 복제된 아이템에 복사되지 않습니다. 계속 이 항목을 복제하시겠어요?" }, "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { - "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." + "message": "사이트에서 인증을 요구합니다. 이 기능은 비밀번호가 없는 계정에서는 아직 지원하지 않습니다." }, "logInWithPasskey": { - "message": "Log in with passkey?" + "message": "패스키로 로그인하시겠어요?" }, "passkeyAlreadyExists": { - "message": "A passkey already exists for this application." + "message": "이미 이 애플리케이션에 해당하는 패스키가 있습니다." }, "noPasskeysFoundForThisApplication": { - "message": "No passkeys found for this application." + "message": "이 애플리케이션에 대한 패스키를 찾을 수 없습니다." }, "noMatchingPasskeyLogin": { - "message": "You do not have a matching login for this site." + "message": "사이트와 일치하는 로그인이 없습니다." }, "confirm": { "message": "Confirm" }, "savePasskey": { - "message": "Save passkey" + "message": "패스키 저장" }, "savePasskeyNewLogin": { - "message": "Save passkey as new login" + "message": "새 로그인으로 패스키 저장" }, "choosePasskey": { - "message": "Choose a login to save this passkey to" + "message": "패스키를 저장할 로그인 선택하기" }, "passkeyItem": { - "message": "Passkey Item" + "message": "패스키 항목" }, "overwritePasskey": { - "message": "Overwrite passkey?" + "message": "비밀번호를 덮어쓰시겠어요?" }, "overwritePasskeyAlert": { - "message": "This item already contains a passkey. Are you sure you want to overwrite the current passkey?" + "message": "이 항목은 이미 패스키가 있습니다. 정말로 현재 패스키를 덮어쓰시겠어요?" }, "featureNotSupported": { "message": "Feature not yet supported" }, "yourPasskeyIsLocked": { - "message": "Authentication required to use passkey. Verify your identity to continue." + "message": "패스키를 사용하려면 인증이 필요합니다. 인증을 진행해주세요." }, "multifactorAuthenticationCancelled": { "message": "Multifactor authentication cancelled" @@ -3004,10 +3004,10 @@ "message": "Success" }, "removePasskey": { - "message": "Remove passkey" + "message": "패스키 제거" }, "passkeyRemoved": { - "message": "Passkey removed" + "message": "패스키 제거됨" }, "unassignedItemsBannerNotice": { "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index 5a52b4f7ef7..f1424df0b9a 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -531,7 +531,7 @@ "message": "Kan de QR-code van de huidige webpagina niet scannen" }, "totpCaptureSuccess": { - "message": "Authenticatie-sleutel toegevoegd" + "message": "Authenticatiesleutel toegevoegd" }, "totpCapture": { "message": "Scan de authenticatie-QR-code van de huidige webpagina" @@ -1673,10 +1673,10 @@ "message": "Browserintegratie is niet ingeschakeld in de Bitwarden-desktopapplicatie. Schakel deze optie in de instellingen binnen de desktop-applicatie in." }, "startDesktopTitle": { - "message": "Bitwarden-desktopapplicatie opstarten" + "message": "Bitwarden desktopapplicatie opstarten" }, "startDesktopDesc": { - "message": "Je moet de Bitwarden-desktopapplicatie starten om deze functie te gebruiken." + "message": "Je moet de Bitwarden desktopapplicatie starten om deze functie te gebruiken." }, "errorEnableBiometricTitle": { "message": "Kon biometrie niet inschakelen" diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index e768a70d528..9b3e8f20fc0 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Menedżer Haseł Bitwarden", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", + "message": "W domu, w pracy, lub w ruchu, Bitwarden z łatwością zabezpiecza wszystkie Twoje hasła, passkeys i poufne informacje.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index c6e62fbd4f3..ef2b6f2dcae 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Bitwarden Gerenciador de Senhas", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", + "message": "Em casa, no trabalho, ou em qualquer lugar, o Bitwarden protege facilmente todas as suas senhas, senhas e informações confidenciais.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -173,10 +173,10 @@ "message": "Alterar Senha Mestra" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "Continuar no aplicativo web?" }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "Você pode alterar a sua senha mestra no aplicativo web Bitwarden." }, "fingerprintPhrase": { "message": "Frase Biométrica", @@ -500,10 +500,10 @@ "message": "A sua nova conta foi criada! Agora você pode iniciar a sessão." }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "Você logou na sua conta com sucesso" }, "youMayCloseThisWindow": { - "message": "You may close this window" + "message": "Você pode fechar esta janela" }, "masterPassSent": { "message": "Enviamos um e-mail com a dica da sua senha mestra." @@ -1500,7 +1500,7 @@ "message": "Código PIN inválido." }, "tooManyInvalidPinEntryAttemptsLoggingOut": { - "message": "Too many invalid PIN entry attempts. Logging out." + "message": "Muitas tentativas de entrada de PIN inválidas. Desconectando." }, "unlockWithBiometrics": { "message": "Desbloquear com a biometria" @@ -2005,7 +2005,7 @@ "message": "Selecionar pasta..." }, "noFoldersFound": { - "message": "No folders found", + "message": "Nenhuma pasta encontrada", "description": "Used as a message within the notification bar when no folders are found" }, "orgPermissionsUpdatedMustSetPassword": { @@ -2017,7 +2017,7 @@ "description": "Used as a card title description on the set password page to explain why the user is there" }, "verificationRequired": { - "message": "Verification required", + "message": "Verificação necessária", "description": "Default title for the user verification dialog." }, "hours": { @@ -2652,40 +2652,40 @@ } }, "tryAgain": { - "message": "Try again" + "message": "Tentar novamente" }, "verificationRequiredForActionSetPinToContinue": { - "message": "Verification required for this action. Set a PIN to continue." + "message": "Verificação necessária para esta ação. Defina um PIN para continuar." }, "setPin": { - "message": "Set PIN" + "message": "Definir PIN" }, "verifyWithBiometrics": { - "message": "Verify with biometrics" + "message": "Verificiar com biometria" }, "awaitingConfirmation": { - "message": "Awaiting confirmation" + "message": "Aguardando confirmação" }, "couldNotCompleteBiometrics": { - "message": "Could not complete biometrics." + "message": "Não foi possível completar a biometria." }, "needADifferentMethod": { - "message": "Need a different method?" + "message": "Precisa de um método diferente?" }, "useMasterPassword": { - "message": "Use master password" + "message": "Usar a senha mestra" }, "usePin": { - "message": "Use PIN" + "message": "Usar PIN" }, "useBiometrics": { - "message": "Use biometrics" + "message": "Usar biometria" }, "enterVerificationCodeSentToEmail": { - "message": "Enter the verification code that was sent to your email." + "message": "Digite o código de verificação que foi enviado para o seu e-mail." }, "resendCode": { - "message": "Resend code" + "message": "Reenviar código" }, "total": { "message": "Total" @@ -2700,19 +2700,19 @@ } }, "launchDuoAndFollowStepsToFinishLoggingIn": { - "message": "Launch Duo and follow the steps to finish logging in." + "message": "Inicie o Duo e siga os passos para finalizar o login." }, "duoRequiredForAccount": { - "message": "Duo two-step login is required for your account." + "message": "A autenticação em duas etapas do Duo é necessária para sua conta." }, "popoutTheExtensionToCompleteLogin": { - "message": "Popout the extension to complete login." + "message": "Abra a extensão para concluir o login." }, "popoutExtension": { - "message": "Popout extension" + "message": "Extensão pop-out" }, "launchDuo": { - "message": "Launch Duo" + "message": "Abrir o Duo" }, "importFormatError": { "message": "Os dados não estão formatados corretamente. Por favor, verifique o seu arquivo de importação e tente novamente." @@ -2846,13 +2846,13 @@ "message": "Nome de usuário ou senha incorretos" }, "incorrectPassword": { - "message": "Incorrect password" + "message": "Senha incorreta" }, "incorrectCode": { - "message": "Incorrect code" + "message": "Código incorreto" }, "incorrectPin": { - "message": "Incorrect PIN" + "message": "PIN incorreto" }, "multifactorAuthenticationFailed": { "message": "Falha na autenticação de múltiplos fatores" @@ -2965,71 +2965,71 @@ "description": "Label indicating the most common import formats" }, "overrideDefaultBrowserAutofillTitle": { - "message": "Make Bitwarden your default password manager?", + "message": "Tornar o Bitwarden seu gerenciador de senhas padrão?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Ignorar esta opção pode causar conflitos entre o menu de autopreenchimento do Bitwarden e o do seu navegador.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { - "message": "Make Bitwarden your default password manager", + "message": "Faça do Bitwarden seu gerenciador de senhas padrão", "description": "Label for the setting that allows overriding the default browser autofill settings" }, "privacyPermissionAdditionNotGrantedTitle": { - "message": "Unable to set Bitwarden as the default password manager", + "message": "Não é possível definir o Bitwarden como o gerenciador de senhas padrão", "description": "Title for the dialog that appears when the user has not granted the extension permission to set privacy settings" }, "privacyPermissionAdditionNotGrantedDescription": { - "message": "You must grant browser privacy permissions to Bitwarden to set it as the default password manager.", + "message": "Você deve conceder permissões de privacidade do navegador ao Bitwarden para defini-lo como o Gerenciador de Senhas padrão.", "description": "Description for the dialog that appears when the user has not granted the extension permission to set privacy settings" }, "makeDefault": { - "message": "Make default", + "message": "Tornar padrão", "description": "Button text for the setting that allows overriding the default browser autofill settings" }, "saveCipherAttemptSuccess": { - "message": "Credentials saved successfully!", + "message": "Credenciais salvas com sucesso!", "description": "Notification message for when saving credentials has succeeded." }, "updateCipherAttemptSuccess": { - "message": "Credentials updated successfully!", + "message": "Credenciais atualizadas com sucesso!", "description": "Notification message for when updating credentials has succeeded." }, "saveCipherAttemptFailed": { - "message": "Error saving credentials. Check console for details.", + "message": "Erro ao salvar credenciais. Verifique o console para detalhes.", "description": "Notification message for when saving credentials has failed." }, "success": { - "message": "Success" + "message": "Sucesso" }, "removePasskey": { - "message": "Remove passkey" + "message": "Remover senha" }, "passkeyRemoved": { - "message": "Passkey removed" + "message": "Chave de acesso removida" }, "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + "message": "Aviso: Itens da organização não atribuídos não estão mais visíveis na visualização Todos os Cofres e só são acessíveis por meio do painel de administração." }, "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + "message": "Aviso: Em 16 de maio, 2024, itens da organização não serão mais visíveis na visualização Todos os Cofres e só serão acessíveis por meio do painel de administração." }, "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", + "message": "Atribua estes itens a uma coleção da", "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." }, "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", + "message": "para torná-los visíveis.", "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." }, "adminConsole": { - "message": "Admin Console" + "message": "Painel de administração" }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "Erro ao atribuir coleção de destino." }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "Erro ao atribuir pasta de destino." } } diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index 5819546800e..acac4d14c61 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -173,10 +173,10 @@ "message": "Промени главну лозинку" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "Ићи на веб апликацију?" }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "Можете променити главну лозинку на Bitwarden веб апликацији." }, "fingerprintPhrase": { "message": "Сигурносна Фраза Сефа", @@ -3001,7 +3001,7 @@ "description": "Notification message for when saving credentials has failed." }, "success": { - "message": "Success" + "message": "Успех" }, "removePasskey": { "message": "Уклонити приступачни кључ" @@ -3010,10 +3010,10 @@ "message": "Приступачни кључ је уклоњен" }, "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + "message": "Напомена: Недодељене ставке организације више нису видљиве у приказу Сви сефови и доступне су само преко Админ конзоле." }, "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + "message": "Напомена: од 16 Маја 2024м недодељене ставке организације више нису видљиве у приказу Сви сефови и доступне су само преко Админ конзоле." }, "unassignedItemsBannerCTAPartOne": { "message": "Assign these items to a collection from the", @@ -3024,12 +3024,12 @@ "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." }, "adminConsole": { - "message": "Admin Console" + "message": "Администраторска конзола" }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "Грешка при додељивању циљне колекције." }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "Грешка при додељивању циљне фасцикле." } } diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index a2f856c31b8..fa4dab6a8fd 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Bitwarden 密码管理器", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", + "message": "无论是在家里、工作中还是在外出时,Bitwarden 都可以轻松地保护您的所有密码、通行密钥和敏感信息。", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -176,7 +176,7 @@ "message": "前往网页 App 吗?" }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "您可以在 Bitwarden 网页应用上更改您的主密码。" }, "fingerprintPhrase": { "message": "指纹短语", @@ -3010,17 +3010,17 @@ "message": "通行密钥已移除" }, "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + "message": "注意:未分配的组织项目在「所有密码库」视图中不再可见,只能通过管理控制台访问。" }, "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + "message": "注意:从 2024 年 5 月 16 日起,未分配的组织项目在「所有密码库」视图中将不再可见,只能通过管理控制台访问。" }, "unassignedItemsBannerCTAPartOne": { "message": "Assign these items to a collection from the", "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." }, "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", + "message": "以使其可见。", "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." }, "adminConsole": { diff --git a/apps/browser/store/locales/de/copy.resx b/apps/browser/store/locales/de/copy.resx index 2267c6c85ed..eb3ab2afd48 100644 --- a/apps/browser/store/locales/de/copy.resx +++ b/apps/browser/store/locales/de/copy.resx @@ -121,55 +121,55 @@ Bitwarden Passwort-Manager
- At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + Zu Hause, am Arbeitsplatz oder unterwegs schützt Bitwarden einfach alle deine Passwörter, Passkeys und vertraulichen Informationen. - Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! + Ausgezeichnet als bester Passwortmanager von PCMag, WIRED, The Verge, CNET, G2 und vielen anderen! -SECURE YOUR DIGITAL LIFE -Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. +SCHÜTZE DEIN DIGITALES LEBEN +Sicher dein digitales Leben und schütze dich vor Passwortdiebstählen, indem du individuelle, sichere Passwörter für jedes Konto erstellest und speicherst. Verwalte alles in einem Ende-zu-Ende verschlüsselten Passwort-Tresor, auf den nur du Zugriff hast. -ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE -Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. +ZUGRIFF AUF DEINE DATEN, ÜBERALL, JEDERZEIT UND AUF JEDEM GERÄT +Verwalte, speichere, sichere und teile einfach eine unbegrenzte Anzahl von Passwörtern auf einer unbegrenzten Anzahl von Geräten ohne Einschränkungen. -EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE -Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. +JEDER SOLLTE DIE MÖGLICHKEIT HABEN, ONLINE GESCHÜTZT ZU BLEIBEN +Verwende Bitwarden kostenlos, ohne Werbung oder Datenverkauf. Bitwarden glaubt, dass jeder die Möglichkeit haben sollte, online geschützt zu bleiben. Premium-Abos bieten Zugang zu erweiterten Funktionen. -EMPOWER YOUR TEAMS WITH BITWARDEN -Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. +STÄRKE DEINE TEAMS MIT BITWARDEN +Tarife für Teams und Enterprise enthalten professionelle Business-Funktionen. Einige Beispiele sind SSO-Integration, Selbst-Hosting, Directory-Integration und SCIM-Bereitstellung, globale Richtlinien, API-Zugang, Ereignisprotokolle und mehr. -Use Bitwarden to secure your workforce and share sensitive information with colleagues. +Nutze Bitwarden, um deine Mitarbeiter abzusichern und sensible Informationen mit Kollegen zu teilen. -More reasons to choose Bitwarden: +Weitere Gründe, Bitwarden zu wählen: -World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Weltklasse-Verschlüsselung +Passwörter werden mit fortschrittlicher Ende-zu-Ende-Verschlüsselung (AES-256 bit, salted hashtag und PBKDF2 SHA-256) geschützt, damit deine Daten sicher und geheim bleiben. -3rd-party Audits -Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. +3rd-Party-Prüfungen +Bitwarden führt regelmäßig umfassende Sicherheitsprüfungen durch Dritte von namhaften Sicherheitsfirmen durch. Diese jährlichen Prüfungen umfassen Quellcode-Bewertungen und Penetration-Tests für Bitwarden-IPs, Server und Webanwendungen. -Advanced 2FA -Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. +Erweiterte 2FA +Schütze deine Zugangsdaten mit einem Authentifikator eines Drittanbieters, per E-Mail verschickten Codes oder FIDO2 WebAuthn-Zugangsadaten wie einem Hardware-Sicherheitsschlüssel oder Passkey. Bitwarden Send -Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. +Übertrage Daten direkt an andere, während die Ende-zu-Ende-Verschlüsselung beibehalten wird und die Verbreitung begrenzt werden kann. -Built-in Generator -Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. +Eingebauter Generator +Erstelle lange, komplexe und eindeutige Passwörter und eindeutige Benutzernamen für jede Website, die du besuchst. Integriere E-Mail-Alias-Anbieter für zusätzlichen Datenschutz. -Global Translations -Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. +Globale Übersetzungen +Es gibt Bitwarden-Übersetzungen für mehr als 60 Sprachen, die von der weltweiten Community über Crowdin übersetzt werden. -Cross-Platform Applications -Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. +Plattformübergreifende Anwendungen +Schütze und teile sensible Daten in deinem Bitwarden Tresor von jedem Browser, mobilen Gerät oder Desktop-Betriebssystem und mehr. -Bitwarden secures more than just passwords -End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! +Bitwarden schützt mehr als nur Passwörter +Ende-zu-Ende verschlüsselte Zugangsverwaltungs-Lösungen von Bitwarden ermöglicht es Organisationen, alles zu sichern, einschließlich Entwicklergeheimnissen und Passkeys. Besuche Bitwarden.com, um mehr über den Bitwarden Secrets Manager und Bitwarden Passwordless.dev zu erfahren! - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + Zu Hause, am Arbeitsplatz oder unterwegs schützt Bitwarden einfach alle deine Passwörter, Passkeys und vertraulichen Informationen. Synchronisiere und greife auf deinen Tresor von unterschiedlichen Geräten aus zu diff --git a/apps/browser/store/locales/ko/copy.resx b/apps/browser/store/locales/ko/copy.resx index fdfb93ad6ae..595663b1cae 100644 --- a/apps/browser/store/locales/ko/copy.resx +++ b/apps/browser/store/locales/ko/copy.resx @@ -121,7 +121,7 @@ Bitwarden Password Manager - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + 집에서도, 직장에서도, 이동 중에도 Bitwarden은 비밀번호, 패스키, 민감 정보를 쉽게 보호합니다. Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! @@ -169,7 +169,7 @@ End-to-end encrypted credential management solutions from Bitwarden empower orga - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + 집에서도, 직장에서도, 이동 중에도 Bitwarden은 비밀번호, 패스키, 민감 정보를 쉽게 보호합니다. 여러 기기에서 보관함에 접근하고 동기화할 수 있습니다. diff --git a/apps/browser/store/locales/pl/copy.resx b/apps/browser/store/locales/pl/copy.resx index 60709c7d4d1..5641c68c488 100644 --- a/apps/browser/store/locales/pl/copy.resx +++ b/apps/browser/store/locales/pl/copy.resx @@ -118,58 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden Password Manager + Menedżer Haseł Bitwarden - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + W domu, w pracy, lub w ruchu, Bitwarden z łatwością zabezpiecza wszystkie Twoje hasła, passkeys i poufne informacje. - Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! + Uznany za najlepszego menedżera haseł przez PCMag, WIRED, The Verge, CNET, G2 i wielu innych! -SECURE YOUR DIGITAL LIFE -Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. +ZABEZPIECZ SWOJE CYFROWE ŻYCIE +Zabezpiecz swoje cyfrowe życie i chroń przed naruszeniami danych, generując i zapisując unikalne, silne hasła do każdego konta. Przechowuj wszystko w zaszyfrowanym end-to-end magazynie haseł, do którego tylko Ty masz dostęp. -ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE -Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. +DOSTĘP DO SWOICH DANYCH W KAŻDYM MIEJSCU, W DOWOLNYM CZASIE, NA KAŻDYM URZĄDZENIU +Z łatwością zarządzaj, przechowuj, zabezpieczaj i udostępniaj nieograniczoną liczbę haseł na nieograniczonej liczbie urządzeń. -EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE -Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. +KAŻDY POWINIEN POSIADAĆ NARZĘDZIA ABY ZACHOWAĆ BEZPIECZEŃSTWO W INTERNECIE +Korzystaj z Bitwarden za darmo, bez reklam i sprzedawania Twoich danych. Bitwarden wierzy, że każdy powinien mieć możliwość zachowania bezpieczeństwa w Internecie. Plany premium oferują dostęp do zaawansowanych funkcji. -EMPOWER YOUR TEAMS WITH BITWARDEN -Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. +WZMOCNIJ SWOJE ZESPOŁY DZIĘKI BITWARDEN +Plany dla Zespołów i Enterprise oferują profesjonalne funkcje biznesowe. Na przykład obejmują integrację z SSO, własny hosting, integrację katalogów i udostępnianie SCIM, zasady globalne, dostęp do API, dzienniki zdarzeń i inne. -Use Bitwarden to secure your workforce and share sensitive information with colleagues. +Użyj Bitwarden, aby zabezpieczyć swoich pracowników i udostępniać poufne informacje współpracownikom. -More reasons to choose Bitwarden: +Więcej powodów, aby wybrać Bitwarden: -World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Szyfrowanie na światowym poziomie +Hasła są chronione za pomocą zaawansowanego, kompleksowego szyfrowania (AES-256-bitowy, solony hashtag i PBKDF2 SHA-256), dzięki czemu Twoje dane pozostają bezpieczne i prywatne. -3rd-party Audits -Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. +Audyty stron trzecich +Bitwarden regularnie przeprowadza kompleksowe audyty bezpieczeństwa stron trzecich we współpracy ze znanymi firmami security. Te coroczne audyty obejmują ocenę kodu źródłowego i testy penetracyjne adresów IP Bitwarden, serwerów i aplikacji internetowych. -Advanced 2FA -Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. +Zaawansowane 2FA +Zabezpiecz swój login za pomocą zewnętrznego narzędzia uwierzytelniającego, kodów przesłanych pocztą elektroniczną lub poświadczeń FIDO2 WebAuthn, takich jak sprzętowy klucz bezpieczeństwa lub hasło. -Bitwarden Send -Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. +Bitwarden Wyślij +Przesyłaj dane bezpośrednio do innych, zachowując kompleksowe szyfrowane bezpieczeństwo i ograniczając ryzyko. -Built-in Generator -Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. +Wbudowany generator +Twórz długie, złożone i różne hasła oraz unikalne nazwy użytkowników dla każdej odwiedzanej witryny. Zintegruj się z dostawcami aliasów e-mail, aby uzyskać dodatkową prywatność. -Global Translations -Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. +Tłumaczenia globalne +Istnieją tłumaczenia Bitwarden na ponad 60 języków, tłumaczone przez globalną społeczność za pośrednictwem Crowdin. -Cross-Platform Applications -Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. +Aplikacje wieloplatformowe +Zabezpiecz i udostępniaj poufne dane w swoim Sejfie Bitwarden z dowolnej przeglądarki, urządzenia mobilnego lub systemu operacyjnego na komputerze stacjonarnym i nie tylko. -Bitwarden secures more than just passwords -End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! +Bitwarden zabezpiecza nie tylko hasła +Rozwiązania do zarządzania danymi zaszyfrownaymi end-to-end od firmy Bitwarden umożliwiają organizacjom zabezpieczanie wszystkiego, w tym tajemnic programistów i kluczy dostępu. Odwiedź Bitwarden.com, aby dowiedzieć się więcej o Mendżerze Sekretów Bitwarden i Bitwarden Passwordless.dev! - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + W domu, w pracy, lub w ruchu, Bitwarden z łatwością zabezpiecza wszystkie Twoje hasła, passkeys i poufne informacje. Synchronizacja i dostęp do sejfu z różnych urządzeń diff --git a/apps/browser/store/locales/pt_BR/copy.resx b/apps/browser/store/locales/pt_BR/copy.resx index 8b99c436d05..067f9357b23 100644 --- a/apps/browser/store/locales/pt_BR/copy.resx +++ b/apps/browser/store/locales/pt_BR/copy.resx @@ -118,10 +118,10 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden Password Manager + Gerenciador de Senhas Bitwarden - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + Em casa, no trabalho, ou em qualquer lugar, o Bitwarden protege facilmente todas as suas senhas, senhas e informações confidenciais. Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! @@ -169,7 +169,7 @@ End-to-end encrypted credential management solutions from Bitwarden empower orga - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + Em casa, no trabalho, ou em qualquer lugar, o Bitwarden protege facilmente todas as suas senhas, senhas e informações confidenciais. Sincronize e acesse o seu cofre através de múltiplos dispositivos diff --git a/apps/browser/store/locales/zh_CN/copy.resx b/apps/browser/store/locales/zh_CN/copy.resx index 94543f8f6f7..d010cb1a7bf 100644 --- a/apps/browser/store/locales/zh_CN/copy.resx +++ b/apps/browser/store/locales/zh_CN/copy.resx @@ -118,58 +118,56 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden Password Manager + Bitwarden 密码管理器 - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + 无论是在家里、工作中还是在外出时,Bitwarden 都可以轻松地保护您的所有密码、通行密钥和敏感信息。 - Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! + 被 PCMag、WIRED、The Verge、CNET、G2 等评为最佳密码管理器! -SECURE YOUR DIGITAL LIFE -Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. +保护您的数字生活 +通过为每个账户生成并保存独特而强大的密码,保护您的数字生活并防范数据泄露。所有内容保存在只有您可以访问的端对端加密的密码库中。 -ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE -Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. +随时随地在任何设备上访问您的数据 +不受任何限制跨无限数量的设备轻松管理、存储、保护和分享不限数量的密码。 -EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE -Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. +每个人都应该拥有的保持在线安全的工具 +使用 Bitwarden 是免费的,没有广告,不会出售数据。Bitwarden 相信每个人都应该拥有保持在线安全的能力。高级计划提供了堆高级功能的访问。 -EMPOWER YOUR TEAMS WITH BITWARDEN -Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. +通过 BITWARDEN 为您的团队提供支持 +团队和企业计划具有专业的商业功能。例如 SSO 集成、自托管、目录集成和 SCIM 配置、全局策略、API 访问、事件日志等。 -Use Bitwarden to secure your workforce and share sensitive information with colleagues. +使用 Bitwarden 保护您的团队,并与同事共享敏感信息。 +选择 Bitwarden 的更多理由: -More reasons to choose Bitwarden: +世界级加密 +密码受到先进的端对端加密(AES-256 位、加盐哈希标签和 PBKDF2 SHA-256)保护,使您的数据保持安全和私密。 -World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +第三方审计 +Bitwarden 定期与知名的安全公司进行全面的第三方安全审计。这些年度审核包括对 Bitwarden IP、服务器和 Web 应用程序的源代码评估和渗透测试。 -3rd-party Audits -Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. - -Advanced 2FA -Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. +高级两步验证 +使用第三方身份验证器、通过电子邮件发送代码或 FIDO2 WebAuthn 凭据(如硬件安全钥匙或通行密钥)保护您的登录。 Bitwarden Send -Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. +直接传输数据给他人,同时保持端对端加密的安全性并防止曝露。 -Built-in Generator -Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. +内置生成器 +为您访问的每个网站创建长、复杂且独特的密码和用户名。与电子邮件别名提供商集成,增加隐私保护。 -Global Translations -Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. +全球翻译 +Bitwarden 的翻译涵盖 60 多种语言,由全球社区通过 Crowdin 翻译。 -Cross-Platform Applications -Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. +跨平台应用程序 +从任何浏览器、移动设备或桌面操作系统中安全地访问和共享 Bitwarden 密码库中的敏感数据。 -Bitwarden secures more than just passwords -End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - +Bitwarden 保护的不仅仅是密码 +Bitwarden 的端对端加密凭据管理解决方案使组织能够保护所有内容,包括开发人员机密和通行密钥体验。访问 Bitwarden.com 了解更多关于Bitwarden Secrets Manager 和 Bitwarden Passwordless.dev 的信息! - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + 无论是在家里、工作中还是在外出时,Bitwarden 都可以轻松地保护您的所有密码、通行密钥和敏感信息。 从多台设备同步和访问密码库 From 68839a80b7b36c785064fb45e9b7ddd05c502da0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 23 Apr 2024 15:30:01 +0000 Subject: [PATCH 043/110] Autosync the updated translations (#8880) 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 | 6 +- apps/web/src/locales/cs/messages.json | 10 +- apps/web/src/locales/da/messages.json | 6 +- apps/web/src/locales/de/messages.json | 82 +++++------ apps/web/src/locales/fr/messages.json | 6 +- apps/web/src/locales/hu/messages.json | 6 +- apps/web/src/locales/ja/messages.json | 6 +- apps/web/src/locales/lv/messages.json | 128 ++++++++--------- apps/web/src/locales/pl/messages.json | 6 +- apps/web/src/locales/pt_BR/messages.json | 170 +++++++++++------------ apps/web/src/locales/ru/messages.json | 6 +- apps/web/src/locales/sk/messages.json | 6 +- apps/web/src/locales/sr/messages.json | 86 ++++++------ apps/web/src/locales/uk/messages.json | 6 +- apps/web/src/locales/zh_CN/messages.json | 58 ++++---- 16 files changed, 297 insertions(+), 297 deletions(-) diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index d53278e93a7..c3ae7b07b6d 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -8042,12 +8042,12 @@ "message": "Bu kolleksiya yalnız admin konsolundan əlçatandır" }, "organizationOptionsMenu": { - "message": "Toggle Organization Menu" + "message": "Təşkilat Menyusuna keç" }, "vaultItemSelect": { - "message": "Select vault item" + "message": "Anbar elementini seç" }, "collectionItemSelect": { - "message": "Select collection item" + "message": "Kolleksiya elementini seç" } } diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index f4ee30ba957..e0d0f001742 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -8042,12 +8042,12 @@ "message": "Тази колекция е достъпна само през административната конзола" }, "organizationOptionsMenu": { - "message": "Toggle Organization Menu" + "message": "Превключване на менюто на организацията" }, "vaultItemSelect": { - "message": "Select vault item" + "message": "Изберете елемент от трезора" }, "collectionItemSelect": { - "message": "Select collection item" + "message": "Изберете елемент от колекцията" } } diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index b6e62aef9aa..72e621b1a55 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -8036,18 +8036,18 @@ "message": "Nový klient byl úspěšně vytvořen" }, "noAccess": { - "message": "No access" + "message": "Žádný přístup" }, "collectionAdminConsoleManaged": { - "message": "This collection is only accessible from the admin console" + "message": "Tato kolekce je přístupná pouze z konzole administrátora" }, "organizationOptionsMenu": { - "message": "Toggle Organization Menu" + "message": "Přepnout menu organizace" }, "vaultItemSelect": { - "message": "Select vault item" + "message": "Vybrat položku trezoru" }, "collectionItemSelect": { - "message": "Select collection item" + "message": "Vybrat položku kolekce" } } diff --git a/apps/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index e5042229fe8..4c8cbf108a4 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -8042,12 +8042,12 @@ "message": "Denne samling er kun tilgængelig via Admin-konsol" }, "organizationOptionsMenu": { - "message": "Toggle Organization Menu" + "message": "Skift Organisationsmenu" }, "vaultItemSelect": { - "message": "Select vault item" + "message": "Vælg boksemne" }, "collectionItemSelect": { - "message": "Select collection item" + "message": "Vælg samlingsemne" } } diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index bd2e0946f61..1c7dd343293 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -4951,7 +4951,7 @@ "message": "Erstelle eine neue Kunden-Organisation, die dir als Anbieter zugeordnet wird. Du kannst auf diese Organisation zugreifen und diese verwalten." }, "newClient": { - "message": "New client" + "message": "Neuer Kunde" }, "addExistingOrganization": { "message": "Bestehende Organisation hinzufügen" @@ -7607,7 +7607,7 @@ "message": "Anbieterportal" }, "success": { - "message": "Success" + "message": "Erfolg" }, "viewCollection": { "message": "Sammlung anzeigen" @@ -7907,30 +7907,30 @@ "message": "Du kannst dich nicht selbst zu einer Gruppe hinzufügen." }, "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "message": "Hinweis: Ab dem 2. Mai 2024 werden nicht zugewiesene Organisationseinträge nicht mehr geräteübergreifend in der Ansicht aller Tresore sichtbar sein und sind nur über die Administrator-Konsole zugänglich. Weise diese Elemente einer Sammlung aus der Administrator-Konsole zu, um sie sichtbar zu machen." }, "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + "message": "Hinweis: Nicht zugewiesene Organisationseinträge sind nicht mehr geräteübergreifend in der Ansicht aller Tresore sichtbar und nun nur über die Administrator-Konsole zugänglich." }, "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + "message": "Hinweis: Ab dem 16. Mai 2024 werden nicht zugewiesene Organisationseinträge nicht mehr geräteübergreifend in der Ansicht aller Tresore sichtbar sein und nur über die Administrator-Konsole zugänglich." }, "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", + "message": "Weise diese Einträge einer Sammlung aus der", "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." }, "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", + "message": "zu, um sie sichtbar zu machen.", "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." }, "deleteProvider": { - "message": "Delete provider" + "message": "Anbieter löschen" }, "deleteProviderConfirmation": { - "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + "message": "Das Löschen eines Anbieters ist dauerhaft und unwiderruflich. Gib dein Master-Passwort ein, um die Löschung des Anbieters und aller zugehörigen Daten zu bestätigen." }, "deleteProviderName": { - "message": "Cannot delete $ID$", + "message": "Kann $ID$ nicht löschen", "placeholders": { "id": { "content": "$1", @@ -7939,7 +7939,7 @@ } }, "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "message": "Du musst die Verknüpfung zu allen Kunden aufheben, bevor du $ID$ löschen kannst", "placeholders": { "id": { "content": "$1", @@ -7948,16 +7948,16 @@ } }, "providerDeleted": { - "message": "Provider deleted" + "message": "Anbieter gelöscht" }, "providerDeletedDesc": { - "message": "The Provider and all associated data has been deleted." + "message": "Der Anbieter und alle zugehörigen Daten wurden gelöscht." }, "deleteProviderRecoverConfirmDesc": { - "message": "You have requested to delete this Provider. Use the button below to confirm." + "message": "Du hast die Löschung dieses Anbieters angefragt. Verwende den Button unten, um dies zu bestätigen." }, "deleteProviderWarning": { - "message": "Deleting your provider is permanent. It cannot be undone." + "message": "Die Löschung deines Anbieters ist dauerhaft. Sie kann nicht widerrufen werden." }, "errorAssigningTargetCollection": { "message": "Fehler beim Zuweisen der Ziel-Sammlung." @@ -7966,88 +7966,88 @@ "message": "Fehler beim Zuweisen des Ziel-Ordners." }, "integrationsAndSdks": { - "message": "Integrations & SDKs", + "message": "Integrationen & SDKs", "description": "The title for the section that deals with integrations and SDKs." }, "integrations": { - "message": "Integrations" + "message": "Integrationen" }, "integrationsDesc": { - "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + "message": "Geheimnisse des Bitwarden Secrets Managers automatisch mit einem Drittanbieter-Dienst synchronisieren." }, "sdks": { "message": "SDKs" }, "sdksDesc": { - "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + "message": "Verwende das Bitwarden Secrets Manager SDK in den folgenden Programmiersprachen, um deine eigenen Anwendungen zu erstellen." }, "setUpGithubActions": { - "message": "Set up Github Actions" + "message": "GitHub Actions einrichten" }, "setUpGitlabCICD": { - "message": "Set up GitLab CI/CD" + "message": "GitLab CI/CD einrichten" }, "setUpAnsible": { - "message": "Set up Ansible" + "message": "Ansible einrichten" }, "cSharpSDKRepo": { - "message": "View C# repository" + "message": "C#-Repository anzeigen" }, "cPlusPlusSDKRepo": { - "message": "View C++ repository" + "message": "C++-Repository anzeigen" }, "jsWebAssemblySDKRepo": { - "message": "View JS WebAssembly repository" + "message": "JS WebAssembly-Repository anzeigen" }, "javaSDKRepo": { - "message": "View Java repository" + "message": "Java-Repository anzeigen" }, "pythonSDKRepo": { - "message": "View Python repository" + "message": "Python-Repository anzeigen" }, "phpSDKRepo": { - "message": "View php repository" + "message": "PHP-Repository anzeigen" }, "rubySDKRepo": { - "message": "View Ruby repository" + "message": "Ruby-Repository anzeigen" }, "goSDKRepo": { - "message": "View Go repository" + "message": "Go-Repository anzeigen" }, "createNewClientToManageAsProvider": { - "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + "message": "Erstelle eine neue Kunden-Organisation, um sie als Anbieter zu verwalten. Zusätzliche Benutzerplätze werden im nächsten Abrechnungszeitraum berücksichtigt." }, "selectAPlan": { - "message": "Select a plan" + "message": "Ein Abo auswählen" }, "thirtyFivePercentDiscount": { - "message": "35% Discount" + "message": "35% Rabatt" }, "monthPerMember": { "message": "month per member" }, "seats": { - "message": "Seats" + "message": "Benutzerplätze" }, "addOrganization": { - "message": "Add organization" + "message": "Organisation hinzufügen" }, "createdNewClient": { - "message": "Successfully created new client" + "message": "Neuer Kunde erfolgreich erstellt" }, "noAccess": { - "message": "No access" + "message": "Kein Zugriff" }, "collectionAdminConsoleManaged": { - "message": "This collection is only accessible from the admin console" + "message": "Diese Sammlung ist nur über die Administrator-Konsole zugänglich" }, "organizationOptionsMenu": { - "message": "Toggle Organization Menu" + "message": "Organisationsmenü umschalten" }, "vaultItemSelect": { - "message": "Select vault item" + "message": "Tresor-Eintrag auswählen" }, "collectionItemSelect": { - "message": "Select collection item" + "message": "Sammlungseintrag auswählen" } } diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index 1f1f3438974..ffb1fe436b1 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -8042,12 +8042,12 @@ "message": "Cette collection n'est accessible qu'à partir de la Console Admin" }, "organizationOptionsMenu": { - "message": "Toggle Organization Menu" + "message": "Afficher/masquer le Menu de l'Organisation" }, "vaultItemSelect": { - "message": "Select vault item" + "message": "Sélectionner un élément du coffre" }, "collectionItemSelect": { - "message": "Select collection item" + "message": "Sélectionner un élément de la collection" } } diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index b85733325b6..008a20b4f06 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -8042,12 +8042,12 @@ "message": "Ez a gyűjtemény csak az adminisztrátori konzolról érhető el." }, "organizationOptionsMenu": { - "message": "Toggle Organization Menu" + "message": "Szervezeti menü váltás" }, "vaultItemSelect": { - "message": "Select vault item" + "message": "Széf elem választás" }, "collectionItemSelect": { - "message": "Select collection item" + "message": "Gyűjtemény elem választás" } } diff --git a/apps/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index a701dd6e8df..18617433207 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -8042,12 +8042,12 @@ "message": "このコレクションは管理コンソールからのみアクセス可能です" }, "organizationOptionsMenu": { - "message": "Toggle Organization Menu" + "message": "組織メニューの切り替え" }, "vaultItemSelect": { - "message": "Select vault item" + "message": "保管庫のアイテムを選択" }, "collectionItemSelect": { - "message": "Select collection item" + "message": "コレクションのアイテムを選択" } } diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index b20f3502314..1ad03eeb825 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -7741,22 +7741,22 @@ "description": "The date header used when a subscription is cancelled." }, "machineAccountsCannotCreate": { - "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + "message": "Mašīnu kontus nav iespējams izveidot apturētās apvienībās. Lūgums vērsties pie savas apvienības īpašnieka pēc palīdzības." }, "machineAccount": { - "message": "Machine account", + "message": "Mašīnas konts", "description": "A machine user which can be used to automate processes and access secrets in the system." }, "machineAccounts": { - "message": "Machine accounts", + "message": "Mašīnu konti", "description": "The title for the section that deals with machine accounts." }, "newMachineAccount": { - "message": "New machine account", + "message": "Jauns mašīnas konts", "description": "Title for creating a new machine account." }, "machineAccountsNoItemsMessage": { - "message": "Create a new machine account to get started automating secret access.", + "message": "Jāizveido jauns mašīnas konts, lai sāktu automatizēt piekļuvi noslēpumiem.", "description": "Message to encourage the user to start creating machine accounts." }, "machineAccountsNoItemsTitle": { @@ -7764,19 +7764,19 @@ "description": "Title to indicate that there are no machine accounts to display." }, "deleteMachineAccounts": { - "message": "Delete machine accounts", + "message": "Izdzēst mašīnu kontus", "description": "Title for the action to delete one or multiple machine accounts." }, "deleteMachineAccount": { - "message": "Delete machine account", + "message": "Izdzēst mašīnas kontu", "description": "Title for the action to delete a single machine account." }, "viewMachineAccount": { - "message": "View machine account", + "message": "Skatīt mašīnas kontu", "description": "Action to view the details of a machine account." }, "deleteMachineAccountDialogMessage": { - "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "message": "Mašīnas konta $MACHINE_ACCOUNT$ izdzēšana ir paliekoša un neatgriezeniska.", "placeholders": { "machine_account": { "content": "$1", @@ -7785,10 +7785,10 @@ } }, "deleteMachineAccountsDialogMessage": { - "message": "Deleting machine accounts is permanent and irreversible." + "message": "Mašīnu kontu izdzēšana ir paliekoša un neatgriezeniska." }, "deleteMachineAccountsConfirmMessage": { - "message": "Delete $COUNT$ machine accounts", + "message": "Izdzēst $COUNT$ mašīnu kontu(s)", "placeholders": { "count": { "content": "$1", @@ -7797,60 +7797,60 @@ } }, "deleteMachineAccountToast": { - "message": "Machine account deleted" + "message": "Mašīnas konts ir izdzēsts" }, "deleteMachineAccountsToast": { - "message": "Machine accounts deleted" + "message": "Mašīnu konti ir izdzēsti" }, "searchMachineAccounts": { - "message": "Search machine accounts", + "message": "Meklēt mašīnu kontus", "description": "Placeholder text for searching machine accounts." }, "editMachineAccount": { - "message": "Edit machine account", + "message": "Labot mašīnas kontu", "description": "Title for editing a machine account." }, "machineAccountName": { - "message": "Machine account name", + "message": "Mašīnas konta nosaukums", "description": "Label for the name of a machine account" }, "machineAccountCreated": { - "message": "Machine account created", + "message": "Mašīnas konts ir izveidots", "description": "Notifies that a new machine account has been created" }, "machineAccountUpdated": { - "message": "Machine account updated", + "message": "Mašīnas konts ir atjaunināts", "description": "Notifies that a machine account has been updated" }, "projectMachineAccountsDescription": { - "message": "Grant machine accounts access to this project." + "message": "Piešķirt mašīnau kontiem piekļuvi šim projektam." }, "projectMachineAccountsSelectHint": { - "message": "Type or select machine accounts" + "message": "Ievadīt vai atlasīt mašīnu kontus" }, "projectEmptyMachineAccountAccessPolicies": { - "message": "Add machine accounts to grant access" + "message": "Jāpievieno mašīnu konti, lai piešķirtu piekļuvi" }, "machineAccountPeopleDescription": { - "message": "Grant groups or people access to this machine account." + "message": "Piešķirt kopām vai cilvēkiem piekļuvi šim mašīnas kontam." }, "machineAccountProjectsDescription": { - "message": "Assign projects to this machine account. " + "message": "Piešķirt projektus šim mašīnas kontam. " }, "createMachineAccount": { - "message": "Create a machine account" + "message": "Izveidot mašīnas kontu" }, "maPeopleWarningMessage": { - "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + "message": "Cilvēku noņemšana no mašīnas konta nenoņem to izveidotās piekļuves pilnvaras. Labākajai drošības pieejai ir ieteicams atsaukt piekļuves pilnvaras, kuras ir izveidojuši cilvēki, kuri ir noņemti no mašīnas konta." }, "smAccessRemovalWarningMaTitle": { - "message": "Remove access to this machine account" + "message": "Noņemt piekļuvi šim mašīnas kontam" }, "smAccessRemovalWarningMaMessage": { - "message": "This action will remove your access to the machine account." + "message": "Šī darbība noņems piekļuvi mašīnas kontam." }, "machineAccountsIncluded": { - "message": "$COUNT$ machine accounts included", + "message": "Iekļauti $COUNT$ mašīnu konti", "placeholders": { "count": { "content": "$1", @@ -7859,7 +7859,7 @@ } }, "additionalMachineAccountCost": { - "message": "$COST$ per month for additional machine accounts", + "message": "$COST$ mēnesī par papildu mašīnu kontiem", "placeholders": { "cost": { "content": "$1", @@ -7868,10 +7868,10 @@ } }, "additionalMachineAccounts": { - "message": "Additional machine accounts" + "message": "Papildu mašīnu konti" }, "includedMachineAccounts": { - "message": "Your plan comes with $COUNT$ machine accounts.", + "message": "Plānā ir iekļauti $COUNT$ mašīnu konti.", "placeholders": { "count": { "content": "$1", @@ -7880,7 +7880,7 @@ } }, "addAdditionalMachineAccounts": { - "message": "You can add additional machine accounts for $COST$ per month.", + "message": "Papildu mašīnu kontus var pievienot par $COST$ mēnesī.", "placeholders": { "cost": { "content": "$1", @@ -7889,25 +7889,25 @@ } }, "limitMachineAccounts": { - "message": "Limit machine accounts (optional)" + "message": "Ierobežot mašīnu kontus (izvēles)" }, "limitMachineAccountsDesc": { - "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + "message": "Uzstāda mašīnu kontu skaita ierobežojumu. Tiklīdz tas ir sasniegts, nebūs iespējams izveidot jaunus mašīnu kontus." }, "machineAccountLimit": { - "message": "Machine account limit (optional)" + "message": "Mašīnu kontu skaita ierobežojums (izvēles)" }, "maxMachineAccountCost": { - "message": "Max potential machine account cost" + "message": "Lielākās iespējamās mašīnas konta izmaksas" }, "machineAccountAccessUpdated": { - "message": "Machine account access updated" + "message": "Mašīnas konta piekļuve ir atjaunināta" }, "restrictedGroupAccessDesc": { "message": "Sevi nevar pievienot kopai." }, "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "message": "Jāņem vērā: no 2024. gada 2. maija nepiešķirti apvienības vienumi vairs nebūs redzami skatā \"Visas glabātavas\" dažādās ierīcēs un būs sasniedzami tikai no pārvaldības konsoles, kur šie vienumi jāpiešķir krājumam, lai padarītu tos redzamus." }, "unassignedItemsBannerNotice": { "message": "Jāņem vērā: nepiešķirti apvienības vienumi vairs nav redzami skatā \"Visas glabātavas\" dažādās ierīcēs un tagad ir pieejami tikai pārvaldības konsolē." @@ -7966,74 +7966,74 @@ "message": "Kļūda mērķa mapes piešķiršanā." }, "integrationsAndSdks": { - "message": "Integrations & SDKs", + "message": "Integrācijas un izstrādātāju rīkkopas", "description": "The title for the section that deals with integrations and SDKs." }, "integrations": { - "message": "Integrations" + "message": "Integrācijas" }, "integrationsDesc": { - "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + "message": "Automātiski sinhronizēt noslēpumus no Bitwarden Noslēpumu pārvaldnieka uz trešās puses pakalpojumu." }, "sdks": { - "message": "SDKs" + "message": "Izstrādātāju rīkkopas" }, "sdksDesc": { - "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + "message": "Bitwarden Noslēpumu pārvaldnieka izstrādātāju rīkkopa ir izmantojama ar zemāk esošajām programmēšanas valodām, lai veidotu pats savas lietotnes." }, "setUpGithubActions": { - "message": "Set up Github Actions" + "message": "Iestatīt GitHub darbības" }, "setUpGitlabCICD": { - "message": "Set up GitLab CI/CD" + "message": "Iestatīt GitLab CI/CD" }, "setUpAnsible": { - "message": "Set up Ansible" + "message": "Iestatīt Ansible" }, "cSharpSDKRepo": { - "message": "View C# repository" + "message": "Skatīt C# glabātavu" }, "cPlusPlusSDKRepo": { - "message": "View C++ repository" + "message": "Skatīt C++ glabātavu" }, "jsWebAssemblySDKRepo": { - "message": "View JS WebAssembly repository" + "message": "Skatīt JS WebAssembly glabātavu" }, "javaSDKRepo": { - "message": "View Java repository" + "message": "Skatīt Java glabātavu" }, "pythonSDKRepo": { - "message": "View Python repository" + "message": "Skatīt Python glabātavu" }, "phpSDKRepo": { - "message": "View php repository" + "message": "Skatīt PHP glabātavu" }, "rubySDKRepo": { - "message": "View Ruby repository" + "message": "Skatīt Ruby glabātavu" }, "goSDKRepo": { - "message": "View Go repository" + "message": "Skatīt Go glabātavu" }, "createNewClientToManageAsProvider": { - "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + "message": "Izveidot jaunu klienta apvienību, ko pārvaldīt kā nodrošinātājam. Papildu vietas tiks atspoguļotas nākamajā norēķinu posmā." }, "selectAPlan": { - "message": "Select a plan" + "message": "Atlasīt plānu" }, "thirtyFivePercentDiscount": { - "message": "35% Discount" + "message": "35% atlaide" }, "monthPerMember": { - "message": "month per member" + "message": "mēnesī par dalībnieku" }, "seats": { - "message": "Seats" + "message": "Vietas" }, "addOrganization": { - "message": "Add organization" + "message": "Pievienot apvienību" }, "createdNewClient": { - "message": "Successfully created new client" + "message": "Veiksmīgi izveidots jauns klients" }, "noAccess": { "message": "Nav piekļuves" @@ -8042,12 +8042,12 @@ "message": "Šis krājums ir pieejams tikai pārvaldības konsolē" }, "organizationOptionsMenu": { - "message": "Toggle Organization Menu" + "message": "Pārslēgt apvienību izvēlni" }, "vaultItemSelect": { - "message": "Select vault item" + "message": "Atlasīt glabātavas vienumu" }, "collectionItemSelect": { - "message": "Select collection item" + "message": "Atlasīt krājuma vienumu" } } diff --git a/apps/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index c54b72c21cd..8fc603c1d5e 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -4053,7 +4053,7 @@ "message": "Wszystkie funkcje zespołów oraz:" }, "includeAllTeamsStarterFeatures": { - "message": "All Teams Starter features, plus:" + "message": "Wszystkie funkcje z Teams Starter, plus:" }, "chooseMonthlyOrAnnualBilling": { "message": "Wybierz miesięczną lub roczną płatność" @@ -6737,7 +6737,7 @@ } }, "teamsStarterPlanInvLimitReachedManageBilling": { - "message": "Teams Starter plans may have up to $SEATCOUNT$ members. Upgrade to your plan to invite more members.", + "message": "Plan Teams Starter może mieć maksymalnie $SEATCOUNT$ członków. Przejdź na wyższy plan, aby zaprosić więcej członków.", "placeholders": { "seatcount": { "content": "$1", @@ -6746,7 +6746,7 @@ } }, "teamsStarterPlanInvLimitReachedNoManageBilling": { - "message": "Teams Starter plans may have up to $SEATCOUNT$ members. Contact your organization owner to upgrade your plan and invite more members.", + "message": "Plan Teams Starter może mieć maksymalnie $SEATCOUNT$ członków. Skontaktuj się z właścicielem organizacji, aby przejść na wyższy plan i zaprosić więcej członków.", "placeholders": { "seatcount": { "content": "$1", diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index 2b296694bb9..06b117ed069 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -627,10 +627,10 @@ "message": "Iniciar sessão com a chave de acesso" }, "invalidPasskeyPleaseTryAgain": { - "message": "Invalid Passkey. Please try again." + "message": "Senha inválida. Por favor, tente novamente." }, "twoFactorForPasskeysNotSupportedOnClientUpdateToLogIn": { - "message": "2FA for passkeys is not supported. Update the app to log in." + "message": "2FA para senhas não é suportada. Atualize o aplicativo para iniciar a sessão." }, "loginWithPasskeyInfo": { "message": "Use uma senha gerada que fará o login automaticamente sem uma senha. Biometrias como reconhecimento facial ou impressão digital, ou outro método de segurança FIDO2 verificarão sua identidade." @@ -1359,11 +1359,11 @@ "description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new item instead. (Optional second half: You may need to wait until your administrator confirms your organization membership.)" }, "onboardingImportDataDetailsPartTwoNoOrgs": { - "message": " instead.", + "message": " em vez disso.", "description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new item instead." }, "onboardingImportDataDetailsPartTwoWithOrgs": { - "message": " instead. You may need to wait until your administrator confirms your organization membership.", + "message": " em vez disso, você pode precisar esperar até que o seu administrador confirme a sua associação à organização.", "description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new item instead. You may need to wait until your administrator confirms your organization membership." }, "importError": { @@ -2802,10 +2802,10 @@ "message": "CLI" }, "bitWebVault": { - "message": "Bitwarden Web vault" + "message": "Cofre Web do Bitwarden" }, "bitSecretsManager": { - "message": "Bitwarden Secrets Manager" + "message": "Gerenciador de Segredos Bitwarden" }, "loggedIn": { "message": "Conectado(a)." @@ -3489,7 +3489,7 @@ "message": "Defina um limite de vagas para sua assinatura. Quando esse limite for atingido, você não poderá convidar novos usuários." }, "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": "Defina um limite de assento para sua assinatura do Gerenciador Secretos. Uma vez que este limite for atingido, você não poderá convidar novos membros." }, "maxSeatLimit": { "message": "Limite Máximo de Vaga (opcional)", @@ -3621,7 +3621,7 @@ "message": "Atualizar Chave de Criptografia" }, "updateEncryptionSchemeDesc": { - "message": "We've changed the encryption scheme to provide better security. Update your encryption key now by entering your master password below." + "message": "Alteramos o esquema de criptografia para fornecer melhor segurança. Atualize sua chave de criptografia agora digitando sua senha mestra abaixo." }, "updateEncryptionKeyWarning": { "message": "Depois de atualizar sua chave de criptografia, é necessário encerrar e iniciar a sessão em todos os aplicativos do Bitwarden que você está usando atualmente (como o aplicativo móvel ou as extensões do navegador). Não encerrar e iniciar sessão (que baixa sua nova chave de criptografia) pode resultar em corrupção de dados. Nós tentaremos desconectá-lo automaticamente, mas isso pode demorar um pouco." @@ -3678,7 +3678,7 @@ "message": "Escolha quando o tempo limite do seu cofre irá se esgotar e execute a ação selecionada." }, "vaultTimeoutLogoutDesc": { - "message": "Choose when your vault will be logged out." + "message": "Escolha quando seu cofre será desconectado." }, "oneMinute": { "message": "1 minuto" @@ -4083,7 +4083,7 @@ "message": "Identificador SSO" }, "ssoIdentifierHintPartOne": { - "message": "Provide this ID to your members to login with SSO. To bypass this step, set up ", + "message": "Forneça esse ID aos seus membros para logar com SSO. Para ignorar essa etapa, configure ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" }, "unlinkSso": { @@ -4695,7 +4695,7 @@ "message": "Inscrito na recuperação de conta" }, "withdrawAccountRecovery": { - "message": "Withdraw from account recovery" + "message": "Retirar da recuperação de conta" }, "enrollPasswordResetSuccess": { "message": "Inscrição com sucesso!" @@ -4704,7 +4704,7 @@ "message": "Retirada com sucesso!" }, "eventEnrollAccountRecovery": { - "message": "User $ID$ enrolled in account recovery.", + "message": "O usuário $ID$ se inscreveu na recuperação de conta.", "placeholders": { "id": { "content": "$1", @@ -4713,7 +4713,7 @@ } }, "eventWithdrawAccountRecovery": { - "message": "User $ID$ withdrew from account recovery.", + "message": "O usuário $ID$ retirou da recuperação de conta.", "placeholders": { "id": { "content": "$1", @@ -4887,7 +4887,7 @@ "message": "Erro" }, "accountRecoveryManageUsers": { - "message": "Manage users must also be granted with the manage account recovery permission" + "message": "Gerenciar usuários também devem ser concedidos com a permissão de gerenciar a recuperação de contas" }, "setupProvider": { "message": "Configuração do Provedor" @@ -5117,7 +5117,7 @@ "message": "Ativar preenchimento automático" }, "activateAutofillPolicyDesc": { - "message": "Activate the auto-fill on page load setting on the browser extension for all existing and new members." + "message": "Ative o autopreenchimento na configuração de carregamento de página na extensão do navegador para todos os membros existentes e novos." }, "experimentalFeature": { "message": "Sites comprometidos ou não confiáveis podem tomar vantagem do autopreenchimento ao carregar a página." @@ -5432,7 +5432,7 @@ "message": "Conector de Chave" }, "memberDecryptionKeyConnectorDescStart": { - "message": "Connect login with SSO to your self-hosted decryption key server. Using this option, members won’t need to use their master passwords to decrypt vault data. The", + "message": "Conecte o login com SSO ao seu servidor de chave de descriptografia auto-hospedado. Usando essa opção, os membros não precisarão usar suas senhas mestres para descriptografar os dados", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Connect login with SSO to your self-hosted decryption key server. Using this option, members won’t need to use their master passwords to decrypt vault data. The require SSO authentication and single organization policies are required to set up Key Connector decryption. Contact Bitwarden Support for set up assistance.'" }, "memberDecryptionKeyConnectorDescLink": { @@ -5440,7 +5440,7 @@ "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Connect login with SSO to your self-hosted decryption key server. Using this option, members won’t need to use their master passwords to decrypt vault data. The require SSO authentication and single organization policies are required to set up Key Connector decryption. Contact Bitwarden Support for set up assistance.'" }, "memberDecryptionKeyConnectorDescEnd": { - "message": "are required to set up Key Connector decryption. Contact Bitwarden Support for set up assistance.", + "message": "são necessários para configurar a descriptografia do conector chave. Contacte o suporte do Bitwarden para configurar a assistência.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Connect login with SSO to your self-hosted decryption key server. Using this option, members won’t need to use their master passwords to decrypt vault data. The require SSO authentication and single organization policies are required to set up Key Connector decryption. Contact Bitwarden Support for set up assistance.'" }, "keyConnectorPolicyRestriction": { @@ -5637,7 +5637,7 @@ } }, "exportingOrganizationVaultDesc": { - "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "message": "Apenas o cofre da organização associado com $ORGANIZATION$ será exportado. Itens do cofre pessoal e itens de outras organizações não serão incluídos.", "placeholders": { "organization": { "content": "$1", @@ -6707,10 +6707,10 @@ "message": "Convidar membro" }, "needsConfirmation": { - "message": "Needs confirmation" + "message": "Precisa de confirmação" }, "memberRole": { - "message": "Member role" + "message": "Função de membro" }, "moreFromBitwarden": { "message": "Mais do Bitwarden" @@ -6719,7 +6719,7 @@ "message": "Trocar Produtos" }, "freeOrgInvLimitReachedManageBilling": { - "message": "Free organizations may have up to $SEATCOUNT$ members. Upgrade to a paid plan to invite more members.", + "message": "Organizações gratuitas podem ter até $SEATCOUNT$ membros. Faça upgrade para um plano pago para convidar mais membros.", "placeholders": { "seatcount": { "content": "$1", @@ -6728,7 +6728,7 @@ } }, "freeOrgInvLimitReachedNoManageBilling": { - "message": "Free organizations may have up to $SEATCOUNT$ members. Contact your organization owner to upgrade.", + "message": "Organizações gratuitas podem ter até $SEATCOUNT$ membros. Entre em contato com o proprietário da sua organização para fazer upgrade.", "placeholders": { "seatcount": { "content": "$1", @@ -6737,7 +6737,7 @@ } }, "teamsStarterPlanInvLimitReachedManageBilling": { - "message": "Teams Starter plans may have up to $SEATCOUNT$ members. Upgrade to your plan to invite more members.", + "message": "Planos de Times Starter podem ter até $SEATCOUNT$ membros. Atualize para o seu plano para convidar mais membros.", "placeholders": { "seatcount": { "content": "$1", @@ -6755,7 +6755,7 @@ } }, "freeOrgMaxCollectionReachedManageBilling": { - "message": "Free organizations may have up to $COLLECTIONCOUNT$ collections. Upgrade to a paid plan to add more collections.", + "message": "Organizações gratuitas podem ter até $COLLECTIONCOUNT$ coleções. Faça o upgrade para um plano pago para adicionar mais coleções.", "placeholders": { "COLLECTIONCOUNT": { "content": "$1", @@ -6764,7 +6764,7 @@ } }, "freeOrgMaxCollectionReachedNoManageBilling": { - "message": "Free organizations may have up to $COLLECTIONCOUNT$ collections. Contact your organization owner to upgrade.", + "message": "Organizações gratuitas podem ter até $COLLECTIONCOUNT$ membros. Entre em contato com o proprietário da sua organização para fazer upgrade.", "placeholders": { "COLLECTIONCOUNT": { "content": "$1", @@ -6779,10 +6779,10 @@ "message": "Exportar dados" }, "exportingOrganizationSecretDataTitle": { - "message": "Exporting Organization Secret Data" + "message": "Exportando dados secretos da organização" }, "exportingOrganizationSecretDataDescription": { - "message": "Only the Secrets Manager data associated with $ORGANIZATION$ will be exported. Items in other products or from other organizations will not be included.", + "message": "Apenas o cofre da organização associado com $ORGANIZATION$ será exportado. Itens do cofre pessoal e itens de outras organizações não serão incluídos.", "placeholders": { "ORGANIZATION": { "content": "$1", @@ -6812,7 +6812,7 @@ "message": "Upload manual" }, "manualUploadDesc": { - "message": "If you do not want to opt into billing sync, manually upload your license here." + "message": "Se você não deseja optar pela sincronização de cobrança, carregue sua licença manualmente aqui." }, "syncLicense": { "message": "Sincronizar licença" @@ -6827,13 +6827,13 @@ "message": "Última sincronização de licença" }, "billingSyncHelp": { - "message": "Billing Sync help" + "message": "Ajuda da Sincronização de faturamento" }, "licensePaidFeaturesHelp": { - "message": "License paid features help" + "message": "Ajuda dos recursos de licença paga" }, "selfHostGracePeriodHelp": { - "message": "After your subscription expires, you have 60 days to apply an updated license file to your organization. Grace period ends $GRACE_PERIOD_END_DATE$.", + "message": "Após a expiração da assinatura, você tem 60 dias para aplicar um arquivo de licença atualizado à sua organização. Fim do período de carência $GRACE_PERIOD_END_DATE$.", "placeholders": { "GRACE_PERIOD_END_DATE": { "content": "$1", @@ -6845,28 +6845,28 @@ "message": "Enviar licença" }, "projectPeopleDescription": { - "message": "Grant groups or people access to this project." + "message": "Conceder acesso a este projeto ou grupos de pessoas." }, "projectPeopleSelectHint": { - "message": "Type or select people or groups" + "message": "Digite ou selecione pessoas ou grupos" }, "projectServiceAccountsDescription": { - "message": "Grant service accounts access to this project." + "message": "Conceder acesso a contas de serviço a este projeto." }, "projectServiceAccountsSelectHint": { - "message": "Type or select service accounts" + "message": "Digite ou selecione contas de serviço" }, "projectEmptyPeopleAccessPolicies": { - "message": "Add people or groups to start collaborating" + "message": "Adicione pessoas ou grupos para começar a colaborar" }, "projectEmptyServiceAccountAccessPolicies": { "message": "Adicione contas de serviço para conceder acesso" }, "serviceAccountPeopleDescription": { - "message": "Grant groups or people access to this service account." + "message": "Conceder acesso a esta conta de serviço a grupos ou pessoas." }, "serviceAccountProjectsDescription": { - "message": "Assign projects to this service account. " + "message": "Atribuir projetos para esta conta de serviço. " }, "serviceAccountEmptyProjectAccesspolicies": { "message": "Adicionar projetos para conceder acesso" @@ -6878,13 +6878,13 @@ "message": "Grupo/Usuário" }, "lowKdfIterations": { - "message": "Low KDF Iterations" + "message": "Iterações KDF baixas" }, "updateLowKdfIterationsDesc": { - "message": "Update your encryption settings to meet new security recommendations and improve account protection." + "message": "Atualize suas configurações de criptografia para atender às novas recomendações de segurança e melhorar a proteção da conta." }, "changeKdfLoggedOutWarning": { - "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login setup. We recommend exporting your vault before changing your encryption settings to prevent data loss." + "message": "O processo desconectará você de todas as sessões ativas. Você precisará iniciar a sessão novamente e concluir a configuração de login em duas etapas. Recomendamos exportar seu cofre antes de alterar suas configurações de criptografia para evitar perda de dados." }, "secretsManager": { "message": "Gerenciador de Segredos" @@ -6924,7 +6924,7 @@ "message": "Ocorreu um erro ao tentar ler o arquivo de importação" }, "accessedSecret": { - "message": "Accessed secret $SECRET_ID$.", + "message": "$SECRET_ID$ secreto acessado.", "placeholders": { "secret_id": { "content": "$1", @@ -6996,10 +6996,10 @@ "message": "Seleção é necessária." }, "saPeopleWarningTitle": { - "message": "Access tokens still available" + "message": "Tokens de acesso ainda disponíveis" }, "saPeopleWarningMessage": { - "message": "Removing people from a service account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a service account." + "message": "Remover pessoas de uma conta de serviço não remove os tokens de acesso que criaram. Para a melhor prática de segurança, é recomendável revogar os tokens de acesso criados por pessoas removidas de uma conta de serviço." }, "smAccessRemovalWarningProjectTitle": { "message": "Remover acesso para esse projeto" @@ -7008,10 +7008,10 @@ "message": "Esta ação removerá seu acesso ao projeto." }, "smAccessRemovalWarningSaTitle": { - "message": "Remove access to this service account" + "message": "Remover acesso a essa conta de serviço" }, "smAccessRemovalWarningSaMessage": { - "message": "This action will remove your access to the service account." + "message": "Essa ação removerá seu acesso à conta de serviço." }, "removeAccess": { "message": "Remover acesso" @@ -7023,16 +7023,16 @@ "message": "Senha mestre exposta" }, "exposedMasterPasswordDesc": { - "message": "Password found in a data breach. Use a unique password to protect your account. Are you sure you want to use an exposed password?" + "message": "A senha foi encontrada em violação de dados. Use uma senha única para proteger sua conta. Tem certeza de que deseja usar uma senha exposta?" }, "weakAndExposedMasterPassword": { - "message": "Weak and Exposed Master Password" + "message": "Senha Mestra Fraca e Exposta" }, "weakAndBreachedMasterPasswordDesc": { - "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?" + "message": "Senha fraca identificada e encontrada em um vazamento de dados. Use uma senha forte e única para proteger a sua conta. Tem certeza de que deseja usar essa senha?" }, "characterMinimum": { - "message": "$LENGTH$ character minimum", + "message": "Mínimo de $LENGTH$ caracteres", "placeholders": { "length": { "content": "$1", @@ -7041,7 +7041,7 @@ } }, "masterPasswordMinimumlength": { - "message": "Master password must be at least $LENGTH$ characters long.", + "message": "A senha mestra deve ter pelo menos $LENGTH$ caracteres.", "placeholders": { "length": { "content": "$1", @@ -7057,14 +7057,14 @@ "message": "Descartar" }, "notAvailableForFreeOrganization": { - "message": "This feature is not available for free organizations. Contact your organization owner to upgrade." + "message": "Este recurso não está disponível para organizações gratuitas. Entre em contato com o seu proprietário para atualizar." }, "smProjectSecretsNoItemsNoAccess": { - "message": "Contact your organization's admin to manage secrets for this project.", + "message": "Entre em contato com o administrador da sua organização para gerenciar segredos para este projeto.", "description": "The message shown to the user under a project's secrets tab when the user only has read access to the project." }, "enforceOnLoginDesc": { - "message": "Require existing members to change their passwords" + "message": "Exigir que os membros existentes alterem suas senhas" }, "smProjectDeleteAccessRestricted": { "message": "Você não tem permissão para excluir este projeto", @@ -7081,16 +7081,16 @@ "message": "Sessão iniciada" }, "deviceApprovalRequired": { - "message": "Device approval required. Select an approval option below:" + "message": "Aprovação do dispositivo necessária. Selecione uma opção de aprovação abaixo:" }, "rememberThisDevice": { "message": "Lembrar deste dispositivo" }, "uncheckIfPublicDevice": { - "message": "Uncheck if using a public device" + "message": "Desmarque se estiver usando um dispositivo público" }, "approveFromYourOtherDevice": { - "message": "Approve from your other device" + "message": "Aprovar do seu outro dispositivo" }, "requestAdminApproval": { "message": "Solicitar aprovação do administrador" @@ -7099,17 +7099,17 @@ "message": "Aprovar com a senha mestre" }, "trustedDeviceEncryption": { - "message": "Trusted device encryption" + "message": "Criptografia de dispositivo confiável" }, "trustedDevices": { "message": "Dispositivos confiáveis" }, "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", + "message": "Uma vez autenticados, os membros descriptografarão os dados do cofre usando uma chave armazenada no seu dispositivo", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" }, "memberDecryptionOptionTdeDescriptionLinkOne": { - "message": "single organization", + "message": "organização única", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" }, "memberDecryptionOptionTdeDescriptionPartTwo": { @@ -7117,7 +7117,7 @@ "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" }, "memberDecryptionOptionTdeDescriptionLinkTwo": { - "message": "SSO required", + "message": "Necessário SSO", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" }, "memberDecryptionOptionTdeDescriptionPartThree": { @@ -7125,11 +7125,11 @@ "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" }, "memberDecryptionOptionTdeDescriptionLinkThree": { - "message": "account recovery administration", + "message": "gerenciar recuperação de conta", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" }, "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", + "message": "política com inscrição automática será ativada quando esta opção for utilizada.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { @@ -7157,7 +7157,7 @@ "message": "Recuperar conta" }, "updatedTempPassword": { - "message": "User updated a password issued through account recovery." + "message": "O usuário atualizou uma senha emitida através da recuperação de conta." }, "activatedAccessToSecretsManager": { "message": "Acesso ativado ao Gerenciador de Segredos", @@ -7232,7 +7232,7 @@ "message": "Remover membros que não têm senhas mestres sem definir uma para eles pode restringir o acesso à sua conta completa." }, "approvedAuthRequest": { - "message": "Approved device for $ID$.", + "message": "Dispositivo aprovado para $ID$.", "placeholders": { "id": { "content": "$1", @@ -7241,7 +7241,7 @@ } }, "rejectedAuthRequest": { - "message": "Denied device for $ID$.", + "message": "Dispositivo negado para $ID$.", "placeholders": { "id": { "content": "$1", @@ -7250,7 +7250,7 @@ } }, "requestedDeviceApproval": { - "message": "Requested device approval." + "message": "Aprovação do dispositivo solicitada." }, "startYour7DayFreeTrialOfBitwardenFor": { "message": "Comece o seu período de teste gratuito de 7 dias do Bitwarden para $ORG$", @@ -7262,7 +7262,7 @@ } }, "startYour7DayFreeTrialOfBitwardenSecretsManagerFor": { - "message": "Start your 7-Day free trial of Bitwarden Secrets Manager for $ORG$", + "message": "Inicie o seu período de teste gratuito de 7 dias do Bitwarden Secrets Manager para $ORG$", "placeholders": { "org": { "content": "$1", @@ -7280,19 +7280,19 @@ "message": "Sinalização de região selecionada" }, "accountSuccessfullyCreated": { - "message": "Account successfully created!" + "message": "Conta criada com sucesso!" }, "adminApprovalRequested": { - "message": "Admin approval requested" + "message": "Aprovação do administrador necessária" }, "adminApprovalRequestSentToAdmins": { - "message": "Your request has been sent to your admin." + "message": "Seu pedido foi enviado para seu administrador." }, "youWillBeNotifiedOnceApproved": { - "message": "You will be notified once approved." + "message": "Será notificado assim que for aprovado." }, "troubleLoggingIn": { - "message": "Trouble logging in?" + "message": "Problemas em efetuar login?" }, "loginApproved": { "message": "Sessão aprovada" @@ -7324,10 +7324,10 @@ } }, "secretsManagerForPlanDesc": { - "message": "For engineering and DevOps teams to manage secrets throughout the software development lifecycle." + "message": "Para equipes de engenharia e DevOps gerenciar segredos durante todo o ciclo de vida do desenvolvimento de software." }, "free2PersonOrganization": { - "message": "Free 2-person Organizations" + "message": "Organizações gratuitas com 2 pessoas" }, "unlimitedSecrets": { "message": "Segredos ilimitados" @@ -7366,7 +7366,7 @@ "message": "Assine o Gerenciador de Segredos" }, "addSecretsManagerUpgradeDesc": { - "message": "Add Secrets Manager to your upgraded plan to maintain access to any secrets created with your previous plan." + "message": "Adicione o Gerenciador de Segredos ao seu plano atualizado para manter o acesso a todos os segredos criados com seu plano anterior." }, "additionalServiceAccounts": { "message": "Contas de serviço adicionais" @@ -7420,13 +7420,13 @@ "message": "Limitar contas de serviço (opcional)" }, "limitServiceAccountsDesc": { - "message": "Set a limit for your service accounts. Once this limit is reached, you will not be able to create new service accounts." + "message": "Defina um limite para suas contas de máquina. Quando este limite for atingido, você não poderá criar novas contas de máquina." }, "serviceAccountLimit": { "message": "Limite de contas de serviço (opcional)" }, "maxServiceAccountCost": { - "message": "Max potential service account cost" + "message": "Custo máximo da conta de serviço potencial" }, "loggedInExclamation": { "message": "Conectado!" @@ -7459,10 +7459,10 @@ "message": "Chave de acesso" }, "passkeyNotCopied": { - "message": "Passkey will not be copied" + "message": "A senha não será copiada" }, "passkeyNotCopiedAlert": { - "message": "The passkey will not be copied to the cloned item. Do you want to continue cloning this item?" + "message": "A senha não será copiada para o item clonado. Deseja continuar clonando este item?" }, "modifiedCollectionManagement": { "message": "Definição de gerenciamento da coleção $ID$ modificada.", @@ -7484,7 +7484,7 @@ "message": "Use a extensão para salvar rapidamente credenciais e formulários de autopreenchimento sem abrir o aplicativo web." }, "projectAccessUpdated": { - "message": "Project access updated" + "message": "Acesso ao projeto atualizado" }, "unexpectedErrorSend": { "message": "Ocorreu um erro inesperado ao carregar este Envio. Tente novamente mais tarde." @@ -7505,7 +7505,7 @@ "message": "Você não tem acesso para gerenciar esta coleção." }, "grantCollectionAccess": { - "message": "Grant groups or members access to this collection." + "message": "Conceder acesso de grupos ou membros a esta coleção." }, "grantCollectionAccessMembersOnly": { "message": "Conceder acesso a essa coleção." @@ -8042,12 +8042,12 @@ "message": "This collection is only accessible from the admin console" }, "organizationOptionsMenu": { - "message": "Toggle Organization Menu" + "message": "Alternar Menu da Organização" }, "vaultItemSelect": { - "message": "Select vault item" + "message": "Selecionar item do cofre" }, "collectionItemSelect": { - "message": "Select collection item" + "message": "Selecionar item da coleção" } } diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index 67b23b0e89c..213a9d1965d 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -8042,12 +8042,12 @@ "message": "Эта коллекция доступна только из консоли администратора" }, "organizationOptionsMenu": { - "message": "Toggle Organization Menu" + "message": "Переключить меню организации" }, "vaultItemSelect": { - "message": "Select vault item" + "message": "Выбрать элемент хранилища" }, "collectionItemSelect": { - "message": "Select collection item" + "message": "Выбрать элемент коллекции" } } diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index 756723129ff..652b1b08fda 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -8042,12 +8042,12 @@ "message": "Táto zbierka je dostupná iba z administrátorskej konzoly" }, "organizationOptionsMenu": { - "message": "Toggle Organization Menu" + "message": "Prepnúť menu organizácie" }, "vaultItemSelect": { - "message": "Select vault item" + "message": "Vyberte položku z trezora" }, "collectionItemSelect": { - "message": "Select collection item" + "message": "Vyberte položku zo zbierky" } } diff --git a/apps/web/src/locales/sr/messages.json b/apps/web/src/locales/sr/messages.json index f927113fad9..de563afe509 100644 --- a/apps/web/src/locales/sr/messages.json +++ b/apps/web/src/locales/sr/messages.json @@ -7607,7 +7607,7 @@ "message": "Портал провајдера" }, "success": { - "message": "Success" + "message": "Успех" }, "viewCollection": { "message": "Преглед колекције" @@ -7904,16 +7904,16 @@ "message": "Приступ налога машине ажуриран" }, "restrictedGroupAccessDesc": { - "message": "You cannot add yourself to a group." + "message": "Не можете да се додате у групу." }, "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "message": "Обавештење: 2. маја 2024. недодељене ставке организације више неће бити видљиве у вашем приказу Сви сефови на свим уређајима и биће им доступне само преко Админ конзоле. Доделите ове ставке колекцији са Админ конзолом да бисте их учинили видљивим." }, "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + "message": "Напомена: Недодељене ставке организације више нису видљиве у вашем приказу Сви сефови на свим уређајима и сада су доступне само преко Админ конзоле." }, "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + "message": "Обавештење: 16. маја 2024. недодељене ставке организације више неће бити видљиве у вашем приказу Сви сефови на свим уређајима и биће им доступне само преко Админ конзоле." }, "unassignedItemsBannerCTAPartOne": { "message": "Assign these items to a collection from the", @@ -7924,13 +7924,13 @@ "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." }, "deleteProvider": { - "message": "Delete provider" + "message": "Избриши провајдера" }, "deleteProviderConfirmation": { - "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + "message": "Брисање провајдера је трајно и неповратно. Унесите своју главну лозинку да бисте потврдили брисање провајдера и свих повезаних података." }, "deleteProviderName": { - "message": "Cannot delete $ID$", + "message": "Не може да се избрише $ID$", "placeholders": { "id": { "content": "$1", @@ -7939,7 +7939,7 @@ } }, "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "message": "Морате прекинути везу са свим клијентима да бисте могли да избришете $ID$", "placeholders": { "id": { "content": "$1", @@ -7948,106 +7948,106 @@ } }, "providerDeleted": { - "message": "Provider deleted" + "message": "Провајдер је избрисан" }, "providerDeletedDesc": { - "message": "The Provider and all associated data has been deleted." + "message": "Провајдер и сви повезани подаци су избрисани." }, "deleteProviderRecoverConfirmDesc": { - "message": "You have requested to delete this Provider. Use the button below to confirm." + "message": "Захтевали сте брисање овог провајдера. Користите дугме испод да потврдите." }, "deleteProviderWarning": { - "message": "Deleting your provider is permanent. It cannot be undone." + "message": "Брисање провајдера је трајно. Не може се поништити." }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "Грешка при додељивању циљне колекције." }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "Грешка при додељивању циљне фасцикле." }, "integrationsAndSdks": { - "message": "Integrations & SDKs", + "message": "Интеграције & SDK", "description": "The title for the section that deals with integrations and SDKs." }, "integrations": { - "message": "Integrations" + "message": "Интеграције" }, "integrationsDesc": { - "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + "message": "Аутоматски синхронизујте тајне од Bitwarden Secrets Manager са сервисима треће стране." }, "sdks": { - "message": "SDKs" + "message": "SDK" }, "sdksDesc": { - "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + "message": "Употребите Bitwarden Secrets Manager SDK на следећим програмским језицима да направите сопствене апликације." }, "setUpGithubActions": { - "message": "Set up Github Actions" + "message": "Подесити акције GitHub-а" }, "setUpGitlabCICD": { - "message": "Set up GitLab CI/CD" + "message": "Подесити GitLab CI/CD" }, "setUpAnsible": { - "message": "Set up Ansible" + "message": "Подесити Ansible" }, "cSharpSDKRepo": { - "message": "View C# repository" + "message": "Преглед C# спремишта" }, "cPlusPlusSDKRepo": { - "message": "View C++ repository" + "message": "Преглед C++ спремишта" }, "jsWebAssemblySDKRepo": { - "message": "View JS WebAssembly repository" + "message": "Преглед JS WebAssembly спремишта" }, "javaSDKRepo": { - "message": "View Java repository" + "message": "Преглед Java спремишта" }, "pythonSDKRepo": { - "message": "View Python repository" + "message": "Преглед Python спремишта" }, "phpSDKRepo": { - "message": "View php repository" + "message": "Преглед php спремишта" }, "rubySDKRepo": { - "message": "View Ruby repository" + "message": "Преглед Ruby спремишта" }, "goSDKRepo": { - "message": "View Go repository" + "message": "Преглед Go спремишта" }, "createNewClientToManageAsProvider": { - "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + "message": "Креирајте нову клијентску организацију којом ћете управљати као добављач. Додатна места ће се одразити у следећем обрачунском циклусу." }, "selectAPlan": { - "message": "Select a plan" + "message": "Изаберите пакет" }, "thirtyFivePercentDiscount": { - "message": "35% Discount" + "message": "Попуст од 35%" }, "monthPerMember": { - "message": "month per member" + "message": "месечно по члану" }, "seats": { - "message": "Seats" + "message": "Места" }, "addOrganization": { - "message": "Add organization" + "message": "Додај организацију" }, "createdNewClient": { - "message": "Successfully created new client" + "message": "Нови клијент је успешно креиран" }, "noAccess": { - "message": "No access" + "message": "Немате приступ" }, "collectionAdminConsoleManaged": { - "message": "This collection is only accessible from the admin console" + "message": "Овој колекцији се може приступити само са администраторске конзоле" }, "organizationOptionsMenu": { - "message": "Toggle Organization Menu" + "message": "Укључи мени Организација" }, "vaultItemSelect": { - "message": "Select vault item" + "message": "Изаберите ставку сефа" }, "collectionItemSelect": { - "message": "Select collection item" + "message": "Изаберите ставку колекције" } } diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index c9434d72d61..e7b9722c9da 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -8042,12 +8042,12 @@ "message": "Ця збірка доступна тільки з консолі адміністратора" }, "organizationOptionsMenu": { - "message": "Toggle Organization Menu" + "message": "Перемкнути меню організації" }, "vaultItemSelect": { - "message": "Select vault item" + "message": "Вибрати елемент сховища" }, "collectionItemSelect": { - "message": "Select collection item" + "message": "Вибрати елемент збірки" } } diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index a9a0d854577..292041624c6 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -7907,20 +7907,20 @@ "message": "您不能将自己添加到群组。" }, "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "message": "注意:从 2024 年 5 月 2 日起,未分配的组织项目在您所有设备的「所有密码库」视图中将不再可见,只能通过管理控制台访问。通过管理控制台将这些项目分配给集合以使其可见。" }, "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + "message": "注意:未分配的组织项目在您所有设备的「所有密码库」视图中不再可见,现在只能通过管理控制台访问。" }, "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + "message": "注意:从 2024 年 5 月 2 日起,未分配的组织项目在您所有设备的「所有密码库」视图中将不再可见,只能通过管理控制台访问。" }, "unassignedItemsBannerCTAPartOne": { "message": "Assign these items to a collection from the", "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." }, "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", + "message": "以使其可见。", "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." }, "deleteProvider": { @@ -7966,88 +7966,88 @@ "message": "分配目标文件夹时出错。" }, "integrationsAndSdks": { - "message": "Integrations & SDKs", + "message": "集成和 SDK", "description": "The title for the section that deals with integrations and SDKs." }, "integrations": { - "message": "Integrations" + "message": "集成" }, "integrationsDesc": { - "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + "message": "通过 Bitwarden 机密管理器将机密自动同步到第三方服务。" }, "sdks": { - "message": "SDKs" + "message": "SDK" }, "sdksDesc": { - "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + "message": "使用以下编程语言的 Bitwarden 机密管理器 SDK 来构建您自己的应用程序。" }, "setUpGithubActions": { - "message": "Set up Github Actions" + "message": "设置 Github Actions" }, "setUpGitlabCICD": { - "message": "Set up GitLab CI/CD" + "message": "设置 GitLab CI/CD" }, "setUpAnsible": { - "message": "Set up Ansible" + "message": "设置 Ansible" }, "cSharpSDKRepo": { - "message": "View C# repository" + "message": "查看 C# 存储库" }, "cPlusPlusSDKRepo": { - "message": "View C++ repository" + "message": "查看 C++ 存储库" }, "jsWebAssemblySDKRepo": { - "message": "View JS WebAssembly repository" + "message": "查看 JS WebAssembly 存储库" }, "javaSDKRepo": { - "message": "View Java repository" + "message": "查看 Java 存储库" }, "pythonSDKRepo": { - "message": "View Python repository" + "message": "查看 Python 存储库" }, "phpSDKRepo": { - "message": "View php repository" + "message": "查看 php 存储库" }, "rubySDKRepo": { - "message": "View Ruby repository" + "message": "查看 Ruby 存储库" }, "goSDKRepo": { - "message": "View Go repository" + "message": "查看 Go 存储库" }, "createNewClientToManageAsProvider": { - "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + "message": "创建一个新的客户组织作为提供商来管理。附加席位将反映在下一个计费周期中。" }, "selectAPlan": { "message": "选择套餐" }, "thirtyFivePercentDiscount": { - "message": "35% Discount" + "message": "35% 折扣" }, "monthPerMember": { "message": "month per member" }, "seats": { - "message": "Seats" + "message": "席位" }, "addOrganization": { - "message": "Add organization" + "message": "添加组织" }, "createdNewClient": { "message": "Successfully created new client" }, "noAccess": { - "message": "No access" + "message": "暂无权限" }, "collectionAdminConsoleManaged": { - "message": "This collection is only accessible from the admin console" + "message": "此集合只能从管理控制台访问" }, "organizationOptionsMenu": { - "message": "Toggle Organization Menu" + "message": "切换组织菜单" }, "vaultItemSelect": { - "message": "Select vault item" + "message": "选择密码库项目" }, "collectionItemSelect": { - "message": "Select collection item" + "message": "选择集合项目" } } From 7f207d25590e3363de5063d5f763977b98d09445 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 23 Apr 2024 15:30:11 +0000 Subject: [PATCH 044/110] Autosync the updated translations (#8879) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/af/messages.json | 3 + apps/desktop/src/locales/ar/messages.json | 3 + apps/desktop/src/locales/az/messages.json | 3 + apps/desktop/src/locales/be/messages.json | 3 + apps/desktop/src/locales/bg/messages.json | 3 + apps/desktop/src/locales/bn/messages.json | 3 + apps/desktop/src/locales/bs/messages.json | 3 + apps/desktop/src/locales/ca/messages.json | 3 + apps/desktop/src/locales/cs/messages.json | 3 + apps/desktop/src/locales/cy/messages.json | 3 + apps/desktop/src/locales/da/messages.json | 3 + apps/desktop/src/locales/de/messages.json | 7 +- apps/desktop/src/locales/el/messages.json | 3 + apps/desktop/src/locales/en_GB/messages.json | 3 + apps/desktop/src/locales/en_IN/messages.json | 3 + apps/desktop/src/locales/eo/messages.json | 3 + apps/desktop/src/locales/es/messages.json | 3 + apps/desktop/src/locales/et/messages.json | 3 + apps/desktop/src/locales/eu/messages.json | 3 + apps/desktop/src/locales/fa/messages.json | 3 + apps/desktop/src/locales/fi/messages.json | 3 + apps/desktop/src/locales/fil/messages.json | 3 + apps/desktop/src/locales/fr/messages.json | 3 + apps/desktop/src/locales/gl/messages.json | 3 + apps/desktop/src/locales/he/messages.json | 3 + apps/desktop/src/locales/hi/messages.json | 3 + apps/desktop/src/locales/hr/messages.json | 3 + apps/desktop/src/locales/hu/messages.json | 3 + apps/desktop/src/locales/id/messages.json | 3 + apps/desktop/src/locales/it/messages.json | 3 + apps/desktop/src/locales/ja/messages.json | 3 + apps/desktop/src/locales/ka/messages.json | 3 + apps/desktop/src/locales/km/messages.json | 3 + apps/desktop/src/locales/kn/messages.json | 3 + apps/desktop/src/locales/ko/messages.json | 3 + apps/desktop/src/locales/lt/messages.json | 3 + apps/desktop/src/locales/lv/messages.json | 3 + apps/desktop/src/locales/me/messages.json | 3 + apps/desktop/src/locales/ml/messages.json | 3 + apps/desktop/src/locales/mr/messages.json | 3 + apps/desktop/src/locales/my/messages.json | 3 + apps/desktop/src/locales/nb/messages.json | 3 + apps/desktop/src/locales/ne/messages.json | 3 + apps/desktop/src/locales/nl/messages.json | 3 + apps/desktop/src/locales/nn/messages.json | 3 + apps/desktop/src/locales/or/messages.json | 3 + apps/desktop/src/locales/pl/messages.json | 3 + apps/desktop/src/locales/pt_BR/messages.json | 73 ++++++++++---------- apps/desktop/src/locales/pt_PT/messages.json | 3 + apps/desktop/src/locales/ro/messages.json | 3 + apps/desktop/src/locales/ru/messages.json | 3 + apps/desktop/src/locales/si/messages.json | 3 + apps/desktop/src/locales/sk/messages.json | 3 + apps/desktop/src/locales/sl/messages.json | 3 + apps/desktop/src/locales/sr/messages.json | 15 ++-- apps/desktop/src/locales/sv/messages.json | 3 + apps/desktop/src/locales/te/messages.json | 3 + apps/desktop/src/locales/th/messages.json | 3 + apps/desktop/src/locales/tr/messages.json | 3 + apps/desktop/src/locales/uk/messages.json | 3 + apps/desktop/src/locales/vi/messages.json | 3 + apps/desktop/src/locales/zh_CN/messages.json | 7 +- apps/desktop/src/locales/zh_TW/messages.json | 3 + 63 files changed, 234 insertions(+), 45 deletions(-) diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json index afdfc90d766..97067b788a5 100644 --- a/apps/desktop/src/locales/af/messages.json +++ b/apps/desktop/src/locales/af/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json index 7869b0894bc..2d25269fffe 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -2697,6 +2697,9 @@ "message": "تنسيقات مشتركة", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index d4cea4f06ec..cf664abf413 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -2697,6 +2697,9 @@ "message": "Ortaq formatlar", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Problemlərin aradan qaldırılması" }, diff --git a/apps/desktop/src/locales/be/messages.json b/apps/desktop/src/locales/be/messages.json index 53e3ec2d127..e0133e5a742 100644 --- a/apps/desktop/src/locales/be/messages.json +++ b/apps/desktop/src/locales/be/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index f4886c420f6..0c5dc257421 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -2697,6 +2697,9 @@ "message": "Често използвани формати", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Отстраняване на проблеми" }, diff --git a/apps/desktop/src/locales/bn/messages.json b/apps/desktop/src/locales/bn/messages.json index abd2c1cfaed..626734ebffc 100644 --- a/apps/desktop/src/locales/bn/messages.json +++ b/apps/desktop/src/locales/bn/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/bs/messages.json b/apps/desktop/src/locales/bs/messages.json index 825bd6344e0..9d5685cca90 100644 --- a/apps/desktop/src/locales/bs/messages.json +++ b/apps/desktop/src/locales/bs/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index 6c48d6cb0b6..d8c0f32948b 100644 --- a/apps/desktop/src/locales/ca/messages.json +++ b/apps/desktop/src/locales/ca/messages.json @@ -2697,6 +2697,9 @@ "message": "Formats comuns", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Resolució de problemes" }, diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index 550b10a31c1..328ebe15ec2 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -2697,6 +2697,9 @@ "message": "Společné formáty", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Řešení problémů" }, diff --git a/apps/desktop/src/locales/cy/messages.json b/apps/desktop/src/locales/cy/messages.json index b1cc9e63d31..62f2e608bbf 100644 --- a/apps/desktop/src/locales/cy/messages.json +++ b/apps/desktop/src/locales/cy/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index f2a84a3c293..95a054a7fb5 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -2697,6 +2697,9 @@ "message": "Almindelige formater", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Fejlfinding" }, diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index e5e3945abca..6518a56b45a 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -1633,10 +1633,10 @@ "message": "Browser-Integration wird nicht unterstützt" }, "browserIntegrationErrorTitle": { - "message": "Error enabling browser integration" + "message": "Fehler beim Aktivieren der Browser-Integration" }, "browserIntegrationErrorDesc": { - "message": "An error has occurred while enabling browser integration." + "message": "Beim Aktivieren der Browser-Integration ist ein Fehler aufgetreten." }, "browserIntegrationMasOnlyDesc": { "message": "Leider wird die Browser-Integration derzeit nur in der Mac App Store Version unterstützt." @@ -2697,6 +2697,9 @@ "message": "Gängigste Formate", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Problembehandlung" }, diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index 41e6a62a2fd..87360c33ce0 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -2697,6 +2697,9 @@ "message": "Κοινές μορφές", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Αντιμετώπιση Προβλημάτων" }, diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index 2658610df37..5c8c32b7c14 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json index 0542da9ddc8..abfa0b1c0da 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/eo/messages.json b/apps/desktop/src/locales/eo/messages.json index 1c4cc4f0bef..427f08f8052 100644 --- a/apps/desktop/src/locales/eo/messages.json +++ b/apps/desktop/src/locales/eo/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index ec5da442931..f7df93bdd72 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/et/messages.json b/apps/desktop/src/locales/et/messages.json index 3850cc1d858..02cd737baac 100644 --- a/apps/desktop/src/locales/et/messages.json +++ b/apps/desktop/src/locales/et/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json index b21108b6add..2067b2dcc2e 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/fa/messages.json b/apps/desktop/src/locales/fa/messages.json index 08356d410d7..ef34f8222ae 100644 --- a/apps/desktop/src/locales/fa/messages.json +++ b/apps/desktop/src/locales/fa/messages.json @@ -2697,6 +2697,9 @@ "message": "فرمت‌های رایج", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index fca24c197a1..33c593e3aaa 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -2697,6 +2697,9 @@ "message": "Yleiset muodot", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Vianetsintä" }, diff --git a/apps/desktop/src/locales/fil/messages.json b/apps/desktop/src/locales/fil/messages.json index 6d5f85fca8c..d28a4b568c3 100644 --- a/apps/desktop/src/locales/fil/messages.json +++ b/apps/desktop/src/locales/fil/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index 1097624b147..86550b736f3 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -2697,6 +2697,9 @@ "message": "Formats communs", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Résolution de problèmes" }, diff --git a/apps/desktop/src/locales/gl/messages.json b/apps/desktop/src/locales/gl/messages.json index 90648699c0f..889a2beeee0 100644 --- a/apps/desktop/src/locales/gl/messages.json +++ b/apps/desktop/src/locales/gl/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json index 73599c012ee..3b155ffdf3e 100644 --- a/apps/desktop/src/locales/he/messages.json +++ b/apps/desktop/src/locales/he/messages.json @@ -2697,6 +2697,9 @@ "message": "תסדירים נפוצים", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/hi/messages.json b/apps/desktop/src/locales/hi/messages.json index 4f8bb9b4bb8..af28c666813 100644 --- a/apps/desktop/src/locales/hi/messages.json +++ b/apps/desktop/src/locales/hi/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json index 220c8bfab23..01983d5891e 100644 --- a/apps/desktop/src/locales/hr/messages.json +++ b/apps/desktop/src/locales/hr/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index 149d48284ed..5c91fb4b944 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -2697,6 +2697,9 @@ "message": "Általános formátumok", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Hibaelhárítás" }, diff --git a/apps/desktop/src/locales/id/messages.json b/apps/desktop/src/locales/id/messages.json index 3194b0f7d31..2173224f541 100644 --- a/apps/desktop/src/locales/id/messages.json +++ b/apps/desktop/src/locales/id/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index 08ae2d9da85..93882cf6985 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -2697,6 +2697,9 @@ "message": "Formati comuni", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Successo" + }, "troubleshooting": { "message": "Risoluzione problemi" }, diff --git a/apps/desktop/src/locales/ja/messages.json b/apps/desktop/src/locales/ja/messages.json index b07eab20cc7..ab6c0be95f7 100644 --- a/apps/desktop/src/locales/ja/messages.json +++ b/apps/desktop/src/locales/ja/messages.json @@ -2697,6 +2697,9 @@ "message": "一般的な形式", "description": "Label indicating the most common import formats" }, + "success": { + "message": "成功" + }, "troubleshooting": { "message": "トラブルシューティング" }, diff --git a/apps/desktop/src/locales/ka/messages.json b/apps/desktop/src/locales/ka/messages.json index 90648699c0f..889a2beeee0 100644 --- a/apps/desktop/src/locales/ka/messages.json +++ b/apps/desktop/src/locales/ka/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/km/messages.json b/apps/desktop/src/locales/km/messages.json index 90648699c0f..889a2beeee0 100644 --- a/apps/desktop/src/locales/km/messages.json +++ b/apps/desktop/src/locales/km/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/kn/messages.json b/apps/desktop/src/locales/kn/messages.json index 162cba3a75e..eb0cbcf6be7 100644 --- a/apps/desktop/src/locales/kn/messages.json +++ b/apps/desktop/src/locales/kn/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json index 09b1767af0b..8e50ade96cd 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json index de77aa8fbf3..e9de6970054 100644 --- a/apps/desktop/src/locales/lt/messages.json +++ b/apps/desktop/src/locales/lt/messages.json @@ -2697,6 +2697,9 @@ "message": "Dažni formatai", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index 521a0afcf20..aa057f54ab6 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -2697,6 +2697,9 @@ "message": "Izplatīti veidoli", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Sarežģījumu novēršana" }, diff --git a/apps/desktop/src/locales/me/messages.json b/apps/desktop/src/locales/me/messages.json index ed458379b8b..1f49961b469 100644 --- a/apps/desktop/src/locales/me/messages.json +++ b/apps/desktop/src/locales/me/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/ml/messages.json b/apps/desktop/src/locales/ml/messages.json index b94b9d1b796..96811b9dba8 100644 --- a/apps/desktop/src/locales/ml/messages.json +++ b/apps/desktop/src/locales/ml/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/mr/messages.json b/apps/desktop/src/locales/mr/messages.json index 90648699c0f..889a2beeee0 100644 --- a/apps/desktop/src/locales/mr/messages.json +++ b/apps/desktop/src/locales/mr/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/my/messages.json b/apps/desktop/src/locales/my/messages.json index 5142c8e61f7..0ee0db69ef2 100644 --- a/apps/desktop/src/locales/my/messages.json +++ b/apps/desktop/src/locales/my/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/nb/messages.json b/apps/desktop/src/locales/nb/messages.json index e190cfc236a..7bf132bdac1 100644 --- a/apps/desktop/src/locales/nb/messages.json +++ b/apps/desktop/src/locales/nb/messages.json @@ -2697,6 +2697,9 @@ "message": "Vanlige formater", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/ne/messages.json b/apps/desktop/src/locales/ne/messages.json index bd58a18b0d3..13e14668054 100644 --- a/apps/desktop/src/locales/ne/messages.json +++ b/apps/desktop/src/locales/ne/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index 3ca07307105..b5f2a413d6f 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -2697,6 +2697,9 @@ "message": "Veelvoorkomende formaten", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Probleemoplossing" }, diff --git a/apps/desktop/src/locales/nn/messages.json b/apps/desktop/src/locales/nn/messages.json index 12e11b32c11..35e7173d74a 100644 --- a/apps/desktop/src/locales/nn/messages.json +++ b/apps/desktop/src/locales/nn/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/or/messages.json b/apps/desktop/src/locales/or/messages.json index 73635585516..cd83d2ea698 100644 --- a/apps/desktop/src/locales/or/messages.json +++ b/apps/desktop/src/locales/or/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index df7a158a3a9..250c557309c 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -2697,6 +2697,9 @@ "message": "Popularne formaty", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Sukces" + }, "troubleshooting": { "message": "Rozwiązywanie problemów" }, diff --git a/apps/desktop/src/locales/pt_BR/messages.json b/apps/desktop/src/locales/pt_BR/messages.json index f651e0b0607..12db01d8cd1 100644 --- a/apps/desktop/src/locales/pt_BR/messages.json +++ b/apps/desktop/src/locales/pt_BR/messages.json @@ -561,10 +561,10 @@ "message": "A sua nova conta foi criada! Agora você pode iniciar a sessão." }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "Você logou na sua conta com sucesso" }, "youMayCloseThisWindow": { - "message": "You may close this window" + "message": "Você pode fechar esta janela" }, "masterPassSent": { "message": "Enviamos um e-mail com a dica da sua senha mestra." @@ -801,10 +801,10 @@ "message": "Alterar Senha Mestra" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "Continuar no aplicativo web?" }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "Você pode alterar a sua senha mestra no aplicativo web Bitwarden." }, "fingerprintPhrase": { "message": "Frase biométrica", @@ -1402,7 +1402,7 @@ "message": "Código PIN inválido." }, "tooManyInvalidPinEntryAttemptsLoggingOut": { - "message": "Too many invalid PIN entry attempts. Logging out." + "message": "Muitas tentativas de entrada de PIN inválidas. Desconectando." }, "unlockWithWindowsHello": { "message": "Desbloquear com o Windows Hello" @@ -1557,7 +1557,7 @@ "description": "Used as a card title description on the set password page to explain why the user is there" }, "verificationRequired": { - "message": "Verification required", + "message": "Verificação necessária", "description": "Default title for the user verification dialog." }, "currentMasterPass": { @@ -1633,10 +1633,10 @@ "message": "Integração com o navegador não suportado" }, "browserIntegrationErrorTitle": { - "message": "Error enabling browser integration" + "message": "Erro ao ativar a integração do navegador" }, "browserIntegrationErrorDesc": { - "message": "An error has occurred while enabling browser integration." + "message": "Ocorreu um erro ao permitir a integração do navegador." }, "browserIntegrationMasOnlyDesc": { "message": "Infelizmente, por ora, a integração do navegador só é suportada na versão da Mac App Store." @@ -1654,10 +1654,10 @@ "message": "Ative uma camada adicional de segurança, exigindo validação de frase de impressão digital ao estabelecer uma ligação entre o computador e o navegador. Quando ativado, isto requer intervenção do usuário e verificação cada vez que uma conexão é estabelecida." }, "enableHardwareAcceleration": { - "message": "Use hardware acceleration" + "message": "Utilizar aceleração de hardware" }, "enableHardwareAccelerationDesc": { - "message": "By default this setting is ON. Turn OFF only if you experience graphical issues. Restart is required." + "message": "Por padrão esta configuração está ativada. Desligar apenas se tiver problemas gráficos. Reiniciar é necessário." }, "approve": { "message": "Aprovar" @@ -1898,40 +1898,40 @@ "message": "A sua senha mestra não atende a uma ou mais das políticas da sua organização. Para acessar o cofre, você deve atualizar a sua senha mestra agora. O processo desconectará você da sessão atual, exigindo que você inicie a sessão novamente. Sessões ativas em outros dispositivos podem continuar ativas por até uma hora." }, "tryAgain": { - "message": "Try again" + "message": "Tentar novamente" }, "verificationRequiredForActionSetPinToContinue": { - "message": "Verification required for this action. Set a PIN to continue." + "message": "Verificação necessária para esta ação. Defina um PIN para continuar." }, "setPin": { - "message": "Set PIN" + "message": "Definir PIN" }, "verifyWithBiometrics": { - "message": "Verify with biometrics" + "message": "Verificiar com biometria" }, "awaitingConfirmation": { - "message": "Awaiting confirmation" + "message": "Aguardando confirmação" }, "couldNotCompleteBiometrics": { - "message": "Could not complete biometrics." + "message": "Não foi possível completar a biometria." }, "needADifferentMethod": { - "message": "Need a different method?" + "message": "Precisa de um método diferente?" }, "useMasterPassword": { - "message": "Use master password" + "message": "Usar a senha mestra" }, "usePin": { - "message": "Use PIN" + "message": "Usar PIN" }, "useBiometrics": { - "message": "Use biometrics" + "message": "Usar biometria" }, "enterVerificationCodeSentToEmail": { - "message": "Enter the verification code that was sent to your email." + "message": "Digite o código de verificação que foi enviado para o seu e-mail." }, "resendCode": { - "message": "Resend code" + "message": "Reenviar código" }, "hours": { "message": "Horas" @@ -2541,13 +2541,13 @@ } }, "launchDuoAndFollowStepsToFinishLoggingIn": { - "message": "Launch Duo and follow the steps to finish logging in." + "message": "Inicie o Duo e siga os passos para finalizar o login." }, "duoRequiredByOrgForAccount": { - "message": "Duo two-step login is required for your account." + "message": "A autenticação em duas etapas do Duo é necessária para sua conta." }, "launchDuo": { - "message": "Launch Duo in Browser" + "message": "Iniciar o Duo no navegador" }, "importFormatError": { "message": "Os dados não estão formatados corretamente. Por favor, verifique o seu arquivo de importação e tente novamente." @@ -2630,13 +2630,13 @@ "message": "Nome de usuário ou senha incorretos" }, "incorrectPassword": { - "message": "Incorrect password" + "message": "Senha incorreta" }, "incorrectCode": { - "message": "Incorrect code" + "message": "Código incorreto" }, "incorrectPin": { - "message": "Incorrect PIN" + "message": "PIN incorreto" }, "multifactorAuthenticationFailed": { "message": "Falha na autenticação de múltiplos fatores" @@ -2697,25 +2697,28 @@ "message": "Formatos comuns", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { - "message": "Troubleshooting" + "message": "Solução de problemas" }, "disableHardwareAccelerationRestart": { - "message": "Disable hardware acceleration and restart" + "message": "Desativar aceleração de hardware e reiniciar" }, "enableHardwareAccelerationRestart": { - "message": "Enable hardware acceleration and restart" + "message": "Ativar aceleração de hardware e reiniciar" }, "removePasskey": { - "message": "Remove passkey" + "message": "Remover senha" }, "passkeyRemoved": { - "message": "Passkey removed" + "message": "Chave de acesso removida" }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "Erro ao atribuir coleção de destino." }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "Erro ao atribuir pasta de destino." } } diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index 4e58dad2cf3..97ce9a8b885 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -2697,6 +2697,9 @@ "message": "Formatos comuns", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Resolução de problemas" }, diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json index 3fe73f28a75..978f57eb9b7 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index cc182812a6b..c9b3b95b39c 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -2697,6 +2697,9 @@ "message": "Основные форматы", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Успешно" + }, "troubleshooting": { "message": "Устранение проблем" }, diff --git a/apps/desktop/src/locales/si/messages.json b/apps/desktop/src/locales/si/messages.json index 261ae1c9b8d..3d439971442 100644 --- a/apps/desktop/src/locales/si/messages.json +++ b/apps/desktop/src/locales/si/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index 6ef52a83eef..13d720dbfb4 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -2697,6 +2697,9 @@ "message": "Bežné formáty", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Riešenie problémov" }, diff --git a/apps/desktop/src/locales/sl/messages.json b/apps/desktop/src/locales/sl/messages.json index 8c9c158c87b..8cb06dcf0ca 100644 --- a/apps/desktop/src/locales/sl/messages.json +++ b/apps/desktop/src/locales/sl/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index edafdb55a28..04b7e4cf298 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -801,10 +801,10 @@ "message": "Промени главну лозинку" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "Ићи на веб апликацију?" }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "Можете променити главну лозинку на Bitwarden веб апликацији." }, "fingerprintPhrase": { "message": "Сигурносна Фраза Сефа", @@ -1633,10 +1633,10 @@ "message": "Интеграција са претраживачем није подржана" }, "browserIntegrationErrorTitle": { - "message": "Error enabling browser integration" + "message": "Грешка при омогућавању интеграције прегледача" }, "browserIntegrationErrorDesc": { - "message": "An error has occurred while enabling browser integration." + "message": "Дошло је до грешке при омогућавању интеграције прегледача." }, "browserIntegrationMasOnlyDesc": { "message": "Нажалост, интеграција прегледача за сада је подржана само у верзији Mac App Store." @@ -2697,6 +2697,9 @@ "message": "Уобичајени формати", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Решавање проблема" }, @@ -2713,9 +2716,9 @@ "message": "Приступачни кључ је уклоњен" }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "Грешка при додељивању циљне колекције." }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "Грешка при додељивању циљне фасцикле." } } diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index 6342424fd44..bd21c0f328a 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -2697,6 +2697,9 @@ "message": "Vanliga format", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Felsökning" }, diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json index 90648699c0f..889a2beeee0 100644 --- a/apps/desktop/src/locales/te/messages.json +++ b/apps/desktop/src/locales/te/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/th/messages.json b/apps/desktop/src/locales/th/messages.json index 5b6a1b9d0b7..f1cd5351f7f 100644 --- a/apps/desktop/src/locales/th/messages.json +++ b/apps/desktop/src/locales/th/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index 42a8f207c73..3e7229c41b8 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Sorun giderme" }, diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index ac7b7c32435..9ee76520933 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -2697,6 +2697,9 @@ "message": "Поширені формати", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Успішно" + }, "troubleshooting": { "message": "Усунення проблем" }, diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json index aac9995db13..0c0e6f6df73 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index 0560466cf81..9837be29e3d 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -801,10 +801,10 @@ "message": "修改主密码" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "前往网页 App 吗?" }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "您可以在 Bitwarden 网页应用上更改您的主密码。" }, "fingerprintPhrase": { "message": "指纹短语", @@ -2697,6 +2697,9 @@ "message": "常规格式", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "故障排除" }, diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index 9eb12e23cf8..5f768b0a43e 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -2697,6 +2697,9 @@ "message": "常見格式", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "疑難排解" }, From 242ee306cf32674d968ba2b1639126d5d2c45486 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Tue, 23 Apr 2024 20:34:02 +0200 Subject: [PATCH 045/110] Shorten extension description to 112 characters as that is a limit setup by Apple (#8884) Safari extension description is limited to 112 chars Add that restriction within the description Co-authored-by: Daniel James Smith --- apps/browser/src/_locales/en/messages.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 7e6e333689f..1c0b1788957 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." From bc43f3f78f41c1b171522d54665cfbfaedbb13e4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 23 Apr 2024 19:22:48 +0000 Subject: [PATCH 046/110] Autosync the updated translations (#8886) 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 | 4 ++-- 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 | 2 +- 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/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 | 2 +- apps/browser/src/_locales/zh_TW/messages.json | 4 ++-- 60 files changed, 118 insertions(+), 118 deletions(-) diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index e08894be0b1..996142b5ad5 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "قم بالتسجيل أو إنشاء حساب جديد للوصول إلى خزنتك الآمنة." diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 1e5062d8c67..a58ada8eb17 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Güvənli anbarınıza müraciət etmək üçün giriş edin və ya yeni bir hesab yaradın." diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 91ff397b3ae..82fd4fa5d4e 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Увайдзіце або стварыце новы ўліковы запіс для доступу да бяспечнага сховішча." diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 33be2608b4b..b6d41cb622a 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Впишете се или създайте нов абонамент, за да достъпите защитен трезор." diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index a12308648a4..dec1bc6cfae 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "আপনার সুরক্ষিত ভল্টে প্রবেশ করতে লগ ইন করুন অথবা একটি নতুন অ্যাকাউন্ট তৈরি করুন।" diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index 7f406fabee9..9d3113e3f60 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Prijavite se ili napravite novi račun da biste pristupili svom sigurnom trezoru." diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index 7c8bd63aead..8063ba79d81 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Inicieu sessió o creeu un compte nou per accedir a la caixa forta." diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index bd3c6882df0..ee58f3d2637 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Pro přístup do Vašeho bezpečného trezoru se přihlaste nebo si vytvořte nový účet." diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 2be868872c6..a80dca5f92e 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Mewngofnodwch neu crëwch gyfrif newydd i gael mynediad i'ch cell ddiogel." diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index 777c3b484f6..215d79eb217 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log ind eller opret en ny konto for at få adgang til din sikre boks." diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index d55d499b3c1..fbc193dbaec 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Zu Hause, am Arbeitsplatz oder unterwegs schützt Bitwarden einfach alle deine Passwörter, Passkeys und vertraulichen Informationen.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Melde dich an oder erstelle ein neues Konto, um auf deinen Tresor zuzugreifen." diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index 8c65e61e53f..5c85aeff58b 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Συνδεθείτε ή δημιουργήστε ένα νέο λογαριασμό για να αποκτήσετε πρόσβαση στο ασφαλές vault σας." diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index e4d90adf1ab..087cd3faa8d 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index 7cc17240d20..f370af7f364 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index 3e488bce4c7..9e89f453df8 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Identifícate o crea una nueva cuenta para acceder a tu caja fuerte." diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index 785a3e49860..5705a5a0d24 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Logi oma olemasolevasse kontosse sisse või loo uus konto." diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index 9a07b9d9aea..ee3b5f13292 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Saioa hasi edo sortu kontu berri bat zure kutxa gotorrera sartzeko." diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index c68dc43ef4c..e2f0e96c862 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "وارد شوید یا یک حساب کاربری بسازید تا به گاوصندوق امن‌تان دسترسی یابید." diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 2cdb6a2379e..746f4f45be5 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Kotona, töissä tai reissussa, Bitwarden suojaa helposti kaikki salasanasi, avainkoodisi ja arkaluonteiset tietosi.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Käytä salattua holviasi kirjautumalla sisään tai luo uusi tili." diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index 0dfb4a39c91..abb999d032a 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Maglog-in o gumawa ng bagong account para ma-access ang iyong ligtas na kahadeyero." diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index 742e31ee58c..de35f718325 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Identifiez-vous ou créez un nouveau compte pour accéder à votre coffre sécurisé." diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index b4c151eeb03..3dd737f0a8c 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index 61482da54a6..5d343ae8070 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "צור חשבון חדש או התחבר כדי לגשת לכספת המאובטחת שלך." diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index b76405eed85..fa4051d3e9e 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "अपनी सुरक्षित तिजोरी में प्रवेश करने के लिए नया खाता बनाएं या लॉग इन करें।" diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index 2dc500bc1ee..c9b8741509b 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Prijavi se ili stvori novi račun za pristup svojem sigurnom trezoru." diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index e47f2cda1fc..5d5b1744354 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Bejelentkezés vagy új fiók létrehozása a biztonsági széf eléréséhez." diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index d4399d8e15d..b54e854d276 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Masuk atau buat akun baru untuk mengakses brankas Anda." diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 93ae6821909..91d10253a02 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Accedi o crea un nuovo account per accedere alla tua cassaforte." diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 52ff21727af..967dc222e50 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "安全なデータ保管庫へアクセスするためにログインまたはアカウントを作成してください。" diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index 2c18502eca3..c73c366195e 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index 5d1b024c607..b6384bb8408 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index 047270808e8..178cd7c45f3 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "ನಿಮ್ಮ ಸುರಕ್ಷಿತ ವಾಲ್ಟ್ ಅನ್ನು ಪ್ರವೇಶಿಸಲು ಲಾಗ್ ಇನ್ ಮಾಡಿ ಅಥವಾ ಹೊಸ ಖಾತೆಯನ್ನು ರಚಿಸಿ." diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index 1724225b0e2..95a7727b830 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -8,7 +8,7 @@ }, "extDesc": { "message": "집에서도, 직장에서도, 이동 중에도 Bitwarden은 비밀번호, 패스키, 민감 정보를 쉽게 보호합니다.", - "description": "Extension description" + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "안전 보관함에 접근하려면 로그인하거나 새 계정을 만드세요." diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index b1a2c857e02..a01c5069e81 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Prisijunkite arba sukurkite naują paskyrą, kad galėtumėte pasiekti saugyklą." diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index 4055693486f..f24f0a93fc9 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Jāpiesakās vai jāizveido jauns konts, lai piekļūtu drošajai glabātavai." diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index d9703137fec..334027b407b 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "നിങ്ങളുടെ സുരക്ഷിത വാൾട്ടിലേക്കു പ്രവേശിക്കാൻ ലോഗിൻ ചെയ്യുക അല്ലെങ്കിൽ ഒരു പുതിയ അക്കൗണ്ട് സൃഷ്ടിക്കുക." diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index f67f617d3b1..b0e9f8abc18 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "तुमच्या सुरक्षित तिजोरीत पोहचण्यासाठी लॉग इन करा किंवा नवीन खाते उघडा." diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index 5d1b024c607..b6384bb8408 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 649163a8dc1..163154b2f2c 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Logg på eller opprett en ny konto for å få tilgang til ditt sikre hvelv." diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index 5d1b024c607..b6384bb8408 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index f1424df0b9a..cd76fc9684e 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log in of maak een nieuw account aan om toegang te krijgen tot je beveiligde kluis." diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index 5d1b024c607..b6384bb8408 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index 5d1b024c607..b6384bb8408 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index 9b3e8f20fc0..d3d9106c15e 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "W domu, w pracy, lub w ruchu, Bitwarden z łatwością zabezpiecza wszystkie Twoje hasła, passkeys i poufne informacje.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Zaloguj się lub utwórz nowe konto, aby uzyskać dostęp do Twojego bezpiecznego sejfu." diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index ef2b6f2dcae..417bc977ebe 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Em casa, no trabalho, ou em qualquer lugar, o Bitwarden protege facilmente todas as suas senhas, senhas e informações confidenciais.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Inicie a sessão ou crie uma nova conta para acessar seu cofre seguro." diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index 06ba8eed26b..6d6fd702769 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Inicie sessão ou crie uma nova conta para aceder ao seu cofre seguro." diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index b3e0a2066fc..780bf69b938 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Autentificați-vă sau creați un cont nou pentru a accesa seiful dvs. securizat." diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index e594dbdce2c..927095a3f6a 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Войдите или создайте новый аккаунт для доступа к вашему защищенному хранилищу." diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index 05e2dc3edd4..33b03f574be 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "ඔබගේ ආරක්ෂිත සුරක්ෂිතාගාරය වෙත පිවිසීමට හෝ නව ගිණුමක් නිර්මාණය කරන්න." diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index eab1d105eb0..c84cfbb778e 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Prihláste sa, alebo vytvorte nový účet pre prístup k vášmu bezpečnému trezoru." diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index 935678efc82..4a6b7cd2140 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Prijavite se ali ustvarite nov račun za dostop do svojega varnega trezorja." diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index acac4d14c61..a04a7ecd70d 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Пријавите се или креирајте нови налог за приступ сефу." diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index d96e86b8d3e..2b9ec59ec27 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Logga in eller skapa ett nytt konto för att komma åt ditt säkra valv." diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index 5d1b024c607..b6384bb8408 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index 794d0e6c22d..7e1dda99be8 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "ล็อกอิน หรือ สร้างบัญชีใหม่ เพื่อใช้งานตู้นิรภัยของคุณ" diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 8408253b866..8a8bb6ea600 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Güvenli kasanıza ulaşmak için giriş yapın veya yeni bir hesap oluşturun." diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index b590b92041b..27293fc992b 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Для доступу до сховища увійдіть в обліковий запис, або створіть новий." diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index ab1d0d515b7..4eba4ffaeaa 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Đăng nhập hoặc tạo tài khoản mới để truy cập kho lưu trữ của bạn." diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index fa4dab6a8fd..3cf2f96da16 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -8,7 +8,7 @@ }, "extDesc": { "message": "无论是在家里、工作中还是在外出时,Bitwarden 都可以轻松地保护您的所有密码、通行密钥和敏感信息。", - "description": "Extension description" + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "登录或者创建一个账户来访问您的安全密码库。" diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index 1ecfdfc50eb..eb35cd08c77 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "登入或建立帳戶以存取您的安全密碼庫。" From 6b0628b81e33ae158231ccbd3d5efc3d09794167 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 23 Apr 2024 19:27:46 +0000 Subject: [PATCH 047/110] Autosync the updated translations (#8885) 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/bg/messages.json | 2 +- apps/desktop/src/locales/cs/messages.json | 2 +- apps/desktop/src/locales/da/messages.json | 2 +- apps/desktop/src/locales/de/messages.json | 2 +- apps/desktop/src/locales/fi/messages.json | 2 +- apps/desktop/src/locales/pt_PT/messages.json | 2 +- apps/desktop/src/locales/sk/messages.json | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index cf664abf413..1ecd18eee75 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -2698,7 +2698,7 @@ "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "Uğurlu" }, "troubleshooting": { "message": "Problemlərin aradan qaldırılması" diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index 0c5dc257421..d53034d61c4 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -2698,7 +2698,7 @@ "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "Успех" }, "troubleshooting": { "message": "Отстраняване на проблеми" diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index 328ebe15ec2..e68fe8fffc4 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -2698,7 +2698,7 @@ "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "Úspěch" }, "troubleshooting": { "message": "Řešení problémů" diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index 95a054a7fb5..0e578a6f66e 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -2698,7 +2698,7 @@ "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "Gennemført" }, "troubleshooting": { "message": "Fejlfinding" diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index 6518a56b45a..d04c2795f33 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -2698,7 +2698,7 @@ "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "Erfolg" }, "troubleshooting": { "message": "Problembehandlung" diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index 33c593e3aaa..517b437d030 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -2698,7 +2698,7 @@ "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "Onnistui" }, "troubleshooting": { "message": "Vianetsintä" diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index 97ce9a8b885..14f0ec5d2f7 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -2698,7 +2698,7 @@ "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "Com sucesso" }, "troubleshooting": { "message": "Resolução de problemas" diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index 13d720dbfb4..6499486b9d5 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -2698,7 +2698,7 @@ "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "Úspech" }, "troubleshooting": { "message": "Riešenie problémov" From 790c9a614141e91debfadfeff1607bfcd50ff8f0 Mon Sep 17 00:00:00 2001 From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Date: Tue, 23 Apr 2024 16:00:47 -0400 Subject: [PATCH 048/110] Fixed race condition where this.canAccessPremium would be undefined before the sync could complete (#8887) --- .../angular/src/vault/components/view.component.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/libs/angular/src/vault/components/view.component.ts b/libs/angular/src/vault/components/view.component.ts index 42349737f0d..27d6e14b118 100644 --- a/libs/angular/src/vault/components/view.component.ts +++ b/libs/angular/src/vault/components/view.component.ts @@ -9,7 +9,7 @@ import { OnInit, Output, } from "@angular/core"; -import { firstValueFrom, Subject, takeUntil } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; @@ -69,7 +69,6 @@ export class ViewComponent implements OnDestroy, OnInit { private totpInterval: any; private previousCipherId: string; private passwordReprompted = false; - private directiveIsDestroyed$ = new Subject(); get fido2CredentialCreationDateValue(): string { const dateCreated = this.i18nService.t("dateCreated"); @@ -119,19 +118,11 @@ export class ViewComponent implements OnDestroy, OnInit { } }); }); - - this.billingAccountProfileStateService.hasPremiumFromAnySource$ - .pipe(takeUntil(this.directiveIsDestroyed$)) - .subscribe((canAccessPremium: boolean) => { - this.canAccessPremium = canAccessPremium; - }); } ngOnDestroy() { this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); this.cleanUp(); - this.directiveIsDestroyed$.next(true); - this.directiveIsDestroyed$.complete(); } async load() { @@ -141,6 +132,9 @@ export class ViewComponent implements OnDestroy, OnInit { this.cipher = await cipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(cipher), ); + this.canAccessPremium = await firstValueFrom( + this.billingAccountProfileStateService.hasPremiumFromAnySource$, + ); this.showPremiumRequiredTotp = this.cipher.login.totp && !this.canAccessPremium && !this.cipher.organizationUseTotp; From 8ef5340635ac641d702f104f001baf40cd960b0c Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Tue, 23 Apr 2024 16:57:19 -0400 Subject: [PATCH 049/110] Trust our own copy of authenticatedAccounts until all accounts are initialized (#8888) --- .../src/platform/services/state.service.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index f660cd7a342..412176e235e 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -115,14 +115,19 @@ export class StateService< return; } + // Get all likely authenticated accounts + const authenticatedAccounts = ( + (await this.storageService.get(keys.authenticatedAccounts)) ?? [] + ).filter((account) => account != null); + await this.updateState(async (state) => { - state.authenticatedAccounts = - (await this.storageService.get(keys.authenticatedAccounts)) ?? []; - for (const i in state.authenticatedAccounts) { - if (i != null) { - state = await this.syncAccountFromDisk(state.authenticatedAccounts[i]); - } + for (const i in authenticatedAccounts) { + state = await this.syncAccountFromDisk(authenticatedAccounts[i]); } + + // After all individual accounts have been added + state.authenticatedAccounts = authenticatedAccounts; + const storedActiveUser = await this.storageService.get(keys.activeUserId); if (storedActiveUser != null) { state.activeUserId = storedActiveUser; From 1520d95bbc862d875b75764695262a45a85c300e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 23 Apr 2024 15:21:25 -0700 Subject: [PATCH 050/110] [deps] Auth: Update @types/node-ipc to v9.2.3 (#7248) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../native-messaging-test-runner/package-lock.json | 10 ++++++---- apps/desktop/native-messaging-test-runner/package.json | 2 +- package-lock.json | 8 ++++---- package.json | 2 +- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/apps/desktop/native-messaging-test-runner/package-lock.json b/apps/desktop/native-messaging-test-runner/package-lock.json index e2961eb9eed..747d8ec9811 100644 --- a/apps/desktop/native-messaging-test-runner/package-lock.json +++ b/apps/desktop/native-messaging-test-runner/package-lock.json @@ -20,15 +20,17 @@ "devDependencies": { "@tsconfig/node16": "1.0.4", "@types/node": "18.19.29", - "@types/node-ipc": "9.2.0", + "@types/node-ipc": "9.2.3", "typescript": "4.7.4" } }, "../../../libs/common": { + "name": "@bitwarden/common", "version": "0.0.0", "license": "GPL-3.0" }, "../../../libs/node": { + "name": "@bitwarden/node", "version": "0.0.0", "license": "GPL-3.0", "dependencies": { @@ -105,9 +107,9 @@ } }, "node_modules/@types/node-ipc": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/@types/node-ipc/-/node-ipc-9.2.0.tgz", - "integrity": "sha512-0v1oucUgINvWPhknecSBE5xkz74sVgeZgiL/LkWXNTSzFaGspEToA4oR56hjza0Jkk6DsS2EiNU3M2R2KQza9A==", + "version": "9.2.3", + "resolved": "https://registry.npmjs.org/@types/node-ipc/-/node-ipc-9.2.3.tgz", + "integrity": "sha512-/MvSiF71fYf3+zwqkh/zkVkZj1hl1Uobre9EMFy08mqfJNAmpR0vmPgOUdEIDVgifxHj6G1vYMPLSBLLxoDACQ==", "dev": true, "dependencies": { "@types/node": "*" diff --git a/apps/desktop/native-messaging-test-runner/package.json b/apps/desktop/native-messaging-test-runner/package.json index c5726131199..72b2587a4ae 100644 --- a/apps/desktop/native-messaging-test-runner/package.json +++ b/apps/desktop/native-messaging-test-runner/package.json @@ -25,7 +25,7 @@ "devDependencies": { "@tsconfig/node16": "1.0.4", "@types/node": "18.19.29", - "@types/node-ipc": "9.2.0", + "@types/node-ipc": "9.2.3", "typescript": "4.7.4" }, "_moduleAliases": { diff --git a/package-lock.json b/package-lock.json index f5932a25b80..ad27ada66bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -111,7 +111,7 @@ "@types/node": "18.19.29", "@types/node-fetch": "2.6.4", "@types/node-forge": "1.3.11", - "@types/node-ipc": "9.2.0", + "@types/node-ipc": "9.2.3", "@types/papaparse": "5.3.14", "@types/proper-lockfile": "4.1.4", "@types/react": "16.14.57", @@ -10544,9 +10544,9 @@ } }, "node_modules/@types/node-ipc": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/@types/node-ipc/-/node-ipc-9.2.0.tgz", - "integrity": "sha512-0v1oucUgINvWPhknecSBE5xkz74sVgeZgiL/LkWXNTSzFaGspEToA4oR56hjza0Jkk6DsS2EiNU3M2R2KQza9A==", + "version": "9.2.3", + "resolved": "https://registry.npmjs.org/@types/node-ipc/-/node-ipc-9.2.3.tgz", + "integrity": "sha512-/MvSiF71fYf3+zwqkh/zkVkZj1hl1Uobre9EMFy08mqfJNAmpR0vmPgOUdEIDVgifxHj6G1vYMPLSBLLxoDACQ==", "dev": true, "dependencies": { "@types/node": "*" diff --git a/package.json b/package.json index 057e737903a..09065b234e3 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "@types/node": "18.19.29", "@types/node-fetch": "2.6.4", "@types/node-forge": "1.3.11", - "@types/node-ipc": "9.2.0", + "@types/node-ipc": "9.2.3", "@types/papaparse": "5.3.14", "@types/proper-lockfile": "4.1.4", "@types/react": "16.14.57", From 423d8c71b520a578169a49577779f2cc79ac710d Mon Sep 17 00:00:00 2001 From: watsondm <129207532+watsondm@users.noreply.github.com> Date: Wed, 24 Apr 2024 11:01:51 -0400 Subject: [PATCH 051/110] CLOUDOPS-1592 Remove artifacts R2 steps from desktop release workflows (#8897) * CLOUDOPS-1592 Remove artifacts R2 steps from desktop release workflows * CLOUDOPS-1592 Remove artifacts R2 steps from staged rollout workflow --- .github/workflows/release-desktop-beta.yml | 20 +---------- .github/workflows/release-desktop.yml | 21 +---------- .github/workflows/staged-rollout-desktop.yml | 37 ++++---------------- 3 files changed, 8 insertions(+), 70 deletions(-) diff --git a/.github/workflows/release-desktop-beta.yml b/.github/workflows/release-desktop-beta.yml index b9e2d7a8c85..46f4ffad57d 100644 --- a/.github/workflows/release-desktop-beta.yml +++ b/.github/workflows/release-desktop-beta.yml @@ -955,11 +955,7 @@ jobs: keyvault: "bitwarden-ci" secrets: "aws-electron-access-id, aws-electron-access-key, - aws-electron-bucket-name, - r2-electron-access-id, - r2-electron-access-key, - r2-electron-bucket-name, - cf-prod-account" + aws-electron-bucket-name" - name: Download all artifacts uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 @@ -985,20 +981,6 @@ jobs: --recursive \ --quiet - - name: Publish artifacts to R2 - env: - AWS_ACCESS_KEY_ID: ${{ steps.retrieve-secrets.outputs.r2-electron-access-id }} - AWS_SECRET_ACCESS_KEY: ${{ steps.retrieve-secrets.outputs.r2-electron-access-key }} - AWS_DEFAULT_REGION: 'us-east-1' - AWS_S3_BUCKET_NAME: ${{ steps.retrieve-secrets.outputs.r2-electron-bucket-name }} - CF_ACCOUNT: ${{ steps.retrieve-secrets.outputs.cf-prod-account }} - working-directory: apps/desktop/artifacts - run: | - aws s3 cp ./ $AWS_S3_BUCKET_NAME/desktop/ \ - --recursive \ - --quiet \ - --endpoint-url https://${CF_ACCOUNT}.r2.cloudflarestorage.com - - name: Update deployment status to Success if: ${{ success() }} uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index cf857d71772..dc6957d00d6 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -115,11 +115,7 @@ jobs: keyvault: "bitwarden-ci" secrets: "aws-electron-access-id, aws-electron-access-key, - aws-electron-bucket-name, - r2-electron-access-id, - r2-electron-access-key, - r2-electron-bucket-name, - cf-prod-account" + aws-electron-bucket-name" - name: Download all artifacts if: ${{ github.event.inputs.release_type != 'Dry Run' }} @@ -169,21 +165,6 @@ jobs: --recursive \ --quiet - - name: Publish artifacts to R2 - if: ${{ github.event.inputs.release_type != 'Dry Run' && github.event.inputs.electron_publish == 'true' }} - env: - AWS_ACCESS_KEY_ID: ${{ steps.retrieve-secrets.outputs.r2-electron-access-id }} - AWS_SECRET_ACCESS_KEY: ${{ steps.retrieve-secrets.outputs.r2-electron-access-key }} - AWS_DEFAULT_REGION: 'us-east-1' - AWS_S3_BUCKET_NAME: ${{ steps.retrieve-secrets.outputs.r2-electron-bucket-name }} - CF_ACCOUNT: ${{ steps.retrieve-secrets.outputs.cf-prod-account }} - working-directory: apps/desktop/artifacts - run: | - aws s3 cp ./ $AWS_S3_BUCKET_NAME/desktop/ \ - --recursive \ - --quiet \ - --endpoint-url https://${CF_ACCOUNT}.r2.cloudflarestorage.com - - name: Get checksum files uses: bitwarden/gh-actions/get-checksum@main with: diff --git a/.github/workflows/staged-rollout-desktop.yml b/.github/workflows/staged-rollout-desktop.yml index a5b5fc69b19..a6ca2f1e319 100644 --- a/.github/workflows/staged-rollout-desktop.yml +++ b/.github/workflows/staged-rollout-desktop.yml @@ -31,29 +31,21 @@ jobs: keyvault: "bitwarden-ci" secrets: "aws-electron-access-id, aws-electron-access-key, - aws-electron-bucket-name, - r2-electron-access-id, - r2-electron-access-key, - r2-electron-bucket-name, - cf-prod-account" + aws-electron-bucket-name" - - name: Download channel update info files from R2 + - name: Download channel update info files from S3 env: - AWS_ACCESS_KEY_ID: ${{ steps.retrieve-secrets.outputs.r2-electron-access-id }} - AWS_SECRET_ACCESS_KEY: ${{ steps.retrieve-secrets.outputs.r2-electron-access-key }} - AWS_DEFAULT_REGION: 'us-east-1' - AWS_S3_BUCKET_NAME: ${{ steps.retrieve-secrets.outputs.r2-electron-bucket-name }} - CF_ACCOUNT: ${{ steps.retrieve-secrets.outputs.cf-prod-account }} + AWS_ACCESS_KEY_ID: ${{ steps.retrieve-secrets.outputs.aws-electron-access-id }} + AWS_SECRET_ACCESS_KEY: ${{ steps.retrieve-secrets.outputs.aws-electron-access-key }} + AWS_DEFAULT_REGION: 'us-west-2' + AWS_S3_BUCKET_NAME: ${{ steps.retrieve-secrets.outputs.aws-electron-bucket-name }} run: | aws s3 cp $AWS_S3_BUCKET_NAME/desktop/latest.yml . \ --quiet \ - --endpoint-url https://${CF_ACCOUNT}.r2.cloudflarestorage.com aws s3 cp $AWS_S3_BUCKET_NAME/desktop/latest-linux.yml . \ --quiet \ - --endpoint-url https://${CF_ACCOUNT}.r2.cloudflarestorage.com aws s3 cp $AWS_S3_BUCKET_NAME/desktop/latest-mac.yml . \ --quiet \ - --endpoint-url https://${CF_ACCOUNT}.r2.cloudflarestorage.com - name: Check new rollout percentage env: @@ -95,20 +87,3 @@ jobs: aws s3 cp latest-mac.yml $AWS_S3_BUCKET_NAME/desktop/ \ --acl "public-read" - - - name: Publish channel update info files to R2 - env: - AWS_ACCESS_KEY_ID: ${{ steps.retrieve-secrets.outputs.r2-electron-access-id }} - AWS_SECRET_ACCESS_KEY: ${{ steps.retrieve-secrets.outputs.r2-electron-access-key }} - AWS_DEFAULT_REGION: 'us-east-1' - AWS_S3_BUCKET_NAME: ${{ steps.retrieve-secrets.outputs.r2-electron-bucket-name }} - CF_ACCOUNT: ${{ steps.retrieve-secrets.outputs.cf-prod-account }} - run: | - aws s3 cp latest.yml $AWS_S3_BUCKET_NAME/desktop/ \ - --endpoint-url https://${CF_ACCOUNT}.r2.cloudflarestorage.com - - aws s3 cp latest-linux.yml $AWS_S3_BUCKET_NAME/desktop/ \ - --endpoint-url https://${CF_ACCOUNT}.r2.cloudflarestorage.com - - aws s3 cp latest-mac.yml $AWS_S3_BUCKET_NAME/desktop/ \ - --endpoint-url https://${CF_ACCOUNT}.r2.cloudflarestorage.com From 493b79b8881b44b34c2a19ded009308960b16fac Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Wed, 24 Apr 2024 11:14:53 -0400 Subject: [PATCH 052/110] Only Run Migrations in True Background (#8548) * Only Run Migrations in True Background * Use `isPrivateMode` * Use `popupOnlyContext` --- apps/browser/src/background/main.background.ts | 3 ++- apps/browser/src/popup/services/init.service.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index a64ee2b8a00..6069e9b5e96 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1095,7 +1095,8 @@ export default class MainBackground { async bootstrap() { this.containerService.attachToGlobal(self); - await this.stateService.init({ runMigrations: !this.isPrivateMode }); + // Only the "true" background should run migrations + await this.stateService.init({ runMigrations: !this.popupOnlyContext }); // This is here instead of in 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 c9e6d66c2af..ee842565d75 100644 --- a/apps/browser/src/popup/services/init.service.ts +++ b/apps/browser/src/popup/services/init.service.ts @@ -22,7 +22,7 @@ export class InitService { init() { return async () => { - await this.stateService.init(); + await this.stateService.init({ runMigrations: false }); // Browser background is responsible for migrations await this.i18nService.init(); if (!BrowserPopupUtils.inPopup(window)) { From b7957d6e28f7826b042e41063c2d4642aa09ece5 Mon Sep 17 00:00:00 2001 From: Jake Fink Date: Wed, 24 Apr 2024 11:19:10 -0400 Subject: [PATCH 053/110] set keypair before creating hub connection for admin requests (#8898) --- .../src/auth/components/login-via-auth-request.component.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/angular/src/auth/components/login-via-auth-request.component.ts b/libs/angular/src/auth/components/login-via-auth-request.component.ts index 5a1180cd38c..3b827669a5c 100644 --- a/libs/angular/src/auth/components/login-via-auth-request.component.ts +++ b/libs/angular/src/auth/components/login-via-auth-request.component.ts @@ -221,7 +221,8 @@ export class LoginViaAuthRequestComponent } // Request still pending response from admin - // So, create hub connection so that any approvals will be received via push notification + // set keypair and create hub connection so that any approvals will be received via push notification + this.authRequestKeyPair = { privateKey: adminAuthReqStorable.privateKey, publicKey: null }; await this.anonymousHubService.createHubConnection(adminAuthReqStorable.id); } From 94fe9bd053b24211032430258416b4d7116b93f4 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Wed, 24 Apr 2024 11:20:13 -0400 Subject: [PATCH 054/110] Remove `StateService` `useAccountCache` (#8882) * Remove Account Cache from StateService * Remove Extra Change * Fix Desktop Build --- .../state-service.factory.ts | 2 - .../services/browser-state.service.spec.ts | 4 -- .../services/default-browser-state.service.ts | 40 ------------------- .../src/app/services/services.module.ts | 2 - apps/desktop/src/main.ts | 1 - apps/web/src/app/core/core.module.ts | 5 --- apps/web/src/app/core/state/state.service.ts | 3 -- libs/angular/src/services/injection-tokens.ts | 1 - .../src/services/jslib-services.module.ts | 6 --- .../src/platform/services/state.service.ts | 31 -------------- 10 files changed, 95 deletions(-) diff --git a/apps/browser/src/platform/background/service-factories/state-service.factory.ts b/apps/browser/src/platform/background/service-factories/state-service.factory.ts index 5567e009901..026a29668eb 100644 --- a/apps/browser/src/platform/background/service-factories/state-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/state-service.factory.ts @@ -30,7 +30,6 @@ import { type StateServiceFactoryOptions = FactoryOptions & { stateServiceOptions: { - useAccountCache?: boolean; stateFactory: StateFactory; }; }; @@ -64,7 +63,6 @@ export async function stateServiceFactory( await environmentServiceFactory(cache, opts), await tokenServiceFactory(cache, opts), await migrationRunnerFactory(cache, opts), - opts.stateServiceOptions.useAccountCache, ), ); // TODO: If we run migration through a chrome installed/updated event we can turn off running migrations diff --git a/apps/browser/src/platform/services/browser-state.service.spec.ts b/apps/browser/src/platform/services/browser-state.service.spec.ts index 8f43998321d..f06126dcf5c 100644 --- a/apps/browser/src/platform/services/browser-state.service.spec.ts +++ b/apps/browser/src/platform/services/browser-state.service.spec.ts @@ -27,7 +27,6 @@ describe("Browser State Service", () => { let diskStorageService: MockProxy; let logService: MockProxy; let stateFactory: MockProxy>; - let useAccountCache: boolean; let environmentService: MockProxy; let tokenService: MockProxy; let migrationRunner: MockProxy; @@ -46,8 +45,6 @@ describe("Browser State Service", () => { environmentService = mock(); tokenService = mock(); migrationRunner = mock(); - // turn off account cache for tests - useAccountCache = false; state = new State(new GlobalState()); state.accounts[userId] = new Account({ @@ -78,7 +75,6 @@ describe("Browser State Service", () => { environmentService, tokenService, migrationRunner, - useAccountCache, ); }); diff --git a/apps/browser/src/platform/services/default-browser-state.service.ts b/apps/browser/src/platform/services/default-browser-state.service.ts index f1f306dbc0d..b9cd2190764 100644 --- a/apps/browser/src/platform/services/default-browser-state.service.ts +++ b/apps/browser/src/platform/services/default-browser-state.service.ts @@ -15,7 +15,6 @@ import { MigrationRunner } from "@bitwarden/common/platform/services/migration-r import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service"; import { Account } from "../../models/account"; -import { BrowserApi } from "../browser/browser-api"; import { browserSession, sessionSync } from "../decorators/session-sync-observable"; import { BrowserStateService } from "./abstractions/browser-state.service"; @@ -45,7 +44,6 @@ export class DefaultBrowserStateService environmentService: EnvironmentService, tokenService: TokenService, migrationRunner: MigrationRunner, - useAccountCache = true, ) { super( storageService, @@ -57,45 +55,7 @@ export class DefaultBrowserStateService environmentService, tokenService, migrationRunner, - useAccountCache, ); - - // TODO: This is a hack to fix having a disk cache on both the popup and - // the background page that can get out of sync. We need to work out the - // best way to handle caching with multiple instances of the state service. - if (useAccountCache) { - BrowserApi.storageChangeListener((changes, namespace) => { - if (namespace === "local") { - for (const key of Object.keys(changes)) { - if (key !== "accountActivity" && this.accountDiskCache.value[key]) { - this.deleteDiskCache(key); - } - } - } - }); - - BrowserApi.addListener( - chrome.runtime.onMessage, - (message: { command: string }, _, respond) => { - if (message.command === "initializeDiskCache") { - respond(JSON.stringify(this.accountDiskCache.value)); - } - }, - ); - } - } - - override async initAccountState(): Promise { - if (this.isRecoveredSession && this.useAccountCache) { - // request cache initialization - - const response = await BrowserApi.sendMessageWithResponse("initializeDiskCache"); - this.accountDiskCache.next(JSON.parse(response)); - - return; - } - - await super.initAccountState(); } async addAccount(account: Account) { diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index d1d51c0f1c2..c15743ba5cd 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -4,7 +4,6 @@ import { Subject, merge } from "rxjs"; import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; import { SECURE_STORAGE, - STATE_SERVICE_USE_CACHE, LOCALES_DIRECTORY, SYSTEM_LANGUAGE, MEMORY_STORAGE, @@ -205,7 +204,6 @@ const safeProviders: SafeProvider[] = [ EnvironmentService, TokenService, MigrationRunner, - STATE_SERVICE_USE_CACHE, ], }), safeProvider({ diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index bffd2002ff1..da4c14b4aa6 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -205,7 +205,6 @@ export class Main { this.environmentService, this.tokenService, this.migrationRunner, - false, // Do not use disk caching because this will get out of sync with the renderer service ); this.desktopSettingsService = new DesktopSettingsService(stateProvider); diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index a2747647564..7a956500399 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -5,7 +5,6 @@ import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/sa import { SECURE_STORAGE, STATE_FACTORY, - STATE_SERVICE_USE_CACHE, LOCALES_DIRECTORY, SYSTEM_LANGUAGE, MEMORY_STORAGE, @@ -78,10 +77,6 @@ const safeProviders: SafeProvider[] = [ provide: STATE_FACTORY, useValue: new StateFactory(GlobalState, Account), }), - safeProvider({ - provide: STATE_SERVICE_USE_CACHE, - useValue: false, - }), safeProvider({ provide: I18nServiceAbstraction, useClass: I18nService, diff --git a/apps/web/src/app/core/state/state.service.ts b/apps/web/src/app/core/state/state.service.ts index 1ae62d8591d..185509e1502 100644 --- a/apps/web/src/app/core/state/state.service.ts +++ b/apps/web/src/app/core/state/state.service.ts @@ -4,7 +4,6 @@ import { MEMORY_STORAGE, SECURE_STORAGE, STATE_FACTORY, - STATE_SERVICE_USE_CACHE, } from "@bitwarden/angular/services/injection-tokens"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; @@ -34,7 +33,6 @@ export class StateService extends BaseStateService { environmentService: EnvironmentService, tokenService: TokenService, migrationRunner: MigrationRunner, - @Inject(STATE_SERVICE_USE_CACHE) useAccountCache = true, ) { super( storageService, @@ -46,7 +44,6 @@ export class StateService extends BaseStateService { environmentService, tokenService, migrationRunner, - useAccountCache, ); } diff --git a/libs/angular/src/services/injection-tokens.ts b/libs/angular/src/services/injection-tokens.ts index 6fffe722fbd..413fc5b5306 100644 --- a/libs/angular/src/services/injection-tokens.ts +++ b/libs/angular/src/services/injection-tokens.ts @@ -36,7 +36,6 @@ export const MEMORY_STORAGE = new SafeInjectionToken("SECURE_STORAGE"); export const STATE_FACTORY = new SafeInjectionToken("STATE_FACTORY"); -export const STATE_SERVICE_USE_CACHE = new SafeInjectionToken("STATE_SERVICE_USE_CACHE"); export const LOGOUT_CALLBACK = new SafeInjectionToken< (expired: boolean, userId?: string) => Promise >("LOGOUT_CALLBACK"); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index f31bcb1c513..a63f4f0f7dd 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -269,7 +269,6 @@ import { SafeInjectionToken, SECURE_STORAGE, STATE_FACTORY, - STATE_SERVICE_USE_CACHE, SUPPORTS_SECURE_STORAGE, SYSTEM_LANGUAGE, SYSTEM_THEME_OBSERVABLE, @@ -313,10 +312,6 @@ const safeProviders: SafeProvider[] = [ provide: STATE_FACTORY, useValue: new StateFactory(GlobalState, Account), }), - safeProvider({ - provide: STATE_SERVICE_USE_CACHE, - useValue: true, - }), safeProvider({ provide: LOGOUT_CALLBACK, useFactory: @@ -690,7 +685,6 @@ const safeProviders: SafeProvider[] = [ EnvironmentService, TokenServiceAbstraction, MigrationRunner, - STATE_SERVICE_USE_CACHE, ], }), safeProvider({ diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 412176e235e..8758f6d200f 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -65,8 +65,6 @@ export class StateService< private hasBeenInited = false; protected isRecoveredSession = false; - protected accountDiskCache = new BehaviorSubject>({}); - // default account serializer, must be overridden by child class protected accountDeserializer = Account.fromJSON as (json: Jsonify) => TAccount; @@ -80,7 +78,6 @@ export class StateService< protected environmentService: EnvironmentService, protected tokenService: TokenService, private migrationRunner: MigrationRunner, - protected useAccountCache: boolean = true, ) {} async init(initOptions: InitOptions = {}): Promise { @@ -995,13 +992,6 @@ export class StateService< return null; } - if (this.useAccountCache) { - const cachedAccount = this.accountDiskCache.value[options.userId]; - if (cachedAccount != null) { - return cachedAccount; - } - } - const account = options?.useSecureStorage ? (await this.secureStorageService.get(options.userId, options)) ?? (await this.storageService.get( @@ -1009,8 +999,6 @@ export class StateService< this.reconcileOptions(options, { htmlStorageLocation: HtmlStorageLocation.Local }), )) : await this.storageService.get(options.userId, options); - - this.setDiskCache(options.userId, account); return account; } @@ -1040,8 +1028,6 @@ export class StateService< : this.storageService; await storageLocation.save(`${options.userId}`, account, options); - - this.deleteDiskCache(options.userId); } protected async saveAccountToMemory(account: TAccount): Promise { @@ -1241,9 +1227,6 @@ export class StateService< await this.updateState(async (state) => { userId = userId ?? state.activeUserId; delete state.accounts[userId]; - - this.deleteDiskCache(userId); - return state; }); } @@ -1357,20 +1340,6 @@ export class StateService< return await this.setState(updatedState); }); } - - private setDiskCache(key: string, value: TAccount, options?: StorageOptions) { - if (this.useAccountCache) { - this.accountDiskCache.value[key] = value; - this.accountDiskCache.next(this.accountDiskCache.value); - } - } - - protected deleteDiskCache(key: string) { - if (this.useAccountCache) { - delete this.accountDiskCache.value[key]; - this.accountDiskCache.next(this.accountDiskCache.value); - } - } } function withPrototypeForArrayMembers( From a12c140792c708073623356b45f0dfef58566cb0 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Wed, 24 Apr 2024 12:37:19 -0400 Subject: [PATCH 055/110] =?UTF-8?q?Revert=20"Revert=20"Auth/PM-6689=20-=20?= =?UTF-8?q?Migrate=20Security=20Stamp=20to=20Token=20Service=20and=20St?= =?UTF-8?q?=E2=80=A6"=20(#8889)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 100b43dd8f7ac23cb888b0f031353aa68beffb82. --- .../browser/src/background/main.background.ts | 1 + apps/cli/src/bw.ts | 1 + .../src/services/jslib-services.module.ts | 1 + .../login-strategies/login.strategy.spec.ts | 4 - .../common/login-strategies/login.strategy.ts | 9 +-- .../src/auth/abstractions/token.service.ts | 6 ++ .../src/auth/services/token.service.spec.ts | 79 +++++++++++++++++++ .../common/src/auth/services/token.service.ts | 25 ++++++ .../src/auth/services/token.state.spec.ts | 2 + libs/common/src/auth/services/token.state.ts | 5 ++ .../platform/abstractions/state.service.ts | 2 - .../models/domain/account-tokens.spec.ts | 9 --- .../platform/models/domain/account.spec.ts | 4 +- .../src/platform/models/domain/account.ts | 18 ----- .../src/platform/services/state.service.ts | 17 ---- .../src/vault/services/sync/sync.service.ts | 6 +- 16 files changed, 126 insertions(+), 63 deletions(-) delete mode 100644 libs/common/src/platform/models/domain/account-tokens.spec.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 6069e9b5e96..bee102be46a 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -813,6 +813,7 @@ export default class MainBackground { this.avatarService, logoutCallback, this.billingAccountProfileStateService, + this.tokenService, ); this.eventUploadService = new EventUploadService( this.apiService, diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 58329128b8a..437f807bc61 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -631,6 +631,7 @@ export class Main { this.avatarService, async (expired: boolean) => await this.logout(), this.billingAccountProfileStateService, + this.tokenService, ); this.totpService = new TotpService(this.cryptoFunctionService, this.logService); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index a63f4f0f7dd..45f11befa68 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -623,6 +623,7 @@ const safeProviders: SafeProvider[] = [ AvatarServiceAbstraction, LOGOUT_CALLBACK, BillingAccountProfileStateService, + TokenServiceAbstraction, ], }), safeProvider({ diff --git a/libs/auth/src/common/login-strategies/login.strategy.spec.ts b/libs/auth/src/common/login-strategies/login.strategy.spec.ts index 431f736e949..e0833342ce3 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -27,7 +27,6 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Account, AccountProfile, - AccountTokens, AccountKeys, } from "@bitwarden/common/platform/models/domain/account"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; @@ -213,9 +212,6 @@ describe("LoginStrategy", () => { kdfType: kdf, }, }, - tokens: { - ...new AccountTokens(), - }, keys: new AccountKeys(), }), ); diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index a6dc1931839..a73c32e1208 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -27,11 +27,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { - Account, - AccountProfile, - AccountTokens, -} from "@bitwarden/common/platform/models/domain/account"; +import { Account, AccountProfile } from "@bitwarden/common/platform/models/domain/account"; import { UserId } from "@bitwarden/common/types/guid"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; @@ -192,9 +188,6 @@ export abstract class LoginStrategy { kdfType: tokenResponse.kdf, }, }, - tokens: { - ...new AccountTokens(), - }, }), ); diff --git a/libs/common/src/auth/abstractions/token.service.ts b/libs/common/src/auth/abstractions/token.service.ts index 75bb3838828..fc3bd317f47 100644 --- a/libs/common/src/auth/abstractions/token.service.ts +++ b/libs/common/src/auth/abstractions/token.service.ts @@ -213,4 +213,10 @@ export abstract class TokenService { * @returns A promise that resolves with a boolean representing the user's external authN status. */ getIsExternal: () => Promise; + + /** Gets the active or passed in user's security stamp */ + getSecurityStamp: (userId?: UserId) => Promise; + + /** Sets the security stamp for the active or passed in user */ + setSecurityStamp: (securityStamp: string, userId?: UserId) => Promise; } diff --git a/libs/common/src/auth/services/token.service.spec.ts b/libs/common/src/auth/services/token.service.spec.ts index d32c4d8e1cd..3e92053d2f7 100644 --- a/libs/common/src/auth/services/token.service.spec.ts +++ b/libs/common/src/auth/services/token.service.spec.ts @@ -23,6 +23,7 @@ import { EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, REFRESH_TOKEN_DISK, REFRESH_TOKEN_MEMORY, + SECURITY_STAMP_MEMORY, } from "./token.state"; describe("TokenService", () => { @@ -2191,6 +2192,84 @@ describe("TokenService", () => { }); }); + describe("Security Stamp methods", () => { + const mockSecurityStamp = "securityStamp"; + + describe("setSecurityStamp", () => { + it("should throw an error if no user id is provided and there is no active user in global state", async () => { + // Act + // note: don't await here because we want to test the error + const result = tokenService.setSecurityStamp(mockSecurityStamp); + // Assert + await expect(result).rejects.toThrow("User id not found. Cannot set security stamp."); + }); + + it("should set the security stamp in memory when there is an active user in global state", async () => { + // Arrange + globalStateProvider + .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) + .stateSubject.next(userIdFromAccessToken); + + // Act + await tokenService.setSecurityStamp(mockSecurityStamp); + + // Assert + expect( + singleUserStateProvider.getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY).nextMock, + ).toHaveBeenCalledWith(mockSecurityStamp); + }); + + it("should set the security stamp in memory for the specified user id", async () => { + // Act + await tokenService.setSecurityStamp(mockSecurityStamp, userIdFromAccessToken); + + // Assert + expect( + singleUserStateProvider.getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY).nextMock, + ).toHaveBeenCalledWith(mockSecurityStamp); + }); + }); + + describe("getSecurityStamp", () => { + it("should throw an error if no user id is provided and there is no active user in global state", async () => { + // Act + // note: don't await here because we want to test the error + const result = tokenService.getSecurityStamp(); + // Assert + await expect(result).rejects.toThrow("User id not found. Cannot get security stamp."); + }); + + it("should return the security stamp from memory with no user id specified (uses global active user)", async () => { + // Arrange + globalStateProvider + .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) + .stateSubject.next(userIdFromAccessToken); + + singleUserStateProvider + .getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY) + .stateSubject.next([userIdFromAccessToken, mockSecurityStamp]); + + // Act + const result = await tokenService.getSecurityStamp(); + + // Assert + expect(result).toEqual(mockSecurityStamp); + }); + + it("should return the security stamp from memory for the specified user id", async () => { + // Arrange + singleUserStateProvider + .getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY) + .stateSubject.next([userIdFromAccessToken, mockSecurityStamp]); + + // Act + const result = await tokenService.getSecurityStamp(userIdFromAccessToken); + // Assert + expect(result).toEqual(mockSecurityStamp); + }); + }); + }); + // Helpers function createTokenService(supportsSecureStorage: boolean) { return new TokenService( diff --git a/libs/common/src/auth/services/token.service.ts b/libs/common/src/auth/services/token.service.ts index c24a2c186b8..40036a8453c 100644 --- a/libs/common/src/auth/services/token.service.ts +++ b/libs/common/src/auth/services/token.service.ts @@ -32,6 +32,7 @@ import { EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, REFRESH_TOKEN_DISK, REFRESH_TOKEN_MEMORY, + SECURITY_STAMP_MEMORY, } from "./token.state"; export enum TokenStorageLocation { @@ -850,6 +851,30 @@ export class TokenService implements TokenServiceAbstraction { return Array.isArray(decoded.amr) && decoded.amr.includes("external"); } + async getSecurityStamp(userId?: UserId): Promise { + userId ??= await firstValueFrom(this.activeUserIdGlobalState.state$); + + if (!userId) { + throw new Error("User id not found. Cannot get security stamp."); + } + + const securityStamp = await this.getStateValueByUserIdAndKeyDef(userId, SECURITY_STAMP_MEMORY); + + return securityStamp; + } + + async setSecurityStamp(securityStamp: string, userId?: UserId): Promise { + userId ??= await firstValueFrom(this.activeUserIdGlobalState.state$); + + if (!userId) { + throw new Error("User id not found. Cannot set security stamp."); + } + + await this.singleUserStateProvider + .get(userId, SECURITY_STAMP_MEMORY) + .update((_) => securityStamp); + } + private async getStateValueByUserIdAndKeyDef( userId: UserId, storageLocation: UserKeyDefinition, diff --git a/libs/common/src/auth/services/token.state.spec.ts b/libs/common/src/auth/services/token.state.spec.ts index dc00fec383c..bb82410fac1 100644 --- a/libs/common/src/auth/services/token.state.spec.ts +++ b/libs/common/src/auth/services/token.state.spec.ts @@ -10,6 +10,7 @@ import { EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, REFRESH_TOKEN_DISK, REFRESH_TOKEN_MEMORY, + SECURITY_STAMP_MEMORY, } from "./token.state"; describe.each([ @@ -22,6 +23,7 @@ describe.each([ [API_KEY_CLIENT_ID_MEMORY, "apiKeyClientIdMemory"], [API_KEY_CLIENT_SECRET_DISK, "apiKeyClientSecretDisk"], [API_KEY_CLIENT_SECRET_MEMORY, "apiKeyClientSecretMemory"], + [SECURITY_STAMP_MEMORY, "securityStamp"], ])( "deserializes state key definitions", ( diff --git a/libs/common/src/auth/services/token.state.ts b/libs/common/src/auth/services/token.state.ts index 458d6846c17..57d85f2a559 100644 --- a/libs/common/src/auth/services/token.state.ts +++ b/libs/common/src/auth/services/token.state.ts @@ -69,3 +69,8 @@ export const API_KEY_CLIENT_SECRET_MEMORY = new UserKeyDefinition( clearOn: [], // Manually handled }, ); + +export const SECURITY_STAMP_MEMORY = new UserKeyDefinition(TOKEN_MEMORY, "securityStamp", { + deserializer: (securityStamp) => securityStamp, + clearOn: ["logout"], +}); diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index 051604f0ae5..f1d4b3848ef 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -181,8 +181,6 @@ export abstract class StateService { * Sets the user's Pin, encrypted by the user key */ setProtectedPin: (value: string, options?: StorageOptions) => Promise; - getSecurityStamp: (options?: StorageOptions) => Promise; - setSecurityStamp: (value: string, options?: StorageOptions) => Promise; getUserId: (options?: StorageOptions) => Promise; getVaultTimeout: (options?: StorageOptions) => Promise; setVaultTimeout: (value: number, options?: StorageOptions) => Promise; diff --git a/libs/common/src/platform/models/domain/account-tokens.spec.ts b/libs/common/src/platform/models/domain/account-tokens.spec.ts deleted file mode 100644 index 733b3908e9a..00000000000 --- a/libs/common/src/platform/models/domain/account-tokens.spec.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { AccountTokens } from "./account"; - -describe("AccountTokens", () => { - describe("fromJSON", () => { - it("should deserialize to an instance of itself", () => { - expect(AccountTokens.fromJSON({})).toBeInstanceOf(AccountTokens); - }); - }); -}); diff --git a/libs/common/src/platform/models/domain/account.spec.ts b/libs/common/src/platform/models/domain/account.spec.ts index 0c76c16cc2d..77c242b6ff5 100644 --- a/libs/common/src/platform/models/domain/account.spec.ts +++ b/libs/common/src/platform/models/domain/account.spec.ts @@ -1,4 +1,4 @@ -import { Account, AccountKeys, AccountProfile, AccountSettings, AccountTokens } from "./account"; +import { Account, AccountKeys, AccountProfile, AccountSettings } from "./account"; describe("Account", () => { describe("fromJSON", () => { @@ -10,14 +10,12 @@ describe("Account", () => { const keysSpy = jest.spyOn(AccountKeys, "fromJSON"); const profileSpy = jest.spyOn(AccountProfile, "fromJSON"); const settingsSpy = jest.spyOn(AccountSettings, "fromJSON"); - const tokensSpy = jest.spyOn(AccountTokens, "fromJSON"); Account.fromJSON({}); expect(keysSpy).toHaveBeenCalled(); expect(profileSpy).toHaveBeenCalled(); expect(settingsSpy).toHaveBeenCalled(); - expect(tokensSpy).toHaveBeenCalled(); }); }); }); diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index 5a9a7646962..cd416ec1f94 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -171,24 +171,11 @@ export class AccountSettings { } } -export class AccountTokens { - securityStamp?: string; - - static fromJSON(obj: Jsonify): AccountTokens { - if (obj == null) { - return null; - } - - return Object.assign(new AccountTokens(), obj); - } -} - export class Account { data?: AccountData = new AccountData(); keys?: AccountKeys = new AccountKeys(); profile?: AccountProfile = new AccountProfile(); settings?: AccountSettings = new AccountSettings(); - tokens?: AccountTokens = new AccountTokens(); constructor(init: Partial) { Object.assign(this, { @@ -208,10 +195,6 @@ export class Account { ...new AccountSettings(), ...init?.settings, }, - tokens: { - ...new AccountTokens(), - ...init?.tokens, - }, }); } @@ -225,7 +208,6 @@ export class Account { data: AccountData.fromJSON(json?.data), profile: AccountProfile.fromJSON(json?.profile), settings: AccountSettings.fromJSON(json?.settings), - tokens: AccountTokens.fromJSON(json?.tokens), }); } } diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 8758f6d200f..0c7cdd22d29 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -841,23 +841,6 @@ export class StateService< ); } - async getSecurityStamp(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) - )?.tokens?.securityStamp; - } - - async setSecurityStamp(value: string, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - account.tokens.securityStamp = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - } - async getUserId(options?: StorageOptions): Promise { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) diff --git a/libs/common/src/vault/services/sync/sync.service.ts b/libs/common/src/vault/services/sync/sync.service.ts index ff8e9f1f4f5..73869ff488e 100644 --- a/libs/common/src/vault/services/sync/sync.service.ts +++ b/libs/common/src/vault/services/sync/sync.service.ts @@ -15,6 +15,7 @@ import { AccountService } from "../../../auth/abstractions/account.service"; import { AvatarService } from "../../../auth/abstractions/avatar.service"; import { KeyConnectorService } from "../../../auth/abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "../../../auth/abstractions/master-password.service.abstraction"; +import { TokenService } from "../../../auth/abstractions/token.service"; import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason"; import { DomainSettingsService } from "../../../autofill/services/domain-settings.service"; import { BillingAccountProfileStateService } from "../../../billing/abstractions/account/billing-account-profile-state.service"; @@ -73,6 +74,7 @@ export class SyncService implements SyncServiceAbstraction { private avatarService: AvatarService, private logoutCallback: (expired: boolean) => Promise, private billingAccountProfileStateService: BillingAccountProfileStateService, + private tokenService: TokenService, ) {} async getLastSync(): Promise { @@ -309,7 +311,7 @@ export class SyncService implements SyncServiceAbstraction { } private async syncProfile(response: ProfileResponse) { - const stamp = await this.stateService.getSecurityStamp(); + const stamp = await this.tokenService.getSecurityStamp(response.id as UserId); if (stamp != null && stamp !== response.securityStamp) { if (this.logoutCallback != null) { await this.logoutCallback(true); @@ -323,7 +325,7 @@ export class SyncService implements SyncServiceAbstraction { await this.cryptoService.setProviderKeys(response.providers); await this.cryptoService.setOrgKeys(response.organizations, response.providerOrganizations); await this.avatarService.setSyncAvatarColor(response.id as UserId, response.avatarColor); - await this.stateService.setSecurityStamp(response.securityStamp); + await this.tokenService.setSecurityStamp(response.securityStamp, response.id as UserId); await this.stateService.setEmailVerified(response.emailVerified); await this.billingAccountProfileStateService.setHasPremium( From 5dc83cd34c9a5e93f1fce7a614af6af11b3e383e Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Wed, 24 Apr 2024 12:54:54 -0400 Subject: [PATCH 056/110] PM-6787 - Rename DeviceTrustCryptoService to DeviceTrustService (#8819) --- ...ory.ts => device-trust-service.factory.ts} | 20 ++-- .../login-strategy-service.factory.ts | 10 +- apps/browser/src/auth/popup/lock.component.ts | 6 +- .../popup/login-via-auth-request.component.ts | 6 +- .../browser/src/background/main.background.ts | 10 +- .../src/popup/services/services.module.ts | 6 +- apps/cli/src/bw.ts | 10 +- apps/desktop/src/auth/lock.component.spec.ts | 6 +- apps/desktop/src/auth/lock.component.ts | 6 +- .../login/login-via-auth-request.component.ts | 6 +- .../user-key-rotation.service.spec.ts | 8 +- .../key-rotation/user-key-rotation.service.ts | 6 +- ...base-login-decryption-options.component.ts | 12 +- .../src/auth/components/lock.component.ts | 6 +- .../login-via-auth-request.component.ts | 6 +- libs/angular/src/auth/guards/lock.guard.ts | 6 +- .../angular/src/auth/guards/redirect.guard.ts | 6 +- .../guards/tde-decryption-required.guard.ts | 6 +- .../src/services/jslib-services.module.ts | 10 +- .../auth-request-login.strategy.spec.ts | 12 +- .../auth-request-login.strategy.ts | 6 +- .../sso-login.strategy.spec.ts | 38 +++--- .../login-strategies/sso-login.strategy.ts | 10 +- .../login-strategy.service.spec.ts | 8 +- .../login-strategy.service.ts | 8 +- ...ts => device-trust.service.abstraction.ts} | 5 +- ...=> device-trust.service.implementation.ts} | 4 +- ...e.spec.ts => device-trust.service.spec.ts} | 111 ++++++++---------- libs/common/src/state-migrations/migrate.ts | 4 +- ...vice-trust-svc-to-state-providers.spec.ts} | 12 +- ...te-device-trust-svc-to-state-providers.ts} | 4 +- ...resh-token-migrated-state-provider-flag.ts | 2 +- 32 files changed, 182 insertions(+), 194 deletions(-) rename apps/browser/src/auth/background/service-factories/{device-trust-crypto-service.factory.ts => device-trust-service.factory.ts} (79%) rename libs/common/src/auth/abstractions/{device-trust-crypto.service.abstraction.ts => device-trust.service.abstraction.ts} (89%) rename libs/common/src/auth/services/{device-trust-crypto.service.implementation.ts => device-trust.service.implementation.ts} (98%) rename libs/common/src/auth/services/{device-trust-crypto.service.spec.ts => device-trust.service.spec.ts} (86%) rename libs/common/src/state-migrations/migrations/{53-migrate-device-trust-crypto-svc-to-state-providers.spec.ts => 53-migrate-device-trust-svc-to-state-providers.spec.ts} (92%) rename libs/common/src/state-migrations/migrations/{53-migrate-device-trust-crypto-svc-to-state-providers.ts => 53-migrate-device-trust-svc-to-state-providers.ts} (94%) diff --git a/apps/browser/src/auth/background/service-factories/device-trust-crypto-service.factory.ts b/apps/browser/src/auth/background/service-factories/device-trust-service.factory.ts similarity index 79% rename from apps/browser/src/auth/background/service-factories/device-trust-crypto-service.factory.ts rename to apps/browser/src/auth/background/service-factories/device-trust-service.factory.ts index cac6f9bbe8a..106bcbcf72d 100644 --- a/apps/browser/src/auth/background/service-factories/device-trust-crypto-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/device-trust-service.factory.ts @@ -1,5 +1,5 @@ -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; -import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device-trust-crypto.service.implementation"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { DeviceTrustService } from "@bitwarden/common/auth/services/device-trust.service.implementation"; import { DevicesApiServiceInitOptions, @@ -52,9 +52,9 @@ import { userDecryptionOptionsServiceFactory, } from "./user-decryption-options-service.factory"; -type DeviceTrustCryptoServiceFactoryOptions = FactoryOptions; +type DeviceTrustServiceFactoryOptions = FactoryOptions; -export type DeviceTrustCryptoServiceInitOptions = DeviceTrustCryptoServiceFactoryOptions & +export type DeviceTrustServiceInitOptions = DeviceTrustServiceFactoryOptions & KeyGenerationServiceInitOptions & CryptoFunctionServiceInitOptions & CryptoServiceInitOptions & @@ -67,16 +67,16 @@ export type DeviceTrustCryptoServiceInitOptions = DeviceTrustCryptoServiceFactor SecureStorageServiceInitOptions & UserDecryptionOptionsServiceInitOptions; -export function deviceTrustCryptoServiceFactory( - cache: { deviceTrustCryptoService?: DeviceTrustCryptoServiceAbstraction } & CachedServices, - opts: DeviceTrustCryptoServiceInitOptions, -): Promise { +export function deviceTrustServiceFactory( + cache: { deviceTrustService?: DeviceTrustServiceAbstraction } & CachedServices, + opts: DeviceTrustServiceInitOptions, +): Promise { return factory( cache, - "deviceTrustCryptoService", + "deviceTrustService", opts, async () => - new DeviceTrustCryptoService( + new DeviceTrustService( await keyGenerationServiceFactory(cache, opts), await cryptoFunctionServiceFactory(cache, opts), await cryptoServiceFactory(cache, opts), diff --git a/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts b/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts index f184072cce5..075ba614b7a 100644 --- a/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts @@ -65,9 +65,9 @@ import { AuthRequestServiceInitOptions, } from "./auth-request-service.factory"; import { - deviceTrustCryptoServiceFactory, - DeviceTrustCryptoServiceInitOptions, -} from "./device-trust-crypto-service.factory"; + deviceTrustServiceFactory, + DeviceTrustServiceInitOptions, +} from "./device-trust-service.factory"; import { keyConnectorServiceFactory, KeyConnectorServiceInitOptions, @@ -102,7 +102,7 @@ export type LoginStrategyServiceInitOptions = LoginStrategyServiceFactoryOptions EncryptServiceInitOptions & PolicyServiceInitOptions & PasswordStrengthServiceInitOptions & - DeviceTrustCryptoServiceInitOptions & + DeviceTrustServiceInitOptions & AuthRequestServiceInitOptions & UserDecryptionOptionsServiceInitOptions & GlobalStateProviderInitOptions & @@ -135,7 +135,7 @@ export function loginStrategyServiceFactory( await encryptServiceFactory(cache, opts), await passwordStrengthServiceFactory(cache, opts), await policyServiceFactory(cache, opts), - await deviceTrustCryptoServiceFactory(cache, opts), + await deviceTrustServiceFactory(cache, opts), await authRequestServiceFactory(cache, opts), await internalUserDecryptionOptionServiceFactory(cache, opts), await globalStateProviderFactory(cache, opts), diff --git a/apps/browser/src/auth/popup/lock.component.ts b/apps/browser/src/auth/popup/lock.component.ts index 16c32337cf5..78039d793f5 100644 --- a/apps/browser/src/auth/popup/lock.component.ts +++ b/apps/browser/src/auth/popup/lock.component.ts @@ -11,7 +11,7 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; @@ -60,7 +60,7 @@ export class LockComponent extends BaseLockComponent { passwordStrengthService: PasswordStrengthServiceAbstraction, private authService: AuthService, dialogService: DialogService, - deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, + deviceTrustService: DeviceTrustServiceAbstraction, userVerificationService: UserVerificationService, pinCryptoService: PinCryptoServiceAbstraction, private routerService: BrowserRouterService, @@ -85,7 +85,7 @@ export class LockComponent extends BaseLockComponent { policyService, passwordStrengthService, dialogService, - deviceTrustCryptoService, + deviceTrustService, userVerificationService, pinCryptoService, biometricStateService, diff --git a/apps/browser/src/auth/popup/login-via-auth-request.component.ts b/apps/browser/src/auth/popup/login-via-auth-request.component.ts index 52f311ce7b7..158296058e5 100644 --- a/apps/browser/src/auth/popup/login-via-auth-request.component.ts +++ b/apps/browser/src/auth/popup/login-via-auth-request.component.ts @@ -12,7 +12,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AnonymousHubService } from "@bitwarden/common/auth/abstractions/anonymous-hub.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -47,7 +47,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { stateService: StateService, loginEmailService: LoginEmailServiceAbstraction, syncService: SyncService, - deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, + deviceTrustService: DeviceTrustServiceAbstraction, authRequestService: AuthRequestServiceAbstraction, loginStrategyService: LoginStrategyServiceAbstraction, accountService: AccountService, @@ -69,7 +69,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { validationService, stateService, loginEmailService, - deviceTrustCryptoService, + deviceTrustService, authRequestService, loginStrategyService, accountService, diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index bee102be46a..dc93de7803d 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -30,7 +30,7 @@ import { ProviderService } from "@bitwarden/common/admin-console/services/provid import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service"; import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/auth/abstractions/key-connector.service"; @@ -45,7 +45,7 @@ import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/for import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { AvatarService } from "@bitwarden/common/auth/services/avatar.service"; -import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device-trust-crypto.service.implementation"; +import { DeviceTrustService } from "@bitwarden/common/auth/services/device-trust.service.implementation"; import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation"; import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation"; import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service"; @@ -318,7 +318,7 @@ export default class MainBackground { configApiService: ConfigApiServiceAbstraction; devicesApiService: DevicesApiServiceAbstraction; devicesService: DevicesServiceAbstraction; - deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction; + deviceTrustService: DeviceTrustServiceAbstraction; authRequestService: AuthRequestServiceAbstraction; accountService: AccountServiceAbstraction; globalStateProvider: GlobalStateProvider; @@ -612,7 +612,7 @@ export default class MainBackground { this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); this.devicesApiService = new DevicesApiServiceImplementation(this.apiService); - this.deviceTrustCryptoService = new DeviceTrustCryptoService( + this.deviceTrustService = new DeviceTrustService( this.keyGenerationService, this.cryptoFunctionService, this.cryptoService, @@ -670,7 +670,7 @@ export default class MainBackground { this.encryptService, this.passwordStrengthService, this.policyService, - this.deviceTrustCryptoService, + this.deviceTrustService, this.authRequestService, this.userDecryptionOptionsService, this.globalStateProvider, diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index a7da6b76127..38068d18495 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -28,7 +28,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; @@ -250,8 +250,8 @@ const safeProviders: SafeProvider[] = [ deps: [], }), safeProvider({ - provide: DeviceTrustCryptoServiceAbstraction, - useFactory: getBgService("deviceTrustCryptoService"), + provide: DeviceTrustServiceAbstraction, + useFactory: getBgService("deviceTrustService"), deps: [], }), safeProvider({ diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 437f807bc61..8163aa29452 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -28,13 +28,13 @@ import { ProviderApiService } from "@bitwarden/common/admin-console/services/pro import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { AvatarService } from "@bitwarden/common/auth/services/avatar.service"; -import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device-trust-crypto.service.implementation"; +import { DeviceTrustService } from "@bitwarden/common/auth/services/device-trust.service.implementation"; import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation"; import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service"; import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service"; @@ -217,7 +217,7 @@ export class Main { syncNotifierService: SyncNotifierService; sendApiService: SendApiService; devicesApiService: DevicesApiServiceAbstraction; - deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction; + deviceTrustService: DeviceTrustServiceAbstraction; authRequestService: AuthRequestService; configApiService: ConfigApiServiceAbstraction; configService: ConfigService; @@ -460,7 +460,7 @@ export class Main { this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); this.devicesApiService = new DevicesApiServiceImplementation(this.apiService); - this.deviceTrustCryptoService = new DeviceTrustCryptoService( + this.deviceTrustService = new DeviceTrustService( this.keyGenerationService, this.cryptoFunctionService, this.cryptoService, @@ -505,7 +505,7 @@ export class Main { this.encryptService, this.passwordStrengthService, this.policyService, - this.deviceTrustCryptoService, + this.deviceTrustService, this.authRequestService, this.userDecryptionOptionsService, this.globalStateProvider, diff --git a/apps/desktop/src/auth/lock.component.spec.ts b/apps/desktop/src/auth/lock.component.spec.ts index c125eba022f..480e443eab1 100644 --- a/apps/desktop/src/auth/lock.component.spec.ts +++ b/apps/desktop/src/auth/lock.component.spec.ts @@ -13,7 +13,7 @@ import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeou import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; @@ -145,8 +145,8 @@ describe("LockComponent", () => { useValue: mock(), }, { - provide: DeviceTrustCryptoServiceAbstraction, - useValue: mock(), + provide: DeviceTrustServiceAbstraction, + useValue: mock(), }, { provide: UserVerificationService, diff --git a/apps/desktop/src/auth/lock.component.ts b/apps/desktop/src/auth/lock.component.ts index 16b58c5bbed..b8feef4ab52 100644 --- a/apps/desktop/src/auth/lock.component.ts +++ b/apps/desktop/src/auth/lock.component.ts @@ -10,7 +10,7 @@ import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeou import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { DeviceType } from "@bitwarden/common/enums"; @@ -58,7 +58,7 @@ export class LockComponent extends BaseLockComponent { passwordStrengthService: PasswordStrengthServiceAbstraction, logService: LogService, dialogService: DialogService, - deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, + deviceTrustService: DeviceTrustServiceAbstraction, userVerificationService: UserVerificationService, pinCryptoService: PinCryptoServiceAbstraction, biometricStateService: BiometricStateService, @@ -82,7 +82,7 @@ export class LockComponent extends BaseLockComponent { policyService, passwordStrengthService, dialogService, - deviceTrustCryptoService, + deviceTrustService, userVerificationService, pinCryptoService, biometricStateService, diff --git a/apps/desktop/src/auth/login/login-via-auth-request.component.ts b/apps/desktop/src/auth/login/login-via-auth-request.component.ts index 0a339030ba2..2d0f560205b 100644 --- a/apps/desktop/src/auth/login/login-via-auth-request.component.ts +++ b/apps/desktop/src/auth/login/login-via-auth-request.component.ts @@ -13,7 +13,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AnonymousHubService } from "@bitwarden/common/auth/abstractions/anonymous-hub.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -55,7 +55,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { syncService: SyncService, stateService: StateService, loginEmailService: LoginEmailServiceAbstraction, - deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, + deviceTrustService: DeviceTrustServiceAbstraction, authRequestService: AuthRequestServiceAbstraction, loginStrategyService: LoginStrategyServiceAbstraction, accountService: AccountService, @@ -77,7 +77,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { validationService, stateService, loginEmailService, - deviceTrustCryptoService, + deviceTrustService, authRequestService, loginStrategyService, accountService, diff --git a/apps/web/src/app/auth/key-rotation/user-key-rotation.service.spec.ts b/apps/web/src/app/auth/key-rotation/user-key-rotation.service.spec.ts index 0997f188641..ed665fe7732 100644 --- a/apps/web/src/app/auth/key-rotation/user-key-rotation.service.spec.ts +++ b/apps/web/src/app/auth/key-rotation/user-key-rotation.service.spec.ts @@ -1,7 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -42,7 +42,7 @@ describe("KeyRotationService", () => { let mockSendService: MockProxy; let mockEmergencyAccessService: MockProxy; let mockResetPasswordService: MockProxy; - let mockDeviceTrustCryptoService: MockProxy; + let mockDeviceTrustService: MockProxy; let mockCryptoService: MockProxy; let mockEncryptService: MockProxy; let mockStateService: MockProxy; @@ -60,7 +60,7 @@ describe("KeyRotationService", () => { mockSendService = mock(); mockEmergencyAccessService = mock(); mockResetPasswordService = mock(); - mockDeviceTrustCryptoService = mock(); + mockDeviceTrustService = mock(); mockCryptoService = mock(); mockEncryptService = mock(); mockStateService = mock(); @@ -74,7 +74,7 @@ describe("KeyRotationService", () => { mockSendService, mockEmergencyAccessService, mockResetPasswordService, - mockDeviceTrustCryptoService, + mockDeviceTrustService, mockCryptoService, mockEncryptService, mockStateService, diff --git a/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts b/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts index f5812d341a5..2ff48809a07 100644 --- a/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts +++ b/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts @@ -2,7 +2,7 @@ import { Injectable } from "@angular/core"; import { firstValueFrom } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -33,7 +33,7 @@ export class UserKeyRotationService { private sendService: SendService, private emergencyAccessService: EmergencyAccessService, private resetPasswordService: OrganizationUserResetPasswordService, - private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, + private deviceTrustService: DeviceTrustServiceAbstraction, private cryptoService: CryptoService, private encryptService: EncryptService, private stateService: StateService, @@ -96,7 +96,7 @@ export class UserKeyRotationService { } const activeAccount = await firstValueFrom(this.accountService.activeAccount$); - await this.deviceTrustCryptoService.rotateDevicesTrust( + await this.deviceTrustService.rotateDevicesTrust( activeAccount.id, newUserKey, masterPasswordHash, diff --git a/libs/angular/src/auth/components/base-login-decryption-options.component.ts b/libs/angular/src/auth/components/base-login-decryption-options.component.ts index 8345bb99396..0e58c03a54d 100644 --- a/libs/angular/src/auth/components/base-login-decryption-options.component.ts +++ b/libs/angular/src/auth/components/base-login-decryption-options.component.ts @@ -23,7 +23,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { PasswordResetEnrollmentServiceAbstraction } from "@bitwarden/common/auth/abstractions/password-reset-enrollment.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; @@ -93,7 +93,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { protected apiService: ApiService, protected i18nService: I18nService, protected validationService: ValidationService, - protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, + protected deviceTrustService: DeviceTrustServiceAbstraction, protected platformUtilsService: PlatformUtilsService, protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, protected passwordResetEnrollmentService: PasswordResetEnrollmentServiceAbstraction, @@ -156,7 +156,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { } private async setRememberDeviceDefaultValue() { - const rememberDeviceFromState = await this.deviceTrustCryptoService.getShouldTrustDevice( + const rememberDeviceFromState = await this.deviceTrustService.getShouldTrustDevice( this.activeAccountId, ); @@ -169,9 +169,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { this.rememberDevice.valueChanges .pipe( switchMap((value) => - defer(() => - this.deviceTrustCryptoService.setShouldTrustDevice(this.activeAccountId, value), - ), + defer(() => this.deviceTrustService.setShouldTrustDevice(this.activeAccountId, value)), ), takeUntil(this.destroy$), ) @@ -288,7 +286,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { await this.passwordResetEnrollmentService.enroll(this.data.organizationId); if (this.rememberDeviceForm.value.rememberDevice) { - await this.deviceTrustCryptoService.trustDevice(this.activeAccountId); + await this.deviceTrustService.trustDevice(this.activeAccountId); } } catch (error) { this.validationService.showError(error); diff --git a/libs/angular/src/auth/components/lock.component.ts b/libs/angular/src/auth/components/lock.component.ts index 9c2ed55357f..927fbb27b16 100644 --- a/libs/angular/src/auth/components/lock.component.ts +++ b/libs/angular/src/auth/components/lock.component.ts @@ -11,7 +11,7 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; @@ -74,7 +74,7 @@ export class LockComponent implements OnInit, OnDestroy { protected policyService: InternalPolicyService, protected passwordStrengthService: PasswordStrengthServiceAbstraction, protected dialogService: DialogService, - protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, + protected deviceTrustService: DeviceTrustServiceAbstraction, protected userVerificationService: UserVerificationService, protected pinCryptoService: PinCryptoServiceAbstraction, protected biometricStateService: BiometricStateService, @@ -277,7 +277,7 @@ export class LockComponent implements OnInit, OnDestroy { // Now that we have a decrypted user key in memory, we can check if we // need to establish trust on the current device const activeAccount = await firstValueFrom(this.accountService.activeAccount$); - await this.deviceTrustCryptoService.trustDeviceIfRequired(activeAccount.id); + await this.deviceTrustService.trustDeviceIfRequired(activeAccount.id); await this.doContinue(evaluatePasswordAfterUnlock); } diff --git a/libs/angular/src/auth/components/login-via-auth-request.component.ts b/libs/angular/src/auth/components/login-via-auth-request.component.ts index 3b827669a5c..a60468e244a 100644 --- a/libs/angular/src/auth/components/login-via-auth-request.component.ts +++ b/libs/angular/src/auth/components/login-via-auth-request.component.ts @@ -12,7 +12,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AnonymousHubService } from "@bitwarden/common/auth/abstractions/anonymous-hub.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { AuthRequestType } from "@bitwarden/common/auth/enums/auth-request-type"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable"; @@ -86,7 +86,7 @@ export class LoginViaAuthRequestComponent private validationService: ValidationService, private stateService: StateService, private loginEmailService: LoginEmailServiceAbstraction, - private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, + private deviceTrustService: DeviceTrustServiceAbstraction, private authRequestService: AuthRequestServiceAbstraction, private loginStrategyService: LoginStrategyServiceAbstraction, private accountService: AccountService, @@ -402,7 +402,7 @@ export class LoginViaAuthRequestComponent // Now that we have a decrypted user key in memory, we can check if we // need to establish trust on the current device const activeAccount = await firstValueFrom(this.accountService.activeAccount$); - await this.deviceTrustCryptoService.trustDeviceIfRequired(activeAccount.id); + await this.deviceTrustService.trustDeviceIfRequired(activeAccount.id); // TODO: don't forget to use auto enrollment service everywhere we trust device diff --git a/libs/angular/src/auth/guards/lock.guard.ts b/libs/angular/src/auth/guards/lock.guard.ts index 6f71d77a63d..8cd5290ebc8 100644 --- a/libs/angular/src/auth/guards/lock.guard.ts +++ b/libs/angular/src/auth/guards/lock.guard.ts @@ -8,7 +8,7 @@ import { import { firstValueFrom } from "rxjs"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { ClientType } from "@bitwarden/common/enums"; @@ -30,7 +30,7 @@ export function lockGuard(): CanActivateFn { ) => { const authService = inject(AuthService); const cryptoService = inject(CryptoService); - const deviceTrustCryptoService = inject(DeviceTrustCryptoServiceAbstraction); + const deviceTrustService = inject(DeviceTrustServiceAbstraction); const platformUtilService = inject(PlatformUtilsService); const messagingService = inject(MessagingService); const router = inject(Router); @@ -53,7 +53,7 @@ export function lockGuard(): CanActivateFn { // User is authN and in locked state. - const tdeEnabled = await firstValueFrom(deviceTrustCryptoService.supportsDeviceTrust$); + const tdeEnabled = await firstValueFrom(deviceTrustService.supportsDeviceTrust$); // Create special exception which allows users to go from the login-initiated page to the lock page for the approve w/ MP flow // The MP check is necessary to prevent direct manual navigation from other locked state pages for users who don't have a MP diff --git a/libs/angular/src/auth/guards/redirect.guard.ts b/libs/angular/src/auth/guards/redirect.guard.ts index ca9152186d0..0c43673c349 100644 --- a/libs/angular/src/auth/guards/redirect.guard.ts +++ b/libs/angular/src/auth/guards/redirect.guard.ts @@ -3,7 +3,7 @@ import { CanActivateFn, Router } from "@angular/router"; import { firstValueFrom } from "rxjs"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -31,7 +31,7 @@ export function redirectGuard(overrides: Partial = {}): CanActiv return async (route) => { const authService = inject(AuthService); const cryptoService = inject(CryptoService); - const deviceTrustCryptoService = inject(DeviceTrustCryptoServiceAbstraction); + const deviceTrustService = inject(DeviceTrustServiceAbstraction); const router = inject(Router); const authStatus = await authService.getAuthStatus(); @@ -46,7 +46,7 @@ export function redirectGuard(overrides: Partial = {}): CanActiv // If locked, TDE is enabled, and the user hasn't decrypted yet, then redirect to the // login decryption options component. - const tdeEnabled = await firstValueFrom(deviceTrustCryptoService.supportsDeviceTrust$); + const tdeEnabled = await firstValueFrom(deviceTrustService.supportsDeviceTrust$); const everHadUserKey = await firstValueFrom(cryptoService.everHadUserKey$); if (authStatus === AuthenticationStatus.Locked && tdeEnabled && !everHadUserKey) { return router.createUrlTree([routes.notDecrypted], { queryParams: route.queryParams }); diff --git a/libs/angular/src/auth/guards/tde-decryption-required.guard.ts b/libs/angular/src/auth/guards/tde-decryption-required.guard.ts index 146c6e19a21..524ce7dce50 100644 --- a/libs/angular/src/auth/guards/tde-decryption-required.guard.ts +++ b/libs/angular/src/auth/guards/tde-decryption-required.guard.ts @@ -8,7 +8,7 @@ import { import { firstValueFrom } from "rxjs"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -22,11 +22,11 @@ export function tdeDecryptionRequiredGuard(): CanActivateFn { return async (_: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { const authService = inject(AuthService); const cryptoService = inject(CryptoService); - const deviceTrustCryptoService = inject(DeviceTrustCryptoServiceAbstraction); + const deviceTrustService = inject(DeviceTrustServiceAbstraction); const router = inject(Router); const authStatus = await authService.getAuthStatus(); - const tdeEnabled = await firstValueFrom(deviceTrustCryptoService.supportsDeviceTrust$); + const tdeEnabled = await firstValueFrom(deviceTrustService.supportsDeviceTrust$); const everHadUserKey = await firstValueFrom(cryptoService.everHadUserKey$); if (authStatus !== AuthenticationStatus.Locked || !tdeEnabled || everHadUserKey) { return router.createUrlTree(["/"]); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 45f11befa68..aabf823c0be 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -60,7 +60,7 @@ import { import { AnonymousHubService as AnonymousHubServiceAbstraction } from "@bitwarden/common/auth/abstractions/anonymous-hub.service"; import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service"; import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/auth/abstractions/key-connector.service"; @@ -82,7 +82,7 @@ import { AccountServiceImplementation } from "@bitwarden/common/auth/services/ac import { AnonymousHubService } from "@bitwarden/common/auth/services/anonymous-hub.service"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { AvatarService } from "@bitwarden/common/auth/services/avatar.service"; -import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device-trust-crypto.service.implementation"; +import { DeviceTrustService } from "@bitwarden/common/auth/services/device-trust.service.implementation"; import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation"; import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation"; import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service"; @@ -385,7 +385,7 @@ const safeProviders: SafeProvider[] = [ EncryptService, PasswordStrengthServiceAbstraction, PolicyServiceAbstraction, - DeviceTrustCryptoServiceAbstraction, + DeviceTrustServiceAbstraction, AuthRequestServiceAbstraction, InternalUserDecryptionOptionsServiceAbstraction, GlobalStateProvider, @@ -949,8 +949,8 @@ const safeProviders: SafeProvider[] = [ deps: [DevicesApiServiceAbstraction], }), safeProvider({ - provide: DeviceTrustCryptoServiceAbstraction, - useClass: DeviceTrustCryptoService, + provide: DeviceTrustServiceAbstraction, + useClass: DeviceTrustService, deps: [ KeyGenerationServiceAbstraction, CryptoFunctionServiceAbstraction, diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts index 0ce6c9fed76..4e0b1ac3acc 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts @@ -1,7 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; @@ -42,7 +42,7 @@ describe("AuthRequestLoginStrategy", () => { let stateService: MockProxy; let twoFactorService: MockProxy; let userDecryptionOptions: MockProxy; - let deviceTrustCryptoService: MockProxy; + let deviceTrustService: MockProxy; let billingAccountProfileStateService: MockProxy; const mockUserId = Utils.newGuid() as UserId; @@ -75,7 +75,7 @@ describe("AuthRequestLoginStrategy", () => { stateService = mock(); twoFactorService = mock(); userDecryptionOptions = mock(); - deviceTrustCryptoService = mock(); + deviceTrustService = mock(); billingAccountProfileStateService = mock(); accountService = mockAccountServiceWith(mockUserId); @@ -99,7 +99,7 @@ describe("AuthRequestLoginStrategy", () => { stateService, twoFactorService, userDecryptionOptions, - deviceTrustCryptoService, + deviceTrustService, billingAccountProfileStateService, ); @@ -132,7 +132,7 @@ describe("AuthRequestLoginStrategy", () => { ); expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key); expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey); - expect(deviceTrustCryptoService.trustDeviceIfRequired).toHaveBeenCalled(); + expect(deviceTrustService.trustDeviceIfRequired).toHaveBeenCalled(); expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey); }); @@ -160,6 +160,6 @@ describe("AuthRequestLoginStrategy", () => { expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey); // trustDeviceIfRequired should be called - expect(deviceTrustCryptoService.trustDeviceIfRequired).not.toHaveBeenCalled(); + expect(deviceTrustService.trustDeviceIfRequired).not.toHaveBeenCalled(); }); }); diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts index 4035a7be58c..5220e432de7 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts @@ -3,7 +3,6 @@ import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; @@ -18,6 +17,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/src/auth/abstractions/device-trust.service.abstraction"; import { UserId } from "@bitwarden/common/types/guid"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; @@ -61,7 +61,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy { stateService: StateService, twoFactorService: TwoFactorService, userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, - private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, + private deviceTrustService: DeviceTrustServiceAbstraction, billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( @@ -147,7 +147,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy { await this.trySetUserKeyWithMasterKey(); // Establish trust if required after setting user key - await this.deviceTrustCryptoService.trustDeviceIfRequired(userId); + await this.deviceTrustService.trustDeviceIfRequired(userId); } } diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts index b78ad6dea62..df33415247f 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts @@ -1,7 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; @@ -50,7 +50,7 @@ describe("SsoLoginStrategy", () => { let twoFactorService: MockProxy; let userDecryptionOptionsService: MockProxy; let keyConnectorService: MockProxy; - let deviceTrustCryptoService: MockProxy; + let deviceTrustService: MockProxy; let authRequestService: MockProxy; let i18nService: MockProxy; let billingAccountProfileStateService: MockProxy; @@ -82,7 +82,7 @@ describe("SsoLoginStrategy", () => { twoFactorService = mock(); userDecryptionOptionsService = mock(); keyConnectorService = mock(); - deviceTrustCryptoService = mock(); + deviceTrustService = mock(); authRequestService = mock(); i18nService = mock(); billingAccountProfileStateService = mock(); @@ -106,7 +106,7 @@ describe("SsoLoginStrategy", () => { twoFactorService, userDecryptionOptionsService, keyConnectorService, - deviceTrustCryptoService, + deviceTrustService, authRequestService, i18nService, billingAccountProfileStateService, @@ -209,8 +209,8 @@ describe("SsoLoginStrategy", () => { ); apiService.postIdentityToken.mockResolvedValue(idTokenResponse); - deviceTrustCryptoService.getDeviceKey.mockResolvedValue(mockDeviceKey); - deviceTrustCryptoService.decryptUserKeyWithDeviceKey.mockResolvedValue(mockUserKey); + deviceTrustService.getDeviceKey.mockResolvedValue(mockDeviceKey); + deviceTrustService.decryptUserKeyWithDeviceKey.mockResolvedValue(mockUserKey); const cryptoSvcSetUserKeySpy = jest.spyOn(cryptoService, "setUserKey"); @@ -218,8 +218,8 @@ describe("SsoLoginStrategy", () => { await ssoLoginStrategy.logIn(credentials); // Assert - expect(deviceTrustCryptoService.getDeviceKey).toHaveBeenCalledTimes(1); - expect(deviceTrustCryptoService.decryptUserKeyWithDeviceKey).toHaveBeenCalledTimes(1); + expect(deviceTrustService.getDeviceKey).toHaveBeenCalledTimes(1); + expect(deviceTrustService.decryptUserKeyWithDeviceKey).toHaveBeenCalledTimes(1); expect(cryptoSvcSetUserKeySpy).toHaveBeenCalledTimes(1); expect(cryptoSvcSetUserKeySpy).toHaveBeenCalledWith(mockUserKey); }); @@ -232,8 +232,8 @@ describe("SsoLoginStrategy", () => { ); apiService.postIdentityToken.mockResolvedValue(idTokenResponse); // Set deviceKey to be null - deviceTrustCryptoService.getDeviceKey.mockResolvedValue(null); - deviceTrustCryptoService.decryptUserKeyWithDeviceKey.mockResolvedValue(mockUserKey); + deviceTrustService.getDeviceKey.mockResolvedValue(null); + deviceTrustService.decryptUserKeyWithDeviceKey.mockResolvedValue(mockUserKey); // Act await ssoLoginStrategy.logIn(credentials); @@ -254,7 +254,7 @@ describe("SsoLoginStrategy", () => { // Arrange const idTokenResponse = mockIdTokenResponseWithModifiedTrustedDeviceOption(valueName, null); apiService.postIdentityToken.mockResolvedValue(idTokenResponse); - deviceTrustCryptoService.getDeviceKey.mockResolvedValue(mockDeviceKey); + deviceTrustService.getDeviceKey.mockResolvedValue(mockDeviceKey); // Act await ssoLoginStrategy.logIn(credentials); @@ -271,9 +271,9 @@ describe("SsoLoginStrategy", () => { userDecryptionOptsServerResponseWithTdeOption, ); apiService.postIdentityToken.mockResolvedValue(idTokenResponse); - deviceTrustCryptoService.getDeviceKey.mockResolvedValue(mockDeviceKey); + deviceTrustService.getDeviceKey.mockResolvedValue(mockDeviceKey); // Set userKey to be null - deviceTrustCryptoService.decryptUserKeyWithDeviceKey.mockResolvedValue(null); + deviceTrustService.decryptUserKeyWithDeviceKey.mockResolvedValue(null); // Act await ssoLoginStrategy.logIn(credentials); @@ -321,7 +321,7 @@ describe("SsoLoginStrategy", () => { await ssoLoginStrategy.logIn(credentials); expect(authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash).toHaveBeenCalled(); - expect(deviceTrustCryptoService.decryptUserKeyWithDeviceKey).not.toHaveBeenCalled(); + expect(deviceTrustService.decryptUserKeyWithDeviceKey).not.toHaveBeenCalled(); }); it("sets the user key from approved admin request if exists", async () => { @@ -338,7 +338,7 @@ describe("SsoLoginStrategy", () => { await ssoLoginStrategy.logIn(credentials); expect(authRequestService.setUserKeyAfterDecryptingSharedUserKey).toHaveBeenCalled(); - expect(deviceTrustCryptoService.decryptUserKeyWithDeviceKey).not.toHaveBeenCalled(); + expect(deviceTrustService.decryptUserKeyWithDeviceKey).not.toHaveBeenCalled(); }); it("attempts to establish a trusted device if successful", async () => { @@ -355,7 +355,7 @@ describe("SsoLoginStrategy", () => { await ssoLoginStrategy.logIn(credentials); expect(authRequestService.setUserKeyAfterDecryptingSharedUserKey).toHaveBeenCalled(); - expect(deviceTrustCryptoService.trustDeviceIfRequired).toHaveBeenCalled(); + expect(deviceTrustService.trustDeviceIfRequired).toHaveBeenCalled(); }); it("clears the admin auth request if server returns a 404, meaning it was deleted", async () => { @@ -369,7 +369,7 @@ describe("SsoLoginStrategy", () => { authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash, ).not.toHaveBeenCalled(); expect(authRequestService.setUserKeyAfterDecryptingSharedUserKey).not.toHaveBeenCalled(); - expect(deviceTrustCryptoService.trustDeviceIfRequired).not.toHaveBeenCalled(); + expect(deviceTrustService.trustDeviceIfRequired).not.toHaveBeenCalled(); }); it("attempts to login with a trusted device if admin auth request isn't successful", async () => { @@ -382,11 +382,11 @@ describe("SsoLoginStrategy", () => { }; apiService.getAuthRequest.mockResolvedValue(adminAuthResponse as AuthRequestResponse); cryptoService.hasUserKey.mockResolvedValue(false); - deviceTrustCryptoService.getDeviceKey.mockResolvedValue("DEVICE_KEY" as any); + deviceTrustService.getDeviceKey.mockResolvedValue("DEVICE_KEY" as any); await ssoLoginStrategy.logIn(credentials); - expect(deviceTrustCryptoService.decryptUserKeyWithDeviceKey).toHaveBeenCalled(); + expect(deviceTrustService.decryptUserKeyWithDeviceKey).toHaveBeenCalled(); }); }); }); diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.ts index d8efd789840..dc63f0fae1d 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.ts @@ -3,7 +3,6 @@ import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; @@ -22,6 +21,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/src/auth/abstractions/device-trust.service.abstraction"; import { UserId } from "@bitwarden/common/types/guid"; import { @@ -94,7 +94,7 @@ export class SsoLoginStrategy extends LoginStrategy { twoFactorService: TwoFactorService, userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, private keyConnectorService: KeyConnectorService, - private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, + private deviceTrustService: DeviceTrustServiceAbstraction, private authRequestService: AuthRequestServiceAbstraction, private i18nService: I18nService, billingAccountProfileStateService: BillingAccountProfileStateService, @@ -298,7 +298,7 @@ export class SsoLoginStrategy extends LoginStrategy { if (await this.cryptoService.hasUserKey()) { // Now that we have a decrypted user key in memory, we can check if we // need to establish trust on the current device - await this.deviceTrustCryptoService.trustDeviceIfRequired(userId); + await this.deviceTrustService.trustDeviceIfRequired(userId); // if we successfully decrypted the user key, we can delete the admin auth request out of state // TODO: eventually we post and clean up DB as well once consumed on client @@ -314,7 +314,7 @@ export class SsoLoginStrategy extends LoginStrategy { const userId = (await this.stateService.getUserId()) as UserId; - const deviceKey = await this.deviceTrustCryptoService.getDeviceKey(userId); + const deviceKey = await this.deviceTrustService.getDeviceKey(userId); const encDevicePrivateKey = trustedDeviceOption?.encryptedPrivateKey; const encUserKey = trustedDeviceOption?.encryptedUserKey; @@ -322,7 +322,7 @@ export class SsoLoginStrategy extends LoginStrategy { return; } - const userKey = await this.deviceTrustCryptoService.decryptUserKeyWithDeviceKey( + const userKey = await this.deviceTrustService.decryptUserKeyWithDeviceKey( userId, encDevicePrivateKey, encUserKey, diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts index fcc0220d0ab..33708885e26 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts @@ -2,7 +2,7 @@ import { MockProxy, mock } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; @@ -62,7 +62,7 @@ describe("LoginStrategyService", () => { let encryptService: MockProxy; let passwordStrengthService: MockProxy; let policyService: MockProxy; - let deviceTrustCryptoService: MockProxy; + let deviceTrustService: MockProxy; let authRequestService: MockProxy; let userDecryptionOptionsService: MockProxy; let billingAccountProfileStateService: MockProxy; @@ -90,7 +90,7 @@ describe("LoginStrategyService", () => { encryptService = mock(); passwordStrengthService = mock(); policyService = mock(); - deviceTrustCryptoService = mock(); + deviceTrustService = mock(); authRequestService = mock(); userDecryptionOptionsService = mock(); billingAccountProfileStateService = mock(); @@ -114,7 +114,7 @@ describe("LoginStrategyService", () => { encryptService, passwordStrengthService, policyService, - deviceTrustCryptoService, + deviceTrustService, authRequestService, userDecryptionOptionsService, stateProvider, diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts index a8bd7bc2ff2..aee74e66078 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts @@ -10,7 +10,6 @@ import { import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; @@ -36,6 +35,7 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv import { KdfType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { GlobalState, GlobalStateProvider } from "@bitwarden/common/platform/state"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/src/auth/abstractions/device-trust.service.abstraction"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { MasterKey } from "@bitwarden/common/types/key"; @@ -100,7 +100,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { protected encryptService: EncryptService, protected passwordStrengthService: PasswordStrengthServiceAbstraction, protected policyService: PolicyService, - protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, + protected deviceTrustService: DeviceTrustServiceAbstraction, protected authRequestService: AuthRequestServiceAbstraction, protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, protected stateProvider: GlobalStateProvider, @@ -371,7 +371,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.twoFactorService, this.userDecryptionOptionsService, this.keyConnectorService, - this.deviceTrustCryptoService, + this.deviceTrustService, this.authRequestService, this.i18nService, this.billingAccountProfileStateService, @@ -410,7 +410,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.stateService, this.twoFactorService, this.userDecryptionOptionsService, - this.deviceTrustCryptoService, + this.deviceTrustService, this.billingAccountProfileStateService, ); case AuthenticationType.WebAuthn: diff --git a/libs/common/src/auth/abstractions/device-trust-crypto.service.abstraction.ts b/libs/common/src/auth/abstractions/device-trust.service.abstraction.ts similarity index 89% rename from libs/common/src/auth/abstractions/device-trust-crypto.service.abstraction.ts rename to libs/common/src/auth/abstractions/device-trust.service.abstraction.ts index 53fe2140353..123f7103386 100644 --- a/libs/common/src/auth/abstractions/device-trust-crypto.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/device-trust.service.abstraction.ts @@ -3,9 +3,10 @@ import { Observable } from "rxjs"; import { EncString } from "../../platform/models/domain/enc-string"; import { UserId } from "../../types/guid"; import { DeviceKey, UserKey } from "../../types/key"; -import { DeviceResponse } from "../abstractions/devices/responses/device.response"; -export abstract class DeviceTrustCryptoServiceAbstraction { +import { DeviceResponse } from "./devices/responses/device.response"; + +export abstract class DeviceTrustServiceAbstraction { supportsDeviceTrust$: Observable; /** * @description Retrieves the users choice to trust the device which can only happen after decryption diff --git a/libs/common/src/auth/services/device-trust-crypto.service.implementation.ts b/libs/common/src/auth/services/device-trust.service.implementation.ts similarity index 98% rename from libs/common/src/auth/services/device-trust-crypto.service.implementation.ts rename to libs/common/src/auth/services/device-trust.service.implementation.ts index 6fb58eab289..ccf87acaf85 100644 --- a/libs/common/src/auth/services/device-trust-crypto.service.implementation.ts +++ b/libs/common/src/auth/services/device-trust.service.implementation.ts @@ -17,7 +17,7 @@ import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypt import { DEVICE_TRUST_DISK_LOCAL, StateProvider, UserKeyDefinition } from "../../platform/state"; import { UserId } from "../../types/guid"; import { UserKey, DeviceKey } from "../../types/key"; -import { DeviceTrustCryptoServiceAbstraction } from "../abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "../abstractions/device-trust.service.abstraction"; import { DeviceResponse } from "../abstractions/devices/responses/device.response"; import { DevicesApiServiceAbstraction } from "../abstractions/devices-api.service.abstraction"; import { SecretVerificationRequest } from "../models/request/secret-verification.request"; @@ -42,7 +42,7 @@ export const SHOULD_TRUST_DEVICE = new UserKeyDefinition( }, ); -export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstraction { +export class DeviceTrustService implements DeviceTrustServiceAbstraction { private readonly platformSupportsSecureStorage = this.platformUtilsService.supportsSecureStorage(); private readonly deviceKeySecureStorageKey: string = "_deviceKey"; diff --git a/libs/common/src/auth/services/device-trust-crypto.service.spec.ts b/libs/common/src/auth/services/device-trust.service.spec.ts similarity index 86% rename from libs/common/src/auth/services/device-trust-crypto.service.spec.ts rename to libs/common/src/auth/services/device-trust.service.spec.ts index af147b3481d..12b8cf2eaaa 100644 --- a/libs/common/src/auth/services/device-trust-crypto.service.spec.ts +++ b/libs/common/src/auth/services/device-trust.service.spec.ts @@ -33,11 +33,11 @@ import { ProtectedDeviceResponse } from "../models/response/protected-device.res import { SHOULD_TRUST_DEVICE, DEVICE_KEY, - DeviceTrustCryptoService, -} from "./device-trust-crypto.service.implementation"; + DeviceTrustService, +} from "./device-trust.service.implementation"; -describe("deviceTrustCryptoService", () => { - let deviceTrustCryptoService: DeviceTrustCryptoService; +describe("deviceTrustService", () => { + let deviceTrustService: DeviceTrustService; const keyGenerationService = mock(); const cryptoFunctionService = mock(); @@ -70,11 +70,11 @@ describe("deviceTrustCryptoService", () => { jest.clearAllMocks(); const supportsSecureStorage = false; // default to false; tests will override as needed // By default all the tests will have a mocked active user in state provider. - deviceTrustCryptoService = createDeviceTrustCryptoService(mockUserId, supportsSecureStorage); + deviceTrustService = createDeviceTrustService(mockUserId, supportsSecureStorage); }); it("instantiates", () => { - expect(deviceTrustCryptoService).not.toBeFalsy(); + expect(deviceTrustService).not.toBeFalsy(); }); describe("User Trust Device Choice For Decryption", () => { @@ -84,7 +84,7 @@ describe("deviceTrustCryptoService", () => { await stateProvider.setUserState(SHOULD_TRUST_DEVICE, newValue, mockUserId); - const result = await deviceTrustCryptoService.getShouldTrustDevice(mockUserId); + const result = await deviceTrustService.getShouldTrustDevice(mockUserId); expect(result).toEqual(newValue); }); @@ -95,9 +95,9 @@ describe("deviceTrustCryptoService", () => { await stateProvider.setUserState(SHOULD_TRUST_DEVICE, false, mockUserId); const newValue = true; - await deviceTrustCryptoService.setShouldTrustDevice(mockUserId, newValue); + await deviceTrustService.setShouldTrustDevice(mockUserId, newValue); - const result = await deviceTrustCryptoService.getShouldTrustDevice(mockUserId); + const result = await deviceTrustService.getShouldTrustDevice(mockUserId); expect(result).toEqual(newValue); }); }); @@ -105,25 +105,25 @@ describe("deviceTrustCryptoService", () => { describe("trustDeviceIfRequired", () => { it("should trust device and reset when getShouldTrustDevice returns true", async () => { - jest.spyOn(deviceTrustCryptoService, "getShouldTrustDevice").mockResolvedValue(true); - jest.spyOn(deviceTrustCryptoService, "trustDevice").mockResolvedValue({} as DeviceResponse); - jest.spyOn(deviceTrustCryptoService, "setShouldTrustDevice").mockResolvedValue(); + jest.spyOn(deviceTrustService, "getShouldTrustDevice").mockResolvedValue(true); + jest.spyOn(deviceTrustService, "trustDevice").mockResolvedValue({} as DeviceResponse); + jest.spyOn(deviceTrustService, "setShouldTrustDevice").mockResolvedValue(); - await deviceTrustCryptoService.trustDeviceIfRequired(mockUserId); + await deviceTrustService.trustDeviceIfRequired(mockUserId); - expect(deviceTrustCryptoService.getShouldTrustDevice).toHaveBeenCalledTimes(1); - expect(deviceTrustCryptoService.trustDevice).toHaveBeenCalledTimes(1); - expect(deviceTrustCryptoService.setShouldTrustDevice).toHaveBeenCalledWith(mockUserId, false); + expect(deviceTrustService.getShouldTrustDevice).toHaveBeenCalledTimes(1); + expect(deviceTrustService.trustDevice).toHaveBeenCalledTimes(1); + expect(deviceTrustService.setShouldTrustDevice).toHaveBeenCalledWith(mockUserId, false); }); it("should not trust device nor reset when getShouldTrustDevice returns false", async () => { const getShouldTrustDeviceSpy = jest - .spyOn(deviceTrustCryptoService, "getShouldTrustDevice") + .spyOn(deviceTrustService, "getShouldTrustDevice") .mockResolvedValue(false); - const trustDeviceSpy = jest.spyOn(deviceTrustCryptoService, "trustDevice"); - const setShouldTrustDeviceSpy = jest.spyOn(deviceTrustCryptoService, "setShouldTrustDevice"); + const trustDeviceSpy = jest.spyOn(deviceTrustService, "trustDevice"); + const setShouldTrustDeviceSpy = jest.spyOn(deviceTrustService, "setShouldTrustDevice"); - await deviceTrustCryptoService.trustDeviceIfRequired(mockUserId); + await deviceTrustService.trustDeviceIfRequired(mockUserId); expect(getShouldTrustDeviceSpy).toHaveBeenCalledTimes(1); expect(trustDeviceSpy).not.toHaveBeenCalled(); @@ -151,7 +151,7 @@ describe("deviceTrustCryptoService", () => { it("returns null when there is not an existing device key", async () => { await stateProvider.setUserState(DEVICE_KEY, null, mockUserId); - const deviceKey = await deviceTrustCryptoService.getDeviceKey(mockUserId); + const deviceKey = await deviceTrustService.getDeviceKey(mockUserId); expect(deviceKey).toBeNull(); expect(secureStorageService.get).not.toHaveBeenCalled(); @@ -160,7 +160,7 @@ describe("deviceTrustCryptoService", () => { it("returns the device key when there is an existing device key", async () => { await stateProvider.setUserState(DEVICE_KEY, existingDeviceKey, mockUserId); - const deviceKey = await deviceTrustCryptoService.getDeviceKey(mockUserId); + const deviceKey = await deviceTrustService.getDeviceKey(mockUserId); expect(deviceKey).not.toBeNull(); expect(deviceKey).toBeInstanceOf(SymmetricCryptoKey); @@ -172,17 +172,14 @@ describe("deviceTrustCryptoService", () => { describe("Secure Storage supported", () => { beforeEach(() => { const supportsSecureStorage = true; - deviceTrustCryptoService = createDeviceTrustCryptoService( - mockUserId, - supportsSecureStorage, - ); + deviceTrustService = createDeviceTrustService(mockUserId, supportsSecureStorage); }); it("returns null when there is not an existing device key for the passed in user id", async () => { secureStorageService.get.mockResolvedValue(null); // Act - const deviceKey = await deviceTrustCryptoService.getDeviceKey(mockUserId); + const deviceKey = await deviceTrustService.getDeviceKey(mockUserId); // Assert expect(deviceKey).toBeNull(); @@ -193,7 +190,7 @@ describe("deviceTrustCryptoService", () => { secureStorageService.get.mockResolvedValue(existingDeviceKeyB64); // Act - const deviceKey = await deviceTrustCryptoService.getDeviceKey(mockUserId); + const deviceKey = await deviceTrustService.getDeviceKey(mockUserId); // Assert expect(deviceKey).not.toBeNull(); @@ -203,7 +200,7 @@ describe("deviceTrustCryptoService", () => { }); it("throws an error when no user id is passed in", async () => { - await expect(deviceTrustCryptoService.getDeviceKey(null)).rejects.toThrow( + await expect(deviceTrustService.getDeviceKey(null)).rejects.toThrow( "UserId is required. Cannot get device key.", ); }); @@ -220,7 +217,7 @@ describe("deviceTrustCryptoService", () => { // TypeScript will allow calling private methods if the object is of type 'any' // This is a hacky workaround, but it allows for cleaner tests - await (deviceTrustCryptoService as any).setDeviceKey(mockUserId, newDeviceKey); + await (deviceTrustService as any).setDeviceKey(mockUserId, newDeviceKey); expect(stateProvider.mock.setUserState).toHaveBeenLastCalledWith( DEVICE_KEY, @@ -232,10 +229,7 @@ describe("deviceTrustCryptoService", () => { describe("Secure Storage supported", () => { beforeEach(() => { const supportsSecureStorage = true; - deviceTrustCryptoService = createDeviceTrustCryptoService( - mockUserId, - supportsSecureStorage, - ); + deviceTrustService = createDeviceTrustService(mockUserId, supportsSecureStorage); }); it("successfully sets the device key in secure storage", async () => { @@ -251,7 +245,7 @@ describe("deviceTrustCryptoService", () => { // Act // TypeScript will allow calling private methods if the object is of type 'any' // This is a hacky workaround, but it allows for cleaner tests - await (deviceTrustCryptoService as any).setDeviceKey(mockUserId, newDeviceKey); + await (deviceTrustService as any).setDeviceKey(mockUserId, newDeviceKey); // Assert expect(stateProvider.mock.setUserState).not.toHaveBeenCalledTimes(2); @@ -268,9 +262,9 @@ describe("deviceTrustCryptoService", () => { new Uint8Array(deviceKeyBytesLength) as CsprngArray, ) as DeviceKey; - await expect( - (deviceTrustCryptoService as any).setDeviceKey(null, newDeviceKey), - ).rejects.toThrow("UserId is required. Cannot set device key."); + await expect((deviceTrustService as any).setDeviceKey(null, newDeviceKey)).rejects.toThrow( + "UserId is required. Cannot set device key.", + ); }); }); @@ -285,7 +279,7 @@ describe("deviceTrustCryptoService", () => { // TypeScript will allow calling private methods if the object is of type 'any' // This is a hacky workaround, but it allows for cleaner tests - const deviceKey = await (deviceTrustCryptoService as any).makeDeviceKey(); + const deviceKey = await (deviceTrustService as any).makeDeviceKey(); expect(keyGenSvcGenerateKeySpy).toHaveBeenCalledTimes(1); expect(keyGenSvcGenerateKeySpy).toHaveBeenCalledWith(deviceKeyBytesLength * 8); @@ -362,7 +356,7 @@ describe("deviceTrustCryptoService", () => { // TypeScript will allow calling private methods if the object is of type 'any' makeDeviceKeySpy = jest - .spyOn(deviceTrustCryptoService as any, "makeDeviceKey") + .spyOn(deviceTrustService as any, "makeDeviceKey") .mockResolvedValue(mockDeviceKey); rsaGenerateKeyPairSpy = jest @@ -398,7 +392,7 @@ describe("deviceTrustCryptoService", () => { }); it("calls the required methods with the correct arguments and returns a DeviceResponse", async () => { - const response = await deviceTrustCryptoService.trustDevice(mockUserId); + const response = await deviceTrustService.trustDevice(mockUserId); expect(makeDeviceKeySpy).toHaveBeenCalledTimes(1); expect(rsaGenerateKeyPairSpy).toHaveBeenCalledTimes(1); @@ -429,7 +423,7 @@ describe("deviceTrustCryptoService", () => { // setup the spy to return null cryptoSvcGetUserKeySpy.mockResolvedValue(null); // check if the expected error is thrown - await expect(deviceTrustCryptoService.trustDevice(mockUserId)).rejects.toThrow( + await expect(deviceTrustService.trustDevice(mockUserId)).rejects.toThrow( "User symmetric key not found", ); @@ -439,7 +433,7 @@ describe("deviceTrustCryptoService", () => { // setup the spy to return undefined cryptoSvcGetUserKeySpy.mockResolvedValue(undefined); // check if the expected error is thrown - await expect(deviceTrustCryptoService.trustDevice(mockUserId)).rejects.toThrow( + await expect(deviceTrustService.trustDevice(mockUserId)).rejects.toThrow( "User symmetric key not found", ); }); @@ -479,9 +473,7 @@ describe("deviceTrustCryptoService", () => { it(`throws an error if ${method} fails`, async () => { const methodSpy = spy(); methodSpy.mockRejectedValue(new Error(errorText)); - await expect(deviceTrustCryptoService.trustDevice(mockUserId)).rejects.toThrow( - errorText, - ); + await expect(deviceTrustService.trustDevice(mockUserId)).rejects.toThrow(errorText); }); test.each([null, undefined])( @@ -489,14 +481,14 @@ describe("deviceTrustCryptoService", () => { async (invalidValue) => { const methodSpy = spy(); methodSpy.mockResolvedValue(invalidValue); - await expect(deviceTrustCryptoService.trustDevice(mockUserId)).rejects.toThrow(); + await expect(deviceTrustService.trustDevice(mockUserId)).rejects.toThrow(); }, ); }, ); it("throws an error when a null user id is passed in", async () => { - await expect(deviceTrustCryptoService.trustDevice(null)).rejects.toThrow( + await expect(deviceTrustService.trustDevice(null)).rejects.toThrow( "UserId is required. Cannot trust device.", ); }); @@ -530,7 +522,7 @@ describe("deviceTrustCryptoService", () => { it("throws an error when a null user id is passed in", async () => { await expect( - deviceTrustCryptoService.decryptUserKeyWithDeviceKey( + deviceTrustService.decryptUserKeyWithDeviceKey( null, mockEncryptedDevicePrivateKey, mockEncryptedUserKey, @@ -540,7 +532,7 @@ describe("deviceTrustCryptoService", () => { }); it("returns null when device key isn't provided", async () => { - const result = await deviceTrustCryptoService.decryptUserKeyWithDeviceKey( + const result = await deviceTrustService.decryptUserKeyWithDeviceKey( mockUserId, mockEncryptedDevicePrivateKey, mockEncryptedUserKey, @@ -558,7 +550,7 @@ describe("deviceTrustCryptoService", () => { .spyOn(cryptoService, "rsaDecrypt") .mockResolvedValue(new Uint8Array(userKeyBytesLength)); - const result = await deviceTrustCryptoService.decryptUserKeyWithDeviceKey( + const result = await deviceTrustService.decryptUserKeyWithDeviceKey( mockUserId, mockEncryptedDevicePrivateKey, mockEncryptedUserKey, @@ -574,9 +566,9 @@ describe("deviceTrustCryptoService", () => { const decryptToBytesSpy = jest .spyOn(encryptService, "decryptToBytes") .mockRejectedValue(new Error("Decryption error")); - const setDeviceKeySpy = jest.spyOn(deviceTrustCryptoService as any, "setDeviceKey"); + const setDeviceKeySpy = jest.spyOn(deviceTrustService as any, "setDeviceKey"); - const result = await deviceTrustCryptoService.decryptUserKeyWithDeviceKey( + const result = await deviceTrustService.decryptUserKeyWithDeviceKey( mockUserId, mockEncryptedDevicePrivateKey, mockEncryptedUserKey, @@ -606,7 +598,7 @@ describe("deviceTrustCryptoService", () => { it("throws an error when a null user id is passed in", async () => { await expect( - deviceTrustCryptoService.rotateDevicesTrust(null, fakeNewUserKey, ""), + deviceTrustService.rotateDevicesTrust(null, fakeNewUserKey, ""), ).rejects.toThrow("UserId is required. Cannot rotate device's trust."); }); @@ -615,7 +607,7 @@ describe("deviceTrustCryptoService", () => { stateProvider.activeUser.getFake(DEVICE_KEY); deviceKeyState.nextState(null); - await deviceTrustCryptoService.rotateDevicesTrust(mockUserId, fakeNewUserKey, ""); + await deviceTrustService.rotateDevicesTrust(mockUserId, fakeNewUserKey, ""); expect(devicesApiService.updateTrust).not.toHaveBeenCalled(); }); @@ -691,7 +683,7 @@ describe("deviceTrustCryptoService", () => { ); }); - await deviceTrustCryptoService.rotateDevicesTrust( + await deviceTrustService.rotateDevicesTrust( mockUserId, fakeNewUserKey, "my_password_hash", @@ -713,10 +705,7 @@ describe("deviceTrustCryptoService", () => { }); // Helpers - function createDeviceTrustCryptoService( - mockUserId: UserId | null, - supportsSecureStorage: boolean, - ) { + function createDeviceTrustService(mockUserId: UserId | null, supportsSecureStorage: boolean) { accountService = mockAccountServiceWith(mockUserId); stateProvider = new FakeStateProvider(accountService); @@ -725,7 +714,7 @@ describe("deviceTrustCryptoService", () => { decryptionOptions.next({} as any); userDecryptionOptionsService.userDecryptionOptions$ = decryptionOptions; - return new DeviceTrustCryptoService( + return new DeviceTrustService( keyGenerationService, cryptoFunctionService, cryptoService, diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index f9a87347317..2d8ef1619e5 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -49,7 +49,7 @@ import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org- import { KeyConnectorMigrator } from "./migrations/50-move-key-connector-to-state-provider"; import { RememberedEmailMigrator } from "./migrations/51-move-remembered-email-to-state-providers"; import { DeleteInstalledVersion } from "./migrations/52-delete-installed-version"; -import { DeviceTrustCryptoServiceStateProviderMigrator } from "./migrations/53-migrate-device-trust-crypto-svc-to-state-providers"; +import { DeviceTrustServiceStateProviderMigrator } from "./migrations/53-migrate-device-trust-svc-to-state-providers"; import { SendMigrator } from "./migrations/54-move-encrypted-sends"; import { MoveMasterKeyStateToProviderMigrator } from "./migrations/55-move-master-key-state-to-provider"; import { AuthRequestMigrator } from "./migrations/56-move-auth-requests"; @@ -117,7 +117,7 @@ export function createMigrationBuilder() { .with(KeyConnectorMigrator, 49, 50) .with(RememberedEmailMigrator, 50, 51) .with(DeleteInstalledVersion, 51, 52) - .with(DeviceTrustCryptoServiceStateProviderMigrator, 52, 53) + .with(DeviceTrustServiceStateProviderMigrator, 52, 53) .with(SendMigrator, 53, 54) .with(MoveMasterKeyStateToProviderMigrator, 54, 55) .with(AuthRequestMigrator, 55, 56) diff --git a/libs/common/src/state-migrations/migrations/53-migrate-device-trust-crypto-svc-to-state-providers.spec.ts b/libs/common/src/state-migrations/migrations/53-migrate-device-trust-svc-to-state-providers.spec.ts similarity index 92% rename from libs/common/src/state-migrations/migrations/53-migrate-device-trust-crypto-svc-to-state-providers.spec.ts rename to libs/common/src/state-migrations/migrations/53-migrate-device-trust-svc-to-state-providers.spec.ts index 79366a47167..343fbd03d9d 100644 --- a/libs/common/src/state-migrations/migrations/53-migrate-device-trust-crypto-svc-to-state-providers.spec.ts +++ b/libs/common/src/state-migrations/migrations/53-migrate-device-trust-svc-to-state-providers.spec.ts @@ -5,9 +5,9 @@ import { mockMigrationHelper } from "../migration-helper.spec"; import { DEVICE_KEY, - DeviceTrustCryptoServiceStateProviderMigrator, + DeviceTrustServiceStateProviderMigrator, SHOULD_TRUST_DEVICE, -} from "./53-migrate-device-trust-crypto-svc-to-state-providers"; +} from "./53-migrate-device-trust-svc-to-state-providers"; // Represents data in state service pre-migration function preMigrationJson() { @@ -79,14 +79,14 @@ function rollbackJSON() { }; } -describe("DeviceTrustCryptoServiceStateProviderMigrator", () => { +describe("DeviceTrustServiceStateProviderMigrator", () => { let helper: MockProxy; - let sut: DeviceTrustCryptoServiceStateProviderMigrator; + let sut: DeviceTrustServiceStateProviderMigrator; describe("migrate", () => { beforeEach(() => { helper = mockMigrationHelper(preMigrationJson(), 52); - sut = new DeviceTrustCryptoServiceStateProviderMigrator(52, 53); + sut = new DeviceTrustServiceStateProviderMigrator(52, 53); }); // it should remove deviceKey and trustDeviceChoiceForDecryption from all accounts @@ -126,7 +126,7 @@ describe("DeviceTrustCryptoServiceStateProviderMigrator", () => { describe("rollback", () => { beforeEach(() => { helper = mockMigrationHelper(rollbackJSON(), 53); - sut = new DeviceTrustCryptoServiceStateProviderMigrator(52, 53); + sut = new DeviceTrustServiceStateProviderMigrator(52, 53); }); it("should null out newly migrated entries in state provider framework", async () => { diff --git a/libs/common/src/state-migrations/migrations/53-migrate-device-trust-crypto-svc-to-state-providers.ts b/libs/common/src/state-migrations/migrations/53-migrate-device-trust-svc-to-state-providers.ts similarity index 94% rename from libs/common/src/state-migrations/migrations/53-migrate-device-trust-crypto-svc-to-state-providers.ts rename to libs/common/src/state-migrations/migrations/53-migrate-device-trust-svc-to-state-providers.ts index e19c7b3fa5a..b6d2c19b156 100644 --- a/libs/common/src/state-migrations/migrations/53-migrate-device-trust-crypto-svc-to-state-providers.ts +++ b/libs/common/src/state-migrations/migrations/53-migrate-device-trust-svc-to-state-providers.ts @@ -16,7 +16,7 @@ type ExpectedAccountType = { }; export const DEVICE_KEY: KeyDefinitionLike = { - key: "deviceKey", // matches KeyDefinition.key in DeviceTrustCryptoService + key: "deviceKey", // matches KeyDefinition.key in DeviceTrustService stateDefinition: { name: "deviceTrust", // matches StateDefinition.name in StateDefinitions }, @@ -29,7 +29,7 @@ export const SHOULD_TRUST_DEVICE: KeyDefinitionLike = { }, }; -export class DeviceTrustCryptoServiceStateProviderMigrator extends Migrator<52, 53> { +export class DeviceTrustServiceStateProviderMigrator extends Migrator<52, 53> { async migrate(helper: MigrationHelper): Promise { const accounts = await helper.getAccounts(); async function migrateAccount(userId: string, account: ExpectedAccountType): Promise { diff --git a/libs/common/src/state-migrations/migrations/58-remove-refresh-token-migrated-state-provider-flag.ts b/libs/common/src/state-migrations/migrations/58-remove-refresh-token-migrated-state-provider-flag.ts index 9c6d3776fe8..1fb36092677 100644 --- a/libs/common/src/state-migrations/migrations/58-remove-refresh-token-migrated-state-provider-flag.ts +++ b/libs/common/src/state-migrations/migrations/58-remove-refresh-token-migrated-state-provider-flag.ts @@ -4,7 +4,7 @@ import { IRREVERSIBLE, Migrator } from "../migrator"; type ExpectedAccountType = NonNullable; export const REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE: KeyDefinitionLike = { - key: "refreshTokenMigratedToSecureStorage", // matches KeyDefinition.key in DeviceTrustCryptoService + key: "refreshTokenMigratedToSecureStorage", // matches KeyDefinition.key stateDefinition: { name: "token", // matches StateDefinition.name in StateDefinitions }, From e89c82defeb9d5bfdbb1ae6c532e0041a2fb6986 Mon Sep 17 00:00:00 2001 From: Will Martin Date: Wed, 24 Apr 2024 14:52:29 -0400 Subject: [PATCH 057/110] [CL-236] Card component (#8900) * add card component; adjust section margin on small screens --- libs/components/src/card/card.component.ts | 15 +++++ libs/components/src/card/card.stories.ts | 62 +++++++++++++++++++ libs/components/src/card/index.ts | 1 + libs/components/src/index.ts | 1 + .../src/section/section.component.ts | 2 +- .../components/src/section/section.stories.ts | 2 +- 6 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 libs/components/src/card/card.component.ts create mode 100644 libs/components/src/card/card.stories.ts create mode 100644 libs/components/src/card/index.ts diff --git a/libs/components/src/card/card.component.ts b/libs/components/src/card/card.component.ts new file mode 100644 index 00000000000..da61d536642 --- /dev/null +++ b/libs/components/src/card/card.component.ts @@ -0,0 +1,15 @@ +import { CommonModule } from "@angular/common"; +import { ChangeDetectionStrategy, Component } from "@angular/core"; + +@Component({ + selector: "bit-card", + standalone: true, + imports: [CommonModule], + template: ``, + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + class: + "tw-box-border tw-block tw-bg-background tw-text-main tw-border-solid tw-border-b tw-border-0 tw-border-b-secondary-300 tw-rounded-lg tw-py-4 tw-px-3", + }, +}) +export class CardComponent {} diff --git a/libs/components/src/card/card.stories.ts b/libs/components/src/card/card.stories.ts new file mode 100644 index 00000000000..702a8aeb631 --- /dev/null +++ b/libs/components/src/card/card.stories.ts @@ -0,0 +1,62 @@ +import { Meta, StoryObj, componentWrapperDecorator, moduleMetadata } from "@storybook/angular"; + +import { SectionComponent } from "../section"; +import { TypographyModule } from "../typography"; + +import { CardComponent } from "./card.component"; + +export default { + title: "Component Library/Card", + component: CardComponent, + decorators: [ + moduleMetadata({ + imports: [TypographyModule, SectionComponent], + }), + componentWrapperDecorator( + (story) => `
${story}
`, + ), + ], +} as Meta; + +type Story = StoryObj; + +/** Cards are presentational containers. */ +export const Default: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + +

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras vitae congue risus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nunc elementum odio nibh, eget pellentesque sem ornare vitae. Etiam vel ante et velit fringilla egestas a sed sem. Fusce molestie nisl et nisi accumsan dapibus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Sed eu risus ex.

+
+ `, + }), +}; + +/** Cards are often paired with [Sections](/docs/component-library-section--docs). */ +export const WithinSections: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + +

Bar

+ +

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras vitae congue risus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nunc elementum odio nibh, eget pellentesque sem ornare vitae. Etiam vel ante et velit fringilla egestas a sed sem. Fusce molestie nisl et nisi accumsan dapibus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Sed eu risus ex.

+
+
+ + +

Bar

+ +

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras vitae congue risus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nunc elementum odio nibh, eget pellentesque sem ornare vitae. Etiam vel ante et velit fringilla egestas a sed sem. Fusce molestie nisl et nisi accumsan dapibus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Sed eu risus ex.

+
+
+ + +

Bar

+ +

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras vitae congue risus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nunc elementum odio nibh, eget pellentesque sem ornare vitae. Etiam vel ante et velit fringilla egestas a sed sem. Fusce molestie nisl et nisi accumsan dapibus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Sed eu risus ex.

+
+
+ `, + }), +}; diff --git a/libs/components/src/card/index.ts b/libs/components/src/card/index.ts new file mode 100644 index 00000000000..8151bac4c8b --- /dev/null +++ b/libs/components/src/card/index.ts @@ -0,0 +1 @@ +export * from "./card.component"; diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index 527d5f36153..36185911a6b 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -7,6 +7,7 @@ export * from "./breadcrumbs"; export * from "./button"; export { ButtonType } from "./shared/button-like.abstraction"; export * from "./callout"; +export * from "./card"; export * from "./checkbox"; export * from "./color-password"; export * from "./container"; diff --git a/libs/components/src/section/section.component.ts b/libs/components/src/section/section.component.ts index a681dcf7d95..a60e232eec7 100644 --- a/libs/components/src/section/section.component.ts +++ b/libs/components/src/section/section.component.ts @@ -6,7 +6,7 @@ import { Component } from "@angular/core"; standalone: true, imports: [CommonModule], template: ` -
+
`, diff --git a/libs/components/src/section/section.stories.ts b/libs/components/src/section/section.stories.ts index fb9948e9bec..65b6a67d476 100644 --- a/libs/components/src/section/section.stories.ts +++ b/libs/components/src/section/section.stories.ts @@ -17,7 +17,7 @@ export default { type Story = StoryObj; -/** Sections are simple containers that apply a bottom margin. They often contain a heading. */ +/** Sections are simple containers that apply a responsive bottom margin. They often contain a heading. */ export const Default: Story = { render: (args) => ({ props: args, From a8ba48898b13222b8742d9b0d63ca238368b99ff Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Wed, 24 Apr 2024 16:29:00 -0400 Subject: [PATCH 058/110] Use new endpoint to determine SM standalone (#8904) --- .../organizations/members/people.component.ts | 13 +++++++------ .../src/services/jslib-services.module.ts | 1 - .../billilng-api.service.abstraction.ts | 5 ++++- .../abstractions/organization-billing.service.ts | 2 -- .../organization-billing-metadata.response.ts | 10 ++++++++++ .../src/billing/services/billing-api.service.ts | 16 ++++++++++++++++ .../services/organization-billing.service.ts | 15 --------------- 7 files changed, 37 insertions(+), 25 deletions(-) create mode 100644 libs/common/src/billing/models/response/organization-billing-metadata.response.ts diff --git a/apps/web/src/app/admin-console/organizations/members/people.component.ts b/apps/web/src/app/admin-console/organizations/members/people.component.ts index 0df247d7b09..af04d83c34d 100644 --- a/apps/web/src/app/admin-console/organizations/members/people.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/people.component.ts @@ -37,7 +37,7 @@ import { import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request"; -import { OrganizationBillingServiceAbstraction as OrganizationBillingService } from "@bitwarden/common/billing/abstractions/organization-billing.service"; +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction"; import { ProductType } from "@bitwarden/common/enums"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -121,7 +121,7 @@ export class PeopleComponent extends BasePeopleComponent { private groupService: GroupService, private collectionService: CollectionService, organizationManagementPreferencesService: OrganizationManagementPreferencesService, - private organizationBillingService: OrganizationBillingService, + private billingApiService: BillingApiServiceAbstraction, ) { super( apiService, @@ -190,10 +190,11 @@ export class PeopleComponent extends BasePeopleComponent { .find((p) => p.organizationId === this.organization.id); this.orgResetPasswordPolicyEnabled = resetPasswordPolicy?.enabled; - this.orgIsOnSecretsManagerStandalone = - await this.organizationBillingService.isOnSecretsManagerStandalone( - this.organization.id, - ); + const billingMetadata = await this.billingApiService.getOrganizationBillingMetadata( + this.organization.id, + ); + + this.orgIsOnSecretsManagerStandalone = billingMetadata.isOnSecretsManagerStandalone; await this.load(); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index aabf823c0be..42879a84241 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1058,7 +1058,6 @@ const safeProviders: SafeProvider[] = [ useClass: OrganizationBillingService, deps: [ ApiServiceAbstraction, - BillingApiServiceAbstraction, CryptoServiceAbstraction, EncryptService, I18nServiceAbstraction, diff --git a/libs/common/src/billing/abstractions/billilng-api.service.abstraction.ts b/libs/common/src/billing/abstractions/billilng-api.service.abstraction.ts index 15f0d4b551b..063b3c370b0 100644 --- a/libs/common/src/billing/abstractions/billilng-api.service.abstraction.ts +++ b/libs/common/src/billing/abstractions/billilng-api.service.abstraction.ts @@ -1,4 +1,5 @@ import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request"; +import { OrganizationBillingMetadataResponse } from "../../billing/models/response/organization-billing-metadata.response"; import { OrganizationBillingStatusResponse } from "../../billing/models/response/organization-billing-status.response"; import { OrganizationSubscriptionResponse } from "../../billing/models/response/organization-subscription.response"; import { PlanResponse } from "../../billing/models/response/plan.response"; @@ -12,13 +13,15 @@ export abstract class BillingApiServiceAbstraction { organizationId: string, request: SubscriptionCancellationRequest, ) => Promise; - cancelPremiumUserSubscription: (request: SubscriptionCancellationRequest) => Promise; createClientOrganization: ( providerId: string, request: CreateClientOrganizationRequest, ) => Promise; getBillingStatus: (id: string) => Promise; + getOrganizationBillingMetadata: ( + organizationId: string, + ) => Promise; getOrganizationSubscription: ( organizationId: string, ) => Promise; diff --git a/libs/common/src/billing/abstractions/organization-billing.service.ts b/libs/common/src/billing/abstractions/organization-billing.service.ts index 0917025eec1..d19724b600a 100644 --- a/libs/common/src/billing/abstractions/organization-billing.service.ts +++ b/libs/common/src/billing/abstractions/organization-billing.service.ts @@ -41,8 +41,6 @@ export type SubscriptionInformation = { }; export abstract class OrganizationBillingServiceAbstraction { - isOnSecretsManagerStandalone: (organizationId: string) => Promise; - purchaseSubscription: (subscription: SubscriptionInformation) => Promise; startFree: (subscription: SubscriptionInformation) => Promise; diff --git a/libs/common/src/billing/models/response/organization-billing-metadata.response.ts b/libs/common/src/billing/models/response/organization-billing-metadata.response.ts new file mode 100644 index 00000000000..33d7907fa88 --- /dev/null +++ b/libs/common/src/billing/models/response/organization-billing-metadata.response.ts @@ -0,0 +1,10 @@ +import { BaseResponse } from "../../../models/response/base.response"; + +export class OrganizationBillingMetadataResponse extends BaseResponse { + isOnSecretsManagerStandalone: boolean; + + constructor(response: any) { + super(response); + this.isOnSecretsManagerStandalone = this.getResponseProperty("IsOnSecretsManagerStandalone"); + } +} diff --git a/libs/common/src/billing/services/billing-api.service.ts b/libs/common/src/billing/services/billing-api.service.ts index 1c119b971da..d21c1c9046a 100644 --- a/libs/common/src/billing/services/billing-api.service.ts +++ b/libs/common/src/billing/services/billing-api.service.ts @@ -1,3 +1,5 @@ +import { OrganizationBillingMetadataResponse } from "@bitwarden/common/billing/models/response/organization-billing-metadata.response"; + import { ApiService } from "../../abstractions/api.service"; import { BillingApiServiceAbstraction } from "../../billing/abstractions/billilng-api.service.abstraction"; import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request"; @@ -53,6 +55,20 @@ export class BillingApiService implements BillingApiServiceAbstraction { return new OrganizationBillingStatusResponse(r); } + async getOrganizationBillingMetadata( + organizationId: string, + ): Promise { + const r = await this.apiService.send( + "GET", + "/organizations/" + organizationId + "/billing/metadata", + null, + true, + true, + ); + + return new OrganizationBillingMetadataResponse(r); + } + async getOrganizationSubscription( organizationId: string, ): Promise { diff --git a/libs/common/src/billing/services/organization-billing.service.ts b/libs/common/src/billing/services/organization-billing.service.ts index fb2084bb6a7..6b326472c97 100644 --- a/libs/common/src/billing/services/organization-billing.service.ts +++ b/libs/common/src/billing/services/organization-billing.service.ts @@ -9,7 +9,6 @@ import { I18nService } from "../../platform/abstractions/i18n.service"; import { EncString } from "../../platform/models/domain/enc-string"; import { OrgKey } from "../../types/key"; import { SyncService } from "../../vault/abstractions/sync/sync.service.abstraction"; -import { BillingApiServiceAbstraction as BillingApiService } from "../abstractions/billilng-api.service.abstraction"; import { OrganizationBillingServiceAbstraction, OrganizationInformation, @@ -29,7 +28,6 @@ interface OrganizationKeys { export class OrganizationBillingService implements OrganizationBillingServiceAbstraction { constructor( private apiService: ApiService, - private billingApiService: BillingApiService, private cryptoService: CryptoService, private encryptService: EncryptService, private i18nService: I18nService, @@ -37,19 +35,6 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs private syncService: SyncService, ) {} - async isOnSecretsManagerStandalone(organizationId: string): Promise { - const response = await this.billingApiService.getOrganizationSubscription(organizationId); - if (response.customerDiscount?.id === "sm-standalone") { - const productIds = response.subscription.items.map((item) => item.productId); - return ( - response.customerDiscount?.appliesTo.filter((appliesToProductId) => - productIds.includes(appliesToProductId), - ).length > 0 - ); - } - return false; - } - async purchaseSubscription(subscription: SubscriptionInformation): Promise { const request = new OrganizationCreateRequest(); From 3f4adff2c502a7a47d6cc1e2b867c176c57f12df Mon Sep 17 00:00:00 2001 From: Jake Fink Date: Wed, 24 Apr 2024 16:32:18 -0400 Subject: [PATCH 059/110] set auto key on command in cli (#8905) --- apps/cli/src/bw.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 8163aa29452..be3ad9ea0e1 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -77,6 +77,7 @@ import { MigrationBuilderService } from "@bitwarden/common/platform/services/mig import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; import { StateService } from "@bitwarden/common/platform/services/state.service"; import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; +import { UserKeyInitService } from "@bitwarden/common/platform/services/user-key-init.service"; import { ActiveUserStateProvider, DerivedStateProvider, @@ -233,6 +234,7 @@ export class Main { biometricStateService: BiometricStateService; billingAccountProfileStateService: BillingAccountProfileStateService; providerApiService: ProviderApiServiceAbstraction; + userKeyInitService: UserKeyInitService; constructor() { let p = null; @@ -692,6 +694,12 @@ export class Main { ); this.providerApiService = new ProviderApiService(this.apiService); + + this.userKeyInitService = new UserKeyInitService( + this.accountService, + this.cryptoService, + this.logService, + ); } async run() { @@ -735,6 +743,7 @@ export class Main { this.containerService.attachToGlobal(global); await this.i18nService.init(); this.twoFactorService.init(); + this.userKeyInitService.listenForActiveUserChangesToSetUserKey(); } } From a6755f5f202527e558ccc3cb5cb5c7b0921f3e39 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Wed, 24 Apr 2024 16:54:16 -0400 Subject: [PATCH 060/110] [PM-7687] Fix `reloadPopup` Recursion (#8902) * Fix Message Sending Recursion * Remove Change That Didn't Help * Prefer `isExternalMessage` Guard * Rollback Compare Change --------- Co-authored-by: Cesar Gonzalez --- apps/browser/src/background/runtime.background.ts | 6 ++++-- .../local-backed-session-storage.service.ts | 13 ++++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index f457889e963..294346fe9f9 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -10,7 +10,7 @@ import { SystemService } from "@bitwarden/common/platform/abstractions/system.se import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherType } from "@bitwarden/common/vault/enums"; -import { MessageListener } from "../../../../libs/common/src/platform/messaging"; +import { MessageListener, isExternalMessage } from "../../../../libs/common/src/platform/messaging"; import { closeUnlockPopout, openSsoAuthResultPopout, @@ -266,7 +266,9 @@ export default class RuntimeBackground { break; } case "reloadPopup": - this.messagingService.send("reloadPopup"); + if (isExternalMessage(msg)) { + this.messagingService.send("reloadPopup"); + } break; case "emailVerificationRequired": this.messagingService.send("showDialog", { diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.ts index 5432e8d918b..0fa359181dc 100644 --- a/apps/browser/src/platform/services/local-backed-session-storage.service.ts +++ b/apps/browser/src/platform/services/local-backed-session-storage.service.ts @@ -92,9 +92,16 @@ export class LocalBackedSessionStorageService // This is for observation purposes only. At some point, we don't want to write to local session storage if the value is the same. if (this.platformUtilsService.isDev()) { const existingValue = this.cache[key] as T; - if (this.compareValues(existingValue, obj)) { - this.logService.warning(`Possible unnecessary write to local session storage. Key: ${key}`); - this.logService.warning(obj as any); + try { + if (this.compareValues(existingValue, obj)) { + this.logService.warning( + `Possible unnecessary write to local session storage. Key: ${key}`, + ); + this.logService.warning(obj as any); + } + } catch (err) { + this.logService.warning(`Error while comparing values for key: ${key}`); + this.logService.warning(err); } } From dba910d0b946176344f13f55e081b3a7965ee9f4 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Wed, 24 Apr 2024 23:41:35 +0200 Subject: [PATCH 061/110] Create and use `safeGetString()` instead of `instanceof` checks to determine type (#8906) `safeGetString` takes a `string` or `EncString` and return the appropiate value based on it's type Co-authored-by: Daniel James Smith --- libs/common/src/models/export/card.export.ts | 23 +++----- .../common/src/models/export/cipher.export.ts | 22 ++----- .../src/models/export/collection.export.ts | 8 +-- .../models/export/fido2-credential.export.ts | 41 +++++-------- libs/common/src/models/export/field.export.ts | 11 ++-- .../common/src/models/export/folder.export.ts | 8 +-- .../src/models/export/identity.export.ts | 59 +++++++------------ .../src/models/export/login-uri.export.ts | 8 +-- libs/common/src/models/export/login.export.ts | 19 ++---- .../models/export/password-history.export.ts | 8 +-- libs/common/src/models/export/utils.ts | 12 ++++ 11 files changed, 82 insertions(+), 137 deletions(-) create mode 100644 libs/common/src/models/export/utils.ts diff --git a/libs/common/src/models/export/card.export.ts b/libs/common/src/models/export/card.export.ts index 55bb3a7be14..151b447e864 100644 --- a/libs/common/src/models/export/card.export.ts +++ b/libs/common/src/models/export/card.export.ts @@ -2,6 +2,8 @@ import { EncString } from "../../platform/models/domain/enc-string"; import { Card as CardDomain } from "../../vault/models/domain/card"; import { CardView } from "../../vault/models/view/card.view"; +import { safeGetString } from "./utils"; + export class CardExport { static template(): CardExport { const req = new CardExport(); @@ -46,20 +48,11 @@ export class CardExport { return; } - if (o instanceof CardView) { - this.cardholderName = o.cardholderName; - this.brand = o.brand; - this.number = o.number; - this.expMonth = o.expMonth; - this.expYear = o.expYear; - this.code = o.code; - } else { - this.cardholderName = o.cardholderName?.encryptedString; - this.brand = o.brand?.encryptedString; - this.number = o.number?.encryptedString; - this.expMonth = o.expMonth?.encryptedString; - this.expYear = o.expYear?.encryptedString; - this.code = o.code?.encryptedString; - } + this.cardholderName = safeGetString(o.cardholderName); + this.brand = safeGetString(o.brand); + this.number = safeGetString(o.number); + this.expMonth = safeGetString(o.expMonth); + this.expYear = safeGetString(o.expYear); + this.code = safeGetString(o.code); } } diff --git a/libs/common/src/models/export/cipher.export.ts b/libs/common/src/models/export/cipher.export.ts index 3ae6c9757dd..64583f7fcef 100644 --- a/libs/common/src/models/export/cipher.export.ts +++ b/libs/common/src/models/export/cipher.export.ts @@ -10,6 +10,7 @@ import { IdentityExport } from "./identity.export"; import { LoginExport } from "./login.export"; import { PasswordHistoryExport } from "./password-history.export"; import { SecureNoteExport } from "./secure-note.export"; +import { safeGetString } from "./utils"; export class CipherExport { static template(): CipherExport { @@ -145,23 +146,16 @@ export class CipherExport { this.type = o.type; this.reprompt = o.reprompt; - if (o instanceof CipherView) { - this.name = o.name; - this.notes = o.notes; - } else { - this.name = o.name?.encryptedString; - this.notes = o.notes?.encryptedString; + this.name = safeGetString(o.name); + this.notes = safeGetString(o.notes); + if ("key" in o) { this.key = o.key?.encryptedString; } this.favorite = o.favorite; if (o.fields != null) { - if (o instanceof CipherView) { - this.fields = o.fields.map((f) => new FieldExport(f)); - } else { - this.fields = o.fields.map((f) => new FieldExport(f)); - } + this.fields = o.fields.map((f) => new FieldExport(f)); } switch (o.type) { @@ -180,11 +174,7 @@ export class CipherExport { } if (o.passwordHistory != null) { - if (o instanceof CipherView) { - this.passwordHistory = o.passwordHistory.map((ph) => new PasswordHistoryExport(ph)); - } else { - this.passwordHistory = o.passwordHistory.map((ph) => new PasswordHistoryExport(ph)); - } + this.passwordHistory = o.passwordHistory.map((ph) => new PasswordHistoryExport(ph)); } this.creationDate = o.creationDate; diff --git a/libs/common/src/models/export/collection.export.ts b/libs/common/src/models/export/collection.export.ts index 48251d581f9..c94d5bc0ca5 100644 --- a/libs/common/src/models/export/collection.export.ts +++ b/libs/common/src/models/export/collection.export.ts @@ -2,6 +2,8 @@ import { EncString } from "../../platform/models/domain/enc-string"; import { Collection as CollectionDomain } from "../../vault/models/domain/collection"; import { CollectionView } from "../../vault/models/view/collection.view"; +import { safeGetString } from "./utils"; + export class CollectionExport { static template(): CollectionExport { const req = new CollectionExport(); @@ -36,11 +38,7 @@ export class CollectionExport { // Use build method instead of ctor so that we can control order of JSON stringify for pretty print build(o: CollectionView | CollectionDomain) { this.organizationId = o.organizationId; - if (o instanceof CollectionView) { - this.name = o.name; - } else { - this.name = o.name?.encryptedString; - } + this.name = safeGetString(o.name); this.externalId = o.externalId; } } diff --git a/libs/common/src/models/export/fido2-credential.export.ts b/libs/common/src/models/export/fido2-credential.export.ts index d41b7d67c93..4c60d148db0 100644 --- a/libs/common/src/models/export/fido2-credential.export.ts +++ b/libs/common/src/models/export/fido2-credential.export.ts @@ -2,6 +2,8 @@ import { EncString } from "../../platform/models/domain/enc-string"; import { Fido2Credential } from "../../vault/models/domain/fido2-credential"; import { Fido2CredentialView } from "../../vault/models/view/fido2-credential.view"; +import { safeGetString } from "./utils"; + /** * Represents format of Fido2 Credentials in JSON exports. */ @@ -99,33 +101,18 @@ export class Fido2CredentialExport { return; } - if (o instanceof Fido2CredentialView) { - this.credentialId = o.credentialId; - this.keyType = o.keyType; - this.keyAlgorithm = o.keyAlgorithm; - this.keyCurve = o.keyCurve; - this.keyValue = o.keyValue; - this.rpId = o.rpId; - this.userHandle = o.userHandle; - this.userName = o.userName; - this.counter = String(o.counter); - this.rpName = o.rpName; - this.userDisplayName = o.userDisplayName; - this.discoverable = String(o.discoverable); - } else { - this.credentialId = o.credentialId?.encryptedString; - this.keyType = o.keyType?.encryptedString; - this.keyAlgorithm = o.keyAlgorithm?.encryptedString; - this.keyCurve = o.keyCurve?.encryptedString; - this.keyValue = o.keyValue?.encryptedString; - this.rpId = o.rpId?.encryptedString; - this.userHandle = o.userHandle?.encryptedString; - this.userName = o.userName?.encryptedString; - this.counter = o.counter?.encryptedString; - this.rpName = o.rpName?.encryptedString; - this.userDisplayName = o.userDisplayName?.encryptedString; - this.discoverable = o.discoverable?.encryptedString; - } + this.credentialId = safeGetString(o.credentialId); + this.keyType = safeGetString(o.keyType); + this.keyAlgorithm = safeGetString(o.keyAlgorithm); + this.keyCurve = safeGetString(o.keyCurve); + this.keyValue = safeGetString(o.keyValue); + this.rpId = safeGetString(o.rpId); + this.userHandle = safeGetString(o.userHandle); + this.userName = safeGetString(o.userName); + this.counter = safeGetString(String(o.counter)); + this.rpName = safeGetString(o.rpName); + this.userDisplayName = safeGetString(o.userDisplayName); + this.discoverable = safeGetString(String(o.discoverable)); this.creationDate = o.creationDate; } } diff --git a/libs/common/src/models/export/field.export.ts b/libs/common/src/models/export/field.export.ts index 098249312c2..5ba341af617 100644 --- a/libs/common/src/models/export/field.export.ts +++ b/libs/common/src/models/export/field.export.ts @@ -3,6 +3,8 @@ import { FieldType, LinkedIdType } from "../../vault/enums"; import { Field as FieldDomain } from "../../vault/models/domain/field"; import { FieldView } from "../../vault/models/view/field.view"; +import { safeGetString } from "./utils"; + export class FieldExport { static template(): FieldExport { const req = new FieldExport(); @@ -38,13 +40,8 @@ export class FieldExport { return; } - if (o instanceof FieldView) { - this.name = o.name; - this.value = o.value; - } else { - this.name = o.name?.encryptedString; - this.value = o.value?.encryptedString; - } + this.name = safeGetString(o.name); + this.value = safeGetString(o.value); this.type = o.type; this.linkedId = o.linkedId; } diff --git a/libs/common/src/models/export/folder.export.ts b/libs/common/src/models/export/folder.export.ts index 4015034ebe5..6a2a63a77d5 100644 --- a/libs/common/src/models/export/folder.export.ts +++ b/libs/common/src/models/export/folder.export.ts @@ -2,6 +2,8 @@ import { EncString } from "../../platform/models/domain/enc-string"; import { Folder as FolderDomain } from "../../vault/models/domain/folder"; import { FolderView } from "../../vault/models/view/folder.view"; +import { safeGetString } from "./utils"; + export class FolderExport { static template(): FolderExport { const req = new FolderExport(); @@ -23,10 +25,6 @@ export class FolderExport { // Use build method instead of ctor so that we can control order of JSON stringify for pretty print build(o: FolderView | FolderDomain) { - if (o instanceof FolderView) { - this.name = o.name; - } else { - this.name = o.name?.encryptedString; - } + this.name = safeGetString(o.name); } } diff --git a/libs/common/src/models/export/identity.export.ts b/libs/common/src/models/export/identity.export.ts index 2eb9c8364f2..6722333d79f 100644 --- a/libs/common/src/models/export/identity.export.ts +++ b/libs/common/src/models/export/identity.export.ts @@ -2,6 +2,8 @@ import { EncString } from "../../platform/models/domain/enc-string"; import { Identity as IdentityDomain } from "../../vault/models/domain/identity"; import { IdentityView } from "../../vault/models/view/identity.view"; +import { safeGetString } from "./utils"; + export class IdentityExport { static template(): IdentityExport { const req = new IdentityExport(); @@ -94,44 +96,23 @@ export class IdentityExport { return; } - if (o instanceof IdentityView) { - this.title = o.title; - this.firstName = o.firstName; - this.middleName = o.middleName; - this.lastName = o.lastName; - this.address1 = o.address1; - this.address2 = o.address2; - this.address3 = o.address3; - this.city = o.city; - this.state = o.state; - this.postalCode = o.postalCode; - this.country = o.country; - this.company = o.company; - this.email = o.email; - this.phone = o.phone; - this.ssn = o.ssn; - this.username = o.username; - this.passportNumber = o.passportNumber; - this.licenseNumber = o.licenseNumber; - } else { - this.title = o.title?.encryptedString; - this.firstName = o.firstName?.encryptedString; - this.middleName = o.middleName?.encryptedString; - this.lastName = o.lastName?.encryptedString; - this.address1 = o.address1?.encryptedString; - this.address2 = o.address2?.encryptedString; - this.address3 = o.address3?.encryptedString; - this.city = o.city?.encryptedString; - this.state = o.state?.encryptedString; - this.postalCode = o.postalCode?.encryptedString; - this.country = o.country?.encryptedString; - this.company = o.company?.encryptedString; - this.email = o.email?.encryptedString; - this.phone = o.phone?.encryptedString; - this.ssn = o.ssn?.encryptedString; - this.username = o.username?.encryptedString; - this.passportNumber = o.passportNumber?.encryptedString; - this.licenseNumber = o.licenseNumber?.encryptedString; - } + this.title = safeGetString(o.title); + this.firstName = safeGetString(o.firstName); + this.middleName = safeGetString(o.middleName); + this.lastName = safeGetString(o.lastName); + this.address1 = safeGetString(o.address1); + this.address2 = safeGetString(o.address2); + this.address3 = safeGetString(o.address3); + this.city = safeGetString(o.city); + this.state = safeGetString(o.state); + this.postalCode = safeGetString(o.postalCode); + this.country = safeGetString(o.country); + this.company = safeGetString(o.company); + this.email = safeGetString(o.email); + this.phone = safeGetString(o.phone); + this.ssn = safeGetString(o.ssn); + this.username = safeGetString(o.username); + this.passportNumber = safeGetString(o.passportNumber); + this.licenseNumber = safeGetString(o.licenseNumber); } } diff --git a/libs/common/src/models/export/login-uri.export.ts b/libs/common/src/models/export/login-uri.export.ts index 83a7d25eff8..a0534460612 100644 --- a/libs/common/src/models/export/login-uri.export.ts +++ b/libs/common/src/models/export/login-uri.export.ts @@ -3,6 +3,8 @@ import { EncString } from "../../platform/models/domain/enc-string"; import { LoginUri as LoginUriDomain } from "../../vault/models/domain/login-uri"; import { LoginUriView } from "../../vault/models/view/login-uri.view"; +import { safeGetString } from "./utils"; + export class LoginUriExport { static template(): LoginUriExport { const req = new LoginUriExport(); @@ -33,10 +35,8 @@ export class LoginUriExport { return; } - if (o instanceof LoginUriView) { - this.uri = o.uri; - } else { - this.uri = o.uri?.encryptedString; + this.uri = safeGetString(o.uri); + if ("uriChecksum" in o) { this.uriChecksum = o.uriChecksum?.encryptedString; } this.match = o.match; diff --git a/libs/common/src/models/export/login.export.ts b/libs/common/src/models/export/login.export.ts index a5d9348c2ca..6982d386c32 100644 --- a/libs/common/src/models/export/login.export.ts +++ b/libs/common/src/models/export/login.export.ts @@ -4,6 +4,7 @@ import { LoginView } from "../../vault/models/view/login.view"; import { Fido2CredentialExport } from "./fido2-credential.export"; import { LoginUriExport } from "./login-uri.export"; +import { safeGetString } from "./utils"; export class LoginExport { static template(): LoginExport { @@ -53,25 +54,15 @@ export class LoginExport { } if (o.uris != null) { - if (o instanceof LoginView) { - this.uris = o.uris.map((u) => new LoginUriExport(u)); - } else { - this.uris = o.uris.map((u) => new LoginUriExport(u)); - } + this.uris = o.uris.map((u) => new LoginUriExport(u)); } if (o.fido2Credentials != null) { this.fido2Credentials = o.fido2Credentials.map((key) => new Fido2CredentialExport(key)); } - if (o instanceof LoginView) { - this.username = o.username; - this.password = o.password; - this.totp = o.totp; - } else { - this.username = o.username?.encryptedString; - this.password = o.password?.encryptedString; - this.totp = o.totp?.encryptedString; - } + this.username = safeGetString(o.username); + this.password = safeGetString(o.password); + this.totp = safeGetString(o.totp); } } diff --git a/libs/common/src/models/export/password-history.export.ts b/libs/common/src/models/export/password-history.export.ts index 0bdbc6697ac..fff22de8def 100644 --- a/libs/common/src/models/export/password-history.export.ts +++ b/libs/common/src/models/export/password-history.export.ts @@ -2,6 +2,8 @@ import { EncString } from "../../platform/models/domain/enc-string"; import { Password } from "../../vault/models/domain/password"; import { PasswordHistoryView } from "../../vault/models/view/password-history.view"; +import { safeGetString } from "./utils"; + export class PasswordHistoryExport { static template(): PasswordHistoryExport { const req = new PasswordHistoryExport(); @@ -30,11 +32,7 @@ export class PasswordHistoryExport { return; } - if (o instanceof PasswordHistoryView) { - this.password = o.password; - } else { - this.password = o.password?.encryptedString; - } + this.password = safeGetString(o.password); this.lastUsedDate = o.lastUsedDate; } } diff --git a/libs/common/src/models/export/utils.ts b/libs/common/src/models/export/utils.ts new file mode 100644 index 00000000000..630b4898503 --- /dev/null +++ b/libs/common/src/models/export/utils.ts @@ -0,0 +1,12 @@ +import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; + +export function safeGetString(value: string | EncString) { + if (value == null) { + return null; + } + + if (typeof value == "string") { + return value; + } + return value?.encryptedString; +} From 1e4158fd878994093710db9d072f356a9fc8bdc8 Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Thu, 25 Apr 2024 11:26:01 -0700 Subject: [PATCH 062/110] [PM-5735] Create kdf Service (#8715) * key connector migration initial * migrator complete * fix dependencies * finalized tests * fix deps and sync main * clean up definition file * fixing tests * fixed tests * fixing CLI, Browser, Desktop builds * fixed factory options * reverting exports * implemented UserKeyDefinition clearOn * Initial Kdf Service Changes * rename and account setting kdfconfig * fixing tests and renaming migration * fixed DI ordering for browser * rename and fix DI * Clean up Migrations * fixing migrations * begin data structure changes for kdf config * Make KDF more type safe; co-author: jlf0dev * fixing tests * Fixed CLI login and comments * set now accepts userId and test updates --------- Co-authored-by: Jake Fink --- .../kdf-config-service.factory.ts | 28 ++++ .../login-strategy-service.factory.ts | 5 +- .../pin-crypto-service.factory.ts | 6 +- .../user-verification-service.factory.ts | 5 +- apps/browser/src/auth/popup/lock.component.ts | 3 + .../browser/src/background/main.background.ts | 14 +- .../crypto-service.factory.ts | 14 +- .../services/browser-crypto.service.ts | 3 + apps/cli/src/auth/commands/login.command.ts | 6 +- apps/cli/src/auth/commands/unlock.command.ts | 7 +- apps/cli/src/bw.ts | 13 +- apps/cli/src/commands/serve.command.ts | 1 + apps/cli/src/program.ts | 3 + .../src/app/services/services.module.ts | 2 + apps/desktop/src/auth/lock.component.spec.ts | 5 + apps/desktop/src/auth/lock.component.ts | 3 + .../src/auth/set-password.component.ts | 3 + .../services/electron-crypto.service.spec.ts | 3 + .../services/electron-crypto.service.ts | 3 + ...rganization-user-reset-password.service.ts | 16 +- .../services/emergency-access.service.ts | 33 ++-- .../user-key-rotation.service.spec.ts | 4 + .../key-rotation/user-key-rotation.service.ts | 5 +- .../account/change-email.component.ts | 6 +- .../settings/change-password.component.ts | 3 + .../emergency-access-takeover.component.ts | 3 + .../change-kdf-confirmation.component.ts | 22 +-- .../change-kdf/change-kdf.component.html | 12 +- .../change-kdf/change-kdf.component.ts | 31 ++-- .../src/app/auth/update-password.component.ts | 3 + .../vault/individual-vault/vault.component.ts | 8 +- .../components/change-password.component.ts | 10 +- .../src/auth/components/lock.component.ts | 6 +- .../src/auth/components/register.component.ts | 9 +- .../auth/components/set-password.component.ts | 18 +-- .../src/auth/components/set-pin.component.ts | 5 +- .../components/update-password.component.ts | 6 +- .../update-temp-password.component.ts | 7 +- .../src/services/jslib-services.module.ts | 15 +- .../auth-request-login.strategy.spec.ts | 4 + .../auth-request-login.strategy.ts | 3 + .../login-strategies/login.strategy.spec.ts | 7 +- .../common/login-strategies/login.strategy.ts | 19 ++- .../password-login.strategy.spec.ts | 4 + .../password-login.strategy.ts | 3 + .../sso-login.strategy.spec.ts | 4 + .../login-strategies/sso-login.strategy.ts | 3 + .../user-api-login.strategy.spec.ts | 4 + .../user-api-login.strategy.ts | 3 + .../webauthn-login.strategy.spec.ts | 4 + .../webauthn-login.strategy.ts | 3 + .../login-strategy.service.spec.ts | 4 + .../login-strategy.service.ts | 32 ++-- .../pin-crypto.service.implementation.ts | 8 +- .../pin-crypto/pin-crypto.service.spec.ts | 9 +- .../auth/abstractions/kdf-config.service.ts | 7 + .../src/auth/models/domain/kdf-config.ts | 91 ++++++++++- .../request/set-key-connector-key.request.ts | 16 +- .../auth/services/kdf-config.service.spec.ts | 104 ++++++++++++ .../src/auth/services/kdf-config.service.ts | 41 +++++ .../auth/services/key-connector.service.ts | 10 +- .../user-verification.service.ts | 8 +- .../platform/abstractions/crypto.service.ts | 31 +--- .../abstractions/key-generation.service.ts | 3 - .../platform/abstractions/state.service.ts | 6 - .../src/platform/enums/kdf-type.enum.ts | 4 +- .../platform/services/crypto.service.spec.ts | 3 + .../src/platform/services/crypto.service.ts | 79 ++------- .../services/key-generation.service.spec.ts | 13 +- .../services/key-generation.service.ts | 5 +- .../src/platform/services/state.service.ts | 46 +----- .../src/platform/state/state-definitions.ts | 1 + libs/common/src/state-migrations/migrate.ts | 6 +- ...-move-kdf-config-to-state-provider.spec.ts | 153 ++++++++++++++++++ .../59-move-kdf-config-to-state-provider.ts | 78 +++++++++ .../src/tools/send/services/send.service.ts | 5 +- .../bitwarden-password-protected-importer.ts | 18 ++- .../src/services/base-vault-export.service.ts | 18 ++- .../individual-vault-export.service.spec.ts | 18 +-- .../individual-vault-export.service.ts | 6 +- .../src/services/org-vault-export.service.ts | 6 +- .../src/services/vault-export.service.spec.ts | 18 +-- 82 files changed, 896 insertions(+), 361 deletions(-) create mode 100644 apps/browser/src/auth/background/service-factories/kdf-config-service.factory.ts create mode 100644 libs/common/src/auth/abstractions/kdf-config.service.ts create mode 100644 libs/common/src/auth/services/kdf-config.service.spec.ts create mode 100644 libs/common/src/auth/services/kdf-config.service.ts create mode 100644 libs/common/src/state-migrations/migrations/59-move-kdf-config-to-state-provider.spec.ts create mode 100644 libs/common/src/state-migrations/migrations/59-move-kdf-config-to-state-provider.ts diff --git a/apps/browser/src/auth/background/service-factories/kdf-config-service.factory.ts b/apps/browser/src/auth/background/service-factories/kdf-config-service.factory.ts new file mode 100644 index 00000000000..eb5ba3a264c --- /dev/null +++ b/apps/browser/src/auth/background/service-factories/kdf-config-service.factory.ts @@ -0,0 +1,28 @@ +import { KdfConfigService as AbstractKdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; +import { KdfConfigService } from "@bitwarden/common/auth/services/kdf-config.service"; + +import { + FactoryOptions, + CachedServices, + factory, +} from "../../../platform/background/service-factories/factory-options"; +import { + StateProviderInitOptions, + stateProviderFactory, +} from "../../../platform/background/service-factories/state-provider.factory"; + +type KdfConfigServiceFactoryOptions = FactoryOptions; + +export type KdfConfigServiceInitOptions = KdfConfigServiceFactoryOptions & StateProviderInitOptions; + +export function kdfConfigServiceFactory( + cache: { kdfConfigService?: AbstractKdfConfigService } & CachedServices, + opts: KdfConfigServiceInitOptions, +): Promise { + return factory( + cache, + "kdfConfigService", + opts, + async () => new KdfConfigService(await stateProviderFactory(cache, opts)), + ); +} diff --git a/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts b/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts index 075ba614b7a..c4143004319 100644 --- a/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts @@ -68,6 +68,7 @@ import { deviceTrustServiceFactory, DeviceTrustServiceInitOptions, } from "./device-trust-service.factory"; +import { kdfConfigServiceFactory, KdfConfigServiceInitOptions } from "./kdf-config-service.factory"; import { keyConnectorServiceFactory, KeyConnectorServiceInitOptions, @@ -106,7 +107,8 @@ export type LoginStrategyServiceInitOptions = LoginStrategyServiceFactoryOptions AuthRequestServiceInitOptions & UserDecryptionOptionsServiceInitOptions & GlobalStateProviderInitOptions & - BillingAccountProfileStateServiceInitOptions; + BillingAccountProfileStateServiceInitOptions & + KdfConfigServiceInitOptions; export function loginStrategyServiceFactory( cache: { loginStrategyService?: LoginStrategyServiceAbstraction } & CachedServices, @@ -140,6 +142,7 @@ export function loginStrategyServiceFactory( await internalUserDecryptionOptionServiceFactory(cache, opts), await globalStateProviderFactory(cache, opts), await billingAccountProfileStateServiceFactory(cache, opts), + await kdfConfigServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/auth/background/service-factories/pin-crypto-service.factory.ts b/apps/browser/src/auth/background/service-factories/pin-crypto-service.factory.ts index f5360f48fa3..db16245f672 100644 --- a/apps/browser/src/auth/background/service-factories/pin-crypto-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/pin-crypto-service.factory.ts @@ -22,13 +22,16 @@ import { stateServiceFactory, } from "../../../platform/background/service-factories/state-service.factory"; +import { KdfConfigServiceInitOptions, kdfConfigServiceFactory } from "./kdf-config-service.factory"; + type PinCryptoServiceFactoryOptions = FactoryOptions; export type PinCryptoServiceInitOptions = PinCryptoServiceFactoryOptions & StateServiceInitOptions & CryptoServiceInitOptions & VaultTimeoutSettingsServiceInitOptions & - LogServiceInitOptions; + LogServiceInitOptions & + KdfConfigServiceInitOptions; export function pinCryptoServiceFactory( cache: { pinCryptoService?: PinCryptoServiceAbstraction } & CachedServices, @@ -44,6 +47,7 @@ export function pinCryptoServiceFactory( await cryptoServiceFactory(cache, opts), await vaultTimeoutSettingsServiceFactory(cache, opts), await logServiceFactory(cache, opts), + await kdfConfigServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/auth/background/service-factories/user-verification-service.factory.ts b/apps/browser/src/auth/background/service-factories/user-verification-service.factory.ts index a8b67b21cac..d6f9ce76241 100644 --- a/apps/browser/src/auth/background/service-factories/user-verification-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/user-verification-service.factory.ts @@ -32,6 +32,7 @@ import { } from "../../../platform/background/service-factories/state-service.factory"; import { accountServiceFactory, AccountServiceInitOptions } from "./account-service.factory"; +import { KdfConfigServiceInitOptions, kdfConfigServiceFactory } from "./kdf-config-service.factory"; import { internalMasterPasswordServiceFactory, MasterPasswordServiceInitOptions, @@ -59,7 +60,8 @@ export type UserVerificationServiceInitOptions = UserVerificationServiceFactoryO PinCryptoServiceInitOptions & LogServiceInitOptions & VaultTimeoutSettingsServiceInitOptions & - PlatformUtilsServiceInitOptions; + PlatformUtilsServiceInitOptions & + KdfConfigServiceInitOptions; export function userVerificationServiceFactory( cache: { userVerificationService?: AbstractUserVerificationService } & CachedServices, @@ -82,6 +84,7 @@ export function userVerificationServiceFactory( await logServiceFactory(cache, opts), await vaultTimeoutSettingsServiceFactory(cache, opts), await platformUtilsServiceFactory(cache, opts), + await kdfConfigServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/auth/popup/lock.component.ts b/apps/browser/src/auth/popup/lock.component.ts index 78039d793f5..4d47417df6d 100644 --- a/apps/browser/src/auth/popup/lock.component.ts +++ b/apps/browser/src/auth/popup/lock.component.ts @@ -12,6 +12,7 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; @@ -66,6 +67,7 @@ export class LockComponent extends BaseLockComponent { private routerService: BrowserRouterService, biometricStateService: BiometricStateService, accountService: AccountService, + kdfConfigService: KdfConfigService, ) { super( masterPasswordService, @@ -90,6 +92,7 @@ export class LockComponent extends BaseLockComponent { pinCryptoService, biometricStateService, accountService, + kdfConfigService, ); this.successRoute = "/tabs/current"; this.isInitialLockScreen = (window as any).previousPopupUrl == null; diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index dc93de7803d..b4375df7d50 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -33,6 +33,7 @@ import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/aut import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; +import { KdfConfigService as kdfConfigServiceAbstraction } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; @@ -48,6 +49,7 @@ import { AvatarService } from "@bitwarden/common/auth/services/avatar.service"; import { DeviceTrustService } from "@bitwarden/common/auth/services/device-trust.service.implementation"; import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation"; import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation"; +import { KdfConfigService } from "@bitwarden/common/auth/services/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service"; import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service"; import { SsoLoginService } from "@bitwarden/common/auth/services/sso-login.service"; @@ -339,6 +341,7 @@ export default class MainBackground { intraprocessMessagingSubject: Subject>; userKeyInitService: UserKeyInitService; scriptInjectorService: BrowserScriptInjectorService; + kdfConfigService: kdfConfigServiceAbstraction; onUpdatedRan: boolean; onReplacedRan: boolean; @@ -542,6 +545,9 @@ export default class MainBackground { this.masterPasswordService = new MasterPasswordService(this.stateProvider); this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider); + + this.kdfConfigService = new KdfConfigService(this.stateProvider); + this.cryptoService = new BrowserCryptoService( this.masterPasswordService, this.keyGenerationService, @@ -553,6 +559,7 @@ export default class MainBackground { this.accountService, this.stateProvider, this.biometricStateService, + this.kdfConfigService, ); this.appIdService = new AppIdService(this.globalStateProvider); @@ -675,6 +682,7 @@ export default class MainBackground { this.userDecryptionOptionsService, this.globalStateProvider, this.billingAccountProfileStateService, + this.kdfConfigService, ); this.ssoLoginService = new SsoLoginService(this.stateProvider); @@ -725,6 +733,7 @@ export default class MainBackground { this.cryptoService, this.vaultTimeoutSettingsService, this.logService, + this.kdfConfigService, ); this.userVerificationService = new UserVerificationService( @@ -739,6 +748,7 @@ export default class MainBackground { this.logService, this.vaultTimeoutSettingsService, this.platformUtilsService, + this.kdfConfigService, ); this.vaultFilterService = new VaultFilterService( @@ -861,7 +871,7 @@ export default class MainBackground { this.cipherService, this.cryptoService, this.cryptoFunctionService, - this.stateService, + this.kdfConfigService, ); this.organizationVaultExportService = new OrganizationVaultExportService( @@ -869,8 +879,8 @@ export default class MainBackground { this.apiService, this.cryptoService, this.cryptoFunctionService, - this.stateService, this.collectionService, + this.kdfConfigService, ); this.exportService = new VaultExportService( diff --git a/apps/browser/src/platform/background/service-factories/crypto-service.factory.ts b/apps/browser/src/platform/background/service-factories/crypto-service.factory.ts index ed4fde162c0..1f848e1d0f9 100644 --- a/apps/browser/src/platform/background/service-factories/crypto-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/crypto-service.factory.ts @@ -4,6 +4,10 @@ import { AccountServiceInitOptions, accountServiceFactory, } from "../../../auth/background/service-factories/account-service.factory"; +import { + KdfConfigServiceInitOptions, + kdfConfigServiceFactory, +} from "../../../auth/background/service-factories/kdf-config-service.factory"; import { internalMasterPasswordServiceFactory, MasterPasswordServiceInitOptions, @@ -18,7 +22,10 @@ import { } from "../../background/service-factories/log-service.factory"; import { BrowserCryptoService } from "../../services/browser-crypto.service"; -import { biometricStateServiceFactory } from "./biometric-state-service.factory"; +import { + BiometricStateServiceInitOptions, + biometricStateServiceFactory, +} from "./biometric-state-service.factory"; import { cryptoFunctionServiceFactory, CryptoFunctionServiceInitOptions, @@ -46,7 +53,9 @@ export type CryptoServiceInitOptions = CryptoServiceFactoryOptions & LogServiceInitOptions & StateServiceInitOptions & AccountServiceInitOptions & - StateProviderInitOptions; + StateProviderInitOptions & + BiometricStateServiceInitOptions & + KdfConfigServiceInitOptions; export function cryptoServiceFactory( cache: { cryptoService?: AbstractCryptoService } & CachedServices, @@ -68,6 +77,7 @@ export function cryptoServiceFactory( await accountServiceFactory(cache, opts), await stateProviderFactory(cache, opts), await biometricStateServiceFactory(cache, opts), + await kdfConfigServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/platform/services/browser-crypto.service.ts b/apps/browser/src/platform/services/browser-crypto.service.ts index d7533a22d6e..cd23c916c69 100644 --- a/apps/browser/src/platform/services/browser-crypto.service.ts +++ b/apps/browser/src/platform/services/browser-crypto.service.ts @@ -1,6 +1,7 @@ import { firstValueFrom } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -28,6 +29,7 @@ export class BrowserCryptoService extends CryptoService { accountService: AccountService, stateProvider: StateProvider, private biometricStateService: BiometricStateService, + kdfConfigService: KdfConfigService, ) { super( masterPasswordService, @@ -39,6 +41,7 @@ export class BrowserCryptoService extends CryptoService { stateService, accountService, stateProvider, + kdfConfigService, ); } override async hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: UserId): Promise { diff --git a/apps/cli/src/auth/commands/login.command.ts b/apps/cli/src/auth/commands/login.command.ts index a91e876e92d..3606285c723 100644 --- a/apps/cli/src/auth/commands/login.command.ts +++ b/apps/cli/src/auth/commands/login.command.ts @@ -16,6 +16,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; @@ -68,6 +69,7 @@ export class LoginCommand { protected policyApiService: PolicyApiServiceAbstraction, protected orgService: OrganizationService, protected logoutCallback: () => Promise, + protected kdfConfigService: KdfConfigService, ) {} async run(email: string, password: string, options: OptionValues) { @@ -563,14 +565,12 @@ export class LoginCommand { message: "Master Password Hint (optional):", }); const masterPasswordHint = hint.input; - const kdf = await this.stateService.getKdfType(); - const kdfConfig = await this.stateService.getKdfConfig(); + const kdfConfig = await this.kdfConfigService.getKdfConfig(); // Create new key and hash new password const newMasterKey = await this.cryptoService.makeMasterKey( masterPassword, this.email.trim().toLowerCase(), - kdf, kdfConfig, ); const newPasswordHash = await this.cryptoService.hashMasterKey(masterPassword, newMasterKey); diff --git a/apps/cli/src/auth/commands/unlock.command.ts b/apps/cli/src/auth/commands/unlock.command.ts index d52468139ae..6b97b59c881 100644 --- a/apps/cli/src/auth/commands/unlock.command.ts +++ b/apps/cli/src/auth/commands/unlock.command.ts @@ -3,6 +3,7 @@ import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request"; @@ -34,6 +35,7 @@ export class UnlockCommand { private syncService: SyncService, private organizationApiService: OrganizationApiServiceAbstraction, private logout: () => Promise, + private kdfConfigService: KdfConfigService, ) {} async run(password: string, cmdOptions: Record) { @@ -48,9 +50,8 @@ export class UnlockCommand { await this.setNewSessionKey(); const email = await this.stateService.getEmail(); - const kdf = await this.stateService.getKdfType(); - const kdfConfig = await this.stateService.getKdfConfig(); - const masterKey = await this.cryptoService.makeMasterKey(password, email, kdf, kdfConfig); + const kdfConfig = await this.kdfConfigService.getKdfConfig(); + const masterKey = await this.cryptoService.makeMasterKey(password, email, kdfConfig); const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; const storedMasterKeyHash = await firstValueFrom( this.masterPasswordService.masterKeyHash$(userId), diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index be3ad9ea0e1..ffe6c128b58 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -30,12 +30,14 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; +import { KdfConfigService as KdfConfigServiceAbstraction } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { AvatarService } from "@bitwarden/common/auth/services/avatar.service"; import { DeviceTrustService } from "@bitwarden/common/auth/services/device-trust.service.implementation"; import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation"; +import { KdfConfigService } from "@bitwarden/common/auth/services/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service"; import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service"; import { TokenService } from "@bitwarden/common/auth/services/token.service"; @@ -235,6 +237,7 @@ export class Main { billingAccountProfileStateService: BillingAccountProfileStateService; providerApiService: ProviderApiServiceAbstraction; userKeyInitService: UserKeyInitService; + kdfConfigService: KdfConfigServiceAbstraction; constructor() { let p = null; @@ -357,6 +360,8 @@ export class Main { this.masterPasswordService = new MasterPasswordService(this.stateProvider); + this.kdfConfigService = new KdfConfigService(this.stateProvider); + this.cryptoService = new CryptoService( this.masterPasswordService, this.keyGenerationService, @@ -367,6 +372,7 @@ export class Main { this.stateService, this.accountService, this.stateProvider, + this.kdfConfigService, ); this.appIdService = new AppIdService(this.globalStateProvider); @@ -512,6 +518,7 @@ export class Main { this.userDecryptionOptionsService, this.globalStateProvider, this.billingAccountProfileStateService, + this.kdfConfigService, ); this.authService = new AuthService( @@ -574,6 +581,7 @@ export class Main { this.cryptoService, this.vaultTimeoutSettingsService, this.logService, + this.kdfConfigService, ); this.userVerificationService = new UserVerificationService( @@ -588,6 +596,7 @@ export class Main { this.logService, this.vaultTimeoutSettingsService, this.platformUtilsService, + this.kdfConfigService, ); this.vaultTimeoutService = new VaultTimeoutService( @@ -654,7 +663,7 @@ export class Main { this.cipherService, this.cryptoService, this.cryptoFunctionService, - this.stateService, + this.kdfConfigService, ); this.organizationExportService = new OrganizationVaultExportService( @@ -662,8 +671,8 @@ export class Main { this.apiService, this.cryptoService, this.cryptoFunctionService, - this.stateService, this.collectionService, + this.kdfConfigService, ); this.exportService = new VaultExportService( diff --git a/apps/cli/src/commands/serve.command.ts b/apps/cli/src/commands/serve.command.ts index 76447f769c1..7a11dc4b4a6 100644 --- a/apps/cli/src/commands/serve.command.ts +++ b/apps/cli/src/commands/serve.command.ts @@ -134,6 +134,7 @@ export class ServeCommand { this.main.syncService, this.main.organizationApiService, async () => await this.main.logout(), + this.main.kdfConfigService, ); this.sendCreateCommand = new SendCreateCommand( diff --git a/apps/cli/src/program.ts b/apps/cli/src/program.ts index fa71a88f54e..5d26b0850e5 100644 --- a/apps/cli/src/program.ts +++ b/apps/cli/src/program.ts @@ -156,6 +156,7 @@ export class Program { this.main.policyApiService, this.main.organizationService, async () => await this.main.logout(), + this.main.kdfConfigService, ); const response = await command.run(email, password, options); this.processResponse(response, true); @@ -265,6 +266,7 @@ export class Program { this.main.syncService, this.main.organizationApiService, async () => await this.main.logout(), + this.main.kdfConfigService, ); const response = await command.run(password, cmd); this.processResponse(response); @@ -627,6 +629,7 @@ export class Program { this.main.syncService, this.main.organizationApiService, this.main.logout, + this.main.kdfConfigService, ); const response = await command.run(null, null); if (!response.success) { diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index c15743ba5cd..b888df80133 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -21,6 +21,7 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaul import { PolicyService as PolicyServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service"; +import { KdfConfigService as KdfConfigServiceAbstraction } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; @@ -258,6 +259,7 @@ const safeProviders: SafeProvider[] = [ AccountServiceAbstraction, StateProvider, BiometricStateService, + KdfConfigServiceAbstraction, ], }), safeProvider({ diff --git a/apps/desktop/src/auth/lock.component.spec.ts b/apps/desktop/src/auth/lock.component.spec.ts index 480e443eab1..f998e75d7a0 100644 --- a/apps/desktop/src/auth/lock.component.spec.ts +++ b/apps/desktop/src/auth/lock.component.spec.ts @@ -14,6 +14,7 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; @@ -164,6 +165,10 @@ describe("LockComponent", () => { provide: AccountService, useValue: accountService, }, + { + provide: KdfConfigService, + useValue: mock(), + }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); diff --git a/apps/desktop/src/auth/lock.component.ts b/apps/desktop/src/auth/lock.component.ts index b8feef4ab52..8e87b6663fc 100644 --- a/apps/desktop/src/auth/lock.component.ts +++ b/apps/desktop/src/auth/lock.component.ts @@ -11,6 +11,7 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { DeviceType } from "@bitwarden/common/enums"; @@ -63,6 +64,7 @@ export class LockComponent extends BaseLockComponent { pinCryptoService: PinCryptoServiceAbstraction, biometricStateService: BiometricStateService, accountService: AccountService, + kdfConfigService: KdfConfigService, ) { super( masterPasswordService, @@ -87,6 +89,7 @@ export class LockComponent extends BaseLockComponent { pinCryptoService, biometricStateService, accountService, + kdfConfigService, ); } diff --git a/apps/desktop/src/auth/set-password.component.ts b/apps/desktop/src/auth/set-password.component.ts index 93dfe0abd84..feea5edd867 100644 --- a/apps/desktop/src/auth/set-password.component.ts +++ b/apps/desktop/src/auth/set-password.component.ts @@ -9,6 +9,7 @@ import { OrganizationUserService } from "@bitwarden/common/admin-console/abstrac import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; @@ -52,6 +53,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, dialogService: DialogService, + kdfConfigService: KdfConfigService, ) { super( accountService, @@ -73,6 +75,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On userDecryptionOptionsService, ssoLoginService, dialogService, + kdfConfigService, ); } diff --git a/apps/desktop/src/platform/services/electron-crypto.service.spec.ts b/apps/desktop/src/platform/services/electron-crypto.service.spec.ts index 3d9171b52ea..86463dccaab 100644 --- a/apps/desktop/src/platform/services/electron-crypto.service.spec.ts +++ b/apps/desktop/src/platform/services/electron-crypto.service.spec.ts @@ -1,6 +1,7 @@ import { FakeStateProvider } from "@bitwarden/common/../spec/fake-state-provider"; import { mock } from "jest-mock-extended"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -35,6 +36,7 @@ describe("electronCryptoService", () => { let accountService: FakeAccountService; let stateProvider: FakeStateProvider; const biometricStateService = mock(); + const kdfConfigService = mock(); const mockUserId = "mock user id" as UserId; @@ -54,6 +56,7 @@ describe("electronCryptoService", () => { accountService, stateProvider, biometricStateService, + kdfConfigService, ); }); diff --git a/apps/desktop/src/platform/services/electron-crypto.service.ts b/apps/desktop/src/platform/services/electron-crypto.service.ts index d113a18200e..0ed0f73d413 100644 --- a/apps/desktop/src/platform/services/electron-crypto.service.ts +++ b/apps/desktop/src/platform/services/electron-crypto.service.ts @@ -1,6 +1,7 @@ import { firstValueFrom } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -31,6 +32,7 @@ export class ElectronCryptoService extends CryptoService { accountService: AccountService, stateProvider: StateProvider, private biometricStateService: BiometricStateService, + kdfConfigService: KdfConfigService, ) { super( masterPasswordService, @@ -42,6 +44,7 @@ export class ElectronCryptoService extends CryptoService { stateService, accountService, stateProvider, + kdfConfigService, ); } diff --git a/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts b/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts index cd94513f19b..fcdbe1e4962 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts @@ -7,10 +7,15 @@ import { OrganizationUserResetPasswordRequest, OrganizationUserResetPasswordWithIdRequest, } from "@bitwarden/common/admin-console/abstractions/organization-user/requests"; -import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; +import { + Argon2KdfConfig, + KdfConfig, + PBKDF2KdfConfig, +} from "@bitwarden/common/auth/models/domain/kdf-config"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { KdfType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; @@ -90,12 +95,17 @@ export class OrganizationUserResetPasswordService { const decValue = await this.cryptoService.rsaDecrypt(response.resetPasswordKey, decPrivateKey); const existingUserKey = new SymmetricCryptoKey(decValue) as UserKey; + // determine Kdf Algorithm + const kdfConfig: KdfConfig = + response.kdf === KdfType.PBKDF2_SHA256 + ? new PBKDF2KdfConfig(response.kdfIterations) + : new Argon2KdfConfig(response.kdfIterations, response.kdfMemory, response.kdfParallelism); + // Create new master key and hash new password const newMasterKey = await this.cryptoService.makeMasterKey( newMasterPassword, email.trim().toLowerCase(), - response.kdf, - new KdfConfig(response.kdfIterations, response.kdfMemory, response.kdfParallelism), + kdfConfig, ); const newMasterKeyHash = await this.cryptoService.hashMasterKey( newMasterPassword, diff --git a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts index 6bcb933e515..dbc1ce820c6 100644 --- a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts +++ b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts @@ -3,10 +3,15 @@ import { Injectable } from "@angular/core"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyData } from "@bitwarden/common/admin-console/models/data/policy.data"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; -import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; +import { + Argon2KdfConfig, + KdfConfig, + PBKDF2KdfConfig, +} from "@bitwarden/common/auth/models/domain/kdf-config"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { KdfType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; @@ -231,16 +236,22 @@ export class EmergencyAccessService { const grantorUserKey = new SymmetricCryptoKey(grantorKeyBuffer) as UserKey; - const masterKey = await this.cryptoService.makeMasterKey( - masterPassword, - email, - takeoverResponse.kdf, - new KdfConfig( - takeoverResponse.kdfIterations, - takeoverResponse.kdfMemory, - takeoverResponse.kdfParallelism, - ), - ); + let config: KdfConfig; + + switch (takeoverResponse.kdf) { + case KdfType.PBKDF2_SHA256: + config = new PBKDF2KdfConfig(takeoverResponse.kdfIterations); + break; + case KdfType.Argon2id: + config = new Argon2KdfConfig( + takeoverResponse.kdfIterations, + takeoverResponse.kdfMemory, + takeoverResponse.kdfParallelism, + ); + break; + } + + const masterKey = await this.cryptoService.makeMasterKey(masterPassword, email, config); const masterKeyHash = await this.cryptoService.hashMasterKey(masterPassword, masterKey); const encKey = await this.cryptoService.encryptUserKeyWithMasterKey(masterKey, grantorUserKey); diff --git a/apps/web/src/app/auth/key-rotation/user-key-rotation.service.spec.ts b/apps/web/src/app/auth/key-rotation/user-key-rotation.service.spec.ts index ed665fe7732..ec685569318 100644 --- a/apps/web/src/app/auth/key-rotation/user-key-rotation.service.spec.ts +++ b/apps/web/src/app/auth/key-rotation/user-key-rotation.service.spec.ts @@ -2,6 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -47,6 +48,7 @@ describe("KeyRotationService", () => { let mockEncryptService: MockProxy; let mockStateService: MockProxy; let mockConfigService: MockProxy; + let mockKdfConfigService: MockProxy; const mockUserId = Utils.newGuid() as UserId; const mockAccountService: FakeAccountService = mockAccountServiceWith(mockUserId); @@ -65,6 +67,7 @@ describe("KeyRotationService", () => { mockEncryptService = mock(); mockStateService = mock(); mockConfigService = mock(); + mockKdfConfigService = mock(); keyRotationService = new UserKeyRotationService( mockMasterPasswordService, @@ -80,6 +83,7 @@ describe("KeyRotationService", () => { mockStateService, mockAccountService, mockConfigService, + mockKdfConfigService, ); }); diff --git a/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts b/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts index 2ff48809a07..94c62081154 100644 --- a/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts +++ b/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts @@ -3,6 +3,7 @@ import { firstValueFrom } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -39,6 +40,7 @@ export class UserKeyRotationService { private stateService: StateService, private accountService: AccountService, private configService: ConfigService, + private kdfConfigService: KdfConfigService, ) {} /** @@ -54,8 +56,7 @@ export class UserKeyRotationService { const masterKey = await this.cryptoService.makeMasterKey( masterPassword, await this.stateService.getEmail(), - await this.stateService.getKdfType(), - await this.stateService.getKdfConfig(), + await this.kdfConfigService.getKdfConfig(), ); if (!masterKey) { diff --git a/apps/web/src/app/auth/settings/account/change-email.component.ts b/apps/web/src/app/auth/settings/account/change-email.component.ts index 372b344b107..e5a3c72337b 100644 --- a/apps/web/src/app/auth/settings/account/change-email.component.ts +++ b/apps/web/src/app/auth/settings/account/change-email.component.ts @@ -2,6 +2,7 @@ import { Component, OnInit } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { EmailTokenRequest } from "@bitwarden/common/auth/models/request/email-token.request"; import { EmailRequest } from "@bitwarden/common/auth/models/request/email.request"; @@ -37,6 +38,7 @@ export class ChangeEmailComponent implements OnInit { private logService: LogService, private stateService: StateService, private formBuilder: FormBuilder, + private kdfConfigService: KdfConfigService, ) {} async ngOnInit() { @@ -83,12 +85,10 @@ export class ChangeEmailComponent implements OnInit { step1Value.masterPassword, await this.cryptoService.getOrDeriveMasterKey(step1Value.masterPassword), ); - const kdf = await this.stateService.getKdfType(); - const kdfConfig = await this.stateService.getKdfConfig(); + const kdfConfig = await this.kdfConfigService.getKdfConfig(); const newMasterKey = await this.cryptoService.makeMasterKey( step1Value.masterPassword, newEmail, - kdf, kdfConfig, ); request.newMasterPasswordHash = await this.cryptoService.hashMasterKey( diff --git a/apps/web/src/app/auth/settings/change-password.component.ts b/apps/web/src/app/auth/settings/change-password.component.ts index 6d16893170e..454d96f2bde 100644 --- a/apps/web/src/app/auth/settings/change-password.component.ts +++ b/apps/web/src/app/auth/settings/change-password.component.ts @@ -5,6 +5,7 @@ import { ChangePasswordComponent as BaseChangePasswordComponent } from "@bitward import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -48,6 +49,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent { dialogService: DialogService, private userVerificationService: UserVerificationService, private keyRotationService: UserKeyRotationService, + kdfConfigService: KdfConfigService, ) { super( i18nService, @@ -58,6 +60,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent { policyService, stateService, dialogService, + kdfConfigService, ); } diff --git a/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts b/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts index 575c6f4a237..73b1fa775dc 100644 --- a/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts @@ -5,6 +5,7 @@ import { takeUntil } from "rxjs"; import { ChangePasswordComponent } from "@bitwarden/angular/auth/components/change-password.component"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -58,6 +59,7 @@ export class EmergencyAccessTakeoverComponent private logService: LogService, dialogService: DialogService, private dialogRef: DialogRef, + kdfConfigService: KdfConfigService, ) { super( i18nService, @@ -68,6 +70,7 @@ export class EmergencyAccessTakeoverComponent policyService, stateService, dialogService, + kdfConfigService, ); } diff --git a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.ts b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.ts index 0284c665d8f..985fb3e038a 100644 --- a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.ts +++ b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.ts @@ -3,6 +3,7 @@ import { Component, Inject } from "@angular/core"; import { FormGroup, FormControl, Validators } from "@angular/forms"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; import { KdfRequest } from "@bitwarden/common/models/request/kdf.request"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -18,7 +19,6 @@ import { KdfType } from "@bitwarden/common/platform/enums"; templateUrl: "change-kdf-confirmation.component.html", }) export class ChangeKdfConfirmationComponent { - kdf: KdfType; kdfConfig: KdfConfig; form = new FormGroup({ @@ -37,9 +37,9 @@ export class ChangeKdfConfirmationComponent { private messagingService: MessagingService, private stateService: StateService, private logService: LogService, - @Inject(DIALOG_DATA) params: { kdf: KdfType; kdfConfig: KdfConfig }, + private kdfConfigService: KdfConfigService, + @Inject(DIALOG_DATA) params: { kdfConfig: KdfConfig }, ) { - this.kdf = params.kdf; this.kdfConfig = params.kdfConfig; this.masterPassword = null; } @@ -65,22 +65,24 @@ export class ChangeKdfConfirmationComponent { private async makeKeyAndSaveAsync() { const masterPassword = this.form.value.masterPassword; + + // Ensure the KDF config is valid. + this.kdfConfig.validateKdfConfig(); + const request = new KdfRequest(); - request.kdf = this.kdf; + request.kdf = this.kdfConfig.kdfType; request.kdfIterations = this.kdfConfig.iterations; - request.kdfMemory = this.kdfConfig.memory; - request.kdfParallelism = this.kdfConfig.parallelism; + if (this.kdfConfig.kdfType === KdfType.Argon2id) { + request.kdfMemory = this.kdfConfig.memory; + request.kdfParallelism = this.kdfConfig.parallelism; + } const masterKey = await this.cryptoService.getOrDeriveMasterKey(masterPassword); request.masterPasswordHash = await this.cryptoService.hashMasterKey(masterPassword, masterKey); const email = await this.stateService.getEmail(); - // Ensure the KDF config is valid. - this.cryptoService.validateKdfConfig(this.kdf, this.kdfConfig); - const newMasterKey = await this.cryptoService.makeMasterKey( masterPassword, email, - this.kdf, this.kdfConfig, ); request.newMasterPasswordHash = await this.cryptoService.hashMasterKey( diff --git a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.html b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.html index 9b16c446bea..8b1dec8e139 100644 --- a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.html +++ b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.html @@ -19,14 +19,14 @@ - +
- + - +
- +

{{ "kdfIterationsDesc" | i18n: (PBKDF2_ITERATIONS.defaultValue | number) }}

@@ -100,7 +100,7 @@ {{ "kdfIterationsWarning" | i18n: (100000 | number) }}
- +

{{ "argon2Desc" | i18n }}

{{ "argon2Warning" | i18n }}
diff --git a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.ts b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.ts index d91fb8d083f..5c05f1ba2a6 100644 --- a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.ts +++ b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.ts @@ -1,7 +1,11 @@ import { Component, OnInit } from "@angular/core"; -import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; +import { + Argon2KdfConfig, + KdfConfig, + PBKDF2KdfConfig, +} from "@bitwarden/common/auth/models/domain/kdf-config"; import { DEFAULT_KDF_CONFIG, PBKDF2_ITERATIONS, @@ -19,7 +23,6 @@ import { ChangeKdfConfirmationComponent } from "./change-kdf-confirmation.compon templateUrl: "change-kdf.component.html", }) export class ChangeKdfComponent implements OnInit { - kdf = KdfType.PBKDF2_SHA256; kdfConfig: KdfConfig = DEFAULT_KDF_CONFIG; kdfType = KdfType; kdfOptions: any[] = []; @@ -31,8 +34,8 @@ export class ChangeKdfComponent implements OnInit { protected ARGON2_PARALLELISM = ARGON2_PARALLELISM; constructor( - private stateService: StateService, private dialogService: DialogService, + private kdfConfigService: KdfConfigService, ) { this.kdfOptions = [ { name: "PBKDF2 SHA-256", value: KdfType.PBKDF2_SHA256 }, @@ -41,19 +44,22 @@ export class ChangeKdfComponent implements OnInit { } async ngOnInit() { - this.kdf = await this.stateService.getKdfType(); - this.kdfConfig = await this.stateService.getKdfConfig(); + this.kdfConfig = await this.kdfConfigService.getKdfConfig(); + } + + isPBKDF2(t: KdfConfig): t is PBKDF2KdfConfig { + return t instanceof PBKDF2KdfConfig; + } + + isArgon2(t: KdfConfig): t is Argon2KdfConfig { + return t instanceof Argon2KdfConfig; } async onChangeKdf(newValue: KdfType) { if (newValue === KdfType.PBKDF2_SHA256) { - this.kdfConfig = new KdfConfig(PBKDF2_ITERATIONS.defaultValue); + this.kdfConfig = new PBKDF2KdfConfig(); } else if (newValue === KdfType.Argon2id) { - this.kdfConfig = new KdfConfig( - ARGON2_ITERATIONS.defaultValue, - ARGON2_MEMORY.defaultValue, - ARGON2_PARALLELISM.defaultValue, - ); + this.kdfConfig = new Argon2KdfConfig(); } else { throw new Error("Unknown KDF type."); } @@ -62,7 +68,6 @@ export class ChangeKdfComponent implements OnInit { async openConfirmationModal() { this.dialogService.open(ChangeKdfConfirmationComponent, { data: { - kdf: this.kdf, kdfConfig: this.kdfConfig, }, }); diff --git a/apps/web/src/app/auth/update-password.component.ts b/apps/web/src/app/auth/update-password.component.ts index 2844d2d8628..123e3e4ac1d 100644 --- a/apps/web/src/app/auth/update-password.component.ts +++ b/apps/web/src/app/auth/update-password.component.ts @@ -4,6 +4,7 @@ import { Router } from "@angular/router"; import { UpdatePasswordComponent as BaseUpdatePasswordComponent } from "@bitwarden/angular/auth/components/update-password.component"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -32,6 +33,7 @@ export class UpdatePasswordComponent extends BaseUpdatePasswordComponent { stateService: StateService, userVerificationService: UserVerificationService, dialogService: DialogService, + kdfConfigService: KdfConfigService, ) { super( router, @@ -46,6 +48,7 @@ export class UpdatePasswordComponent extends BaseUpdatePasswordComponent { userVerificationService, logService, dialogService, + kdfConfigService, ); } } diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 2c20328336b..c97dd93d768 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -35,6 +35,7 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/eve import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; @@ -184,6 +185,7 @@ export class VaultComponent implements OnInit, OnDestroy { private apiService: ApiService, private userVerificationService: UserVerificationService, private billingAccountProfileStateService: BillingAccountProfileStateService, + protected kdfConfigService: KdfConfigService, ) {} async ngOnInit() { @@ -972,10 +974,10 @@ export class VaultComponent implements OnInit, OnDestroy { } async isLowKdfIteration() { - const kdfType = await this.stateService.getKdfType(); - const kdfOptions = await this.stateService.getKdfConfig(); + const kdfConfig = await this.kdfConfigService.getKdfConfig(); return ( - kdfType === KdfType.PBKDF2_SHA256 && kdfOptions.iterations < PBKDF2_ITERATIONS.defaultValue + kdfConfig.kdfType === KdfType.PBKDF2_SHA256 && + kdfConfig.iterations < PBKDF2_ITERATIONS.defaultValue ); } diff --git a/libs/angular/src/auth/components/change-password.component.ts b/libs/angular/src/auth/components/change-password.component.ts index 1086428f4c8..b1f75de58c7 100644 --- a/libs/angular/src/auth/components/change-password.component.ts +++ b/libs/angular/src/auth/components/change-password.component.ts @@ -3,13 +3,13 @@ import { Subject, takeUntil } from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { KdfType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; @@ -31,7 +31,6 @@ export class ChangePasswordComponent implements OnInit, OnDestroy { minimumLength = Utils.minimumPasswordLength; protected email: string; - protected kdf: KdfType; protected kdfConfig: KdfConfig; protected destroy$ = new Subject(); @@ -45,6 +44,7 @@ export class ChangePasswordComponent implements OnInit, OnDestroy { protected policyService: PolicyService, protected stateService: StateService, protected dialogService: DialogService, + protected kdfConfigService: KdfConfigService, ) {} async ngOnInit() { @@ -73,18 +73,14 @@ export class ChangePasswordComponent implements OnInit, OnDestroy { } const email = await this.stateService.getEmail(); - if (this.kdf == null) { - this.kdf = await this.stateService.getKdfType(); - } if (this.kdfConfig == null) { - this.kdfConfig = await this.stateService.getKdfConfig(); + this.kdfConfig = await this.kdfConfigService.getKdfConfig(); } // Create new master key const newMasterKey = await this.cryptoService.makeMasterKey( this.masterPassword, email.trim().toLowerCase(), - this.kdf, this.kdfConfig, ); const newMasterKeyHash = await this.cryptoService.hashMasterKey( diff --git a/libs/angular/src/auth/components/lock.component.ts b/libs/angular/src/auth/components/lock.component.ts index 927fbb27b16..89af31da81a 100644 --- a/libs/angular/src/auth/components/lock.component.ts +++ b/libs/angular/src/auth/components/lock.component.ts @@ -12,6 +12,7 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; @@ -79,6 +80,7 @@ export class LockComponent implements OnInit, OnDestroy { protected pinCryptoService: PinCryptoServiceAbstraction, protected biometricStateService: BiometricStateService, protected accountService: AccountService, + protected kdfConfigService: KdfConfigService, ) {} async ngOnInit() { @@ -208,14 +210,12 @@ export class LockComponent implements OnInit, OnDestroy { } private async doUnlockWithMasterPassword() { + const kdfConfig = await this.kdfConfigService.getKdfConfig(); const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - const kdf = await this.stateService.getKdfType(); - const kdfConfig = await this.stateService.getKdfConfig(); const masterKey = await this.cryptoService.makeMasterKey( this.masterPassword, this.email, - kdf, kdfConfig, ); const storedMasterKeyHash = await firstValueFrom( diff --git a/libs/angular/src/auth/components/register.component.ts b/libs/angular/src/auth/components/register.component.ts index 3cffebe71be..2ba76692902 100644 --- a/libs/angular/src/auth/components/register.component.ts +++ b/libs/angular/src/auth/components/register.component.ts @@ -15,7 +15,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { DEFAULT_KDF_CONFIG, DEFAULT_KDF_TYPE } from "@bitwarden/common/platform/enums"; +import { DEFAULT_KDF_CONFIG } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { DialogService } from "@bitwarden/components"; @@ -273,9 +273,8 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn name: string, ): Promise { const hint = this.formGroup.value.hint; - const kdf = DEFAULT_KDF_TYPE; const kdfConfig = DEFAULT_KDF_CONFIG; - const key = await this.cryptoService.makeMasterKey(masterPassword, email, kdf, kdfConfig); + const key = await this.cryptoService.makeMasterKey(masterPassword, email, kdfConfig); const newUserKey = await this.cryptoService.makeUserKey(key); const masterKeyHash = await this.cryptoService.hashMasterKey(masterPassword, key); const keys = await this.cryptoService.makeKeyPair(newUserKey[0]); @@ -287,10 +286,8 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn newUserKey[1].encryptedString, this.referenceData, this.captchaToken, - kdf, + kdfConfig.kdfType, kdfConfig.iterations, - kdfConfig.memory, - kdfConfig.parallelism, ); request.keys = new KeysRequest(keys[0], keys[1].encryptedString); const orgInvite = await this.stateService.getOrganizationInvitation(); diff --git a/libs/angular/src/auth/components/set-password.component.ts b/libs/angular/src/auth/components/set-password.component.ts index eebf87655b7..00a36434b08 100644 --- a/libs/angular/src/auth/components/set-password.component.ts +++ b/libs/angular/src/auth/components/set-password.component.ts @@ -13,6 +13,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { OrganizationAutoEnrollStatusResponse } from "@bitwarden/common/admin-console/models/response/organization-auto-enroll-status.response"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; @@ -23,11 +24,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { - HashPurpose, - DEFAULT_KDF_TYPE, - DEFAULT_KDF_CONFIG, -} from "@bitwarden/common/platform/enums"; +import { HashPurpose, DEFAULT_KDF_CONFIG } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; @@ -73,6 +70,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { private userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, private ssoLoginService: SsoLoginServiceAbstraction, dialogService: DialogService, + kdfConfigService: KdfConfigService, ) { super( i18nService, @@ -83,6 +81,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { policyService, stateService, dialogService, + kdfConfigService, ); } @@ -139,7 +138,6 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { } async setupSubmitActions() { - this.kdf = DEFAULT_KDF_TYPE; this.kdfConfig = DEFAULT_KDF_CONFIG; return true; } @@ -169,10 +167,8 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { this.hint, this.orgSsoIdentifier, keysRequest, - this.kdf, + this.kdfConfig.kdfType, //always PBKDF2 --> see this.setupSubmitActions this.kdfConfig.iterations, - this.kdfConfig.memory, - this.kdfConfig.parallelism, ); try { if (this.resetPasswordAutoEnroll) { @@ -246,9 +242,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { ); userDecryptionOpts.hasMasterPassword = true; await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts); - - await this.stateService.setKdfType(this.kdf); - await this.stateService.setKdfConfig(this.kdfConfig); + await this.kdfConfigService.setKdfConfig(this.userId, this.kdfConfig); await this.masterPasswordService.setMasterKey(masterKey, this.userId); await this.cryptoService.setUserKey(userKey[0]); diff --git a/libs/angular/src/auth/components/set-pin.component.ts b/libs/angular/src/auth/components/set-pin.component.ts index ade23f4fef9..f0b66b8e704 100644 --- a/libs/angular/src/auth/components/set-pin.component.ts +++ b/libs/angular/src/auth/components/set-pin.component.ts @@ -2,6 +2,7 @@ import { DialogRef } from "@angular/cdk/dialog"; import { Directive, OnInit } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; @@ -22,6 +23,7 @@ export class SetPinComponent implements OnInit { private userVerificationService: UserVerificationService, private stateService: StateService, private formBuilder: FormBuilder, + private kdfConfigService: KdfConfigService, ) {} async ngOnInit() { @@ -43,8 +45,7 @@ export class SetPinComponent implements OnInit { const pinKey = await this.cryptoService.makePinKey( pin, await this.stateService.getEmail(), - await this.stateService.getKdfType(), - await this.stateService.getKdfConfig(), + await this.kdfConfigService.getKdfConfig(), ); const userKey = await this.cryptoService.getUserKey(); const pinProtectedKey = await this.cryptoService.encrypt(userKey.key, pinKey); diff --git a/libs/angular/src/auth/components/update-password.component.ts b/libs/angular/src/auth/components/update-password.component.ts index 2ffffb6c5d8..264f3515424 100644 --- a/libs/angular/src/auth/components/update-password.component.ts +++ b/libs/angular/src/auth/components/update-password.component.ts @@ -4,6 +4,7 @@ import { Router } from "@angular/router"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request"; @@ -44,6 +45,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent { private userVerificationService: UserVerificationService, private logService: LogService, dialogService: DialogService, + kdfConfigService: KdfConfigService, ) { super( i18nService, @@ -54,6 +56,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent { policyService, stateService, dialogService, + kdfConfigService, ); } @@ -90,8 +93,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent { return false; } - this.kdf = await this.stateService.getKdfType(); - this.kdfConfig = await this.stateService.getKdfConfig(); + this.kdfConfig = await this.kdfConfigService.getKdfConfig(); return true; } diff --git a/libs/angular/src/auth/components/update-temp-password.component.ts b/libs/angular/src/auth/components/update-temp-password.component.ts index 54fdc832399..bd6da6b7601 100644 --- a/libs/angular/src/auth/components/update-temp-password.component.ts +++ b/libs/angular/src/auth/components/update-temp-password.component.ts @@ -6,6 +6,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; @@ -59,6 +60,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent { private userVerificationService: UserVerificationService, protected router: Router, dialogService: DialogService, + kdfConfigService: KdfConfigService, private accountService: AccountService, private masterPasswordService: InternalMasterPasswordServiceAbstraction, ) { @@ -71,6 +73,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent { policyService, stateService, dialogService, + kdfConfigService, ); } @@ -104,8 +107,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent { async setupSubmitActions(): Promise { this.email = await this.stateService.getEmail(); - this.kdf = await this.stateService.getKdfType(); - this.kdfConfig = await this.stateService.getKdfConfig(); + this.kdfConfig = await this.kdfConfigService.getKdfConfig(); return true; } @@ -124,7 +126,6 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent { const newMasterKey = await this.cryptoService.makeMasterKey( this.masterPassword, this.email.trim().toLowerCase(), - this.kdf, this.kdfConfig, ); const newPasswordHash = await this.cryptoService.hashMasterKey( diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 42879a84241..88494a1cbb5 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -63,6 +63,7 @@ import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/aut import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; +import { KdfConfigService as KdfConfigServiceAbstraction } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction, @@ -85,6 +86,7 @@ import { AvatarService } from "@bitwarden/common/auth/services/avatar.service"; import { DeviceTrustService } from "@bitwarden/common/auth/services/device-trust.service.implementation"; import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation"; import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation"; +import { KdfConfigService } from "@bitwarden/common/auth/services/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service"; import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service"; import { PasswordResetEnrollmentServiceImplementation } from "@bitwarden/common/auth/services/password-reset-enrollment.service.implementation"; @@ -390,6 +392,7 @@ const safeProviders: SafeProvider[] = [ InternalUserDecryptionOptionsServiceAbstraction, GlobalStateProvider, BillingAccountProfileStateService, + KdfConfigServiceAbstraction, ], }), safeProvider({ @@ -543,6 +546,7 @@ const safeProviders: SafeProvider[] = [ StateServiceAbstraction, AccountServiceAbstraction, StateProvider, + KdfConfigServiceAbstraction, ], }), safeProvider({ @@ -713,7 +717,7 @@ const safeProviders: SafeProvider[] = [ CipherServiceAbstraction, CryptoServiceAbstraction, CryptoFunctionServiceAbstraction, - StateServiceAbstraction, + KdfConfigServiceAbstraction, ], }), safeProvider({ @@ -724,8 +728,8 @@ const safeProviders: SafeProvider[] = [ ApiServiceAbstraction, CryptoServiceAbstraction, CryptoFunctionServiceAbstraction, - StateServiceAbstraction, CollectionServiceAbstraction, + KdfConfigServiceAbstraction, ], }), safeProvider({ @@ -834,6 +838,7 @@ const safeProviders: SafeProvider[] = [ LogService, VaultTimeoutSettingsServiceAbstraction, PlatformUtilsServiceAbstraction, + KdfConfigServiceAbstraction, ], }), safeProvider({ @@ -985,6 +990,7 @@ const safeProviders: SafeProvider[] = [ CryptoServiceAbstraction, VaultTimeoutSettingsServiceAbstraction, LogService, + KdfConfigServiceAbstraction, ], }), safeProvider({ @@ -1150,6 +1156,11 @@ const safeProviders: SafeProvider[] = [ useClass: ProviderApiService, deps: [ApiServiceAbstraction], }), + safeProvider({ + provide: KdfConfigServiceAbstraction, + useClass: KdfConfigService, + deps: [StateProvider], + }), ]; function encryptServiceFactory( diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts index 4e0b1ac3acc..5e70c348f42 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts @@ -2,6 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; @@ -44,6 +45,7 @@ describe("AuthRequestLoginStrategy", () => { let userDecryptionOptions: MockProxy; let deviceTrustService: MockProxy; let billingAccountProfileStateService: MockProxy; + let kdfConfigService: MockProxy; const mockUserId = Utils.newGuid() as UserId; let accountService: FakeAccountService; @@ -77,6 +79,7 @@ describe("AuthRequestLoginStrategy", () => { userDecryptionOptions = mock(); deviceTrustService = mock(); billingAccountProfileStateService = mock(); + kdfConfigService = mock(); accountService = mockAccountServiceWith(mockUserId); masterPasswordService = new FakeMasterPasswordService(); @@ -101,6 +104,7 @@ describe("AuthRequestLoginStrategy", () => { userDecryptionOptions, deviceTrustService, billingAccountProfileStateService, + kdfConfigService, ); tokenResponse = identityTokenResponseFactory(); diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts index 5220e432de7..a66d9879848 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts @@ -3,6 +3,7 @@ import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; @@ -63,6 +64,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy { userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, private deviceTrustService: DeviceTrustServiceAbstraction, billingAccountProfileStateService: BillingAccountProfileStateService, + kdfConfigService: KdfConfigService, ) { super( accountService, @@ -78,6 +80,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy { twoFactorService, userDecryptionOptionsService, billingAccountProfileStateService, + kdfConfigService, ); this.cache = new BehaviorSubject(data); diff --git a/libs/auth/src/common/login-strategies/login.strategy.spec.ts b/libs/auth/src/common/login-strategies/login.strategy.spec.ts index e0833342ce3..7c022db23ba 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -2,6 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; @@ -117,6 +118,7 @@ describe("LoginStrategy", () => { let policyService: MockProxy; let passwordStrengthService: MockProxy; let billingAccountProfileStateService: MockProxy; + let kdfConfigService: MockProxy; let passwordLoginStrategy: PasswordLoginStrategy; let credentials: PasswordLoginCredentials; @@ -136,6 +138,7 @@ describe("LoginStrategy", () => { stateService = mock(); twoFactorService = mock(); userDecryptionOptionsService = mock(); + kdfConfigService = mock(); policyService = mock(); passwordStrengthService = mock(); billingAccountProfileStateService = mock(); @@ -162,6 +165,7 @@ describe("LoginStrategy", () => { policyService, loginStrategyService, billingAccountProfileStateService, + kdfConfigService, ); credentials = new PasswordLoginCredentials(email, masterPassword); }); @@ -208,8 +212,6 @@ describe("LoginStrategy", () => { userId: userId, name: name, email: email, - kdfIterations: kdfIterations, - kdfType: kdf, }, }, keys: new AccountKeys(), @@ -404,6 +406,7 @@ describe("LoginStrategy", () => { policyService, loginStrategyService, billingAccountProfileStateService, + kdfConfigService, ); apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory()); diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index a73c32e1208..06fc98db13e 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -2,12 +2,14 @@ import { BehaviorSubject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; +import { Argon2KdfConfig, PBKDF2KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; import { DeviceRequest } from "@bitwarden/common/auth/models/request/identity-token/device.request"; import { PasswordTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/password-token.request"; import { SsoTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/sso-token.request"; @@ -27,6 +29,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { KdfType } from "@bitwarden/common/platform/enums"; import { Account, AccountProfile } from "@bitwarden/common/platform/models/domain/account"; import { UserId } from "@bitwarden/common/types/guid"; @@ -72,6 +75,7 @@ export abstract class LoginStrategy { protected twoFactorService: TwoFactorService, protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, protected billingAccountProfileStateService: BillingAccountProfileStateService, + protected KdfConfigService: KdfConfigService, ) {} abstract exportCache(): CacheData; @@ -182,10 +186,6 @@ export abstract class LoginStrategy { userId, name: accountInformation.name, email: accountInformation.email, - kdfIterations: tokenResponse.kdfIterations, - kdfMemory: tokenResponse.kdfMemory, - kdfParallelism: tokenResponse.kdfParallelism, - kdfType: tokenResponse.kdf, }, }, }), @@ -195,6 +195,17 @@ export abstract class LoginStrategy { UserDecryptionOptions.fromResponse(tokenResponse), ); + await this.KdfConfigService.setKdfConfig( + userId as UserId, + tokenResponse.kdf === KdfType.PBKDF2_SHA256 + ? new PBKDF2KdfConfig(tokenResponse.kdfIterations) + : new Argon2KdfConfig( + tokenResponse.kdfIterations, + tokenResponse.kdfMemory, + tokenResponse.kdfParallelism, + ), + ); + await this.billingAccountProfileStateService.setHasPremium(accountInformation.premium, false); return userId as UserId; } diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts index b902fff574c..be09448fdd0 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts @@ -2,6 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; @@ -71,6 +72,7 @@ describe("PasswordLoginStrategy", () => { let policyService: MockProxy; let passwordStrengthService: MockProxy; let billingAccountProfileStateService: MockProxy; + let kdfConfigService: MockProxy; let passwordLoginStrategy: PasswordLoginStrategy; let credentials: PasswordLoginCredentials; @@ -94,6 +96,7 @@ describe("PasswordLoginStrategy", () => { policyService = mock(); passwordStrengthService = mock(); billingAccountProfileStateService = mock(); + kdfConfigService = mock(); appIdService.getAppId.mockResolvedValue(deviceId); tokenService.decodeAccessToken.mockResolvedValue({}); @@ -127,6 +130,7 @@ describe("PasswordLoginStrategy", () => { policyService, loginStrategyService, billingAccountProfileStateService, + kdfConfigService, ); credentials = new PasswordLoginCredentials(email, masterPassword); tokenResponse = identityTokenResponseFactory(masterPasswordPolicy); diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.ts b/libs/auth/src/common/login-strategies/password-login.strategy.ts index 2490c35a008..d3ce8fa9e8d 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.ts @@ -5,6 +5,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; @@ -89,6 +90,7 @@ export class PasswordLoginStrategy extends LoginStrategy { private policyService: PolicyService, private loginStrategyService: LoginStrategyServiceAbstraction, billingAccountProfileStateService: BillingAccountProfileStateService, + kdfConfigService: KdfConfigService, ) { super( accountService, @@ -104,6 +106,7 @@ export class PasswordLoginStrategy extends LoginStrategy { twoFactorService, userDecryptionOptionsService, billingAccountProfileStateService, + kdfConfigService, ); this.cache = new BehaviorSubject(data); diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts index df33415247f..3439a1c1991 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts @@ -2,6 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; @@ -54,6 +55,7 @@ describe("SsoLoginStrategy", () => { let authRequestService: MockProxy; let i18nService: MockProxy; let billingAccountProfileStateService: MockProxy; + let kdfConfigService: MockProxy; let ssoLoginStrategy: SsoLoginStrategy; let credentials: SsoLoginCredentials; @@ -86,6 +88,7 @@ describe("SsoLoginStrategy", () => { authRequestService = mock(); i18nService = mock(); billingAccountProfileStateService = mock(); + kdfConfigService = mock(); tokenService.getTwoFactorToken.mockResolvedValue(null); appIdService.getAppId.mockResolvedValue(deviceId); @@ -110,6 +113,7 @@ describe("SsoLoginStrategy", () => { authRequestService, i18nService, billingAccountProfileStateService, + kdfConfigService, ); credentials = new SsoLoginCredentials(ssoCode, ssoCodeVerifier, ssoRedirectUrl, ssoOrgId); }); diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.ts index dc63f0fae1d..c7cd9052f89 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.ts @@ -3,6 +3,7 @@ import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; @@ -98,6 +99,7 @@ export class SsoLoginStrategy extends LoginStrategy { private authRequestService: AuthRequestServiceAbstraction, private i18nService: I18nService, billingAccountProfileStateService: BillingAccountProfileStateService, + kdfConfigService: KdfConfigService, ) { super( accountService, @@ -113,6 +115,7 @@ export class SsoLoginStrategy extends LoginStrategy { twoFactorService, userDecryptionOptionsService, billingAccountProfileStateService, + kdfConfigService, ); this.cache = new BehaviorSubject(data); diff --git a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts index 5e7d7985b11..5fce8b0b825 100644 --- a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts @@ -2,6 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; @@ -49,6 +50,7 @@ describe("UserApiLoginStrategy", () => { let keyConnectorService: MockProxy; let environmentService: MockProxy; let billingAccountProfileStateService: MockProxy; + let kdfConfigService: MockProxy; let apiLogInStrategy: UserApiLoginStrategy; let credentials: UserApiLoginCredentials; @@ -76,6 +78,7 @@ describe("UserApiLoginStrategy", () => { keyConnectorService = mock(); environmentService = mock(); billingAccountProfileStateService = mock(); + kdfConfigService = mock(); appIdService.getAppId.mockResolvedValue(deviceId); tokenService.getTwoFactorToken.mockResolvedValue(null); @@ -98,6 +101,7 @@ describe("UserApiLoginStrategy", () => { environmentService, keyConnectorService, billingAccountProfileStateService, + kdfConfigService, ); credentials = new UserApiLoginCredentials(apiClientId, apiClientSecret); diff --git a/libs/auth/src/common/login-strategies/user-api-login.strategy.ts b/libs/auth/src/common/login-strategies/user-api-login.strategy.ts index 4a0d005b1c0..d7ee6fdc4ba 100644 --- a/libs/auth/src/common/login-strategies/user-api-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/user-api-login.strategy.ts @@ -3,6 +3,7 @@ import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; @@ -57,6 +58,7 @@ export class UserApiLoginStrategy extends LoginStrategy { private environmentService: EnvironmentService, private keyConnectorService: KeyConnectorService, billingAccountProfileStateService: BillingAccountProfileStateService, + protected kdfConfigService: KdfConfigService, ) { super( accountService, @@ -72,6 +74,7 @@ export class UserApiLoginStrategy extends LoginStrategy { twoFactorService, userDecryptionOptionsService, billingAccountProfileStateService, + kdfConfigService, ); this.cache = new BehaviorSubject(data); } diff --git a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts index 1d96921286f..d75e1949803 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts @@ -1,6 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; @@ -42,6 +43,7 @@ describe("WebAuthnLoginStrategy", () => { let twoFactorService!: MockProxy; let userDecryptionOptionsService: MockProxy; let billingAccountProfileStateService: MockProxy; + let kdfConfigService: MockProxy; let webAuthnLoginStrategy!: WebAuthnLoginStrategy; @@ -81,6 +83,7 @@ describe("WebAuthnLoginStrategy", () => { twoFactorService = mock(); userDecryptionOptionsService = mock(); billingAccountProfileStateService = mock(); + kdfConfigService = mock(); tokenService.getTwoFactorToken.mockResolvedValue(null); appIdService.getAppId.mockResolvedValue(deviceId); @@ -101,6 +104,7 @@ describe("WebAuthnLoginStrategy", () => { twoFactorService, userDecryptionOptionsService, billingAccountProfileStateService, + kdfConfigService, ); // Create credentials diff --git a/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts index 8a62a8fb3c0..ac487b3a820 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts @@ -3,6 +3,7 @@ import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; @@ -57,6 +58,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy { twoFactorService: TwoFactorService, userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, billingAccountProfileStateService: BillingAccountProfileStateService, + kdfConfigService: KdfConfigService, ) { super( accountService, @@ -72,6 +74,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy { twoFactorService, userDecryptionOptionsService, billingAccountProfileStateService, + kdfConfigService, ); this.cache = new BehaviorSubject(data); diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts index 33708885e26..f1b5590404f 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts @@ -3,6 +3,7 @@ import { MockProxy, mock } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; @@ -66,6 +67,7 @@ describe("LoginStrategyService", () => { let authRequestService: MockProxy; let userDecryptionOptionsService: MockProxy; let billingAccountProfileStateService: MockProxy; + let kdfConfigService: MockProxy; let stateProvider: FakeGlobalStateProvider; let loginStrategyCacheExpirationState: FakeGlobalState; @@ -95,6 +97,7 @@ describe("LoginStrategyService", () => { userDecryptionOptionsService = mock(); billingAccountProfileStateService = mock(); stateProvider = new FakeGlobalStateProvider(); + kdfConfigService = mock(); sut = new LoginStrategyService( accountService, @@ -119,6 +122,7 @@ describe("LoginStrategyService", () => { userDecryptionOptionsService, stateProvider, billingAccountProfileStateService, + kdfConfigService, ); loginStrategyCacheExpirationState = stateProvider.getFake(CACHE_EXPIRATION_KEY); diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts index aee74e66078..13cca69b3aa 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts @@ -10,13 +10,18 @@ import { import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; -import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; +import { + Argon2KdfConfig, + KdfConfig, + PBKDF2KdfConfig, +} from "@bitwarden/common/auth/models/domain/kdf-config"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { PasswordlessAuthRequest } from "@bitwarden/common/auth/models/request/passwordless-auth.request"; import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; @@ -32,7 +37,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { KdfType } from "@bitwarden/common/platform/enums"; +import { KdfType } from "@bitwarden/common/platform/enums/kdf-type.enum"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { GlobalState, GlobalStateProvider } from "@bitwarden/common/platform/state"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/src/auth/abstractions/device-trust.service.abstraction"; @@ -105,6 +110,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, protected stateProvider: GlobalStateProvider, protected billingAccountProfileStateService: BillingAccountProfileStateService, + protected kdfConfigService: KdfConfigService, ) { this.currentAuthnTypeState = this.stateProvider.get(CURRENT_LOGIN_STRATEGY_KEY); this.loginStrategyCacheState = this.stateProvider.get(CACHE_KEY); @@ -233,24 +239,25 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { async makePreloginKey(masterPassword: string, email: string): Promise { email = email.trim().toLowerCase(); - let kdf: KdfType = null; let kdfConfig: KdfConfig = null; try { const preloginResponse = await this.apiService.postPrelogin(new PreloginRequest(email)); if (preloginResponse != null) { - kdf = preloginResponse.kdf; - kdfConfig = new KdfConfig( - preloginResponse.kdfIterations, - preloginResponse.kdfMemory, - preloginResponse.kdfParallelism, - ); + kdfConfig = + preloginResponse.kdf === KdfType.PBKDF2_SHA256 + ? new PBKDF2KdfConfig(preloginResponse.kdfIterations) + : new Argon2KdfConfig( + preloginResponse.kdfIterations, + preloginResponse.kdfMemory, + preloginResponse.kdfParallelism, + ); } } catch (e) { if (e == null || e.statusCode !== 404) { throw e; } } - return await this.cryptoService.makeMasterKey(masterPassword, email, kdf, kdfConfig); + return await this.cryptoService.makeMasterKey(masterPassword, email, kdfConfig); } // TODO: move to auth request service @@ -354,6 +361,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.policyService, this, this.billingAccountProfileStateService, + this.kdfConfigService, ); case AuthenticationType.Sso: return new SsoLoginStrategy( @@ -375,6 +383,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.authRequestService, this.i18nService, this.billingAccountProfileStateService, + this.kdfConfigService, ); case AuthenticationType.UserApiKey: return new UserApiLoginStrategy( @@ -394,6 +403,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.environmentService, this.keyConnectorService, this.billingAccountProfileStateService, + this.kdfConfigService, ); case AuthenticationType.AuthRequest: return new AuthRequestLoginStrategy( @@ -412,6 +422,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.userDecryptionOptionsService, this.deviceTrustService, this.billingAccountProfileStateService, + this.kdfConfigService, ); case AuthenticationType.WebAuthn: return new WebAuthnLoginStrategy( @@ -429,6 +440,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.twoFactorService, this.userDecryptionOptionsService, this.billingAccountProfileStateService, + this.kdfConfigService, ); } }), diff --git a/libs/auth/src/common/services/pin-crypto/pin-crypto.service.implementation.ts b/libs/auth/src/common/services/pin-crypto/pin-crypto.service.implementation.ts index 149d5d9a53d..85d36b8d73b 100644 --- a/libs/auth/src/common/services/pin-crypto/pin-crypto.service.implementation.ts +++ b/libs/auth/src/common/services/pin-crypto/pin-crypto.service.implementation.ts @@ -1,9 +1,9 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { KdfType } from "@bitwarden/common/platform/enums"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { PinLockType } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service"; import { UserKey } from "@bitwarden/common/types/key"; @@ -16,6 +16,7 @@ export class PinCryptoService implements PinCryptoServiceAbstraction { private cryptoService: CryptoService, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, private logService: LogService, + private kdfConfigService: KdfConfigService, ) {} async decryptUserKeyWithPin(pin: string): Promise { try { @@ -24,8 +25,7 @@ export class PinCryptoService implements PinCryptoServiceAbstraction { const { pinKeyEncryptedUserKey, oldPinKeyEncryptedMasterKey } = await this.getPinKeyEncryptedKeys(pinLockType); - const kdf: KdfType = await this.stateService.getKdfType(); - const kdfConfig: KdfConfig = await this.stateService.getKdfConfig(); + const kdfConfig: KdfConfig = await this.kdfConfigService.getKdfConfig(); let userKey: UserKey; const email = await this.stateService.getEmail(); if (oldPinKeyEncryptedMasterKey) { @@ -33,7 +33,6 @@ export class PinCryptoService implements PinCryptoServiceAbstraction { pinLockType === "TRANSIENT", pin, email, - kdf, kdfConfig, oldPinKeyEncryptedMasterKey, ); @@ -41,7 +40,6 @@ export class PinCryptoService implements PinCryptoServiceAbstraction { userKey = await this.cryptoService.decryptUserKeyWithPin( pin, email, - kdf, kdfConfig, pinKeyEncryptedUserKey, ); diff --git a/libs/auth/src/common/services/pin-crypto/pin-crypto.service.spec.ts b/libs/auth/src/common/services/pin-crypto/pin-crypto.service.spec.ts index 17e0e14c515..c6fddf8efb5 100644 --- a/libs/auth/src/common/services/pin-crypto/pin-crypto.service.spec.ts +++ b/libs/auth/src/common/services/pin-crypto/pin-crypto.service.spec.ts @@ -1,9 +1,10 @@ import { mock } from "jest-mock-extended"; -import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { DEFAULT_KDF_CONFIG } from "@bitwarden/common/platform/enums"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { @@ -13,6 +14,7 @@ import { import { UserKey } from "@bitwarden/common/types/key"; import { PinCryptoService } from "./pin-crypto.service.implementation"; + describe("PinCryptoService", () => { let pinCryptoService: PinCryptoService; @@ -20,6 +22,7 @@ describe("PinCryptoService", () => { const cryptoService = mock(); const vaultTimeoutSettingsService = mock(); const logService = mock(); + const kdfConfigService = mock(); beforeEach(() => { jest.clearAllMocks(); @@ -29,6 +32,7 @@ describe("PinCryptoService", () => { cryptoService, vaultTimeoutSettingsService, logService, + kdfConfigService, ); }); @@ -39,7 +43,6 @@ describe("PinCryptoService", () => { describe("decryptUserKeyWithPin(...)", () => { const mockPin = "1234"; const mockProtectedPin = "protectedPin"; - const DEFAULT_PBKDF2_ITERATIONS = 600000; const mockUserEmail = "user@example.com"; const mockUserKey = new SymmetricCryptoKey(randomBytes(32)) as UserKey; @@ -49,7 +52,7 @@ describe("PinCryptoService", () => { ) { vaultTimeoutSettingsService.isPinLockSet.mockResolvedValue(pinLockType); - stateService.getKdfConfig.mockResolvedValue(new KdfConfig(DEFAULT_PBKDF2_ITERATIONS)); + kdfConfigService.getKdfConfig.mockResolvedValue(DEFAULT_KDF_CONFIG); stateService.getEmail.mockResolvedValue(mockUserEmail); if (migrationStatus === "PRE") { diff --git a/libs/common/src/auth/abstractions/kdf-config.service.ts b/libs/common/src/auth/abstractions/kdf-config.service.ts new file mode 100644 index 00000000000..6b41979e1b9 --- /dev/null +++ b/libs/common/src/auth/abstractions/kdf-config.service.ts @@ -0,0 +1,7 @@ +import { UserId } from "../../types/guid"; +import { KdfConfig } from "../models/domain/kdf-config"; + +export abstract class KdfConfigService { + setKdfConfig: (userId: UserId, KdfConfig: KdfConfig) => Promise; + getKdfConfig: () => Promise; +} diff --git a/libs/common/src/auth/models/domain/kdf-config.ts b/libs/common/src/auth/models/domain/kdf-config.ts index a25ba586e90..ce01f097028 100644 --- a/libs/common/src/auth/models/domain/kdf-config.ts +++ b/libs/common/src/auth/models/domain/kdf-config.ts @@ -1,11 +1,86 @@ -export class KdfConfig { - iterations: number; - memory?: number; - parallelism?: number; +import { Jsonify } from "type-fest"; - constructor(iterations: number, memory?: number, parallelism?: number) { - this.iterations = iterations; - this.memory = memory; - this.parallelism = parallelism; +import { + ARGON2_ITERATIONS, + ARGON2_MEMORY, + ARGON2_PARALLELISM, + KdfType, + PBKDF2_ITERATIONS, +} from "../../../platform/enums/kdf-type.enum"; + +/** + * Represents a type safe KDF configuration. + */ +export type KdfConfig = PBKDF2KdfConfig | Argon2KdfConfig; + +/** + * Password-Based Key Derivation Function 2 (PBKDF2) KDF configuration. + */ +export class PBKDF2KdfConfig { + kdfType: KdfType.PBKDF2_SHA256 = KdfType.PBKDF2_SHA256; + iterations: number; + + constructor(iterations?: number) { + this.iterations = iterations ?? PBKDF2_ITERATIONS.defaultValue; + } + + /** + * Validates the PBKDF2 KDF configuration. + * A Valid PBKDF2 KDF configuration has KDF iterations between the 600_000 and 2_000_000. + */ + validateKdfConfig(): void { + if (!PBKDF2_ITERATIONS.inRange(this.iterations)) { + throw new Error( + `PBKDF2 iterations must be between ${PBKDF2_ITERATIONS.min} and ${PBKDF2_ITERATIONS.max}`, + ); + } + } + + static fromJSON(json: Jsonify): PBKDF2KdfConfig { + return new PBKDF2KdfConfig(json.iterations); + } +} + +/** + * Argon2 KDF configuration. + */ +export class Argon2KdfConfig { + kdfType: KdfType.Argon2id = KdfType.Argon2id; + iterations: number; + memory: number; + parallelism: number; + + constructor(iterations?: number, memory?: number, parallelism?: number) { + this.iterations = iterations ?? ARGON2_ITERATIONS.defaultValue; + this.memory = memory ?? ARGON2_MEMORY.defaultValue; + this.parallelism = parallelism ?? ARGON2_PARALLELISM.defaultValue; + } + + /** + * Validates the Argon2 KDF configuration. + * A Valid Argon2 KDF configuration has iterations between 2 and 10, memory between 16mb and 1024mb, and parallelism between 1 and 16. + */ + validateKdfConfig(): void { + if (!ARGON2_ITERATIONS.inRange(this.iterations)) { + throw new Error( + `Argon2 iterations must be between ${ARGON2_ITERATIONS.min} and ${ARGON2_ITERATIONS.max}`, + ); + } + + if (!ARGON2_MEMORY.inRange(this.memory)) { + throw new Error( + `Argon2 memory must be between ${ARGON2_MEMORY.min}mb and ${ARGON2_MEMORY.max}mb`, + ); + } + + if (!ARGON2_PARALLELISM.inRange(this.parallelism)) { + throw new Error( + `Argon2 parallelism must be between ${ARGON2_PARALLELISM.min} and ${ARGON2_PARALLELISM.max}.`, + ); + } + } + + static fromJSON(json: Jsonify): Argon2KdfConfig { + return new Argon2KdfConfig(json.iterations, json.memory, json.parallelism); } } diff --git a/libs/common/src/auth/models/request/set-key-connector-key.request.ts b/libs/common/src/auth/models/request/set-key-connector-key.request.ts index dfd32689d87..c8081bdec2a 100644 --- a/libs/common/src/auth/models/request/set-key-connector-key.request.ts +++ b/libs/common/src/auth/models/request/set-key-connector-key.request.ts @@ -11,18 +11,14 @@ export class SetKeyConnectorKeyRequest { kdfParallelism?: number; orgIdentifier: string; - constructor( - key: string, - kdf: KdfType, - kdfConfig: KdfConfig, - orgIdentifier: string, - keys: KeysRequest, - ) { + constructor(key: string, kdfConfig: KdfConfig, orgIdentifier: string, keys: KeysRequest) { this.key = key; - this.kdf = kdf; + this.kdf = kdfConfig.kdfType; this.kdfIterations = kdfConfig.iterations; - this.kdfMemory = kdfConfig.memory; - this.kdfParallelism = kdfConfig.parallelism; + if (kdfConfig.kdfType === KdfType.Argon2id) { + this.kdfMemory = kdfConfig.memory; + this.kdfParallelism = kdfConfig.parallelism; + } this.orgIdentifier = orgIdentifier; this.keys = keys; } diff --git a/libs/common/src/auth/services/kdf-config.service.spec.ts b/libs/common/src/auth/services/kdf-config.service.spec.ts new file mode 100644 index 00000000000..67bcf721bc4 --- /dev/null +++ b/libs/common/src/auth/services/kdf-config.service.spec.ts @@ -0,0 +1,104 @@ +import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../spec"; +import { + ARGON2_ITERATIONS, + ARGON2_MEMORY, + ARGON2_PARALLELISM, + PBKDF2_ITERATIONS, +} from "../../platform/enums/kdf-type.enum"; +import { Utils } from "../../platform/misc/utils"; +import { UserId } from "../../types/guid"; +import { Argon2KdfConfig, PBKDF2KdfConfig } from "../models/domain/kdf-config"; + +import { KdfConfigService } from "./kdf-config.service"; + +describe("KdfConfigService", () => { + let sutKdfConfigService: KdfConfigService; + + let fakeStateProvider: FakeStateProvider; + let fakeAccountService: FakeAccountService; + const mockUserId = Utils.newGuid() as UserId; + + beforeEach(() => { + jest.clearAllMocks(); + + fakeAccountService = mockAccountServiceWith(mockUserId); + fakeStateProvider = new FakeStateProvider(fakeAccountService); + sutKdfConfigService = new KdfConfigService(fakeStateProvider); + }); + + it("setKdfConfig(): should set the KDF config", async () => { + const kdfConfig: PBKDF2KdfConfig = new PBKDF2KdfConfig(600_000); + await sutKdfConfigService.setKdfConfig(mockUserId, kdfConfig); + await expect(sutKdfConfigService.getKdfConfig()).resolves.toEqual(kdfConfig); + }); + + it("setKdfConfig(): should get the KDF config", async () => { + const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 64, 4); + await sutKdfConfigService.setKdfConfig(mockUserId, kdfConfig); + await expect(sutKdfConfigService.getKdfConfig()).resolves.toEqual(kdfConfig); + }); + + it("setKdfConfig(): should throw error KDF cannot be null", async () => { + const kdfConfig: Argon2KdfConfig = null; + try { + await sutKdfConfigService.setKdfConfig(mockUserId, kdfConfig); + } catch (e) { + expect(e).toEqual(new Error("kdfConfig cannot be null")); + } + }); + + it("setKdfConfig(): should throw error userId cannot be null", async () => { + const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 64, 4); + try { + await sutKdfConfigService.setKdfConfig(null, kdfConfig); + } catch (e) { + expect(e).toEqual(new Error("userId cannot be null")); + } + }); + + it("getKdfConfig(): should throw error KdfConfig for active user account state is null", async () => { + try { + await sutKdfConfigService.getKdfConfig(); + } catch (e) { + expect(e).toEqual(new Error("KdfConfig for active user account state is null")); + } + }); + + it("validateKdfConfig(): should validate the PBKDF2 KDF config", () => { + const kdfConfig: PBKDF2KdfConfig = new PBKDF2KdfConfig(600_000); + expect(() => kdfConfig.validateKdfConfig()).not.toThrow(); + }); + + it("validateKdfConfig(): should validate the Argon2id KDF config", () => { + const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 64, 4); + expect(() => kdfConfig.validateKdfConfig()).not.toThrow(); + }); + + it("validateKdfConfig(): should throw an error for invalid PBKDF2 iterations", () => { + const kdfConfig: PBKDF2KdfConfig = new PBKDF2KdfConfig(100); + expect(() => kdfConfig.validateKdfConfig()).toThrow( + `PBKDF2 iterations must be between ${PBKDF2_ITERATIONS.min} and ${PBKDF2_ITERATIONS.max}`, + ); + }); + + it("validateKdfConfig(): should throw an error for invalid Argon2 iterations", () => { + const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(11, 64, 4); + expect(() => kdfConfig.validateKdfConfig()).toThrow( + `Argon2 iterations must be between ${ARGON2_ITERATIONS.min} and ${ARGON2_ITERATIONS.max}`, + ); + }); + + it("validateKdfConfig(): should throw an error for invalid Argon2 memory", () => { + const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 1025, 4); + expect(() => kdfConfig.validateKdfConfig()).toThrow( + `Argon2 memory must be between ${ARGON2_MEMORY.min}mb and ${ARGON2_MEMORY.max}mb`, + ); + }); + + it("validateKdfConfig(): should throw an error for invalid Argon2 parallelism", () => { + const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 64, 17); + expect(() => kdfConfig.validateKdfConfig()).toThrow( + `Argon2 parallelism must be between ${ARGON2_PARALLELISM.min} and ${ARGON2_PARALLELISM.max}`, + ); + }); +}); diff --git a/libs/common/src/auth/services/kdf-config.service.ts b/libs/common/src/auth/services/kdf-config.service.ts new file mode 100644 index 00000000000..cfd2a3e1de0 --- /dev/null +++ b/libs/common/src/auth/services/kdf-config.service.ts @@ -0,0 +1,41 @@ +import { firstValueFrom } from "rxjs"; + +import { KdfType } from "../../platform/enums/kdf-type.enum"; +import { KDF_CONFIG_DISK, StateProvider, UserKeyDefinition } from "../../platform/state"; +import { UserId } from "../../types/guid"; +import { KdfConfigService as KdfConfigServiceAbstraction } from "../abstractions/kdf-config.service"; +import { Argon2KdfConfig, KdfConfig, PBKDF2KdfConfig } from "../models/domain/kdf-config"; + +export const KDF_CONFIG = new UserKeyDefinition(KDF_CONFIG_DISK, "kdfConfig", { + deserializer: (kdfConfig: KdfConfig) => { + if (kdfConfig == null) { + return null; + } + return kdfConfig.kdfType === KdfType.PBKDF2_SHA256 + ? PBKDF2KdfConfig.fromJSON(kdfConfig) + : Argon2KdfConfig.fromJSON(kdfConfig); + }, + clearOn: ["logout"], +}); + +export class KdfConfigService implements KdfConfigServiceAbstraction { + constructor(private stateProvider: StateProvider) {} + async setKdfConfig(userId: UserId, kdfConfig: KdfConfig) { + if (!userId) { + throw new Error("userId cannot be null"); + } + if (kdfConfig === null) { + throw new Error("kdfConfig cannot be null"); + } + await this.stateProvider.setUserState(KDF_CONFIG, kdfConfig, userId); + } + + async getKdfConfig(): Promise { + const userId = await firstValueFrom(this.stateProvider.activeUserId$); + const state = await firstValueFrom(this.stateProvider.getUser(userId, KDF_CONFIG).state$); + if (state === null) { + throw new Error("KdfConfig for active user account state is null"); + } + return state; + } +} diff --git a/libs/common/src/auth/services/key-connector.service.ts b/libs/common/src/auth/services/key-connector.service.ts index f8e523cce46..c19185ae917 100644 --- a/libs/common/src/auth/services/key-connector.service.ts +++ b/libs/common/src/auth/services/key-connector.service.ts @@ -7,6 +7,7 @@ import { KeysRequest } from "../../models/request/keys.request"; import { CryptoService } from "../../platform/abstractions/crypto.service"; import { KeyGenerationService } from "../../platform/abstractions/key-generation.service"; import { LogService } from "../../platform/abstractions/log.service"; +import { KdfType } from "../../platform/enums/kdf-type.enum"; import { Utils } from "../../platform/misc/utils"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; import { @@ -20,7 +21,7 @@ import { AccountService } from "../abstractions/account.service"; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "../abstractions/master-password.service.abstraction"; import { TokenService } from "../abstractions/token.service"; -import { KdfConfig } from "../models/domain/kdf-config"; +import { Argon2KdfConfig, KdfConfig, PBKDF2KdfConfig } from "../models/domain/kdf-config"; import { KeyConnectorUserKeyRequest } from "../models/request/key-connector-user-key.request"; import { SetKeyConnectorKeyRequest } from "../models/request/set-key-connector-key.request"; import { IdentityTokenResponse } from "../models/response/identity-token.response"; @@ -133,12 +134,14 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { userDecryptionOptions, } = tokenResponse; const password = await this.keyGenerationService.createKey(512); - const kdfConfig = new KdfConfig(kdfIterations, kdfMemory, kdfParallelism); + const kdfConfig: KdfConfig = + kdf === KdfType.PBKDF2_SHA256 + ? new PBKDF2KdfConfig(kdfIterations) + : new Argon2KdfConfig(kdfIterations, kdfMemory, kdfParallelism); const masterKey = await this.cryptoService.makeMasterKey( password.keyB64, await this.tokenService.getEmail(), - kdf, kdfConfig, ); const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64); @@ -162,7 +165,6 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { const keys = new KeysRequest(pubKey, privKey.encryptedString); const setPasswordRequest = new SetKeyConnectorKeyRequest( userKey[1].encryptedString, - kdf, kdfConfig, orgId, keys, diff --git a/libs/common/src/auth/services/user-verification/user-verification.service.ts b/libs/common/src/auth/services/user-verification/user-verification.service.ts index 5a443b784d6..94adad8bc71 100644 --- a/libs/common/src/auth/services/user-verification/user-verification.service.ts +++ b/libs/common/src/auth/services/user-verification/user-verification.service.ts @@ -13,6 +13,7 @@ import { KeySuffixOptions } from "../../../platform/enums/key-suffix-options.enu import { UserId } from "../../../types/guid"; import { UserKey } from "../../../types/key"; import { AccountService } from "../../abstractions/account.service"; +import { KdfConfigService } from "../../abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction"; import { UserVerificationApiServiceAbstraction } from "../../abstractions/user-verification/user-verification-api.service.abstraction"; import { UserVerificationService as UserVerificationServiceAbstraction } from "../../abstractions/user-verification/user-verification.service.abstraction"; @@ -47,6 +48,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti private logService: LogService, private vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction, private platformUtilsService: PlatformUtilsService, + private kdfConfigService: KdfConfigService, ) {} async getAvailableVerificationOptions( @@ -118,8 +120,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti masterKey = await this.cryptoService.makeMasterKey( verification.secret, await this.stateService.getEmail(), - await this.stateService.getKdfType(), - await this.stateService.getKdfConfig(), + await this.kdfConfigService.getKdfConfig(), ); } request.masterPasswordHash = alreadyHashed @@ -176,8 +177,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti masterKey = await this.cryptoService.makeMasterKey( verification.secret, await this.stateService.getEmail(), - await this.stateService.getKdfType(), - await this.stateService.getKdfConfig(), + await this.kdfConfigService.getKdfConfig(), ); } const passwordValid = await this.cryptoService.compareAndUpdateKeyHash( diff --git a/libs/common/src/platform/abstractions/crypto.service.ts b/libs/common/src/platform/abstractions/crypto.service.ts index 6609a1014e3..79a58f9d57b 100644 --- a/libs/common/src/platform/abstractions/crypto.service.ts +++ b/libs/common/src/platform/abstractions/crypto.service.ts @@ -6,7 +6,7 @@ import { ProfileProviderResponse } from "../../admin-console/models/response/pro import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { OrganizationId, ProviderId, UserId } from "../../types/guid"; import { UserKey, MasterKey, OrgKey, ProviderKey, PinKey, CipherKey } from "../../types/key"; -import { KeySuffixOptions, KdfType, HashPurpose } from "../enums"; +import { KeySuffixOptions, HashPurpose } from "../enums"; import { EncArrayBuffer } from "../models/domain/enc-array-buffer"; import { EncString } from "../models/domain/enc-string"; import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; @@ -114,16 +114,10 @@ export abstract class CryptoService { * Generates a master key from the provided password * @param password The user's master password * @param email The user's email - * @param kdf The user's selected key derivation function to use * @param KdfConfig The user's key derivation function configuration * @returns A master key derived from the provided password */ - abstract makeMasterKey( - password: string, - email: string, - kdf: KdfType, - KdfConfig: KdfConfig, - ): Promise; + abstract makeMasterKey(password: string, email: string, KdfConfig: KdfConfig): Promise; /** * Encrypts the existing (or provided) user key with the * provided master key @@ -258,16 +252,10 @@ export abstract class CryptoService { /** * @param pin The user's pin * @param salt The user's salt - * @param kdf The user's kdf * @param kdfConfig The user's kdf config * @returns A key derived from the user's pin */ - abstract makePinKey( - pin: string, - salt: string, - kdf: KdfType, - kdfConfig: KdfConfig, - ): Promise; + abstract makePinKey(pin: string, salt: string, kdfConfig: KdfConfig): Promise; /** * Clears the user's pin keys from storage * Note: This will remove the stored pin and as a result, @@ -279,7 +267,6 @@ export abstract class CryptoService { * Decrypts the user key with their pin * @param pin The user's PIN * @param salt The user's salt - * @param kdf The user's KDF * @param kdfConfig The user's KDF config * @param pinProtectedUserKey The user's PIN protected symmetric key, if not provided * it will be retrieved from storage @@ -288,7 +275,6 @@ export abstract class CryptoService { abstract decryptUserKeyWithPin( pin: string, salt: string, - kdf: KdfType, kdfConfig: KdfConfig, protectedKeyCs?: EncString, ): Promise; @@ -298,7 +284,6 @@ export abstract class CryptoService { * @param masterPasswordOnRestart True if Master Password on Restart is enabled * @param pin User's PIN * @param email User's email - * @param kdf User's KdfType * @param kdfConfig User's KdfConfig * @param oldPinKey The old Pin key from state (retrieved from different * places depending on if Master Password on Restart was enabled) @@ -308,7 +293,6 @@ export abstract class CryptoService { masterPasswordOnRestart: boolean, pin: string, email: string, - kdf: KdfType, kdfConfig: KdfConfig, oldPinKey: EncString, ): Promise; @@ -358,21 +342,12 @@ export abstract class CryptoService { privateKey: EncString; }>; - /** - * Validate that the KDF config follows the requirements for the given KDF type. - * - * @remarks - * Should always be called before updating a users KDF config. - */ - abstract validateKdfConfig(kdf: KdfType, kdfConfig: KdfConfig): void; - /** * @deprecated Left for migration purposes. Use decryptUserKeyWithPin instead. */ abstract decryptMasterKeyWithPin( pin: string, salt: string, - kdf: KdfType, kdfConfig: KdfConfig, protectedKeyCs?: EncString, ): Promise; diff --git a/libs/common/src/platform/abstractions/key-generation.service.ts b/libs/common/src/platform/abstractions/key-generation.service.ts index 223eb75038f..3a6971ba5d2 100644 --- a/libs/common/src/platform/abstractions/key-generation.service.ts +++ b/libs/common/src/platform/abstractions/key-generation.service.ts @@ -1,6 +1,5 @@ import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { CsprngArray } from "../../types/csprng"; -import { KdfType } from "../enums"; import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; export abstract class KeyGenerationService { @@ -46,14 +45,12 @@ export abstract class KeyGenerationService { * Derives a 32 byte key from a password using a key derivation function. * @param password Password to derive the key from. * @param salt Salt for the key derivation function. - * @param kdf Key derivation function to use. * @param kdfConfig Configuration for the key derivation function. * @returns 32 byte derived key. */ abstract deriveKeyFromPassword( password: string | Uint8Array, salt: string | Uint8Array, - kdf: KdfType, kdfConfig: KdfConfig, ): Promise; } diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index f1d4b3848ef..13c33305d1f 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -1,12 +1,10 @@ import { Observable } from "rxjs"; -import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { BiometricKey } from "../../auth/types/biometric-key"; import { GeneratorOptions } from "../../tools/generator/generator-options"; import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password"; import { UsernameGeneratorOptions } from "../../tools/generator/username"; import { UserId } from "../../types/guid"; -import { KdfType } from "../enums"; import { Account } from "../models/domain/account"; import { EncString } from "../models/domain/enc-string"; import { StorageOptions } from "../models/domain/storage-options"; @@ -149,10 +147,6 @@ export abstract class StateService { */ setEncryptedPinProtected: (value: string, options?: StorageOptions) => Promise; getIsAuthenticated: (options?: StorageOptions) => Promise; - getKdfConfig: (options?: StorageOptions) => Promise; - setKdfConfig: (kdfConfig: KdfConfig, options?: StorageOptions) => Promise; - getKdfType: (options?: StorageOptions) => Promise; - setKdfType: (value: KdfType, options?: StorageOptions) => Promise; getLastActive: (options?: StorageOptions) => Promise; setLastActive: (value: number, options?: StorageOptions) => Promise; getLastSync: (options?: StorageOptions) => Promise; diff --git a/libs/common/src/platform/enums/kdf-type.enum.ts b/libs/common/src/platform/enums/kdf-type.enum.ts index 97157910f5e..fd29bf308c2 100644 --- a/libs/common/src/platform/enums/kdf-type.enum.ts +++ b/libs/common/src/platform/enums/kdf-type.enum.ts @@ -1,4 +1,4 @@ -import { KdfConfig } from "../../auth/models/domain/kdf-config"; +import { PBKDF2KdfConfig } from "../../auth/models/domain/kdf-config"; import { RangeWithDefault } from "../misc/range-with-default"; export enum KdfType { @@ -12,4 +12,4 @@ export const ARGON2_ITERATIONS = new RangeWithDefault(2, 10, 3); export const DEFAULT_KDF_TYPE = KdfType.PBKDF2_SHA256; export const PBKDF2_ITERATIONS = new RangeWithDefault(600_000, 2_000_000, 600_000); -export const DEFAULT_KDF_CONFIG = new KdfConfig(PBKDF2_ITERATIONS.defaultValue); +export const DEFAULT_KDF_CONFIG = new PBKDF2KdfConfig(PBKDF2_ITERATIONS.defaultValue); diff --git a/libs/common/src/platform/services/crypto.service.spec.ts b/libs/common/src/platform/services/crypto.service.spec.ts index 2f68cf2ce7c..d9992adb57a 100644 --- a/libs/common/src/platform/services/crypto.service.spec.ts +++ b/libs/common/src/platform/services/crypto.service.spec.ts @@ -4,6 +4,7 @@ import { firstValueFrom, of, tap } from "rxjs"; import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state"; import { FakeStateProvider } from "../../../spec/fake-state-provider"; +import { KdfConfigService } from "../../auth/abstractions/kdf-config.service"; import { FakeMasterPasswordService } from "../../auth/services/master-password/fake-master-password.service"; import { CsprngArray } from "../../types/csprng"; import { UserId } from "../../types/guid"; @@ -37,6 +38,7 @@ describe("cryptoService", () => { const platformUtilService = mock(); const logService = mock(); const stateService = mock(); + const kdfConfigService = mock(); let stateProvider: FakeStateProvider; const mockUserId = Utils.newGuid() as UserId; @@ -58,6 +60,7 @@ describe("cryptoService", () => { stateService, accountService, stateProvider, + kdfConfigService, ); }); diff --git a/libs/common/src/platform/services/crypto.service.ts b/libs/common/src/platform/services/crypto.service.ts index 3cd443c0738..798173f5138 100644 --- a/libs/common/src/platform/services/crypto.service.ts +++ b/libs/common/src/platform/services/crypto.service.ts @@ -6,6 +6,7 @@ import { ProfileOrganizationResponse } from "../../admin-console/models/response import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response"; import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response"; import { AccountService } from "../../auth/abstractions/account.service"; +import { KdfConfigService } from "../../auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "../../auth/abstractions/master-password.service.abstraction"; import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { Utils } from "../../platform/misc/utils"; @@ -28,16 +29,7 @@ import { KeyGenerationService } from "../abstractions/key-generation.service"; import { LogService } from "../abstractions/log.service"; import { PlatformUtilsService } from "../abstractions/platform-utils.service"; import { StateService } from "../abstractions/state.service"; -import { - KeySuffixOptions, - HashPurpose, - KdfType, - ARGON2_ITERATIONS, - ARGON2_MEMORY, - ARGON2_PARALLELISM, - EncryptionType, - PBKDF2_ITERATIONS, -} from "../enums"; +import { KeySuffixOptions, HashPurpose, EncryptionType } from "../enums"; import { sequentialize } from "../misc/sequentialize"; import { EFFLongWordList } from "../misc/wordlist"; import { EncArrayBuffer } from "../models/domain/enc-array-buffer"; @@ -91,6 +83,7 @@ export class CryptoService implements CryptoServiceAbstraction { protected stateService: StateService, protected accountService: AccountService, protected stateProvider: StateProvider, + protected kdfConfigService: KdfConfigService, ) { // User Key this.activeUserKeyState = stateProvider.getActive(USER_KEY); @@ -283,8 +276,7 @@ export class CryptoService implements CryptoServiceAbstraction { return (masterKey ||= await this.makeMasterKey( password, await this.stateService.getEmail({ userId: userId }), - await this.stateService.getKdfType({ userId: userId }), - await this.stateService.getKdfConfig({ userId: userId }), + await this.kdfConfigService.getKdfConfig(), )); } @@ -295,16 +287,10 @@ export class CryptoService implements CryptoServiceAbstraction { * Does not validate the kdf config to ensure it satisfies the minimum requirements for the given kdf type. * TODO: Move to MasterPasswordService */ - async makeMasterKey( - password: string, - email: string, - kdf: KdfType, - KdfConfig: KdfConfig, - ): Promise { + async makeMasterKey(password: string, email: string, KdfConfig: KdfConfig): Promise { return (await this.keyGenerationService.deriveKeyFromPassword( password, email, - kdf, KdfConfig, )) as MasterKey; } @@ -560,8 +546,8 @@ export class CryptoService implements CryptoServiceAbstraction { await this.stateProvider.setUserState(USER_ENCRYPTED_PRIVATE_KEY, null, userId); } - async makePinKey(pin: string, salt: string, kdf: KdfType, kdfConfig: KdfConfig): Promise { - const pinKey = await this.keyGenerationService.deriveKeyFromPassword(pin, salt, kdf, kdfConfig); + async makePinKey(pin: string, salt: string, kdfConfig: KdfConfig): Promise { + const pinKey = await this.keyGenerationService.deriveKeyFromPassword(pin, salt, kdfConfig); return (await this.stretchKey(pinKey)) as PinKey; } @@ -575,7 +561,6 @@ export class CryptoService implements CryptoServiceAbstraction { async decryptUserKeyWithPin( pin: string, salt: string, - kdf: KdfType, kdfConfig: KdfConfig, pinProtectedUserKey?: EncString, ): Promise { @@ -584,7 +569,7 @@ export class CryptoService implements CryptoServiceAbstraction { if (!pinProtectedUserKey) { throw new Error("No PIN protected key found."); } - const pinKey = await this.makePinKey(pin, salt, kdf, kdfConfig); + const pinKey = await this.makePinKey(pin, salt, kdfConfig); const userKey = await this.encryptService.decryptToBytes(pinProtectedUserKey, pinKey); return new SymmetricCryptoKey(userKey) as UserKey; } @@ -593,7 +578,6 @@ export class CryptoService implements CryptoServiceAbstraction { async decryptMasterKeyWithPin( pin: string, salt: string, - kdf: KdfType, kdfConfig: KdfConfig, pinProtectedMasterKey?: EncString, ): Promise { @@ -604,7 +588,7 @@ export class CryptoService implements CryptoServiceAbstraction { } pinProtectedMasterKey = new EncString(pinProtectedMasterKeyString); } - const pinKey = await this.makePinKey(pin, salt, kdf, kdfConfig); + const pinKey = await this.makePinKey(pin, salt, kdfConfig); const masterKey = await this.encryptService.decryptToBytes(pinProtectedMasterKey, pinKey); return new SymmetricCryptoKey(masterKey) as MasterKey; } @@ -831,8 +815,7 @@ export class CryptoService implements CryptoServiceAbstraction { const pinKey = await this.makePinKey( pin, await this.stateService.getEmail({ userId: userId }), - await this.stateService.getKdfType({ userId: userId }), - await this.stateService.getKdfConfig({ userId: userId }), + await this.kdfConfigService.getKdfConfig(), ); const encPin = await this.encryptService.encrypt(key.key, pinKey); @@ -873,43 +856,6 @@ export class CryptoService implements CryptoServiceAbstraction { return null; } - /** - * Validate that the KDF config follows the requirements for the given KDF type. - * - * @remarks - * Should always be called before updating a users KDF config. - */ - validateKdfConfig(kdf: KdfType, kdfConfig: KdfConfig): void { - switch (kdf) { - case KdfType.PBKDF2_SHA256: - if (!PBKDF2_ITERATIONS.inRange(kdfConfig.iterations)) { - throw new Error( - `PBKDF2 iterations must be between ${PBKDF2_ITERATIONS.min} and ${PBKDF2_ITERATIONS.max}`, - ); - } - break; - case KdfType.Argon2id: - if (!ARGON2_ITERATIONS.inRange(kdfConfig.iterations)) { - throw new Error( - `Argon2 iterations must be between ${ARGON2_ITERATIONS.min} and ${ARGON2_ITERATIONS.max}`, - ); - } - - if (!ARGON2_MEMORY.inRange(kdfConfig.memory)) { - throw new Error( - `Argon2 memory must be between ${ARGON2_MEMORY.min}mb and ${ARGON2_MEMORY.max}mb`, - ); - } - - if (!ARGON2_PARALLELISM.inRange(kdfConfig.parallelism)) { - throw new Error( - `Argon2 parallelism must be between ${ARGON2_PARALLELISM.min} and ${ARGON2_PARALLELISM.max}.`, - ); - } - break; - } - } - protected async clearAllStoredUserKeys(userId?: UserId): Promise { await this.stateService.setUserKeyAutoUnlock(null, { userId: userId }); await this.stateService.setPinKeyEncryptedUserKeyEphemeral(null, { userId: userId }); @@ -1007,16 +953,15 @@ export class CryptoService implements CryptoServiceAbstraction { masterPasswordOnRestart: boolean, pin: string, email: string, - kdf: KdfType, kdfConfig: KdfConfig, oldPinKey: EncString, ): Promise { // Decrypt - const masterKey = await this.decryptMasterKeyWithPin(pin, email, kdf, kdfConfig, oldPinKey); + const masterKey = await this.decryptMasterKeyWithPin(pin, email, kdfConfig, oldPinKey); const encUserKey = await this.stateService.getEncryptedCryptoSymmetricKey(); const userKey = await this.decryptUserKeyWithMasterKey(masterKey, new EncString(encUserKey)); // Migrate - const pinKey = await this.makePinKey(pin, email, kdf, kdfConfig); + const pinKey = await this.makePinKey(pin, email, kdfConfig); const pinProtectedKey = await this.encryptService.encrypt(userKey.key, pinKey); if (masterPasswordOnRestart) { await this.stateService.setDecryptedPinProtected(null); diff --git a/libs/common/src/platform/services/key-generation.service.spec.ts b/libs/common/src/platform/services/key-generation.service.spec.ts index b3e0aa6d4e1..4f04eebd04e 100644 --- a/libs/common/src/platform/services/key-generation.service.spec.ts +++ b/libs/common/src/platform/services/key-generation.service.spec.ts @@ -1,9 +1,8 @@ import { mock } from "jest-mock-extended"; -import { KdfConfig } from "../../auth/models/domain/kdf-config"; +import { Argon2KdfConfig, PBKDF2KdfConfig } from "../../auth/models/domain/kdf-config"; import { CsprngArray } from "../../types/csprng"; import { CryptoFunctionService } from "../abstractions/crypto-function.service"; -import { KdfType } from "../enums"; import { KeyGenerationService } from "./key-generation.service"; @@ -75,12 +74,11 @@ describe("KeyGenerationService", () => { it("should derive a 32 byte key from a password using pbkdf2", async () => { const password = "password"; const salt = "salt"; - const kdf = KdfType.PBKDF2_SHA256; - const kdfConfig = new KdfConfig(600_000); + const kdfConfig = new PBKDF2KdfConfig(600_000); cryptoFunctionService.pbkdf2.mockResolvedValue(new Uint8Array(32)); - const key = await sut.deriveKeyFromPassword(password, salt, kdf, kdfConfig); + const key = await sut.deriveKeyFromPassword(password, salt, kdfConfig); expect(key.key.length).toEqual(32); }); @@ -88,13 +86,12 @@ describe("KeyGenerationService", () => { it("should derive a 32 byte key from a password using argon2id", async () => { const password = "password"; const salt = "salt"; - const kdf = KdfType.Argon2id; - const kdfConfig = new KdfConfig(600_000, 15); + const kdfConfig = new Argon2KdfConfig(3, 16, 4); cryptoFunctionService.hash.mockResolvedValue(new Uint8Array(32)); cryptoFunctionService.argon2.mockResolvedValue(new Uint8Array(32)); - const key = await sut.deriveKeyFromPassword(password, salt, kdf, kdfConfig); + const key = await sut.deriveKeyFromPassword(password, salt, kdfConfig); expect(key.key.length).toEqual(32); }); diff --git a/libs/common/src/platform/services/key-generation.service.ts b/libs/common/src/platform/services/key-generation.service.ts index c592f35e5f0..9202b37100b 100644 --- a/libs/common/src/platform/services/key-generation.service.ts +++ b/libs/common/src/platform/services/key-generation.service.ts @@ -46,17 +46,16 @@ export class KeyGenerationService implements KeyGenerationServiceAbstraction { async deriveKeyFromPassword( password: string | Uint8Array, salt: string | Uint8Array, - kdf: KdfType, kdfConfig: KdfConfig, ): Promise { let key: Uint8Array = null; - if (kdf == null || kdf === KdfType.PBKDF2_SHA256) { + if (kdfConfig.kdfType == null || kdfConfig.kdfType === KdfType.PBKDF2_SHA256) { if (kdfConfig.iterations == null) { kdfConfig.iterations = PBKDF2_ITERATIONS.defaultValue; } key = await this.cryptoFunctionService.pbkdf2(password, salt, "sha256", kdfConfig.iterations); - } else if (kdf == KdfType.Argon2id) { + } else if (kdfConfig.kdfType == KdfType.Argon2id) { if (kdfConfig.iterations == null) { kdfConfig.iterations = ARGON2_ITERATIONS.defaultValue; } diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 0c7cdd22d29..cab5768d2af 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -3,7 +3,6 @@ import { Jsonify, JsonValue } from "type-fest"; import { AccountService } from "../../auth/abstractions/account.service"; import { TokenService } from "../../auth/abstractions/token.service"; -import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { BiometricKey } from "../../auth/types/biometric-key"; import { GeneratorOptions } from "../../tools/generator/generator-options"; import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password"; @@ -19,7 +18,7 @@ import { AbstractMemoryStorageService, AbstractStorageService, } from "../abstractions/storage.service"; -import { HtmlStorageLocation, KdfType, StorageLocation } from "../enums"; +import { HtmlStorageLocation, StorageLocation } from "../enums"; import { StateFactory } from "../factories/state-factory"; import { Utils } from "../misc/utils"; import { Account, AccountData, AccountSettings } from "../models/domain/account"; @@ -643,49 +642,6 @@ export class StateService< ); } - async getKdfConfig(options?: StorageOptions): Promise { - const iterations = ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.profile?.kdfIterations; - const memory = ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.profile?.kdfMemory; - const parallelism = ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.profile?.kdfParallelism; - return new KdfConfig(iterations, memory, parallelism); - } - - async setKdfConfig(config: KdfConfig, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.profile.kdfIterations = config.iterations; - account.profile.kdfMemory = config.memory; - account.profile.kdfParallelism = config.parallelism; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getKdfType(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.profile?.kdfType; - } - - async setKdfType(value: KdfType, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.profile.kdfType = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - async getLastActive(options?: StorageOptions): Promise { options = this.reconcileOptions(options, await this.defaultOnDiskOptions()); diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index e04110f28b2..8847f7fe51a 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -35,6 +35,7 @@ export const BILLING_DISK = new StateDefinition("billing", "disk"); // Auth +export const KDF_CONFIG_DISK = new StateDefinition("kdfConfig", "disk"); export const KEY_CONNECTOR_DISK = new StateDefinition("keyConnector", "disk"); export const ACCOUNT_MEMORY = new StateDefinition("account", "memory"); export const MASTER_PASSWORD_MEMORY = new StateDefinition("masterPassword", "memory"); diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index 2d8ef1619e5..31bc5460b43 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -55,6 +55,7 @@ import { MoveMasterKeyStateToProviderMigrator } from "./migrations/55-move-maste import { AuthRequestMigrator } from "./migrations/56-move-auth-requests"; import { CipherServiceMigrator } from "./migrations/57-move-cipher-service-to-state-provider"; import { RemoveRefreshTokenMigratedFlagMigrator } from "./migrations/58-remove-refresh-token-migrated-state-provider-flag"; +import { KdfConfigMigrator } from "./migrations/59-move-kdf-config-to-state-provider"; import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key"; import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account"; import { MoveStateVersionMigrator } from "./migrations/8-move-state-version"; @@ -62,7 +63,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 58; +export const CURRENT_VERSION = 59; export type MinVersion = typeof MIN_VERSION; export function createMigrationBuilder() { @@ -122,7 +123,8 @@ export function createMigrationBuilder() { .with(MoveMasterKeyStateToProviderMigrator, 54, 55) .with(AuthRequestMigrator, 55, 56) .with(CipherServiceMigrator, 56, 57) - .with(RemoveRefreshTokenMigratedFlagMigrator, 57, CURRENT_VERSION); + .with(RemoveRefreshTokenMigratedFlagMigrator, 57, 58) + .with(KdfConfigMigrator, 58, CURRENT_VERSION); } export async function currentVersion( diff --git a/libs/common/src/state-migrations/migrations/59-move-kdf-config-to-state-provider.spec.ts b/libs/common/src/state-migrations/migrations/59-move-kdf-config-to-state-provider.spec.ts new file mode 100644 index 00000000000..dbce750a7e1 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/59-move-kdf-config-to-state-provider.spec.ts @@ -0,0 +1,153 @@ +import { MockProxy } from "jest-mock-extended"; + +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { mockMigrationHelper } from "../migration-helper.spec"; + +import { KdfConfigMigrator } from "./59-move-kdf-config-to-state-provider"; + +function exampleJSON() { + return { + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["FirstAccount", "SecondAccount"], + FirstAccount: { + profile: { + kdfIterations: 3, + kdfMemory: 64, + kdfParallelism: 5, + kdfType: 1, + otherStuff: "otherStuff1", + }, + otherStuff: "otherStuff2", + }, + SecondAccount: { + profile: { + kdfIterations: 600_001, + kdfMemory: null as number, + kdfParallelism: null as number, + kdfType: 0, + otherStuff: "otherStuff3", + }, + otherStuff: "otherStuff4", + }, + }; +} + +function rollbackJSON() { + return { + user_FirstAccount_kdfConfig_kdfConfig: { + iterations: 3, + memory: 64, + parallelism: 5, + kdfType: 1, + }, + user_SecondAccount_kdfConfig_kdfConfig: { + iterations: 600_001, + memory: null as number, + parallelism: null as number, + kdfType: 0, + }, + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["FirstAccount", "SecondAccount"], + FirstAccount: { + profile: { + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }, + SecondAccount: { + profile: { + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }, + }; +} + +const kdfConfigKeyDefinition: KeyDefinitionLike = { + key: "kdfConfig", + stateDefinition: { + name: "kdfConfig", + }, +}; + +describe("KdfConfigMigrator", () => { + let helper: MockProxy; + let sut: KdfConfigMigrator; + + describe("migrate", () => { + beforeEach(() => { + helper = mockMigrationHelper(exampleJSON(), 59); + sut = new KdfConfigMigrator(58, 59); + }); + + it("should remove kdfType and kdfConfig from Account.Profile", async () => { + await sut.migrate(helper); + + expect(helper.set).toHaveBeenCalledTimes(2); + expect(helper.set).toHaveBeenCalledWith("FirstAccount", { + profile: { + otherStuff: "otherStuff1", + }, + otherStuff: "otherStuff2", + }); + expect(helper.set).toHaveBeenCalledWith("SecondAccount", { + profile: { + otherStuff: "otherStuff3", + }, + otherStuff: "otherStuff4", + }); + expect(helper.setToUser).toHaveBeenCalledWith("FirstAccount", kdfConfigKeyDefinition, { + iterations: 3, + memory: 64, + parallelism: 5, + kdfType: 1, + }); + expect(helper.setToUser).toHaveBeenCalledWith("SecondAccount", kdfConfigKeyDefinition, { + iterations: 600_001, + memory: null as number, + parallelism: null as number, + kdfType: 0, + }); + }); + }); + + describe("rollback", () => { + beforeEach(() => { + helper = mockMigrationHelper(rollbackJSON(), 59); + sut = new KdfConfigMigrator(58, 59); + }); + + it("should null out new KdfConfig account value and set account.profile", async () => { + await sut.rollback(helper); + + expect(helper.setToUser).toHaveBeenCalledTimes(2); + expect(helper.setToUser).toHaveBeenCalledWith("FirstAccount", kdfConfigKeyDefinition, null); + expect(helper.setToUser).toHaveBeenCalledWith("SecondAccount", kdfConfigKeyDefinition, null); + expect(helper.set).toHaveBeenCalledTimes(2); + expect(helper.set).toHaveBeenCalledWith("FirstAccount", { + profile: { + kdfIterations: 3, + kdfMemory: 64, + kdfParallelism: 5, + kdfType: 1, + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }); + expect(helper.set).toHaveBeenCalledWith("SecondAccount", { + profile: { + kdfIterations: 600_001, + kdfMemory: null as number, + kdfParallelism: null as number, + kdfType: 0, + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/59-move-kdf-config-to-state-provider.ts b/libs/common/src/state-migrations/migrations/59-move-kdf-config-to-state-provider.ts new file mode 100644 index 00000000000..332306c6d4b --- /dev/null +++ b/libs/common/src/state-migrations/migrations/59-move-kdf-config-to-state-provider.ts @@ -0,0 +1,78 @@ +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { Migrator } from "../migrator"; + +enum KdfType { + PBKDF2_SHA256 = 0, + Argon2id = 1, +} + +class KdfConfig { + iterations: number; + kdfType: KdfType; + memory?: number; + parallelism?: number; +} + +type ExpectedAccountType = { + profile?: { + kdfIterations: number; + kdfType: KdfType; + kdfMemory?: number; + kdfParallelism?: number; + }; +}; + +const kdfConfigKeyDefinition: KeyDefinitionLike = { + key: "kdfConfig", + stateDefinition: { + name: "kdfConfig", + }, +}; + +export class KdfConfigMigrator extends Migrator<58, 59> { + async migrate(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + async function migrateAccount(userId: string, account: ExpectedAccountType): Promise { + const iterations = account?.profile?.kdfIterations; + const kdfType = account?.profile?.kdfType; + const memory = account?.profile?.kdfMemory; + const parallelism = account?.profile?.kdfParallelism; + + const kdfConfig: KdfConfig = { + iterations: iterations, + kdfType: kdfType, + memory: memory, + parallelism: parallelism, + }; + + if (kdfConfig != null) { + await helper.setToUser(userId, kdfConfigKeyDefinition, kdfConfig); + delete account?.profile?.kdfIterations; + delete account?.profile?.kdfType; + delete account?.profile?.kdfMemory; + delete account?.profile?.kdfParallelism; + } + + await helper.set(userId, account); + } + await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]); + } + + async rollback(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise { + const kdfConfig: KdfConfig = await helper.getFromUser(userId, kdfConfigKeyDefinition); + + if (kdfConfig != null) { + account.profile.kdfIterations = kdfConfig.iterations; + account.profile.kdfType = kdfConfig.kdfType; + account.profile.kdfMemory = kdfConfig.memory; + account.profile.kdfParallelism = kdfConfig.parallelism; + await helper.setToUser(userId, kdfConfigKeyDefinition, null); + } + await helper.set(userId, account); + } + + await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]); + } +} diff --git a/libs/common/src/tools/send/services/send.service.ts b/libs/common/src/tools/send/services/send.service.ts index 33b1f28be0c..fb67de55019 100644 --- a/libs/common/src/tools/send/services/send.service.ts +++ b/libs/common/src/tools/send/services/send.service.ts @@ -1,10 +1,10 @@ import { Observable, concatMap, distinctUntilChanged, firstValueFrom, map } from "rxjs"; +import { PBKDF2KdfConfig } from "../../../auth/models/domain/kdf-config"; import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service"; -import { KdfType } from "../../../platform/enums"; import { Utils } from "../../../platform/misc/utils"; import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer"; import { EncString } from "../../../platform/models/domain/enc-string"; @@ -69,8 +69,7 @@ export class SendService implements InternalSendServiceAbstraction { const passwordKey = await this.keyGenerationService.deriveKeyFromPassword( password, model.key, - KdfType.PBKDF2_SHA256, - { iterations: SEND_KDF_ITERATIONS }, + new PBKDF2KdfConfig(SEND_KDF_ITERATIONS), ); send.password = passwordKey.keyB64; } diff --git a/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.ts b/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.ts index 9e047b063c9..8f9e1abaf11 100644 --- a/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.ts +++ b/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.ts @@ -1,4 +1,8 @@ -import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; +import { + Argon2KdfConfig, + KdfConfig, + PBKDF2KdfConfig, +} from "@bitwarden/common/auth/models/domain/kdf-config"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { KdfType } from "@bitwarden/common/platform/enums"; @@ -69,12 +73,12 @@ export class BitwardenPasswordProtectedImporter extends BitwardenJsonImporter im return false; } - this.key = await this.cryptoService.makePinKey( - password, - jdoc.salt, - jdoc.kdfType, - new KdfConfig(jdoc.kdfIterations, jdoc.kdfMemory, jdoc.kdfParallelism), - ); + const kdfConfig: KdfConfig = + jdoc.kdfType === KdfType.PBKDF2_SHA256 + ? new PBKDF2KdfConfig(jdoc.kdfIterations) + : new Argon2KdfConfig(jdoc.kdfIterations, jdoc.kdfMemory, jdoc.kdfParallelism); + + this.key = await this.cryptoService.makePinKey(password, jdoc.salt, kdfConfig); const encKeyValidation = new EncString(jdoc.encKeyValidation_DO_NOT_EDIT); diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/base-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/base-vault-export.service.ts index 1865c94c7d4..dd5a210bf8a 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/base-vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/base-vault-export.service.ts @@ -1,7 +1,7 @@ +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { KdfType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -12,15 +12,14 @@ export class BaseVaultExportService { constructor( protected cryptoService: CryptoService, private cryptoFunctionService: CryptoFunctionService, - private stateService: StateService, + private kdfConfigService: KdfConfigService, ) {} protected async buildPasswordExport(clearText: string, password: string): Promise { - const kdfType: KdfType = await this.stateService.getKdfType(); - const kdfConfig: KdfConfig = await this.stateService.getKdfConfig(); + const kdfConfig: KdfConfig = await this.kdfConfigService.getKdfConfig(); const salt = Utils.fromBufferToB64(await this.cryptoFunctionService.randomBytes(16)); - const key = await this.cryptoService.makePinKey(password, salt, kdfType, kdfConfig); + const key = await this.cryptoService.makePinKey(password, salt, kdfConfig); const encKeyValidation = await this.cryptoService.encrypt(Utils.newGuid(), key); const encText = await this.cryptoService.encrypt(clearText, key); @@ -29,14 +28,17 @@ export class BaseVaultExportService { encrypted: true, passwordProtected: true, salt: salt, - kdfType: kdfType, + kdfType: kdfConfig.kdfType, kdfIterations: kdfConfig.iterations, - kdfMemory: kdfConfig.memory, - kdfParallelism: kdfConfig.parallelism, encKeyValidation_DO_NOT_EDIT: encKeyValidation.encryptedString, data: encText.encryptedString, }; + if (kdfConfig.kdfType === KdfType.Argon2id) { + jsonDoc.kdfMemory = kdfConfig.memory; + jsonDoc.kdfParallelism = kdfConfig.parallelism; + } + return JSON.stringify(jsonDoc, null, " "); } diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts index fc8faa4b5b9..b30384f9f42 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts @@ -1,13 +1,12 @@ import { mock, MockProxy } from "jest-mock-extended"; -import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { CipherWithIdExport } from "@bitwarden/common/models/export/cipher-with-ids.export"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { KdfType, PBKDF2_ITERATIONS } from "@bitwarden/common/platform/enums"; +import { DEFAULT_KDF_CONFIG, KdfType, PBKDF2_ITERATIONS } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string"; -import { StateService } from "@bitwarden/common/platform/services/state.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -110,10 +109,10 @@ function expectEqualCiphers(ciphers: CipherView[] | Cipher[], jsonResult: string expect(actual).toEqual(JSON.stringify(items)); } -function expectEqualFolderViews(folderviews: FolderView[] | Folder[], jsonResult: string) { +function expectEqualFolderViews(folderViews: FolderView[] | Folder[], jsonResult: string) { const actual = JSON.stringify(JSON.parse(jsonResult).folders); const folders: FolderResponse[] = []; - folderviews.forEach((c) => { + folderViews.forEach((c) => { const folder = new FolderResponse(); folder.id = c.id; folder.name = c.name.toString(); @@ -144,19 +143,18 @@ describe("VaultExportService", () => { let cipherService: MockProxy; let folderService: MockProxy; let cryptoService: MockProxy; - let stateService: MockProxy; + let kdfConfigService: MockProxy; beforeEach(() => { cryptoFunctionService = mock(); cipherService = mock(); folderService = mock(); cryptoService = mock(); - stateService = mock(); + kdfConfigService = mock(); folderService.getAllDecryptedFromState.mockResolvedValue(UserFolderViews); folderService.getAllFromState.mockResolvedValue(UserFolders); - stateService.getKdfType.mockResolvedValue(KdfType.PBKDF2_SHA256); - stateService.getKdfConfig.mockResolvedValue(new KdfConfig(PBKDF2_ITERATIONS.defaultValue)); + kdfConfigService.getKdfConfig.mockResolvedValue(DEFAULT_KDF_CONFIG); cryptoService.encrypt.mockResolvedValue(new EncString("encrypted")); exportService = new IndividualVaultExportService( @@ -164,7 +162,7 @@ describe("VaultExportService", () => { cipherService, cryptoService, cryptoFunctionService, - stateService, + kdfConfigService, ); }); diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts index 5f3bd9de521..ee178767f46 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts @@ -1,9 +1,9 @@ import * as papa from "papaparse"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { CipherWithIdExport, FolderWithIdExport } from "@bitwarden/common/models/export"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -32,9 +32,9 @@ export class IndividualVaultExportService private cipherService: CipherService, cryptoService: CryptoService, cryptoFunctionService: CryptoFunctionService, - stateService: StateService, + kdfConfigService: KdfConfigService, ) { - super(cryptoService, cryptoFunctionService, stateService); + super(cryptoService, cryptoFunctionService, kdfConfigService); } async getExport(format: ExportFormat = "csv"): Promise { diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts index 23465452317..98baf5dca35 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts @@ -1,10 +1,10 @@ import * as papa from "papaparse"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { CipherWithIdExport, CollectionWithIdExport } from "@bitwarden/common/models/export"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; @@ -36,10 +36,10 @@ export class OrganizationVaultExportService private apiService: ApiService, cryptoService: CryptoService, cryptoFunctionService: CryptoFunctionService, - stateService: StateService, private collectionService: CollectionService, + kdfConfigService: KdfConfigService, ) { - super(cryptoService, cryptoFunctionService, stateService); + super(cryptoService, cryptoFunctionService, kdfConfigService); } async getPasswordProtectedExport( diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.spec.ts b/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.spec.ts index fc8faa4b5b9..b30384f9f42 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.spec.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.spec.ts @@ -1,13 +1,12 @@ import { mock, MockProxy } from "jest-mock-extended"; -import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { CipherWithIdExport } from "@bitwarden/common/models/export/cipher-with-ids.export"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { KdfType, PBKDF2_ITERATIONS } from "@bitwarden/common/platform/enums"; +import { DEFAULT_KDF_CONFIG, KdfType, PBKDF2_ITERATIONS } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string"; -import { StateService } from "@bitwarden/common/platform/services/state.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -110,10 +109,10 @@ function expectEqualCiphers(ciphers: CipherView[] | Cipher[], jsonResult: string expect(actual).toEqual(JSON.stringify(items)); } -function expectEqualFolderViews(folderviews: FolderView[] | Folder[], jsonResult: string) { +function expectEqualFolderViews(folderViews: FolderView[] | Folder[], jsonResult: string) { const actual = JSON.stringify(JSON.parse(jsonResult).folders); const folders: FolderResponse[] = []; - folderviews.forEach((c) => { + folderViews.forEach((c) => { const folder = new FolderResponse(); folder.id = c.id; folder.name = c.name.toString(); @@ -144,19 +143,18 @@ describe("VaultExportService", () => { let cipherService: MockProxy; let folderService: MockProxy; let cryptoService: MockProxy; - let stateService: MockProxy; + let kdfConfigService: MockProxy; beforeEach(() => { cryptoFunctionService = mock(); cipherService = mock(); folderService = mock(); cryptoService = mock(); - stateService = mock(); + kdfConfigService = mock(); folderService.getAllDecryptedFromState.mockResolvedValue(UserFolderViews); folderService.getAllFromState.mockResolvedValue(UserFolders); - stateService.getKdfType.mockResolvedValue(KdfType.PBKDF2_SHA256); - stateService.getKdfConfig.mockResolvedValue(new KdfConfig(PBKDF2_ITERATIONS.defaultValue)); + kdfConfigService.getKdfConfig.mockResolvedValue(DEFAULT_KDF_CONFIG); cryptoService.encrypt.mockResolvedValue(new EncString("encrypted")); exportService = new IndividualVaultExportService( @@ -164,7 +162,7 @@ describe("VaultExportService", () => { cipherService, cryptoService, cryptoFunctionService, - stateService, + kdfConfigService, ); }); From e516eec200929f49b3710afc40efc361722da678 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Thu, 25 Apr 2024 14:55:45 -0400 Subject: [PATCH 063/110] Reintroduce null object remove rerouting (#8920) * Reintroduce null object remove rerouting * Test remove redirect --- .../abstract-chrome-storage-api.service.ts | 5 +++++ .../abstractions/chrome-storage-api.service.spec.ts | 11 +++++++++++ 2 files changed, 16 insertions(+) diff --git a/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts b/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts index 64935ab5919..259d6f154aa 100644 --- a/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts +++ b/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts @@ -78,6 +78,11 @@ export default abstract class AbstractChromeStorageService async save(key: string, obj: any): Promise { obj = objToStore(obj); + if (obj == null) { + // Safari does not support set of null values + return this.remove(key); + } + const keyedObj = { [key]: obj }; return new Promise((resolve) => { this.chromeStorageApi.set(keyedObj, () => { diff --git a/apps/browser/src/platform/services/abstractions/chrome-storage-api.service.spec.ts b/apps/browser/src/platform/services/abstractions/chrome-storage-api.service.spec.ts index 812901879d7..ceadc16a58e 100644 --- a/apps/browser/src/platform/services/abstractions/chrome-storage-api.service.spec.ts +++ b/apps/browser/src/platform/services/abstractions/chrome-storage-api.service.spec.ts @@ -62,6 +62,17 @@ describe("ChromeStorageApiService", () => { expect.any(Function), ); }); + + it("removes the key when the value is null", async () => { + const removeMock = chrome.storage.local.remove as jest.Mock; + removeMock.mockImplementation((key, callback) => { + delete store[key]; + callback(); + }); + const key = "key"; + await service.save(key, null); + expect(removeMock).toHaveBeenCalledWith(key, expect.any(Function)); + }); }); describe("get", () => { From cbf7c292f33dc38e3872d4527a970e0b36a3f283 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 25 Apr 2024 15:27:06 -0400 Subject: [PATCH 064/110] [AC-2485] Add redirects to clients components based on FF and provider status (#8839) * Add provider clients redirects based on FF and provider status * Fixing broken test --- .../clients/base-clients.component.ts | 130 +++++++++++ .../providers/clients/clients.component.ts | 187 +++++---------- .../providers/providers-layout.component.html | 7 +- .../providers/providers-layout.component.ts | 3 + ...t-organization-subscription.component.html | 9 +- .../manage-client-organizations.component.ts | 213 ++++++------------ libs/common/src/admin-console/enums/index.ts | 1 + .../enums/provider-status-type.enum.ts | 5 + .../models/data/provider.data.ts | 4 +- .../admin-console/models/domain/provider.ts | 4 +- .../response/profile-provider.response.ts | 4 +- .../services/provider.service.spec.ts | 3 +- 12 files changed, 287 insertions(+), 283 deletions(-) create mode 100644 bitwarden_license/bit-web/src/app/admin-console/providers/clients/base-clients.component.ts create mode 100644 libs/common/src/admin-console/enums/provider-status-type.enum.ts diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/base-clients.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/base-clients.component.ts new file mode 100644 index 00000000000..604d61f3db0 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/base-clients.component.ts @@ -0,0 +1,130 @@ +import { SelectionModel } from "@angular/cdk/collections"; +import { Directive, OnDestroy, OnInit } from "@angular/core"; +import { ActivatedRoute } from "@angular/router"; +import { BehaviorSubject, from, Subject, switchMap } from "rxjs"; +import { first, takeUntil } from "rxjs/operators"; + +import { SearchService } from "@bitwarden/common/abstractions/search.service"; +import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; +import { DialogService, TableDataSource, ToastService } from "@bitwarden/components"; + +import { WebProviderService } from "../services/web-provider.service"; + +@Directive() +export abstract class BaseClientsComponent implements OnInit, OnDestroy { + protected destroy$ = new Subject(); + + private searchText$ = new BehaviorSubject(""); + + get searchText() { + return this.searchText$.value; + } + + set searchText(value: string) { + this.searchText$.next(value); + this.selection.clear(); + this.dataSource.filter = value; + } + + private searching = false; + protected scrolled = false; + protected pageSize = 100; + private pagedClientsCount = 0; + protected selection = new SelectionModel(true, []); + + protected clients: ProviderOrganizationOrganizationDetailsResponse[]; + protected pagedClients: ProviderOrganizationOrganizationDetailsResponse[]; + protected dataSource = new TableDataSource(); + + abstract providerId: string; + + protected constructor( + protected activatedRoute: ActivatedRoute, + protected dialogService: DialogService, + private i18nService: I18nService, + private searchService: SearchService, + private toastService: ToastService, + private validationService: ValidationService, + private webProviderService: WebProviderService, + ) {} + + abstract load(): Promise; + + ngOnInit() { + this.activatedRoute.queryParams + .pipe(first(), takeUntil(this.destroy$)) + .subscribe((queryParams) => { + this.searchText = queryParams.search; + }); + + this.searchText$ + .pipe( + switchMap((searchText) => from(this.searchService.isSearchable(searchText))), + takeUntil(this.destroy$), + ) + .subscribe((isSearchable) => { + this.searching = isSearchable; + }); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + isPaging() { + if (this.searching && this.scrolled) { + this.resetPaging(); + } + return !this.searching && this.clients && this.clients.length > this.pageSize; + } + + resetPaging() { + this.pagedClients = []; + this.loadMore(); + } + + loadMore() { + if (!this.clients || this.clients.length <= this.pageSize) { + return; + } + const pagedLength = this.pagedClients.length; + let pagedSize = this.pageSize; + if (pagedLength === 0 && this.pagedClientsCount > this.pageSize) { + pagedSize = this.pagedClientsCount; + } + if (this.clients.length > pagedLength) { + this.pagedClients = this.pagedClients.concat( + this.clients.slice(pagedLength, pagedLength + pagedSize), + ); + } + this.pagedClientsCount = this.pagedClients.length; + this.scrolled = this.pagedClients.length > this.pageSize; + } + + async remove(organization: ProviderOrganizationOrganizationDetailsResponse) { + const confirmed = await this.dialogService.openSimpleDialog({ + title: organization.organizationName, + content: { key: "detachOrganizationConfirmation" }, + type: "warning", + }); + + if (!confirmed) { + return; + } + + try { + await this.webProviderService.detachOrganization(this.providerId, organization.id); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("detachedOrganization", organization.organizationName), + }); + await this.load(); + } catch (e) { + this.validationService.showError(e); + } + } +} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts index abdfd6defff..54e264c6668 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts @@ -1,29 +1,26 @@ -import { Component, OnInit } from "@angular/core"; +import { Component } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; -import { BehaviorSubject, Subject, firstValueFrom, from } from "rxjs"; -import { first, switchMap, takeUntil } from "rxjs/operators"; +import { combineLatest, firstValueFrom, from } from "rxjs"; +import { concatMap, switchMap, takeUntil } from "rxjs/operators"; -import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; -import { ProviderUserType } from "@bitwarden/common/admin-console/enums"; +import { ProviderStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; import { PlanType } from "@bitwarden/common/billing/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { WebProviderService } from "../services/web-provider.service"; import { AddOrganizationComponent } from "./add-organization.component"; +import { BaseClientsComponent } from "./base-clients.component"; const DisallowedPlanTypes = [ PlanType.Free, @@ -36,90 +33,76 @@ const DisallowedPlanTypes = [ @Component({ templateUrl: "clients.component.html", }) -// eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class ClientsComponent implements OnInit { +export class ClientsComponent extends BaseClientsComponent { providerId: string; addableOrganizations: Organization[]; loading = true; manageOrganizations = false; showAddExisting = false; - clients: ProviderOrganizationOrganizationDetailsResponse[]; - pagedClients: ProviderOrganizationOrganizationDetailsResponse[]; - - protected didScroll = false; - protected pageSize = 100; - protected actionPromise: Promise; - private pagedClientsCount = 0; - - protected enableConsolidatedBilling$ = this.configService.getFeatureFlag$( + protected consolidatedBillingEnabled$ = this.configService.getFeatureFlag$( FeatureFlag.EnableConsolidatedBilling, false, ); - private destroy$ = new Subject(); - private _searchText$ = new BehaviorSubject(""); - private isSearching: boolean = false; - - get searchText() { - return this._searchText$.value; - } - - set searchText(value: string) { - this._searchText$.next(value); - } constructor( - private route: ActivatedRoute, private router: Router, private providerService: ProviderService, private apiService: ApiService, - private searchService: SearchService, - private platformUtilsService: PlatformUtilsService, - private i18nService: I18nService, - private validationService: ValidationService, - private webProviderService: WebProviderService, - private logService: LogService, - private modalService: ModalService, private organizationService: OrganizationService, private organizationApiService: OrganizationApiServiceAbstraction, - private dialogService: DialogService, private configService: ConfigService, - ) {} + activatedRoute: ActivatedRoute, + dialogService: DialogService, + i18nService: I18nService, + searchService: SearchService, + toastService: ToastService, + validationService: ValidationService, + webProviderService: WebProviderService, + ) { + super( + activatedRoute, + dialogService, + i18nService, + searchService, + toastService, + validationService, + webProviderService, + ); + } - async ngOnInit() { - const enableConsolidatedBilling = await firstValueFrom(this.enableConsolidatedBilling$); - - if (enableConsolidatedBilling) { - await this.router.navigate(["../manage-client-organizations"], { relativeTo: this.route }); - } else { - this.route.parent.params - .pipe( - switchMap((params) => { - this.providerId = params.providerId; - return from(this.load()); - }), - takeUntil(this.destroy$), - ) - .subscribe(); - - this.route.queryParams.pipe(first(), takeUntil(this.destroy$)).subscribe((qParams) => { - this.searchText = qParams.search; - }); - - this._searchText$ - .pipe( - switchMap((searchText) => from(this.searchService.isSearchable(searchText))), - takeUntil(this.destroy$), - ) - .subscribe((isSearchable) => { - this.isSearching = isSearchable; - }); - } + ngOnInit() { + this.activatedRoute.parent.params + .pipe( + switchMap((params) => { + this.providerId = params.providerId; + return combineLatest([ + this.providerService.get(this.providerId), + this.consolidatedBillingEnabled$, + ]).pipe( + concatMap(([provider, consolidatedBillingEnabled]) => { + if ( + consolidatedBillingEnabled && + provider.providerStatus === ProviderStatusType.Billable + ) { + return from( + this.router.navigate(["../manage-client-organizations"], { + relativeTo: this.activatedRoute, + }), + ); + } else { + return from(this.load()); + } + }), + ); + }), + takeUntil(this.destroy$), + ) + .subscribe(); } ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); + super.ngOnDestroy(); } async load() { @@ -141,37 +124,6 @@ export class ClientsComponent implements OnInit { this.loading = false; } - isPaging() { - const searching = this.isSearching; - if (searching && this.didScroll) { - this.resetPaging(); - } - return !searching && this.clients && this.clients.length > this.pageSize; - } - - resetPaging() { - this.pagedClients = []; - this.loadMore(); - } - - loadMore() { - if (!this.clients || this.clients.length <= this.pageSize) { - return; - } - const pagedLength = this.pagedClients.length; - let pagedSize = this.pageSize; - if (pagedLength === 0 && this.pagedClientsCount > this.pageSize) { - pagedSize = this.pagedClientsCount; - } - if (this.clients.length > pagedLength) { - this.pagedClients = this.pagedClients.concat( - this.clients.slice(pagedLength, pagedLength + pagedSize), - ); - } - this.pagedClientsCount = this.pagedClients.length; - this.didScroll = this.pagedClients.length > this.pageSize; - } - async addExistingOrganization() { const dialogRef = AddOrganizationComponent.open(this.dialogService, { providerId: this.providerId, @@ -182,33 +134,4 @@ export class ClientsComponent implements OnInit { await this.load(); } } - - async remove(organization: ProviderOrganizationOrganizationDetailsResponse) { - const confirmed = await this.dialogService.openSimpleDialog({ - title: organization.organizationName, - content: { key: "detachOrganizationConfirmation" }, - type: "warning", - }); - - if (!confirmed) { - return false; - } - - this.actionPromise = this.webProviderService.detachOrganization( - this.providerId, - organization.id, - ); - try { - await this.actionPromise; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("detachedOrganization", organization.organizationName), - ); - await this.load(); - } catch (e) { - this.validationService.showError(e); - } - this.actionPromise = null; - } } diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html index ca2b1a35457..55efbe13864 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html @@ -7,7 +7,12 @@ {{ "assignedSeats" | i18n }} - +

diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organizations.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organizations.component.ts index 2184a617cf1..3cc96c4589d 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organizations.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organizations.component.ts @@ -1,21 +1,23 @@ -import { SelectionModel } from "@angular/cdk/collections"; -import { Component, OnDestroy, OnInit } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; -import { BehaviorSubject, firstValueFrom, from, lastValueFrom, Subject } from "rxjs"; -import { first, switchMap, takeUntil } from "rxjs/operators"; +import { Component } from "@angular/core"; +import { ActivatedRoute, Router } from "@angular/router"; +import { combineLatest, firstValueFrom, from, lastValueFrom } from "rxjs"; +import { concatMap, switchMap, takeUntil } from "rxjs/operators"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; -import { ProviderUserType } from "@bitwarden/common/admin-console/enums"; +import { ProviderStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums"; +import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; import { BillingApiServiceAbstraction as BillingApiService } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction"; import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { DialogService, TableDataSource } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; +import { BaseClientsComponent } from "../../../admin-console/providers/clients/base-clients.component"; import { WebProviderService } from "../../../admin-console/providers/services/web-provider.service"; import { @@ -27,127 +29,91 @@ import { ManageClientOrganizationSubscriptionComponent } from "./manage-client-o @Component({ templateUrl: "manage-client-organizations.component.html", }) - -// eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class ManageClientOrganizationsComponent implements OnInit, OnDestroy { +export class ManageClientOrganizationsComponent extends BaseClientsComponent { providerId: string; + provider: Provider; + loading = true; manageOrganizations = false; - private destroy$ = new Subject(); - private _searchText$ = new BehaviorSubject(""); - private isSearching: boolean = false; + private consolidatedBillingEnabled$ = this.configService.getFeatureFlag$( + FeatureFlag.EnableConsolidatedBilling, + false, + ); - get searchText() { - return this._searchText$.value; - } - - set searchText(search: string) { - this._searchText$.value; - - this.selection.clear(); - this.dataSource.filter = search; - } - - clients: ProviderOrganizationOrganizationDetailsResponse[]; - pagedClients: ProviderOrganizationOrganizationDetailsResponse[]; - - protected didScroll = false; - protected pageSize = 100; - protected actionPromise: Promise; - private pagedClientsCount = 0; - selection = new SelectionModel(true, []); - protected dataSource = new TableDataSource(); protected plans: PlanResponse[]; constructor( - private route: ActivatedRoute, - private providerService: ProviderService, private apiService: ApiService, - private searchService: SearchService, - private platformUtilsService: PlatformUtilsService, - private i18nService: I18nService, - private validationService: ValidationService, - private webProviderService: WebProviderService, - private dialogService: DialogService, private billingApiService: BillingApiService, - ) {} - - async ngOnInit() { - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - this.route.parent.params.subscribe(async (params) => { - this.providerId = params.providerId; - - await this.load(); - - /* eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe, rxjs/no-nested-subscribe */ - this.route.queryParams.pipe(first()).subscribe(async (qParams) => { - this.searchText = qParams.search; - }); - }); - - this._searchText$ - .pipe( - switchMap((searchText) => from(this.searchService.isSearchable(searchText))), - takeUntil(this.destroy$), - ) - .subscribe((isSearchable) => { - this.isSearching = isSearchable; - }); + private configService: ConfigService, + private providerService: ProviderService, + private router: Router, + activatedRoute: ActivatedRoute, + dialogService: DialogService, + i18nService: I18nService, + searchService: SearchService, + toastService: ToastService, + validationService: ValidationService, + webProviderService: WebProviderService, + ) { + super( + activatedRoute, + dialogService, + i18nService, + searchService, + toastService, + validationService, + webProviderService, + ); } - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); + ngOnInit() { + this.activatedRoute.parent.params + .pipe( + switchMap((params) => { + this.providerId = params.providerId; + return combineLatest([ + this.providerService.get(this.providerId), + this.consolidatedBillingEnabled$, + ]).pipe( + concatMap(([provider, consolidatedBillingEnabled]) => { + if ( + !consolidatedBillingEnabled || + provider.providerStatus !== ProviderStatusType.Billable + ) { + return from( + this.router.navigate(["../clients"], { + relativeTo: this.activatedRoute, + }), + ); + } else { + this.provider = provider; + this.manageOrganizations = this.provider.type === ProviderUserType.ProviderAdmin; + return from(this.load()); + } + }), + ); + }), + takeUntil(this.destroy$), + ) + .subscribe(); + } + + ngOnDestroy() { + super.ngOnDestroy(); } async load() { - const clientsResponse = await this.apiService.getProviderClients(this.providerId); - this.clients = - clientsResponse.data != null && clientsResponse.data.length > 0 ? clientsResponse.data : []; - this.dataSource.data = this.clients; - this.manageOrganizations = - (await this.providerService.get(this.providerId)).type === ProviderUserType.ProviderAdmin; + this.clients = (await this.apiService.getProviderClients(this.providerId)).data; - const plansResponse = await this.billingApiService.getPlans(); - this.plans = plansResponse.data; + this.dataSource.data = this.clients; + + this.plans = (await this.billingApiService.getPlans()).data; this.loading = false; } - isPaging() { - const searching = this.isSearching; - if (searching && this.didScroll) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.resetPaging(); - } - return !searching && this.clients && this.clients.length > this.pageSize; - } - - async resetPaging() { - this.pagedClients = []; - this.loadMore(); - } - - loadMore() { - if (!this.clients || this.clients.length <= this.pageSize) { - return; - } - const pagedLength = this.pagedClients.length; - let pagedSize = this.pageSize; - if (pagedLength === 0 && this.pagedClientsCount > this.pageSize) { - pagedSize = this.pagedClientsCount; - } - if (this.clients.length > pagedLength) { - this.pagedClients = this.pagedClients.concat( - this.clients.slice(pagedLength, pagedLength + pagedSize), - ); - } - this.pagedClientsCount = this.pagedClients.length; - this.didScroll = this.pagedClients.length > this.pageSize; - } - async manageSubscription(organization: ProviderOrganizationOrganizationDetailsResponse) { if (organization == null) { return; @@ -161,35 +127,6 @@ export class ManageClientOrganizationsComponent implements OnInit, OnDestroy { await this.load(); } - async remove(organization: ProviderOrganizationOrganizationDetailsResponse) { - const confirmed = await this.dialogService.openSimpleDialog({ - title: organization.organizationName, - content: { key: "detachOrganizationConfirmation" }, - type: "warning", - }); - - if (!confirmed) { - return false; - } - - this.actionPromise = this.webProviderService.detachOrganization( - this.providerId, - organization.id, - ); - try { - await this.actionPromise; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("detachedOrganization", organization.organizationName), - ); - await this.load(); - } catch (e) { - this.validationService.showError(e); - } - this.actionPromise = null; - } - createClientOrganization = async () => { const reference = openCreateClientOrganizationDialog(this.dialogService, { data: { diff --git a/libs/common/src/admin-console/enums/index.ts b/libs/common/src/admin-console/enums/index.ts index 0cbdf658053..83b8a941a01 100644 --- a/libs/common/src/admin-console/enums/index.ts +++ b/libs/common/src/admin-console/enums/index.ts @@ -7,3 +7,4 @@ export * from "./provider-type.enum"; export * from "./provider-user-status-type.enum"; export * from "./provider-user-type.enum"; export * from "./scim-provider-type.enum"; +export * from "./provider-status-type.enum"; diff --git a/libs/common/src/admin-console/enums/provider-status-type.enum.ts b/libs/common/src/admin-console/enums/provider-status-type.enum.ts new file mode 100644 index 00000000000..8da60af0eb3 --- /dev/null +++ b/libs/common/src/admin-console/enums/provider-status-type.enum.ts @@ -0,0 +1,5 @@ +export enum ProviderStatusType { + Pending = 0, + Created = 1, + Billable = 2, +} diff --git a/libs/common/src/admin-console/models/data/provider.data.ts b/libs/common/src/admin-console/models/data/provider.data.ts index a8488880259..ff060ae2704 100644 --- a/libs/common/src/admin-console/models/data/provider.data.ts +++ b/libs/common/src/admin-console/models/data/provider.data.ts @@ -1,4 +1,4 @@ -import { ProviderUserStatusType, ProviderUserType } from "../../enums"; +import { ProviderStatusType, ProviderUserStatusType, ProviderUserType } from "../../enums"; import { ProfileProviderResponse } from "../response/profile-provider.response"; export class ProviderData { @@ -9,6 +9,7 @@ export class ProviderData { enabled: boolean; userId: string; useEvents: boolean; + providerStatus: ProviderStatusType; constructor(response: ProfileProviderResponse) { this.id = response.id; @@ -18,5 +19,6 @@ export class ProviderData { this.enabled = response.enabled; this.userId = response.userId; this.useEvents = response.useEvents; + this.providerStatus = response.providerStatus; } } diff --git a/libs/common/src/admin-console/models/domain/provider.ts b/libs/common/src/admin-console/models/domain/provider.ts index d6d3d3c4623..d51f6985477 100644 --- a/libs/common/src/admin-console/models/domain/provider.ts +++ b/libs/common/src/admin-console/models/domain/provider.ts @@ -1,4 +1,4 @@ -import { ProviderUserStatusType, ProviderUserType } from "../../enums"; +import { ProviderStatusType, ProviderUserStatusType, ProviderUserType } from "../../enums"; import { ProviderData } from "../data/provider.data"; export class Provider { @@ -9,6 +9,7 @@ export class Provider { enabled: boolean; userId: string; useEvents: boolean; + providerStatus: ProviderStatusType; constructor(obj?: ProviderData) { if (obj == null) { @@ -22,6 +23,7 @@ export class Provider { this.enabled = obj.enabled; this.userId = obj.userId; this.useEvents = obj.useEvents; + this.providerStatus = obj.providerStatus; } get canAccess() { diff --git a/libs/common/src/admin-console/models/response/profile-provider.response.ts b/libs/common/src/admin-console/models/response/profile-provider.response.ts index eaecc9b8475..701fe843de8 100644 --- a/libs/common/src/admin-console/models/response/profile-provider.response.ts +++ b/libs/common/src/admin-console/models/response/profile-provider.response.ts @@ -1,5 +1,5 @@ import { BaseResponse } from "../../../models/response/base.response"; -import { ProviderUserStatusType, ProviderUserType } from "../../enums"; +import { ProviderStatusType, ProviderUserStatusType, ProviderUserType } from "../../enums"; import { PermissionsApi } from "../api/permissions.api"; export class ProfileProviderResponse extends BaseResponse { @@ -12,6 +12,7 @@ export class ProfileProviderResponse extends BaseResponse { permissions: PermissionsApi; userId: string; useEvents: boolean; + providerStatus: ProviderStatusType; constructor(response: any) { super(response); @@ -24,5 +25,6 @@ export class ProfileProviderResponse extends BaseResponse { this.permissions = new PermissionsApi(this.getResponseProperty("permissions")); this.userId = this.getResponseProperty("UserId"); this.useEvents = this.getResponseProperty("UseEvents"); + this.providerStatus = this.getResponseProperty("ProviderStatus"); } } diff --git a/libs/common/src/admin-console/services/provider.service.spec.ts b/libs/common/src/admin-console/services/provider.service.spec.ts index fcba9d5023e..95da633f5cc 100644 --- a/libs/common/src/admin-console/services/provider.service.spec.ts +++ b/libs/common/src/admin-console/services/provider.service.spec.ts @@ -2,7 +2,7 @@ import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from ". import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state"; import { Utils } from "../../platform/misc/utils"; import { UserId } from "../../types/guid"; -import { ProviderUserStatusType, ProviderUserType } from "../enums"; +import { ProviderStatusType, ProviderUserStatusType, ProviderUserType } from "../enums"; import { ProviderData } from "../models/data/provider.data"; import { Provider } from "../models/domain/provider"; @@ -64,6 +64,7 @@ describe("PROVIDERS key definition", () => { enabled: true, userId: "string", useEvents: true, + providerStatus: ProviderStatusType.Pending, }, }; const result = sut.deserializer(JSON.parse(JSON.stringify(expectedResult))); From 8afe915be1480220d8bb4a327455c6a4c4015b10 Mon Sep 17 00:00:00 2001 From: Jake Fink Date: Thu, 25 Apr 2024 16:45:23 -0400 Subject: [PATCH 065/110] [PM-7564] Move 2fa and login strategy service to popup and add state providers to 2fa service (#8820) * remove 2fa from main.background * remove login strategy service from main.background * move 2fa and login strategy service to popup, init in browser * add state providers to 2fa service - add deserializer helpers * use key definitions for global state * fix calls to 2fa service * remove extra await * add delay to wait for active account emission in popup * add and fix tests * fix cli * really fix cli * remove timeout and wait for active account * verify expected user is active account * fix tests * address feedback --- .../two-factor-service.factory.ts | 6 +- .../popup/two-factor-options.component.ts | 11 ++- .../browser/src/background/main.background.ts | 37 +------- .../src/popup/services/init.service.ts | 3 + .../src/popup/services/services.module.ts | 16 +--- apps/cli/src/auth/commands/login.command.ts | 4 +- apps/cli/src/bw.ts | 6 +- .../src/auth/components/sso.component.spec.ts | 2 +- .../two-factor-options.component.ts | 11 ++- .../auth/components/two-factor.component.ts | 22 +++-- .../src/services/jslib-services.module.ts | 2 +- .../auth-request-login.strategy.spec.ts | 4 +- .../login-strategies/login.strategy.spec.ts | 33 ++++--- .../common/login-strategies/login.strategy.ts | 34 ++++++-- .../password-login.strategy.spec.ts | 4 +- .../sso-login.strategy.spec.ts | 4 +- .../user-api-login.strategy.spec.ts | 4 +- .../webauthn-login.strategy.spec.ts | 10 ++- .../auth/abstractions/two-factor.service.ts | 14 +-- .../src/auth/models/domain/auth-result.ts | 2 +- .../response/identity-two-factor.response.ts | 14 +-- .../src/auth/services/two-factor.service.ts | 86 ++++++++++++------- .../state/deserialization-helpers.spec.ts | 25 ++++++ .../platform/state/deserialization-helpers.ts | 10 +-- .../src/platform/state/key-definition.ts | 2 +- .../src/platform/state/state-definitions.ts | 1 + .../src/platform/state/user-key-definition.ts | 2 +- 27 files changed, 217 insertions(+), 152 deletions(-) create mode 100644 libs/common/src/platform/state/deserialization-helpers.spec.ts diff --git a/apps/browser/src/auth/background/service-factories/two-factor-service.factory.ts b/apps/browser/src/auth/background/service-factories/two-factor-service.factory.ts index 1d79bbbaf1d..5af5eb00177 100644 --- a/apps/browser/src/auth/background/service-factories/two-factor-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/two-factor-service.factory.ts @@ -1,11 +1,13 @@ import { TwoFactorService as AbstractTwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service"; +import { GlobalStateProvider } from "@bitwarden/common/platform/state"; import { FactoryOptions, CachedServices, factory, } from "../../../platform/background/service-factories/factory-options"; +import { globalStateProviderFactory } from "../../../platform/background/service-factories/global-state-provider.factory"; import { I18nServiceInitOptions, i18nServiceFactory, @@ -19,7 +21,8 @@ type TwoFactorServiceFactoryOptions = FactoryOptions; export type TwoFactorServiceInitOptions = TwoFactorServiceFactoryOptions & I18nServiceInitOptions & - PlatformUtilsServiceInitOptions; + PlatformUtilsServiceInitOptions & + GlobalStateProvider; export async function twoFactorServiceFactory( cache: { twoFactorService?: AbstractTwoFactorService } & CachedServices, @@ -33,6 +36,7 @@ export async function twoFactorServiceFactory( new TwoFactorService( await i18nServiceFactory(cache, opts), await platformUtilsServiceFactory(cache, opts), + await globalStateProviderFactory(cache, opts), ), ); service.init(); diff --git a/apps/browser/src/auth/popup/two-factor-options.component.ts b/apps/browser/src/auth/popup/two-factor-options.component.ts index bad2e4a9e77..6191d277add 100644 --- a/apps/browser/src/auth/popup/two-factor-options.component.ts +++ b/apps/browser/src/auth/popup/two-factor-options.component.ts @@ -2,7 +2,10 @@ import { Component } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { TwoFactorOptionsComponent as BaseTwoFactorOptionsComponent } from "@bitwarden/angular/auth/components/two-factor-options.component"; -import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; +import { + TwoFactorProviderDetails, + TwoFactorService, +} from "@bitwarden/common/auth/abstractions/two-factor.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -27,9 +30,9 @@ export class TwoFactorOptionsComponent extends BaseTwoFactorOptionsComponent { this.navigateTo2FA(); } - choose(p: any) { - super.choose(p); - this.twoFactorService.setSelectedProvider(p.type); + override async choose(p: TwoFactorProviderDetails) { + await super.choose(p); + await this.twoFactorService.setSelectedProvider(p.type); this.navigateTo2FA(); } diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index b4375df7d50..758c226bc37 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -3,8 +3,6 @@ import { Subject, firstValueFrom, merge } from "rxjs"; import { PinCryptoServiceAbstraction, PinCryptoService, - LoginStrategyServiceAbstraction, - LoginStrategyService, InternalUserDecryptionOptionsServiceAbstraction, UserDecryptionOptionsService, AuthRequestServiceAbstraction, @@ -38,7 +36,6 @@ import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarde import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service"; -import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { UserVerificationApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification-api.service.abstraction"; import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; @@ -54,7 +51,6 @@ import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connect import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service"; import { SsoLoginService } from "@bitwarden/common/auth/services/sso-login.service"; import { TokenService } from "@bitwarden/common/auth/services/token.service"; -import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service"; import { UserVerificationApiService } from "@bitwarden/common/auth/services/user-verification/user-verification-api.service"; import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service"; import { @@ -277,7 +273,6 @@ export default class MainBackground { containerService: ContainerService; auditService: AuditServiceAbstraction; authService: AuthServiceAbstraction; - loginStrategyService: LoginStrategyServiceAbstraction; loginEmailService: LoginEmailServiceAbstraction; importApiService: ImportApiServiceAbstraction; importService: ImportServiceAbstraction; @@ -301,7 +296,6 @@ export default class MainBackground { providerService: ProviderServiceAbstraction; keyConnectorService: KeyConnectorServiceAbstraction; userVerificationService: UserVerificationServiceAbstraction; - twoFactorService: TwoFactorServiceAbstraction; vaultFilterService: VaultFilterService; usernameGenerationService: UsernameGenerationServiceAbstraction; encryptService: EncryptService; @@ -614,8 +608,6 @@ export default class MainBackground { this.stateService, ); - this.twoFactorService = new TwoFactorService(this.i18nService, this.platformUtilsService); - this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); this.devicesApiService = new DevicesApiServiceImplementation(this.apiService); @@ -659,32 +651,6 @@ export default class MainBackground { this.loginEmailService = new LoginEmailService(this.stateProvider); - this.loginStrategyService = new LoginStrategyService( - this.accountService, - this.masterPasswordService, - this.cryptoService, - this.apiService, - this.tokenService, - this.appIdService, - this.platformUtilsService, - this.messagingService, - this.logService, - this.keyConnectorService, - this.environmentService, - this.stateService, - this.twoFactorService, - this.i18nService, - this.encryptService, - this.passwordStrengthService, - this.policyService, - this.deviceTrustService, - this.authRequestService, - this.userDecryptionOptionsService, - this.globalStateProvider, - this.billingAccountProfileStateService, - this.kdfConfigService, - ); - this.ssoLoginService = new SsoLoginService(this.stateProvider); this.userVerificationApiService = new UserVerificationApiService(this.apiService); @@ -1114,8 +1080,7 @@ export default class MainBackground { this.userKeyInitService.listenForActiveUserChangesToSetUserKey(); await (this.i18nService as I18nService).init(); - await (this.eventUploadService as EventUploadService).init(true); - this.twoFactorService.init(); + (this.eventUploadService as EventUploadService).init(true); if (this.popupOnlyContext) { return; diff --git a/apps/browser/src/popup/services/init.service.ts b/apps/browser/src/popup/services/init.service.ts index ee842565d75..63ce45c9b76 100644 --- a/apps/browser/src/popup/services/init.service.ts +++ b/apps/browser/src/popup/services/init.service.ts @@ -2,6 +2,7 @@ import { DOCUMENT } from "@angular/common"; import { 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 { 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"; @@ -15,6 +16,7 @@ export class InitService { private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private stateService: StateServiceAbstraction, + private twoFactorService: TwoFactorService, private logService: LogServiceAbstraction, private themingService: AbstractThemingService, @Inject(DOCUMENT) private document: Document, @@ -24,6 +26,7 @@ export class InitService { return async () => { await this.stateService.init({ runMigrations: false }); // Browser background is responsible for migrations await this.i18nService.init(); + this.twoFactorService.init(); if (!BrowserPopupUtils.inPopup(window)) { window.document.body.classList.add("body-full"); diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 38068d18495..052e341004c 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -15,10 +15,7 @@ import { INTRAPROCESS_MESSAGING_SUBJECT, } from "@bitwarden/angular/services/injection-tokens"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; -import { - AuthRequestServiceAbstraction, - LoginStrategyServiceAbstraction, -} from "@bitwarden/auth/common"; +import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service"; @@ -33,7 +30,6 @@ import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/d import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; -import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { @@ -168,21 +164,11 @@ const safeProviders: SafeProvider[] = [ useClass: UnauthGuardService, deps: [AuthServiceAbstraction, Router], }), - safeProvider({ - provide: TwoFactorService, - useFactory: getBgService("twoFactorService"), - deps: [], - }), safeProvider({ provide: AuthServiceAbstraction, useFactory: getBgService("authService"), deps: [], }), - safeProvider({ - provide: LoginStrategyServiceAbstraction, - useFactory: getBgService("loginStrategyService"), - deps: [], - }), safeProvider({ provide: SsoLoginServiceAbstraction, useFactory: getBgService("ssoLoginService"), diff --git a/apps/cli/src/auth/commands/login.command.ts b/apps/cli/src/auth/commands/login.command.ts index 3606285c723..bd61727a6c7 100644 --- a/apps/cli/src/auth/commands/login.command.ts +++ b/apps/cli/src/auth/commands/login.command.ts @@ -231,7 +231,7 @@ export class LoginCommand { } } if (response.requiresTwoFactor) { - const twoFactorProviders = this.twoFactorService.getSupportedProviders(null); + const twoFactorProviders = await this.twoFactorService.getSupportedProviders(null); if (twoFactorProviders.length === 0) { return Response.badRequest("No providers available for this client."); } @@ -272,7 +272,7 @@ export class LoginCommand { if ( twoFactorToken == null && - response.twoFactorProviders.size > 1 && + Object.keys(response.twoFactorProviders).length > 1 && selectedProvider.type === TwoFactorProviderType.Email ) { const emailReq = new TwoFactorEmailRequest(); diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index ffe6c128b58..45c394e9121 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -455,7 +455,11 @@ export class Main { this.stateProvider, ); - this.twoFactorService = new TwoFactorService(this.i18nService, this.platformUtilsService); + this.twoFactorService = new TwoFactorService( + this.i18nService, + this.platformUtilsService, + this.globalStateProvider, + ); this.passwordStrengthService = new PasswordStrengthService(); diff --git a/libs/angular/src/auth/components/sso.component.spec.ts b/libs/angular/src/auth/components/sso.component.spec.ts index 269ec51e303..ae644028f99 100644 --- a/libs/angular/src/auth/components/sso.component.spec.ts +++ b/libs/angular/src/auth/components/sso.component.spec.ts @@ -253,7 +253,7 @@ describe("SsoComponent", () => { describe("2FA scenarios", () => { beforeEach(() => { const authResult = new AuthResult(); - authResult.twoFactorProviders = new Map([[TwoFactorProviderType.Authenticator, {}]]); + authResult.twoFactorProviders = { [TwoFactorProviderType.Authenticator]: {} }; // use standard user with MP because this test is not concerned with password reset. selectedUserDecryptionOptions.next(mockUserDecryptionOpts.withMasterPassword); diff --git a/libs/angular/src/auth/components/two-factor-options.component.ts b/libs/angular/src/auth/components/two-factor-options.component.ts index 2808e41cc20..1bbf81fa34f 100644 --- a/libs/angular/src/auth/components/two-factor-options.component.ts +++ b/libs/angular/src/auth/components/two-factor-options.component.ts @@ -2,7 +2,10 @@ import { Directive, EventEmitter, OnInit, Output } from "@angular/core"; import { Router } from "@angular/router"; import { firstValueFrom } from "rxjs"; -import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; +import { + TwoFactorProviderDetails, + TwoFactorService, +} from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -24,11 +27,11 @@ export class TwoFactorOptionsComponent implements OnInit { protected environmentService: EnvironmentService, ) {} - ngOnInit() { - this.providers = this.twoFactorService.getSupportedProviders(this.win); + async ngOnInit() { + this.providers = await this.twoFactorService.getSupportedProviders(this.win); } - choose(p: any) { + async choose(p: TwoFactorProviderDetails) { this.onProviderSelected.emit(p.type); } diff --git a/libs/angular/src/auth/components/two-factor.component.ts b/libs/angular/src/auth/components/two-factor.component.ts index f73f0483be1..8e96c48ba03 100644 --- a/libs/angular/src/auth/components/two-factor.component.ts +++ b/libs/angular/src/auth/components/two-factor.component.ts @@ -102,7 +102,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI } async ngOnInit() { - if (!(await this.authing()) || this.twoFactorService.getProviders() == null) { + if (!(await this.authing()) || (await this.twoFactorService.getProviders()) == null) { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.router.navigate([this.loginRoute]); @@ -145,7 +145,9 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI ); } - this.selectedProviderType = this.twoFactorService.getDefaultProvider(this.webAuthnSupported); + this.selectedProviderType = await this.twoFactorService.getDefaultProvider( + this.webAuthnSupported, + ); await this.init(); } @@ -162,12 +164,14 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI this.cleanupWebAuthn(); this.title = (TwoFactorProviders as any)[this.selectedProviderType].name; - const providerData = this.twoFactorService.getProviders().get(this.selectedProviderType); + const providerData = await this.twoFactorService.getProviders().then((providers) => { + return providers.get(this.selectedProviderType); + }); switch (this.selectedProviderType) { case TwoFactorProviderType.WebAuthn: if (!this.webAuthnNewTab) { - setTimeout(() => { - this.authWebAuthn(); + setTimeout(async () => { + await this.authWebAuthn(); }, 500); } break; @@ -212,7 +216,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI break; case TwoFactorProviderType.Email: this.twoFactorEmail = providerData.Email; - if (this.twoFactorService.getProviders().size > 1) { + if ((await this.twoFactorService.getProviders()).size > 1) { await this.sendEmail(false); } break; @@ -474,8 +478,10 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI this.emailPromise = null; } - authWebAuthn() { - const providerData = this.twoFactorService.getProviders().get(this.selectedProviderType); + async authWebAuthn() { + const providerData = await this.twoFactorService.getProviders().then((providers) => { + return providers.get(this.selectedProviderType); + }); if (!this.webAuthnSupported || this.webAuthn == null) { return; diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 88494a1cbb5..c7b27a25c2a 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -874,7 +874,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: TwoFactorServiceAbstraction, useClass: TwoFactorService, - deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction], + deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction, GlobalStateProvider], }), safeProvider({ provide: FormValidationErrorsServiceAbstraction, diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts index 5e70c348f42..a123e300538 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts @@ -86,7 +86,9 @@ describe("AuthRequestLoginStrategy", () => { tokenService.getTwoFactorToken.mockResolvedValue(null); appIdService.getAppId.mockResolvedValue(deviceId); - tokenService.decodeAccessToken.mockResolvedValue({}); + tokenService.decodeAccessToken.mockResolvedValue({ + sub: mockUserId, + }); authRequestLoginStrategy = new AuthRequestLoginStrategy( cache, diff --git a/libs/auth/src/common/login-strategies/login.strategy.spec.ts b/libs/auth/src/common/login-strategies/login.strategy.spec.ts index 7c022db23ba..3284f6e9474 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -25,11 +25,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { - Account, - AccountProfile, - AccountKeys, -} from "@bitwarden/common/platform/models/domain/account"; +import { Account, AccountProfile } from "@bitwarden/common/platform/models/domain/account"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; @@ -214,7 +210,6 @@ describe("LoginStrategy", () => { email: email, }, }, - keys: new AccountKeys(), }), ); expect(userDecryptionOptionsService.setUserDecryptionOptions).toHaveBeenCalledWith( @@ -223,6 +218,21 @@ describe("LoginStrategy", () => { expect(messagingService.send).toHaveBeenCalledWith("loggedIn"); }); + it("throws if active account isn't found after being initialized", async () => { + const idTokenResponse = identityTokenResponseFactory(); + apiService.postIdentityToken.mockResolvedValue(idTokenResponse); + + const mockVaultTimeoutAction = VaultTimeoutAction.Lock; + const mockVaultTimeout = 1000; + + stateService.getVaultTimeoutAction.mockResolvedValue(mockVaultTimeoutAction); + stateService.getVaultTimeout.mockResolvedValue(mockVaultTimeout); + + accountService.activeAccountSubject.next(null); + + await expect(async () => await passwordLoginStrategy.logIn(credentials)).rejects.toThrow(); + }); + it("builds AuthResult", async () => { const tokenResponse = identityTokenResponseFactory(); tokenResponse.forcePasswordReset = true; @@ -306,8 +316,10 @@ describe("LoginStrategy", () => { expect(tokenService.clearTwoFactorToken).toHaveBeenCalled(); const expected = new AuthResult(); - expected.twoFactorProviders = new Map(); - expected.twoFactorProviders.set(0, null); + expected.twoFactorProviders = { 0: null } as Record< + TwoFactorProviderType, + Record + >; expect(result).toEqual(expected); }); @@ -336,8 +348,9 @@ describe("LoginStrategy", () => { expect(messagingService.send).not.toHaveBeenCalled(); const expected = new AuthResult(); - expected.twoFactorProviders = new Map(); - expected.twoFactorProviders.set(1, { Email: "k***@bitwarden.com" }); + expected.twoFactorProviders = { + [TwoFactorProviderType.Email]: { Email: "k***@bitwarden.com" }, + }; expected.email = userEmail; expected.ssoEmail2FaSessionToken = ssoEmail2FaSessionToken; diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index 06fc98db13e..fd268d955ef 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -1,4 +1,4 @@ -import { BehaviorSubject } from "rxjs"; +import { BehaviorSubject, filter, firstValueFrom, timeout } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -101,7 +101,7 @@ export abstract class LoginStrategy { } protected async startLogIn(): Promise<[AuthResult, IdentityResponse]> { - this.twoFactorService.clearSelectedProvider(); + await this.twoFactorService.clearSelectedProvider(); const tokenRequest = this.cache.value.tokenRequest; const response = await this.apiService.postIdentityToken(tokenRequest); @@ -159,12 +159,12 @@ export abstract class LoginStrategy { * It also sets the access token and refresh token in the token service. * * @param {IdentityTokenResponse} tokenResponse - The response from the server containing the identity token. - * @returns {Promise} - A promise that resolves when the account information has been successfully saved. + * @returns {Promise} - A promise that resolves the the UserId when the account information has been successfully saved. */ protected async saveAccountInformation(tokenResponse: IdentityTokenResponse): Promise { const accountInformation = await this.tokenService.decodeAccessToken(tokenResponse.accessToken); - const userId = accountInformation.sub; + const userId = accountInformation.sub as UserId; const vaultTimeoutAction = await this.stateService.getVaultTimeoutAction({ userId }); const vaultTimeout = await this.stateService.getVaultTimeout({ userId }); @@ -191,6 +191,8 @@ export abstract class LoginStrategy { }), ); + await this.verifyAccountAdded(userId); + await this.userDecryptionOptionsService.setUserDecryptionOptions( UserDecryptionOptions.fromResponse(tokenResponse), ); @@ -207,7 +209,7 @@ export abstract class LoginStrategy { ); await this.billingAccountProfileStateService.setHasPremium(accountInformation.premium, false); - return userId as UserId; + return userId; } protected async processTokenResponse(response: IdentityTokenResponse): Promise { @@ -284,7 +286,7 @@ export abstract class LoginStrategy { const result = new AuthResult(); result.twoFactorProviders = response.twoFactorProviders2; - this.twoFactorService.setProviders(response); + await this.twoFactorService.setProviders(response); this.cache.next({ ...this.cache.value, captchaBypassToken: response.captchaToken ?? null }); result.ssoEmail2FaSessionToken = response.ssoEmail2faSessionToken; result.email = response.email; @@ -306,4 +308,24 @@ export abstract class LoginStrategy { result.captchaSiteKey = response.siteKey; return result; } + + /** + * Verifies that the active account is set after initialization. + * Note: In browser there is a slight delay between when active account emits in background, + * and when it emits in foreground. We're giving the foreground 1 second to catch up. + * If nothing is emitted, we throw an error. + */ + private async verifyAccountAdded(expectedUserId: UserId) { + await firstValueFrom( + this.accountService.activeAccount$.pipe( + filter((account) => account?.id === expectedUserId), + timeout({ + first: 1000, + with: () => { + throw new Error("Expected user never made active user after initialization."); + }, + }), + ), + ); + } } diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts index be09448fdd0..5c1fe9b1fe8 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts @@ -99,7 +99,9 @@ describe("PasswordLoginStrategy", () => { kdfConfigService = mock(); appIdService.getAppId.mockResolvedValue(deviceId); - tokenService.decodeAccessToken.mockResolvedValue({}); + tokenService.decodeAccessToken.mockResolvedValue({ + sub: userId, + }); loginStrategyService.makePreloginKey.mockResolvedValue(masterKey); diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts index 3439a1c1991..416e910b474 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts @@ -92,7 +92,9 @@ describe("SsoLoginStrategy", () => { tokenService.getTwoFactorToken.mockResolvedValue(null); appIdService.getAppId.mockResolvedValue(deviceId); - tokenService.decodeAccessToken.mockResolvedValue({}); + tokenService.decodeAccessToken.mockResolvedValue({ + sub: userId, + }); ssoLoginStrategy = new SsoLoginStrategy( null, diff --git a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts index 5fce8b0b825..130e6c2d89c 100644 --- a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts @@ -82,7 +82,9 @@ describe("UserApiLoginStrategy", () => { appIdService.getAppId.mockResolvedValue(deviceId); tokenService.getTwoFactorToken.mockResolvedValue(null); - tokenService.decodeAccessToken.mockResolvedValue({}); + tokenService.decodeAccessToken.mockResolvedValue({ + sub: userId, + }); apiLogInStrategy = new UserApiLoginStrategy( cache, diff --git a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts index d75e1949803..bbcd3bafdd7 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts @@ -18,7 +18,8 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { FakeAccountService } from "@bitwarden/common/spec"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { PrfKey, UserKey } from "@bitwarden/common/types/key"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; @@ -49,6 +50,7 @@ describe("WebAuthnLoginStrategy", () => { const token = "mockToken"; const deviceId = Utils.newGuid(); + const userId = Utils.newGuid() as UserId; let webAuthnCredentials!: WebAuthnLoginCredentials; @@ -69,7 +71,7 @@ describe("WebAuthnLoginStrategy", () => { beforeEach(() => { jest.clearAllMocks(); - accountService = new FakeAccountService(null); + accountService = mockAccountServiceWith(userId); masterPasswordService = new FakeMasterPasswordService(); cryptoService = mock(); @@ -87,7 +89,9 @@ describe("WebAuthnLoginStrategy", () => { tokenService.getTwoFactorToken.mockResolvedValue(null); appIdService.getAppId.mockResolvedValue(deviceId); - tokenService.decodeAccessToken.mockResolvedValue({}); + tokenService.decodeAccessToken.mockResolvedValue({ + sub: userId, + }); webAuthnLoginStrategy = new WebAuthnLoginStrategy( cache, diff --git a/libs/common/src/auth/abstractions/two-factor.service.ts b/libs/common/src/auth/abstractions/two-factor.service.ts index 3ea7eb8db99..a0a9ecd2afb 100644 --- a/libs/common/src/auth/abstractions/two-factor.service.ts +++ b/libs/common/src/auth/abstractions/two-factor.service.ts @@ -12,12 +12,12 @@ export interface TwoFactorProviderDetails { export abstract class TwoFactorService { init: () => void; - getSupportedProviders: (win: Window) => TwoFactorProviderDetails[]; - getDefaultProvider: (webAuthnSupported: boolean) => TwoFactorProviderType; - setSelectedProvider: (type: TwoFactorProviderType) => void; - clearSelectedProvider: () => void; + getSupportedProviders: (win: Window) => Promise; + getDefaultProvider: (webAuthnSupported: boolean) => Promise; + setSelectedProvider: (type: TwoFactorProviderType) => Promise; + clearSelectedProvider: () => Promise; - setProviders: (response: IdentityTwoFactorResponse) => void; - clearProviders: () => void; - getProviders: () => Map; + setProviders: (response: IdentityTwoFactorResponse) => Promise; + clearProviders: () => Promise; + getProviders: () => Promise>; } diff --git a/libs/common/src/auth/models/domain/auth-result.ts b/libs/common/src/auth/models/domain/auth-result.ts index 993ce08d589..f45466777ea 100644 --- a/libs/common/src/auth/models/domain/auth-result.ts +++ b/libs/common/src/auth/models/domain/auth-result.ts @@ -14,7 +14,7 @@ export class AuthResult { resetMasterPassword = false; forcePasswordReset: ForceSetPasswordReason = ForceSetPasswordReason.None; - twoFactorProviders: Map = null; + twoFactorProviders: Partial>> = null; ssoEmail2FaSessionToken?: string; email: string; requiresEncryptionKeyMigration: boolean; diff --git a/libs/common/src/auth/models/response/identity-two-factor.response.ts b/libs/common/src/auth/models/response/identity-two-factor.response.ts index bc5d2fbf858..dce64e8ef3e 100644 --- a/libs/common/src/auth/models/response/identity-two-factor.response.ts +++ b/libs/common/src/auth/models/response/identity-two-factor.response.ts @@ -4,8 +4,10 @@ import { TwoFactorProviderType } from "../../enums/two-factor-provider-type"; import { MasterPasswordPolicyResponse } from "./master-password-policy.response"; export class IdentityTwoFactorResponse extends BaseResponse { + // contains available two-factor providers twoFactorProviders: TwoFactorProviderType[]; - twoFactorProviders2 = new Map(); + // a map of two-factor providers to necessary data for completion + twoFactorProviders2: Record>; captchaToken: string; ssoEmail2faSessionToken: string; email?: string; @@ -15,15 +17,7 @@ export class IdentityTwoFactorResponse extends BaseResponse { super(response); this.captchaToken = this.getResponseProperty("CaptchaBypassToken"); this.twoFactorProviders = this.getResponseProperty("TwoFactorProviders"); - const twoFactorProviders2 = this.getResponseProperty("TwoFactorProviders2"); - if (twoFactorProviders2 != null) { - for (const prop in twoFactorProviders2) { - // eslint-disable-next-line - if (twoFactorProviders2.hasOwnProperty(prop)) { - this.twoFactorProviders2.set(parseInt(prop, null), twoFactorProviders2[prop]); - } - } - } + this.twoFactorProviders2 = this.getResponseProperty("TwoFactorProviders2"); this.masterPasswordPolicy = new MasterPasswordPolicyResponse( this.getResponseProperty("MasterPasswordPolicy"), ); diff --git a/libs/common/src/auth/services/two-factor.service.ts b/libs/common/src/auth/services/two-factor.service.ts index cd1e5ea1220..50d25561577 100644 --- a/libs/common/src/auth/services/two-factor.service.ts +++ b/libs/common/src/auth/services/two-factor.service.ts @@ -1,5 +1,9 @@ +import { firstValueFrom, map } from "rxjs"; + import { I18nService } from "../../platform/abstractions/i18n.service"; import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service"; +import { Utils } from "../../platform/misc/utils"; +import { GlobalStateProvider, KeyDefinition, TWO_FACTOR_MEMORY } from "../../platform/state"; import { TwoFactorProviderDetails, TwoFactorService as TwoFactorServiceAbstraction, @@ -59,13 +63,36 @@ export const TwoFactorProviders: Partial, TwoFactorProviderType>( + TWO_FACTOR_MEMORY, + "providers", + { + deserializer: (obj) => obj, + }, +); + +// Memory storage as only required during authentication process +export const SELECTED_PROVIDER = new KeyDefinition( + TWO_FACTOR_MEMORY, + "selected", + { + deserializer: (obj) => obj, + }, +); + export class TwoFactorService implements TwoFactorServiceAbstraction { - private twoFactorProvidersData: Map; - private selectedTwoFactorProviderType: TwoFactorProviderType = null; + private providersState = this.globalStateProvider.get(PROVIDERS); + private selectedState = this.globalStateProvider.get(SELECTED_PROVIDER); + readonly providers$ = this.providersState.state$.pipe( + map((providers) => Utils.recordToMap(providers)), + ); + readonly selected$ = this.selectedState.state$; constructor( private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, + private globalStateProvider: GlobalStateProvider, ) {} init() { @@ -93,63 +120,60 @@ export class TwoFactorService implements TwoFactorServiceAbstraction { this.i18nService.t("yubiKeyDesc"); } - getSupportedProviders(win: Window): TwoFactorProviderDetails[] { + async getSupportedProviders(win: Window): Promise { + const data = await firstValueFrom(this.providers$); const providers: any[] = []; - if (this.twoFactorProvidersData == null) { + if (data == null) { return providers; } if ( - this.twoFactorProvidersData.has(TwoFactorProviderType.OrganizationDuo) && + data.has(TwoFactorProviderType.OrganizationDuo) && this.platformUtilsService.supportsDuo() ) { providers.push(TwoFactorProviders[TwoFactorProviderType.OrganizationDuo]); } - if (this.twoFactorProvidersData.has(TwoFactorProviderType.Authenticator)) { + if (data.has(TwoFactorProviderType.Authenticator)) { providers.push(TwoFactorProviders[TwoFactorProviderType.Authenticator]); } - if (this.twoFactorProvidersData.has(TwoFactorProviderType.Yubikey)) { + if (data.has(TwoFactorProviderType.Yubikey)) { providers.push(TwoFactorProviders[TwoFactorProviderType.Yubikey]); } - if ( - this.twoFactorProvidersData.has(TwoFactorProviderType.Duo) && - this.platformUtilsService.supportsDuo() - ) { + if (data.has(TwoFactorProviderType.Duo) && this.platformUtilsService.supportsDuo()) { providers.push(TwoFactorProviders[TwoFactorProviderType.Duo]); } if ( - this.twoFactorProvidersData.has(TwoFactorProviderType.WebAuthn) && + data.has(TwoFactorProviderType.WebAuthn) && this.platformUtilsService.supportsWebAuthn(win) ) { providers.push(TwoFactorProviders[TwoFactorProviderType.WebAuthn]); } - if (this.twoFactorProvidersData.has(TwoFactorProviderType.Email)) { + if (data.has(TwoFactorProviderType.Email)) { providers.push(TwoFactorProviders[TwoFactorProviderType.Email]); } return providers; } - getDefaultProvider(webAuthnSupported: boolean): TwoFactorProviderType { - if (this.twoFactorProvidersData == null) { + async getDefaultProvider(webAuthnSupported: boolean): Promise { + const data = await firstValueFrom(this.providers$); + const selected = await firstValueFrom(this.selected$); + if (data == null) { return null; } - if ( - this.selectedTwoFactorProviderType != null && - this.twoFactorProvidersData.has(this.selectedTwoFactorProviderType) - ) { - return this.selectedTwoFactorProviderType; + if (selected != null && data.has(selected)) { + return selected; } let providerType: TwoFactorProviderType = null; let providerPriority = -1; - this.twoFactorProvidersData.forEach((_value, type) => { + data.forEach((_value, type) => { const provider = (TwoFactorProviders as any)[type]; if (provider != null && provider.priority > providerPriority) { if (type === TwoFactorProviderType.WebAuthn && !webAuthnSupported) { @@ -164,23 +188,23 @@ export class TwoFactorService implements TwoFactorServiceAbstraction { return providerType; } - setSelectedProvider(type: TwoFactorProviderType) { - this.selectedTwoFactorProviderType = type; + async setSelectedProvider(type: TwoFactorProviderType): Promise { + await this.selectedState.update(() => type); } - clearSelectedProvider() { - this.selectedTwoFactorProviderType = null; + async clearSelectedProvider(): Promise { + await this.selectedState.update(() => null); } - setProviders(response: IdentityTwoFactorResponse) { - this.twoFactorProvidersData = response.twoFactorProviders2; + async setProviders(response: IdentityTwoFactorResponse): Promise { + await this.providersState.update(() => response.twoFactorProviders2); } - clearProviders() { - this.twoFactorProvidersData = null; + async clearProviders(): Promise { + await this.providersState.update(() => null); } - getProviders() { - return this.twoFactorProvidersData; + getProviders(): Promise> { + return firstValueFrom(this.providers$); } } diff --git a/libs/common/src/platform/state/deserialization-helpers.spec.ts b/libs/common/src/platform/state/deserialization-helpers.spec.ts new file mode 100644 index 00000000000..b1ae447997f --- /dev/null +++ b/libs/common/src/platform/state/deserialization-helpers.spec.ts @@ -0,0 +1,25 @@ +import { record } from "./deserialization-helpers"; + +describe("deserialization helpers", () => { + describe("record", () => { + it("deserializes a record when keys are strings", () => { + const deserializer = record((value: number) => value); + const input = { + a: 1, + b: 2, + }; + const output = deserializer(input); + expect(output).toEqual(input); + }); + + it("deserializes a record when keys are numbers", () => { + const deserializer = record((value: number) => value); + const input = { + 1: 1, + 2: 2, + }; + const output = deserializer(input); + expect(output).toEqual(input); + }); + }); +}); diff --git a/libs/common/src/platform/state/deserialization-helpers.ts b/libs/common/src/platform/state/deserialization-helpers.ts index d68a3d04446..8fd5d2da198 100644 --- a/libs/common/src/platform/state/deserialization-helpers.ts +++ b/libs/common/src/platform/state/deserialization-helpers.ts @@ -21,7 +21,7 @@ export function array( * * @param valueDeserializer */ -export function record( +export function record( valueDeserializer: (value: Jsonify) => T, ): (record: Jsonify>) => Record { return (jsonValue: Jsonify | null>) => { @@ -29,10 +29,10 @@ export function record( return null; } - const output: Record = {}; - for (const key in jsonValue) { - output[key] = valueDeserializer((jsonValue as Record>)[key]); - } + const output: Record = {} as any; + Object.entries(jsonValue).forEach(([key, value]) => { + output[key as TKey] = valueDeserializer(value); + }); return output; }; } diff --git a/libs/common/src/platform/state/key-definition.ts b/libs/common/src/platform/state/key-definition.ts index b2a8ff8712b..bdabd8df50b 100644 --- a/libs/common/src/platform/state/key-definition.ts +++ b/libs/common/src/platform/state/key-definition.ts @@ -113,7 +113,7 @@ export class KeyDefinition { * }); * ``` */ - static record( + static record( stateDefinition: StateDefinition, key: string, // We have them provide options for the value of the record, depending on future options we add, this could get a little weird. diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 8847f7fe51a..ee5005202fa 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -40,6 +40,7 @@ export const KEY_CONNECTOR_DISK = new StateDefinition("keyConnector", "disk"); export const ACCOUNT_MEMORY = new StateDefinition("account", "memory"); export const MASTER_PASSWORD_MEMORY = new StateDefinition("masterPassword", "memory"); export const MASTER_PASSWORD_DISK = new StateDefinition("masterPassword", "disk"); +export const TWO_FACTOR_MEMORY = new StateDefinition("twoFactor", "memory"); export const AVATAR_DISK = new StateDefinition("avatar", "disk", { web: "disk-local" }); export const ROUTER_DISK = new StateDefinition("router", "disk"); export const LOGIN_EMAIL_DISK = new StateDefinition("loginEmail", "disk", { diff --git a/libs/common/src/platform/state/user-key-definition.ts b/libs/common/src/platform/state/user-key-definition.ts index 3eb9369080c..4c622e29f1e 100644 --- a/libs/common/src/platform/state/user-key-definition.ts +++ b/libs/common/src/platform/state/user-key-definition.ts @@ -120,7 +120,7 @@ export class UserKeyDefinition { * }); * ``` */ - static record( + static record( stateDefinition: StateDefinition, key: string, // We have them provide options for the value of the record, depending on future options we add, this could get a little weird. From d8749a0c56c5266ff0e64536ca859f9f87c95b28 Mon Sep 17 00:00:00 2001 From: SmithThe4th Date: Thu, 25 Apr 2024 16:58:25 -0400 Subject: [PATCH 066/110] [AC-2359] Ownership does not default to an organization when Remove Individual Vault policy is active (#8910) * fixed issue with clearing search index state * clear user index before account is totally cleaned up * added logout clear on option * removed redundant clear index from logout * fixed ownsership dropdown issu where async operations does bot complete early enough before the view is shown --- libs/angular/src/vault/components/add-edit.component.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index d29c74b42dd..d9b73a0e7f3 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -184,8 +184,6 @@ export class AddEditComponent implements OnInit, OnDestroy { FeatureFlag.FlexibleCollectionsV1, false, ); - this.writeableCollections = await this.loadCollections(); - this.canUseReprompt = await this.passwordRepromptService.enabled(); this.policyService .policyAppliesToActiveUser$(PolicyType.PersonalOwnership) @@ -197,6 +195,9 @@ export class AddEditComponent implements OnInit, OnDestroy { takeUntil(this.destroy$), ) .subscribe(); + + this.writeableCollections = await this.loadCollections(); + this.canUseReprompt = await this.passwordRepromptService.enabled(); } ngOnDestroy() { From 2ff3fa92fb6c9f56a738c057c9d70272167b1d67 Mon Sep 17 00:00:00 2001 From: Jake Fink Date: Thu, 25 Apr 2024 17:27:43 -0400 Subject: [PATCH 067/110] [PM-7702] Remove extra content script being injected (#8922) * remove extra content script being injected that results in multiple messages * add conditional logic for when to add script --- apps/browser/src/autofill/services/autofill.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index 8f85d65692f..10e2d84361b 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -130,7 +130,9 @@ export default class AutofillService implements AutofillServiceInterface { if (triggeringOnPageLoad && autoFillOnPageLoadIsEnabled) { injectedScripts.push("autofiller.js"); - } else { + } + + if (!triggeringOnPageLoad) { await this.scriptInjectorService.inject({ tabId: tab.id, injectDetails: { file: "content/content-message-handler.js", runAt: "document_start" }, From c3d4c7aa3d27e75a7e94a44f40fc9916850fa45f Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez Date: Thu, 25 Apr 2024 16:47:20 -0500 Subject: [PATCH 068/110] [PM-7710] Avoid re-indexing ciphers on current tab component and re-setting null storage values for popup components (#8908) * [PM-7710] Avoid re-indexing ciphers on current tab component and re-setting null storage values for popup components * [PM-7710] Avoid re-indexing ciphers on current tab component and re-setting null storage values for popup components --- .../tools/popup/services/browser-send-state.service.ts | 8 ++++++-- .../popup/components/vault/current-tab.component.ts | 2 ++ .../src/vault/services/vault-browser-state.service.ts | 8 ++++++-- libs/common/src/vault/abstractions/cipher.service.ts | 1 + libs/common/src/vault/services/cipher.service.ts | 9 +++++++-- 5 files changed, 22 insertions(+), 6 deletions(-) diff --git a/apps/browser/src/tools/popup/services/browser-send-state.service.ts b/apps/browser/src/tools/popup/services/browser-send-state.service.ts index 52aeb01a925..11e71c9b20f 100644 --- a/apps/browser/src/tools/popup/services/browser-send-state.service.ts +++ b/apps/browser/src/tools/popup/services/browser-send-state.service.ts @@ -46,7 +46,9 @@ export class BrowserSendStateService { * the send component on the browser */ async setBrowserSendComponentState(value: BrowserSendComponentState): Promise { - await this.activeUserBrowserSendComponentState.update(() => value); + await this.activeUserBrowserSendComponentState.update(() => value, { + shouldUpdate: (current) => !(current == null && value == null), + }); } /** Get the active user's browser component state @@ -60,6 +62,8 @@ export class BrowserSendStateService { * @param { BrowserComponentState } value set the scroll position and search text for the send component on the browser */ async setBrowserSendTypeComponentState(value: BrowserComponentState): Promise { - await this.activeUserBrowserSendTypeComponentState.update(() => value); + await this.activeUserBrowserSendTypeComponentState.update(() => value, { + shouldUpdate: (current) => !(current == null && value == null), + }); } } diff --git a/apps/browser/src/vault/popup/components/vault/current-tab.component.ts b/apps/browser/src/vault/popup/components/vault/current-tab.component.ts index 4d2674fd703..d882dfd525d 100644 --- a/apps/browser/src/vault/popup/components/vault/current-tab.component.ts +++ b/apps/browser/src/vault/popup/components/vault/current-tab.component.ts @@ -292,6 +292,8 @@ export class CurrentTabComponent implements OnInit, OnDestroy { const ciphers = await this.cipherService.getAllDecryptedForUrl( this.url, otherTypes.length > 0 ? otherTypes : null, + null, + false, ); this.loginCiphers = []; diff --git a/apps/browser/src/vault/services/vault-browser-state.service.ts b/apps/browser/src/vault/services/vault-browser-state.service.ts index a0d55a9d550..43a28928da5 100644 --- a/apps/browser/src/vault/services/vault-browser-state.service.ts +++ b/apps/browser/src/vault/services/vault-browser-state.service.ts @@ -52,7 +52,9 @@ export class VaultBrowserStateService { } async setBrowserGroupingsComponentState(value: BrowserGroupingsComponentState): Promise { - await this.activeUserVaultBrowserGroupingsComponentState.update(() => value); + await this.activeUserVaultBrowserGroupingsComponentState.update(() => value, { + shouldUpdate: (current) => !(current == null && value == null), + }); } async getBrowserVaultItemsComponentState(): Promise { @@ -60,6 +62,8 @@ export class VaultBrowserStateService { } async setBrowserVaultItemsComponentState(value: BrowserComponentState): Promise { - await this.activeUserVaultBrowserComponentState.update(() => value); + await this.activeUserVaultBrowserComponentState.update(() => value, { + shouldUpdate: (current) => !(current == null && value == null), + }); } } diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index 22e2c54a59a..4337043cdfb 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -33,6 +33,7 @@ export abstract class CipherService { url: string, includeOtherTypes?: CipherType[], defaultMatch?: UriMatchStrategySetting, + reindexCiphers?: boolean, ) => Promise; getAllFromApiForOrganization: (organizationId: string) => Promise; /** diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 0b44636ea6c..fd484ee9029 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -441,6 +441,7 @@ export class CipherService implements CipherServiceAbstraction { url: string, includeOtherTypes?: CipherType[], defaultMatch: UriMatchStrategySetting = null, + reindexCiphers = true, ): Promise { if (url == null && includeOtherTypes == null) { return Promise.resolve([]); @@ -449,7 +450,9 @@ export class CipherService implements CipherServiceAbstraction { const equivalentDomains = await firstValueFrom( this.domainSettingsService.getUrlEquivalentDomains(url), ); - const ciphers = await this.getAllDecrypted(); + const ciphers = reindexCiphers + ? await this.getAllDecrypted() + : await this.getDecryptedCiphers(); defaultMatch ??= await firstValueFrom(this.domainSettingsService.defaultUriMatchStrategy$); return ciphers.filter((cipher) => { @@ -1135,7 +1138,9 @@ export class CipherService implements CipherServiceAbstraction { } async setAddEditCipherInfo(value: AddEditCipherInfo) { - await this.addEditCipherInfoState.update(() => value); + await this.addEditCipherInfoState.update(() => value, { + shouldUpdate: (current) => !(current == null && value == null), + }); } // Helpers From c21a58f2fb5f7ca69eb6c49556d2f1151dde3b0e Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Fri, 26 Apr 2024 08:36:57 +1000 Subject: [PATCH 069/110] Use refCount: true to avoid potential memory leak (#8796) --- .../organizations/manage/group-add-edit.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts index dea6f4999b2..b18effac862 100644 --- a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts @@ -194,7 +194,7 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { }), ); }), - shareReplay({ refCount: false }), + shareReplay({ refCount: true, bufferSize: 1 }), ); restrictGroupAccess$ = combineLatest([ From 788bef6b7a35f0a1e24fa086fddcac94d293f06e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 26 Apr 2024 07:04:21 +0000 Subject: [PATCH 070/110] Autosync the updated translations (#8924) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/fr/messages.json | 4 ++-- apps/desktop/src/locales/hu/messages.json | 2 +- apps/desktop/src/locales/lv/messages.json | 2 +- apps/desktop/src/locales/nl/messages.json | 2 +- apps/desktop/src/locales/zh_CN/messages.json | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index 86550b736f3..e82420a1f51 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -1636,7 +1636,7 @@ "message": "Error enabling browser integration" }, "browserIntegrationErrorDesc": { - "message": "An error has occurred while enabling browser integration." + "message": "Une erreur s'est produite lors de l'action de l'intégration du navigateur." }, "browserIntegrationMasOnlyDesc": { "message": "Malheureusement l'intégration avec le navigateur est uniquement supportée dans la version Mac App Store pour le moment." @@ -2698,7 +2698,7 @@ "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "Succès" }, "troubleshooting": { "message": "Résolution de problèmes" diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index 5c91fb4b944..838b3fc7c8e 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -2698,7 +2698,7 @@ "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "Sikeres" }, "troubleshooting": { "message": "Hibaelhárítás" diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index aa057f54ab6..e2e068362e3 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -2698,7 +2698,7 @@ "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "Izdevās" }, "troubleshooting": { "message": "Sarežģījumu novēršana" diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index b5f2a413d6f..f56572259ba 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -2698,7 +2698,7 @@ "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "Succes" }, "troubleshooting": { "message": "Probleemoplossing" diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index 9837be29e3d..aad13e06ef1 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -2698,7 +2698,7 @@ "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "成功" }, "troubleshooting": { "message": "故障排除" From c7fa376be36a865b5ed9a6d758e9a80ce07a0ca8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 26 Apr 2024 07:05:43 +0000 Subject: [PATCH 071/110] Autosync the updated translations (#8926) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/web/src/locales/ca/messages.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index bf3071b506e..0f958846cd8 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -7748,31 +7748,31 @@ "description": "A machine user which can be used to automate processes and access secrets in the system." }, "machineAccounts": { - "message": "Machine accounts", + "message": "Compte de màquina", "description": "The title for the section that deals with machine accounts." }, "newMachineAccount": { - "message": "New machine account", + "message": "Compte nou de màquina", "description": "Title for creating a new machine account." }, "machineAccountsNoItemsMessage": { - "message": "Create a new machine account to get started automating secret access.", + "message": "Crea un compte de màquina nou per començar a automatitzar l'accés secret.", "description": "Message to encourage the user to start creating machine accounts." }, "machineAccountsNoItemsTitle": { - "message": "Nothing to show yet", + "message": "Encara no hi ha res a mostrar", "description": "Title to indicate that there are no machine accounts to display." }, "deleteMachineAccounts": { - "message": "Delete machine accounts", + "message": "Suprimeix els comptes de màquina", "description": "Title for the action to delete one or multiple machine accounts." }, "deleteMachineAccount": { - "message": "Delete machine account", + "message": "Suprimeix comptes de màquina", "description": "Title for the action to delete a single machine account." }, "viewMachineAccount": { - "message": "View machine account", + "message": "Veure el compte de màquina", "description": "Action to view the details of a machine account." }, "deleteMachineAccountDialogMessage": { From 14b2eb99a2baa74279036b412ad174b31bdc8ef8 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Fri, 26 Apr 2024 12:57:26 +0200 Subject: [PATCH 072/110] [PM-2282] Make feature flags type safe (#8612) Refactors the feature flags in ConfigService to be type safe. It also moves the default value to a centralized location rather than the caller defining it. This ensures consistency across the various places they are used. --- .../fileless-importer.background.ts | 2 +- .../layouts/organization-layout.component.ts | 1 - .../member-dialog/member-dialog.component.ts | 1 - .../settings/account.component.ts | 2 -- .../key-rotation/user-key-rotation.service.ts | 2 +- ...ganization-subscription-cloud.component.ts | 1 - .../src/app/layouts/user-layout.component.ts | 1 - .../collection-dialog.component.ts | 2 +- .../bulk-delete-dialog.component.ts | 1 - .../vault-onboarding.component.ts | 3 +- .../vault/individual-vault/vault.component.ts | 1 - .../vault/org-vault/attachments.component.ts | 2 +- ...-collection-assignment-dialog.component.ts | 5 +-- .../app/vault/org-vault/vault.component.ts | 1 - .../providers/clients/clients.component.ts | 1 - .../providers/providers-layout.component.ts | 2 -- .../providers/settings/account.component.ts | 1 - .../providers/setup/setup.component.ts | 2 -- .../directives/if-feature.directive.spec.ts | 8 ++--- .../src/directives/if-feature.directive.ts | 4 +-- .../platform/guard/feature-flag.guard.spec.ts | 8 ++--- .../vault/components/add-edit.component.ts | 1 - libs/common/src/enums/feature-flag.enum.ts | 36 +++++++++++++++++-- .../abstractions/config/config.service.ts | 14 ++------ .../abstractions/config/server-config.ts | 3 +- .../models/data/server-config.data.ts | 3 +- .../services/config/default-config.service.ts | 17 +++++---- 27 files changed, 67 insertions(+), 58 deletions(-) diff --git a/apps/browser/src/tools/background/fileless-importer.background.ts b/apps/browser/src/tools/background/fileless-importer.background.ts index 07c6408e8d2..fed5541f520 100644 --- a/apps/browser/src/tools/background/fileless-importer.background.ts +++ b/apps/browser/src/tools/background/fileless-importer.background.ts @@ -183,7 +183,7 @@ class FilelessImporterBackground implements FilelessImporterBackgroundInterface return; } - const filelessImportFeatureFlagEnabled = await this.configService.getFeatureFlag( + const filelessImportFeatureFlagEnabled = await this.configService.getFeatureFlag( FeatureFlag.BrowserFilelessImport, ); const userAuthStatus = await this.authService.getAuthStatus(); diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts index b1a84c22f35..7de0c98cd5a 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts @@ -58,7 +58,6 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy { protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$( FeatureFlag.ShowPaymentMethodWarningBanners, - false, ); constructor( diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts index f1af9506505..3cccd6e28f3 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts @@ -218,7 +218,6 @@ export class MemberDialogComponent implements OnDestroy { groups: groups$, flexibleCollectionsV1Enabled: this.configService.getFeatureFlag$( FeatureFlag.FlexibleCollectionsV1, - false, ), }) .pipe(takeUntil(this.destroy$)) diff --git a/apps/web/src/app/admin-console/organizations/settings/account.component.ts b/apps/web/src/app/admin-console/organizations/settings/account.component.ts index 1ce05f7a302..d8091e46aef 100644 --- a/apps/web/src/app/admin-console/organizations/settings/account.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/account.component.ts @@ -44,12 +44,10 @@ export class AccountComponent { protected flexibleCollectionsMigrationEnabled$ = this.configService.getFeatureFlag$( FeatureFlag.FlexibleCollectionsMigration, - false, ); flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$( FeatureFlag.FlexibleCollectionsV1, - false, ); // FormGroup validators taken from server Organization domain object diff --git a/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts b/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts index 94c62081154..dc5f9337247 100644 --- a/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts +++ b/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts @@ -90,7 +90,7 @@ export class UserKeyRotationService { request.emergencyAccessKeys = await this.emergencyAccessService.getRotatedKeys(newUserKey); request.resetPasswordKeys = await this.resetPasswordService.getRotatedKeys(newUserKey); - if (await this.configService.getFeatureFlag(FeatureFlag.KeyRotationImprovements)) { + if (await this.configService.getFeatureFlag(FeatureFlag.KeyRotationImprovements)) { await this.apiService.postUserKeyUpdate(request); } else { await this.rotateUserKeyAndEncryptedDataLegacy(request); diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts index 9326359bd8c..477032debae 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts @@ -84,7 +84,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy this.showUpdatedSubscriptionStatusSection$ = this.configService.getFeatureFlag$( FeatureFlag.AC1795_UpdatedSubscriptionStatusSection, - false, ); } diff --git a/apps/web/src/app/layouts/user-layout.component.ts b/apps/web/src/app/layouts/user-layout.component.ts index fea03528672..eb507bd997e 100644 --- a/apps/web/src/app/layouts/user-layout.component.ts +++ b/apps/web/src/app/layouts/user-layout.component.ts @@ -40,7 +40,6 @@ export class UserLayoutComponent implements OnInit { protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$( FeatureFlag.ShowPaymentMethodWarningBanners, - false, ); constructor( diff --git a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts index 8e0d610c93b..4e95bb4bcc3 100644 --- a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts +++ b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts @@ -76,7 +76,7 @@ export enum CollectionDialogAction { }) export class CollectionDialogComponent implements OnInit, OnDestroy { protected flexibleCollectionsV1Enabled$ = this.configService - .getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1, false) + .getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1) .pipe(first()); private destroy$ = new Subject(); diff --git a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts index 4050823a6d5..a678a05ae34 100644 --- a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts +++ b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts @@ -54,7 +54,6 @@ export class BulkDeleteDialogComponent { private flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$( FeatureFlag.FlexibleCollectionsV1, - false, ); constructor( diff --git a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts index dc3a41cf155..90af89e60ea 100644 --- a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts @@ -60,9 +60,8 @@ export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy { ) {} async ngOnInit() { - this.showOnboardingAccess$ = await this.configService.getFeatureFlag$( + this.showOnboardingAccess$ = await this.configService.getFeatureFlag$( FeatureFlag.VaultOnboarding, - false, ); this.onboardingTasks$ = this.vaultOnboardingService.vaultOnboardingState$; await this.setOnboardingTasks(); diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index c97dd93d768..b956a904456 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -148,7 +148,6 @@ export class VaultComponent implements OnInit, OnDestroy { protected currentSearchText$: Observable; protected flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$( FeatureFlag.FlexibleCollectionsV1, - false, ); private searchText$ = new Subject(); diff --git a/apps/web/src/app/vault/org-vault/attachments.component.ts b/apps/web/src/app/vault/org-vault/attachments.component.ts index cf1f0796ec8..2aecf277e60 100644 --- a/apps/web/src/app/vault/org-vault/attachments.component.ts +++ b/apps/web/src/app/vault/org-vault/attachments.component.ts @@ -60,7 +60,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On async ngOnInit() { await super.ngOnInit(); this.flexibleCollectionsV1Enabled = await firstValueFrom( - this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1, false), + this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1), ); } diff --git a/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts b/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts index 091c6461780..e9f8401d730 100644 --- a/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts +++ b/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts @@ -70,10 +70,7 @@ export class BulkCollectionAssignmentDialogComponent implements OnDestroy, OnIni ) {} async ngOnInit() { - const v1FCEnabled = await this.configService.getFeatureFlag( - FeatureFlag.FlexibleCollectionsV1, - false, - ); + const v1FCEnabled = await this.configService.getFeatureFlag(FeatureFlag.FlexibleCollectionsV1); const org = await this.organizationService.get(this.params.organizationId); if (org.canEditAllCiphers(v1FCEnabled)) { diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index 9de404e9698..243dedef930 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -193,7 +193,6 @@ export class VaultComponent implements OnInit, OnDestroy { this._flexibleCollectionsV1FlagEnabled = await this.configService.getFeatureFlag( FeatureFlag.FlexibleCollectionsV1, - false, ); const filter$ = this.routedVaultFilterService.filter$; diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts index 54e264c6668..6875c3816b0 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts @@ -42,7 +42,6 @@ export class ClientsComponent extends BaseClientsComponent { protected consolidatedBillingEnabled$ = this.configService.getFeatureFlag$( FeatureFlag.EnableConsolidatedBilling, - false, ); constructor( diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts index c78bf476c0c..8dbb6534016 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts @@ -37,12 +37,10 @@ export class ProvidersLayoutComponent { protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$( FeatureFlag.ShowPaymentMethodWarningBanners, - false, ); protected enableConsolidatedBilling$ = this.configService.getFeatureFlag$( FeatureFlag.EnableConsolidatedBilling, - false, ); constructor( diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts index 83038d1bfc4..70eb8af7ba3 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts @@ -30,7 +30,6 @@ export class AccountComponent { protected enableDeleteProvider$ = this.configService.getFeatureFlag$( FeatureFlag.EnableDeleteProvider, - false, ); constructor( diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts index cf9af4f68ad..258088257db 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts @@ -36,12 +36,10 @@ export class SetupComponent implements OnInit { protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$( FeatureFlag.ShowPaymentMethodWarningBanners, - false, ); protected enableConsolidatedBilling$ = this.configService.getFeatureFlag$( FeatureFlag.EnableConsolidatedBilling, - false, ); constructor( diff --git a/libs/angular/src/directives/if-feature.directive.spec.ts b/libs/angular/src/directives/if-feature.directive.spec.ts index 944410be7d5..456220b7911 100644 --- a/libs/angular/src/directives/if-feature.directive.spec.ts +++ b/libs/angular/src/directives/if-feature.directive.spec.ts @@ -3,7 +3,7 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { By } from "@angular/platform-browser"; import { mock, MockProxy } from "jest-mock-extended"; -import { FeatureFlag, FeatureFlagValue } from "@bitwarden/common/enums/feature-flag.enum"; +import { AllowedFeatureFlagTypes, FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -41,10 +41,8 @@ describe("IfFeatureDirective", () => { let content: HTMLElement; let mockConfigService: MockProxy; - const mockConfigFlagValue = (flag: FeatureFlag, flagValue: FeatureFlagValue) => { - mockConfigService.getFeatureFlag.mockImplementation((f, defaultValue) => - flag == f ? Promise.resolve(flagValue) : Promise.resolve(defaultValue), - ); + const mockConfigFlagValue = (flag: FeatureFlag, flagValue: AllowedFeatureFlagTypes) => { + mockConfigService.getFeatureFlag.mockImplementation((f) => Promise.resolve(flagValue as any)); }; const queryContent = (testId: string) => diff --git a/libs/angular/src/directives/if-feature.directive.ts b/libs/angular/src/directives/if-feature.directive.ts index 069f306a895..838bd264adb 100644 --- a/libs/angular/src/directives/if-feature.directive.ts +++ b/libs/angular/src/directives/if-feature.directive.ts @@ -1,6 +1,6 @@ import { Directive, Input, OnInit, TemplateRef, ViewContainerRef } from "@angular/core"; -import { FeatureFlag, FeatureFlagValue } from "@bitwarden/common/enums/feature-flag.enum"; +import { AllowedFeatureFlagTypes, FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -23,7 +23,7 @@ export class IfFeatureDirective implements OnInit { * Optional value to compare against the value of the feature flag in the config service. * @default true */ - @Input() appIfFeatureValue: FeatureFlagValue = true; + @Input() appIfFeatureValue: AllowedFeatureFlagTypes = true; private hasView = false; diff --git a/libs/angular/src/platform/guard/feature-flag.guard.spec.ts b/libs/angular/src/platform/guard/feature-flag.guard.spec.ts index 88637dff978..323e8c26590 100644 --- a/libs/angular/src/platform/guard/feature-flag.guard.spec.ts +++ b/libs/angular/src/platform/guard/feature-flag.guard.spec.ts @@ -34,12 +34,12 @@ describe("canAccessFeature", () => { flag == testFlag ? Promise.resolve(flagValue) : Promise.resolve(defaultValue), ); } else if (typeof flagValue === "string") { - mockConfigService.getFeatureFlag.mockImplementation((flag, defaultValue = "") => - flag == testFlag ? Promise.resolve(flagValue) : Promise.resolve(defaultValue), + mockConfigService.getFeatureFlag.mockImplementation((flag) => + flag == testFlag ? Promise.resolve(flagValue as any) : Promise.resolve(""), ); } else if (typeof flagValue === "number") { - mockConfigService.getFeatureFlag.mockImplementation((flag, defaultValue = 0) => - flag == testFlag ? Promise.resolve(flagValue) : Promise.resolve(defaultValue), + mockConfigService.getFeatureFlag.mockImplementation((flag) => + flag == testFlag ? Promise.resolve(flagValue as any) : Promise.resolve(0), ); } diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index d9b73a0e7f3..177b4289f4b 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -182,7 +182,6 @@ export class AddEditComponent implements OnInit, OnDestroy { async ngOnInit() { this.flexibleCollectionsV1Enabled = await this.configService.getFeatureFlag( FeatureFlag.FlexibleCollectionsV1, - false, ); this.policyService diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 636e9bc4cef..d84494362e3 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -1,3 +1,8 @@ +/** + * Feature flags. + * + * Flags MUST be short lived and SHALL be removed once enabled. + */ export enum FeatureFlag { BrowserFilelessImport = "browser-fileless-import", ItemShare = "item-share", @@ -13,5 +18,32 @@ export enum FeatureFlag { EnableDeleteProvider = "AC-1218-delete-provider", } -// Replace this with a type safe lookup of the feature flag values in PM-2282 -export type FeatureFlagValue = number | string | boolean; +export type AllowedFeatureFlagTypes = boolean | number | string; + +// Helper to ensure the value is treated as a boolean. +const FALSE = false as boolean; + +/** + * Default value for feature flags. + * + * DO NOT enable previously disabled flags, REMOVE them instead. + * We support true as a value as we prefer flags to "enable" not "disable". + */ +export const DefaultFeatureFlagValue = { + [FeatureFlag.BrowserFilelessImport]: FALSE, + [FeatureFlag.ItemShare]: FALSE, + [FeatureFlag.FlexibleCollectionsV1]: FALSE, + [FeatureFlag.VaultOnboarding]: FALSE, + [FeatureFlag.GeneratorToolsModernization]: FALSE, + [FeatureFlag.KeyRotationImprovements]: FALSE, + [FeatureFlag.FlexibleCollectionsMigration]: FALSE, + [FeatureFlag.ShowPaymentMethodWarningBanners]: FALSE, + [FeatureFlag.EnableConsolidatedBilling]: FALSE, + [FeatureFlag.AC1795_UpdatedSubscriptionStatusSection]: FALSE, + [FeatureFlag.UnassignedItemsBanner]: FALSE, + [FeatureFlag.EnableDeleteProvider]: FALSE, +} satisfies Record; + +export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue; + +export type FeatureFlagValueType = DefaultFeatureFlagValueType[Flag]; diff --git a/libs/common/src/platform/abstractions/config/config.service.ts b/libs/common/src/platform/abstractions/config/config.service.ts index 9eca5891ac1..6985430acc9 100644 --- a/libs/common/src/platform/abstractions/config/config.service.ts +++ b/libs/common/src/platform/abstractions/config/config.service.ts @@ -1,7 +1,7 @@ import { Observable } from "rxjs"; import { SemVer } from "semver"; -import { FeatureFlag } from "../../../enums/feature-flag.enum"; +import { FeatureFlag, FeatureFlagValueType } from "../../../enums/feature-flag.enum"; import { Region } from "../environment.service"; import { ServerConfig } from "./server-config"; @@ -14,23 +14,15 @@ export abstract class ConfigService { /** * Retrieves the value of a feature flag for the currently active user * @param key The feature flag to retrieve - * @param defaultValue The default value to return if the feature flag is not set or the server's config is irretrievable * @returns An observable that emits the value of the feature flag, updates as the server config changes */ - getFeatureFlag$: ( - key: FeatureFlag, - defaultValue?: T, - ) => Observable; + getFeatureFlag$: (key: Flag) => Observable>; /** * Retrieves the value of a feature flag for the currently active user * @param key The feature flag to retrieve - * @param defaultValue The default value to return if the feature flag is not set or the server's config is irretrievable * @returns The value of the feature flag */ - getFeatureFlag: ( - key: FeatureFlag, - defaultValue?: T, - ) => Promise; + getFeatureFlag: (key: Flag) => Promise>; /** * Verifies whether the server version meets the minimum required version * @param minimumRequiredServerVersion The minimum version required diff --git a/libs/common/src/platform/abstractions/config/server-config.ts b/libs/common/src/platform/abstractions/config/server-config.ts index 287e359f189..bb186059641 100644 --- a/libs/common/src/platform/abstractions/config/server-config.ts +++ b/libs/common/src/platform/abstractions/config/server-config.ts @@ -1,5 +1,6 @@ import { Jsonify } from "type-fest"; +import { AllowedFeatureFlagTypes } from "../../../enums/feature-flag.enum"; import { ServerConfigData, ThirdPartyServerConfigData, @@ -14,7 +15,7 @@ export class ServerConfig { server?: ThirdPartyServerConfigData; environment?: EnvironmentServerConfigData; utcDate: Date; - featureStates: { [key: string]: string } = {}; + featureStates: { [key: string]: AllowedFeatureFlagTypes } = {}; constructor(serverConfigData: ServerConfigData) { this.version = serverConfigData.version; diff --git a/libs/common/src/platform/models/data/server-config.data.ts b/libs/common/src/platform/models/data/server-config.data.ts index a4819f75678..57e8fbc6284 100644 --- a/libs/common/src/platform/models/data/server-config.data.ts +++ b/libs/common/src/platform/models/data/server-config.data.ts @@ -1,5 +1,6 @@ import { Jsonify } from "type-fest"; +import { AllowedFeatureFlagTypes } from "../../../enums/feature-flag.enum"; import { Region } from "../../abstractions/environment.service"; import { ServerConfigResponse, @@ -13,7 +14,7 @@ export class ServerConfigData { server?: ThirdPartyServerConfigData; environment?: EnvironmentServerConfigData; utcDate: string; - featureStates: { [key: string]: string } = {}; + featureStates: { [key: string]: AllowedFeatureFlagTypes } = {}; constructor(serverConfigResponse: Partial) { this.version = serverConfigResponse?.version; diff --git a/libs/common/src/platform/services/config/default-config.service.ts b/libs/common/src/platform/services/config/default-config.service.ts index e124deccf8c..71b76363a3b 100644 --- a/libs/common/src/platform/services/config/default-config.service.ts +++ b/libs/common/src/platform/services/config/default-config.service.ts @@ -13,7 +13,11 @@ import { } from "rxjs"; import { SemVer } from "semver"; -import { FeatureFlag, FeatureFlagValue } from "../../../enums/feature-flag.enum"; +import { + DefaultFeatureFlagValue, + FeatureFlag, + FeatureFlagValueType, +} from "../../../enums/feature-flag.enum"; import { UserId } from "../../../types/guid"; import { ConfigApiServiceAbstraction } from "../../abstractions/config/config-api.service.abstraction"; import { ConfigService } from "../../abstractions/config/config.service"; @@ -89,20 +93,21 @@ export class DefaultConfigService implements ConfigService { map((config) => config?.environment?.cloudRegion ?? Region.US), ); } - getFeatureFlag$(key: FeatureFlag, defaultValue?: T) { + + getFeatureFlag$(key: Flag) { return this.serverConfig$.pipe( map((serverConfig) => { if (serverConfig?.featureStates == null || serverConfig.featureStates[key] == null) { - return defaultValue; + return DefaultFeatureFlagValue[key]; } - return serverConfig.featureStates[key] as T; + return serverConfig.featureStates[key] as FeatureFlagValueType; }), ); } - async getFeatureFlag(key: FeatureFlag, defaultValue?: T) { - return await firstValueFrom(this.getFeatureFlag$(key, defaultValue)); + async getFeatureFlag(key: Flag) { + return await firstValueFrom(this.getFeatureFlag$(key)); } checkServerMeetsVersionRequirement$(minimumRequiredServerVersion: SemVer) { From 11ba8d188deb8efd52b1b8ff04c2d2e82f168be3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 26 Apr 2024 11:06:19 +0000 Subject: [PATCH 073/110] Autosync the updated translations (#8925) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/src/_locales/pl/messages.json | 2 +- apps/browser/src/_locales/pt_BR/messages.json | 2 +- apps/browser/src/_locales/vi/messages.json | 24 +++++++++---------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index d3d9106c15e..1a56d32a35a 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -7,7 +7,7 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "message": "W domu, w pracy, lub w ruchu, Bitwarden z łatwością zabezpiecza Twoje hasła, passkeys i poufne informacje", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 417bc977ebe..0f40bc63bb0 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -7,7 +7,7 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "message": "Em qual lugar for, o Bitwarden protege suas senhas, chaves de acesso, e informações confidenciais", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index 4eba4ffaeaa..6e530412db6 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Bitwarden - Trình Quản lý Mật khẩu", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "message": "Ở nhà, ở cơ quan, hay trên đường đi, Bitwarden sẽ bảo mật tất cả mật khẩu, passkey, và thông tin cá nhân của bạn", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { @@ -650,7 +650,7 @@ "message": "'Thông báo Thêm đăng nhập' sẽ tự động nhắc bạn lưu các đăng nhập mới vào hầm an toàn của bạn bất cứ khi nào bạn đăng nhập trang web lần đầu tiên." }, "addLoginNotificationDescAlt": { - "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." + "message": "Đưa ra lựa chọn để thêm một mục nếu không tìm thấy mục đó trong hòm của bạn. Áp dụng với mọi tài khoản đăng nhập trên thiết bị." }, "showCardsCurrentTab": { "message": "Hiển thị thẻ trên trang Tab" @@ -685,13 +685,13 @@ "message": "Yêu cầu cập nhật mật khẩu đăng nhập khi phát hiện thay đổi trên trang web." }, "changedPasswordNotificationDescAlt": { - "message": "Ask to update a login's password when a change is detected on a website. Applies to all logged in accounts." + "message": "Đưa ra lựa chọn để cập nhật mật khẩu khi phát hiện có sự thay đổi trên trang web. Áp dụng với mọi tài khoản đăng nhập trên thiết bị." }, "enableUsePasskeys": { - "message": "Ask to save and use passkeys" + "message": "Đưa ra lựa chọn để lưu và sử dụng passkey" }, "usePasskeysDesc": { - "message": "Ask to save new passkeys or log in with passkeys stored in your vault. Applies to all logged in accounts." + "message": "Đưa ra lựa chọn để lưu passkey mới hoặc đăng nhập bằng passkey đã lưu trong hòm. Áp dụng với mọi tài khoản đăng nhập trên thiết bị." }, "notificationChangeDesc": { "message": "Bạn có muốn cập nhật mật khẩu này trên Bitwarden không?" @@ -712,7 +712,7 @@ "message": "Sử dụng một đúp chuột để truy cập vào việc tạo mật khẩu và thông tin đăng nhập phù hợp cho trang web. " }, "contextMenuItemDescAlt": { - "message": "Use a secondary click to access password generation and matching logins for the website. Applies to all logged in accounts." + "message": "Truy cập trình khởi tạo mật khẩu và các mục đăng nhập đã lưu của trang web bằng cách nhấn đúp chuột. Áp dụng với mọi tài khoản đăng nhập trên thiết bị." }, "defaultUriMatchDetection": { "message": "Phương thức kiểm tra URI mặc định", @@ -728,7 +728,7 @@ "message": "Thay đổi màu sắc ứng dụng." }, "themeDescAlt": { - "message": "Change the application's color theme. Applies to all logged in accounts." + "message": "Thay đổi tông màu giao diện của ứng dụng. Áp dụng với mọi tài khoản đăng nhập trên thiết bị." }, "dark": { "message": "Tối", @@ -1061,10 +1061,10 @@ "message": "Tắt cài đặt trình quản lý mật khẩu tích hợp trong trình duyệt của bạn để tránh xung đột." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { - "message": "Edit browser settings." + "message": "Thay đổi cài đặt của trình duyệt." }, "autofillOverlayVisibilityOff": { - "message": "Off", + "message": "Tắt", "description": "Overlay setting select option for disabling autofill overlay" }, "autofillOverlayVisibilityOnFieldFocus": { @@ -1168,7 +1168,7 @@ "message": "Hiển thị một ảnh nhận dạng bên cạnh mỗi lần đăng nhập." }, "faviconDescAlt": { - "message": "Show a recognizable image next to each login. Applies to all logged in accounts." + "message": "Hiển thị một biểu tượng dễ nhận dạng bên cạnh mỗi mục đăng nhập. Áp dụng với mọi tài khoản đăng nhập trên thiết bị." }, "enableBadgeCounter": { "message": "Hiển thị biểu tượng bộ đếm" @@ -1500,7 +1500,7 @@ "message": "Mã PIN không hợp lệ." }, "tooManyInvalidPinEntryAttemptsLoggingOut": { - "message": "Too many invalid PIN entry attempts. Logging out." + "message": "Mã PIN bị gõ sai quá nhiều lần. Đang đăng xuất." }, "unlockWithBiometrics": { "message": "Mở khóa bằng sinh trắc học" From 2fa4c6e4f930d6f543ac799efe339e95368b36d7 Mon Sep 17 00:00:00 2001 From: KiruthigaManivannan <162679756+KiruthigaManivannan@users.noreply.github.com> Date: Fri, 26 Apr 2024 18:24:48 +0530 Subject: [PATCH 074/110] PM-4945 Update Two Factor verify dialog (#8580) * PM-4945 Update Two Factor verify dialog * PM-4945 Addressed review comments * PM-4945 Removed legacy User verification component and used new one --- .../settings/two-factor-setup.component.ts | 3 + .../two-factor-authenticator.component.html | 7 -- .../settings/two-factor-duo.component.html | 7 -- .../settings/two-factor-email.component.html | 7 -- .../two-factor-recovery.component.html | 2 - .../settings/two-factor-setup.component.ts | 73 ++++++++++++++---- .../settings/two-factor-verify.component.html | 36 +++++---- .../settings/two-factor-verify.component.ts | 74 ++++++++++++++----- .../two-factor-webauthn.component.html | 7 -- .../two-factor-yubikey.component.html | 7 -- .../src/app/shared/loose-components.module.ts | 6 +- 11 files changed, 142 insertions(+), 87 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts b/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts index abf1d249e16..80d77968f2d 100644 --- a/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts @@ -10,6 +10,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { DialogService } from "@bitwarden/components"; import { TwoFactorDuoComponent } from "../../../auth/settings/two-factor-duo.component"; import { TwoFactorSetupComponent as BaseTwoFactorSetupComponent } from "../../../auth/settings/two-factor-setup.component"; @@ -22,6 +23,7 @@ import { TwoFactorSetupComponent as BaseTwoFactorSetupComponent } from "../../.. export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent { tabbedHeader = false; constructor( + dialogService: DialogService, apiService: ApiService, modalService: ModalService, messagingService: MessagingService, @@ -31,6 +33,7 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent { billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( + dialogService, apiService, modalService, messagingService, diff --git a/apps/web/src/app/auth/settings/two-factor-authenticator.component.html b/apps/web/src/app/auth/settings/two-factor-authenticator.component.html index 33bf4fb1304..e17714cca79 100644 --- a/apps/web/src/app/auth/settings/two-factor-authenticator.component.html +++ b/apps/web/src/app/auth/settings/two-factor-authenticator.component.html @@ -15,13 +15,6 @@

- -
- - × - - × - - - -
diff --git a/apps/desktop/src/app/layout/account-switcher.component.ts b/apps/desktop/src/app/layout/account-switcher.component.ts index 4e39ab00292..c8a26065c11 100644 --- a/apps/desktop/src/app/layout/account-switcher.component.ts +++ b/apps/desktop/src/app/layout/account-switcher.component.ts @@ -1,19 +1,17 @@ import { animate, state, style, transition, trigger } from "@angular/animations"; import { ConnectedPosition } from "@angular/cdk/overlay"; -import { Component, OnDestroy, OnInit } from "@angular/core"; +import { Component } from "@angular/core"; import { Router } from "@angular/router"; -import { concatMap, firstValueFrom, Subject, takeUntil } from "rxjs"; +import { combineLatest, firstValueFrom, map, Observable, switchMap } from "rxjs"; import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common"; +import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service"; -import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { Account } from "@bitwarden/common/platform/models/domain/account"; import { UserId } from "@bitwarden/common/types/guid"; type ActiveAccount = { @@ -52,12 +50,18 @@ type InactiveAccount = ActiveAccount & { ]), ], }) -export class AccountSwitcherComponent implements OnInit, OnDestroy { - activeAccount?: ActiveAccount; - inactiveAccounts: { [userId: string]: InactiveAccount } = {}; - +export class AccountSwitcherComponent { + activeAccount$: Observable; + inactiveAccounts$: Observable<{ [userId: string]: InactiveAccount }>; authStatus = AuthenticationStatus; + view$: Observable<{ + activeAccount: ActiveAccount | null; + inactiveAccounts: { [userId: string]: InactiveAccount }; + numberOfAccounts: number; + showSwitcher: boolean; + }>; + isOpen = false; overlayPosition: ConnectedPosition[] = [ { @@ -68,21 +72,9 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy { }, ]; - private destroy$ = new Subject(); + showSwitcher$: Observable; - get showSwitcher() { - const userIsInAVault = !Utils.isNullOrWhitespace(this.activeAccount?.email); - const userIsAddingAnAdditionalAccount = Object.keys(this.inactiveAccounts).length > 0; - return userIsInAVault || userIsAddingAnAdditionalAccount; - } - - get numberOfAccounts() { - if (this.inactiveAccounts == null) { - this.isOpen = false; - return 0; - } - return Object.keys(this.inactiveAccounts).length; - } + numberOfAccounts$: Observable; constructor( private stateService: StateService, @@ -90,37 +82,65 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy { private avatarService: AvatarService, private messagingService: MessagingService, private router: Router, - private tokenService: TokenService, private environmentService: EnvironmentService, private loginEmailService: LoginEmailServiceAbstraction, - ) {} + private accountService: AccountService, + ) { + this.activeAccount$ = this.accountService.activeAccount$.pipe( + switchMap(async (active) => { + if (active == null) { + return null; + } - async ngOnInit(): Promise { - this.stateService.accounts$ - .pipe( - concatMap(async (accounts: { [userId: string]: Account }) => { - this.inactiveAccounts = await this.createInactiveAccounts(accounts); + return { + id: active.id, + name: active.name, + email: active.email, + avatarColor: await firstValueFrom(this.avatarService.avatarColor$), + server: (await this.environmentService.getEnvironment())?.getHostname(), + }; + }), + ); + this.inactiveAccounts$ = combineLatest([ + this.activeAccount$, + this.accountService.accounts$, + this.authService.authStatuses$, + ]).pipe( + switchMap(async ([activeAccount, accounts, accountStatuses]) => { + // Filter out logged out accounts and active account + accounts = Object.fromEntries( + Object.entries(accounts).filter( + ([id]: [UserId, AccountInfo]) => + accountStatuses[id] !== AuthenticationStatus.LoggedOut || id === activeAccount?.id, + ), + ); + return this.createInactiveAccounts(accounts); + }), + ); + this.showSwitcher$ = combineLatest([this.activeAccount$, this.inactiveAccounts$]).pipe( + map(([activeAccount, inactiveAccounts]) => { + const hasActiveUser = activeAccount != null; + const userIsAddingAnAdditionalAccount = Object.keys(inactiveAccounts).length > 0; + return hasActiveUser || userIsAddingAnAdditionalAccount; + }), + ); + this.numberOfAccounts$ = this.inactiveAccounts$.pipe( + map((accounts) => Object.keys(accounts).length), + ); - try { - this.activeAccount = { - id: await this.tokenService.getUserId(), - name: (await this.tokenService.getName()) ?? (await this.tokenService.getEmail()), - email: await this.tokenService.getEmail(), - avatarColor: await firstValueFrom(this.avatarService.avatarColor$), - server: (await this.environmentService.getEnvironment())?.getHostname(), - }; - } catch { - this.activeAccount = undefined; - } - }), - takeUntil(this.destroy$), - ) - .subscribe(); - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); + this.view$ = combineLatest([ + this.activeAccount$, + this.inactiveAccounts$, + this.numberOfAccounts$, + this.showSwitcher$, + ]).pipe( + map(([activeAccount, inactiveAccounts, numberOfAccounts, showSwitcher]) => ({ + activeAccount, + inactiveAccounts, + numberOfAccounts, + showSwitcher, + })), + ); } toggle() { @@ -144,11 +164,13 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy { await this.loginEmailService.saveEmailSettings(); await this.router.navigate(["/login"]); - await this.stateService.setActiveUser(null); + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + await this.stateService.clearDecryptedData(activeAccount?.id as UserId); + await this.accountService.switchAccount(null); } private async createInactiveAccounts(baseAccounts: { - [userId: string]: Account; + [userId: string]: AccountInfo; }): Promise<{ [userId: string]: InactiveAccount }> { const inactiveAccounts: { [userId: string]: InactiveAccount } = {}; @@ -159,8 +181,8 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy { inactiveAccounts[userId] = { id: userId, - name: baseAccounts[userId].profile.name, - email: baseAccounts[userId].profile.email, + name: baseAccounts[userId].name, + email: baseAccounts[userId].email, authenticationStatus: await this.authService.getAuthStatus(userId), avatarColor: await firstValueFrom(this.avatarService.getUserAvatarColor$(userId as UserId)), server: (await this.environmentService.getEnvironment(userId))?.getHostname(), diff --git a/apps/desktop/src/app/layout/search/search.component.ts b/apps/desktop/src/app/layout/search/search.component.ts index 9a7226218a6..06c67d8af22 100644 --- a/apps/desktop/src/app/layout/search/search.component.ts +++ b/apps/desktop/src/app/layout/search/search.component.ts @@ -2,7 +2,7 @@ import { Component, OnDestroy, OnInit } from "@angular/core"; import { UntypedFormControl } from "@angular/forms"; import { Subscription } from "rxjs"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { SearchBarService, SearchBarState } from "./search-bar.service"; @@ -18,7 +18,7 @@ export class SearchComponent implements OnInit, OnDestroy { constructor( private searchBarService: SearchBarService, - private stateService: StateService, + private accountService: AccountService, ) { // eslint-disable-next-line rxjs-angular/prefer-takeuntil this.searchBarService.state$.subscribe((state) => { @@ -33,7 +33,7 @@ export class SearchComponent implements OnInit, OnDestroy { ngOnInit() { // eslint-disable-next-line rxjs-angular/prefer-takeuntil - this.activeAccountSubscription = this.stateService.activeAccount$.subscribe((value) => { + this.activeAccountSubscription = this.accountService.activeAccount$.subscribe((_) => { this.searchBarService.setSearchText(""); this.searchText.patchValue(""); }); diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 1e3a7fdfa59..a485b925ba6 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -59,7 +59,6 @@ import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/ge import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service"; import { DialogService } from "@bitwarden/components"; -import { LoginGuard } from "../../auth/guards/login.guard"; import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service"; import { Account } from "../../models/account"; import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; @@ -102,7 +101,6 @@ const safeProviders: SafeProvider[] = [ safeProvider(InitService), safeProvider(NativeMessagingService), safeProvider(SearchBarService), - safeProvider(LoginGuard), safeProvider(DialogService), safeProvider({ provide: APP_INITIALIZER as SafeInjectionToken<() => void>, @@ -192,6 +190,7 @@ const safeProviders: SafeProvider[] = [ AutofillSettingsServiceAbstraction, VaultTimeoutSettingsService, BiometricStateService, + AccountServiceAbstraction, ], }), safeProvider({ diff --git a/apps/desktop/src/app/tools/generator.component.spec.ts b/apps/desktop/src/app/tools/generator.component.spec.ts index 51b5bf93a25..d908de8ef77 100644 --- a/apps/desktop/src/app/tools/generator.component.spec.ts +++ b/apps/desktop/src/app/tools/generator.component.spec.ts @@ -4,6 +4,7 @@ import { ActivatedRoute } from "@angular/router"; import { mock, MockProxy } from "jest-mock-extended"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -59,6 +60,10 @@ describe("GeneratorComponent", () => { provide: CipherService, useValue: mock(), }, + { + provide: AccountService, + useValue: mock(), + }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); diff --git a/apps/desktop/src/app/tools/send/add-edit.component.ts b/apps/desktop/src/app/tools/send/add-edit.component.ts index 7bdd5efbba9..804a3904380 100644 --- a/apps/desktop/src/app/tools/send/add-edit.component.ts +++ b/apps/desktop/src/app/tools/send/add-edit.component.ts @@ -4,6 +4,7 @@ import { FormBuilder } from "@angular/forms"; import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/tools/send/add-edit.component"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -34,6 +35,7 @@ export class AddEditComponent extends BaseAddEditComponent { dialogService: DialogService, formBuilder: FormBuilder, billingAccountProfileStateService: BillingAccountProfileStateService, + accountService: AccountService, ) { super( i18nService, @@ -49,6 +51,7 @@ export class AddEditComponent extends BaseAddEditComponent { dialogService, formBuilder, billingAccountProfileStateService, + accountService, ); } diff --git a/apps/desktop/src/auth/guards/login.guard.ts b/apps/desktop/src/auth/guards/login.guard.ts deleted file mode 100644 index f6c67d5af9c..00000000000 --- a/apps/desktop/src/auth/guards/login.guard.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Injectable } from "@angular/core"; -import { CanActivate } from "@angular/router"; -import { firstValueFrom } from "rxjs"; - -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; - -const maxAllowedAccounts = 5; - -@Injectable() -export class LoginGuard implements CanActivate { - protected homepage = "vault"; - constructor( - private stateService: StateService, - private platformUtilsService: PlatformUtilsService, - private i18nService: I18nService, - ) {} - - async canActivate() { - const accounts = await firstValueFrom(this.stateService.accounts$); - if (accounts != null && Object.keys(accounts).length >= maxAllowedAccounts) { - this.platformUtilsService.showToast("error", null, this.i18nService.t("accountLimitReached")); - return false; - } - - return true; - } -} diff --git a/apps/desktop/src/auth/guards/max-accounts.guard.ts b/apps/desktop/src/auth/guards/max-accounts.guard.ts new file mode 100644 index 00000000000..65c4ac99d01 --- /dev/null +++ b/apps/desktop/src/auth/guards/max-accounts.guard.ts @@ -0,0 +1,38 @@ +import { inject } from "@angular/core"; +import { CanActivateFn } from "@angular/router"; +import { Observable, map } from "rxjs"; + +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { ToastService } from "@bitwarden/components"; + +const maxAllowedAccounts = 5; + +function maxAccountsGuard(): Observable { + const authService = inject(AuthService); + const toastService = inject(ToastService); + const i18nService = inject(I18nService); + + return authService.authStatuses$.pipe( + map((statuses) => + Object.values(statuses).filter((status) => status != AuthenticationStatus.LoggedOut), + ), + map((accounts) => { + if (accounts != null && Object.keys(accounts).length >= maxAllowedAccounts) { + toastService.showToast({ + variant: "error", + title: null, + message: i18nService.t("accountLimitReached"), + }); + return false; + } + + return true; + }), + ); +} + +export function maxAccountsGuardFn(): CanActivateFn { + return () => maxAccountsGuard(); +} diff --git a/apps/desktop/src/auth/lock.component.spec.ts b/apps/desktop/src/auth/lock.component.spec.ts index f998e75d7a0..2137b707f67 100644 --- a/apps/desktop/src/auth/lock.component.spec.ts +++ b/apps/desktop/src/auth/lock.component.spec.ts @@ -13,6 +13,7 @@ import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeou import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; @@ -50,7 +51,7 @@ describe("LockComponent", () => { let component: LockComponent; let fixture: ComponentFixture; let stateServiceMock: MockProxy; - const biometricStateService = mock(); + let biometricStateService: MockProxy; let messagingServiceMock: MockProxy; let broadcasterServiceMock: MockProxy; let platformUtilsServiceMock: MockProxy; @@ -62,7 +63,6 @@ describe("LockComponent", () => { beforeEach(async () => { stateServiceMock = mock(); - stateServiceMock.activeAccount$ = of(null); messagingServiceMock = mock(); broadcasterServiceMock = mock(); @@ -73,6 +73,7 @@ describe("LockComponent", () => { mockMasterPasswordService = new FakeMasterPasswordService(); + biometricStateService = mock(); biometricStateService.dismissedRequirePasswordOnStartCallout$ = of(false); biometricStateService.promptAutomatically$ = of(false); biometricStateService.promptCancelled$ = of(false); @@ -165,6 +166,10 @@ describe("LockComponent", () => { provide: AccountService, useValue: accountService, }, + { + provide: AuthService, + useValue: mock(), + }, { provide: KdfConfigService, useValue: mock(), diff --git a/apps/desktop/src/auth/lock.component.ts b/apps/desktop/src/auth/lock.component.ts index 8e87b6663fc..d95df419e1a 100644 --- a/apps/desktop/src/auth/lock.component.ts +++ b/apps/desktop/src/auth/lock.component.ts @@ -10,6 +10,7 @@ import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeou import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; @@ -64,6 +65,7 @@ export class LockComponent extends BaseLockComponent { pinCryptoService: PinCryptoServiceAbstraction, biometricStateService: BiometricStateService, accountService: AccountService, + authService: AuthService, kdfConfigService: KdfConfigService, ) { super( @@ -89,6 +91,7 @@ export class LockComponent extends BaseLockComponent { pinCryptoService, biometricStateService, accountService, + authService, kdfConfigService, ); } diff --git a/apps/desktop/src/main/menu/menubar.ts b/apps/desktop/src/main/menu/menubar.ts index eb1dacf8250..b71774c5afe 100644 --- a/apps/desktop/src/main/menu/menubar.ts +++ b/apps/desktop/src/main/menu/menubar.ts @@ -65,9 +65,10 @@ export class Menubar { isLocked = updateRequest.accounts[updateRequest.activeUserId]?.isLocked ?? true; } - const isLockable = !isLocked && updateRequest?.accounts[updateRequest.activeUserId]?.isLockable; + const isLockable = + !isLocked && updateRequest?.accounts?.[updateRequest.activeUserId]?.isLockable; const hasMasterPassword = - updateRequest?.accounts[updateRequest.activeUserId]?.hasMasterPassword ?? false; + updateRequest?.accounts?.[updateRequest.activeUserId]?.hasMasterPassword ?? false; this.items = [ new FileMenu( diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index 1da2d94c15b..1939bb11f5f 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -2,7 +2,7 @@ import { DOCUMENT } from "@angular/common"; import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core"; import { NavigationEnd, Router } from "@angular/router"; import * as jq from "jquery"; -import { Subject, switchMap, takeUntil, timer } from "rxjs"; +import { Subject, firstValueFrom, map, switchMap, takeUntil, timer } from "rxjs"; import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; @@ -10,6 +10,7 @@ import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { PaymentMethodWarningsServiceAbstraction as PaymentMethodWarningService } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction"; @@ -51,7 +52,7 @@ const PaymentMethodWarningsRefresh = 60000; // 1 Minute templateUrl: "app.component.html", }) export class AppComponent implements OnDestroy, OnInit { - private lastActivity: number = null; + private lastActivity: Date = null; private idleTimer: number = null; private isIdle = false; private destroy$ = new Subject(); @@ -86,6 +87,7 @@ export class AppComponent implements OnDestroy, OnInit { private stateEventRunnerService: StateEventRunnerService, private paymentMethodWarningService: PaymentMethodWarningService, private organizationService: InternalOrganizationServiceAbstraction, + private accountService: AccountService, ) {} ngOnInit() { @@ -298,15 +300,16 @@ export class AppComponent implements OnDestroy, OnInit { } private async recordActivity() { - const now = new Date().getTime(); - if (this.lastActivity != null && now - this.lastActivity < 250) { + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + const now = new Date(); + if (this.lastActivity != null && now.getTime() - this.lastActivity.getTime() < 250) { return; } this.lastActivity = now; - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.stateService.setLastActive(now); + await this.accountService.setAccountActivity(activeUserId, now); // Idle states if (this.isIdle) { this.isIdle = false; diff --git a/apps/web/src/app/layouts/header/web-header.component.html b/apps/web/src/app/layouts/header/web-header.component.html index e24013de6f2..e2b3e7910ab 100644 --- a/apps/web/src/app/layouts/header/web-header.component.html +++ b/apps/web/src/app/layouts/header/web-header.component.html @@ -58,7 +58,7 @@ [bitMenuTriggerFor]="accountMenu" class="tw-border-0 tw-bg-transparent tw-p-0" > - + @@ -67,7 +67,7 @@ class="tw-flex tw-items-center tw-px-4 tw-py-1 tw-leading-tight tw-text-info" appStopProp > - +
{{ "loggedInAs" | i18n }} diff --git a/apps/web/src/app/layouts/header/web-header.component.ts b/apps/web/src/app/layouts/header/web-header.component.ts index 1f012e52ddc..9906bd53bab 100644 --- a/apps/web/src/app/layouts/header/web-header.component.ts +++ b/apps/web/src/app/layouts/header/web-header.component.ts @@ -1,16 +1,17 @@ import { Component, Input } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { combineLatest, map, Observable } from "rxjs"; +import { map, Observable } from "rxjs"; +import { User } from "@bitwarden/angular/pipes/user-name.pipe"; import { UnassignedItemsBannerService } from "@bitwarden/angular/services/unassigned-items-banner.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { AccountProfile } from "@bitwarden/common/platform/models/domain/account"; +import { UserId } from "@bitwarden/common/types/guid"; @Component({ selector: "app-header", @@ -28,7 +29,7 @@ export class WebHeaderComponent { @Input() icon: string; protected routeData$: Observable<{ titleId: string }>; - protected account$: Observable; + protected account$: Observable; protected canLock$: Observable; protected selfHosted: boolean; protected hostname = location.hostname; @@ -38,12 +39,12 @@ export class WebHeaderComponent { constructor( private route: ActivatedRoute, - private stateService: StateService, private platformUtilsService: PlatformUtilsService, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, private messagingService: MessagingService, protected unassignedItemsBannerService: UnassignedItemsBannerService, private configService: ConfigService, + private accountService: AccountService, ) { this.routeData$ = this.route.data.pipe( map((params) => { @@ -55,14 +56,7 @@ export class WebHeaderComponent { this.selfHosted = this.platformUtilsService.isSelfHost(); - this.account$ = combineLatest([ - this.stateService.activeAccount$, - this.stateService.accounts$, - ]).pipe( - map(([activeAccount, accounts]) => { - return accounts[activeAccount]?.profile; - }), - ); + this.account$ = this.accountService.activeAccount$; this.canLock$ = this.vaultTimeoutSettingsService .availableVaultTimeoutActions$() .pipe(map((actions) => actions.includes(VaultTimeoutAction.Lock))); diff --git a/apps/web/src/app/tools/send/add-edit.component.ts b/apps/web/src/app/tools/send/add-edit.component.ts index ee4be414889..cca416db9cd 100644 --- a/apps/web/src/app/tools/send/add-edit.component.ts +++ b/apps/web/src/app/tools/send/add-edit.component.ts @@ -5,6 +5,7 @@ import { FormBuilder } from "@angular/forms"; import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/tools/send/add-edit.component"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -40,6 +41,7 @@ export class AddEditComponent extends BaseAddEditComponent { billingAccountProfileStateService: BillingAccountProfileStateService, protected dialogRef: DialogRef, @Inject(DIALOG_DATA) params: { sendId: string }, + accountService: AccountService, ) { super( i18nService, @@ -55,6 +57,7 @@ export class AddEditComponent extends BaseAddEditComponent { dialogService, formBuilder, billingAccountProfileStateService, + accountService, ); this.sendId = params.sendId; diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts b/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts index ad80c9f4e58..41aa766e3a4 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts @@ -55,7 +55,6 @@ export default { { provide: StateService, useValue: { - activeAccount$: new BehaviorSubject("1").asObservable(), accounts$: new BehaviorSubject({ "1": { profile: { name: "Foo" } } }).asObservable(), async getShowFavicon() { return true; diff --git a/libs/angular/src/auth/components/lock.component.ts b/libs/angular/src/auth/components/lock.component.ts index 89af31da81a..7eb30d759ac 100644 --- a/libs/angular/src/auth/components/lock.component.ts +++ b/libs/angular/src/auth/components/lock.component.ts @@ -1,7 +1,7 @@ import { Directive, NgZone, OnDestroy, OnInit } from "@angular/core"; import { Router } from "@angular/router"; import { firstValueFrom, Subject } from "rxjs"; -import { concatMap, take, takeUntil } from "rxjs/operators"; +import { concatMap, map, take, takeUntil } from "rxjs/operators"; import { PinCryptoServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -11,10 +11,12 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request"; import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/response/master-password-policy.response"; @@ -30,6 +32,7 @@ import { BiometricStateService } from "@bitwarden/common/platform/biometrics/bio import { HashPurpose, KeySuffixOptions } from "@bitwarden/common/platform/enums"; import { PinLockType } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; +import { UserId } from "@bitwarden/common/types/guid"; import { UserKey } from "@bitwarden/common/types/key"; import { DialogService } from "@bitwarden/components"; @@ -46,6 +49,7 @@ export class LockComponent implements OnInit, OnDestroy { supportsBiometric: boolean; biometricLock: boolean; + private activeUserId: UserId; protected successRoute = "vault"; protected forcePasswordResetRoute = "update-temp-password"; protected onSuccessfulSubmit: () => Promise; @@ -80,14 +84,16 @@ export class LockComponent implements OnInit, OnDestroy { protected pinCryptoService: PinCryptoServiceAbstraction, protected biometricStateService: BiometricStateService, protected accountService: AccountService, + protected authService: AuthService, protected kdfConfigService: KdfConfigService, ) {} async ngOnInit() { - this.stateService.activeAccount$ + this.accountService.activeAccount$ .pipe( - concatMap(async () => { - await this.load(); + concatMap(async (account) => { + this.activeUserId = account?.id; + await this.load(account?.id); }), takeUntil(this.destroy$), ) @@ -116,7 +122,7 @@ export class LockComponent implements OnInit, OnDestroy { }); if (confirmed) { - this.messagingService.send("logout"); + this.messagingService.send("logout", { userId: this.activeUserId }); } } @@ -321,23 +327,35 @@ export class LockComponent implements OnInit, OnDestroy { } } - private async load() { + private async load(userId: UserId) { // TODO: Investigate PM-3515 // The loading of the lock component works as follows: - // 1. First, is locking a valid timeout action? If not, we will log the user out. - // 2. If locking IS a valid timeout action, we proceed to show the user the lock screen. + // 1. If the user is unlocked, we're here in error so we navigate to the home page + // 2. First, is locking a valid timeout action? If not, we will log the user out. + // 3. If locking IS a valid timeout action, we proceed to show the user the lock screen. // The user will be able to unlock as follows: // - If they have a PIN set, they will be presented with the PIN input // - If they have a master password and no PIN, they will be presented with the master password input // - If they have biometrics enabled, they will be presented with the biometric prompt + const isUnlocked = await firstValueFrom( + this.authService + .authStatusFor$(userId) + .pipe(map((status) => status === AuthenticationStatus.Unlocked)), + ); + if (isUnlocked) { + // navigate to home + await this.router.navigate(["/"]); + return; + } + const availableVaultTimeoutActions = await firstValueFrom( - this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(), + this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(userId), ); const supportsLock = availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock); if (!supportsLock) { - return await this.vaultTimeoutService.logOut(); + return await this.vaultTimeoutService.logOut(userId); } this.pinStatus = await this.vaultTimeoutSettingsService.isPinLockSet(); diff --git a/libs/angular/src/pipes/user-name.pipe.ts b/libs/angular/src/pipes/user-name.pipe.ts index 88b088a7e22..f007f4ad873 100644 --- a/libs/angular/src/pipes/user-name.pipe.ts +++ b/libs/angular/src/pipes/user-name.pipe.ts @@ -1,6 +1,6 @@ import { Pipe, PipeTransform } from "@angular/core"; -interface User { +export interface User { name?: string; email?: string; } diff --git a/libs/angular/src/tools/send/add-edit.component.ts b/libs/angular/src/tools/send/add-edit.component.ts index da859e50bfb..b4f7ec171a1 100644 --- a/libs/angular/src/tools/send/add-edit.component.ts +++ b/libs/angular/src/tools/send/add-edit.component.ts @@ -5,6 +5,7 @@ import { Subject, firstValueFrom, takeUntil, map, BehaviorSubject, concatMap } f import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -118,6 +119,7 @@ export class AddEditComponent implements OnInit, OnDestroy { protected dialogService: DialogService, protected formBuilder: FormBuilder, protected billingAccountProfileStateService: BillingAccountProfileStateService, + protected accountService: AccountService, ) { this.typeOptions = [ { name: i18nService.t("sendTypeFile"), value: SendType.File, premium: true }, @@ -215,7 +217,9 @@ export class AddEditComponent implements OnInit, OnDestroy { } async load() { - this.emailVerified = await this.stateService.getEmailVerified(); + this.emailVerified = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.emailVerified ?? false)), + ); this.type = !this.canAccessPremium || !this.emailVerified ? SendType.Text : SendType.File; if (this.send == null) { diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts index a123e300538..0efb9569eb5 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts @@ -128,6 +128,7 @@ describe("AuthRequestLoginStrategy", () => { masterPasswordService.masterKeySubject.next(masterKey); cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); + tokenService.decodeAccessToken.mockResolvedValue({ sub: mockUserId }); await authRequestLoginStrategy.logIn(credentials); diff --git a/libs/auth/src/common/login-strategies/login.strategy.spec.ts b/libs/auth/src/common/login-strategies/login.strategy.spec.ts index 3284f6e9474..c3a8f61d782 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -218,7 +218,7 @@ describe("LoginStrategy", () => { expect(messagingService.send).toHaveBeenCalledWith("loggedIn"); }); - it("throws if active account isn't found after being initialized", async () => { + it("throws if new account isn't active after being initialized", async () => { const idTokenResponse = identityTokenResponseFactory(); apiService.postIdentityToken.mockResolvedValue(idTokenResponse); @@ -228,7 +228,8 @@ describe("LoginStrategy", () => { stateService.getVaultTimeoutAction.mockResolvedValue(mockVaultTimeoutAction); stateService.getVaultTimeout.mockResolvedValue(mockVaultTimeout); - accountService.activeAccountSubject.next(null); + accountService.switchAccount = jest.fn(); // block internal switch to new account + accountService.activeAccountSubject.next(null); // simulate no active account await expect(async () => await passwordLoginStrategy.logIn(credentials)).rejects.toThrow(); }); diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index fd268d955ef..3a3109349e8 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -169,6 +169,12 @@ export abstract class LoginStrategy { const vaultTimeoutAction = await this.stateService.getVaultTimeoutAction({ userId }); const vaultTimeout = await this.stateService.getVaultTimeout({ userId }); + await this.accountService.addAccount(userId, { + name: accountInformation.name, + email: accountInformation.email, + emailVerified: accountInformation.email_verified, + }); + // set access token and refresh token before account initialization so authN status can be accurate // User id will be derived from the access token. await this.tokenService.setTokens( @@ -178,6 +184,8 @@ export abstract class LoginStrategy { tokenResponse.refreshToken, // Note: CLI login via API key sends undefined for refresh token. ); + await this.accountService.switchAccount(userId); + await this.stateService.addAccount( new Account({ profile: { diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts index 5c1fe9b1fe8..c97639f1023 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts @@ -164,6 +164,7 @@ describe("PasswordLoginStrategy", () => { masterPasswordService.masterKeySubject.next(masterKey); cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); + tokenService.decodeAccessToken.mockResolvedValue({ sub: userId }); await passwordLoginStrategy.logIn(credentials); @@ -199,6 +200,7 @@ describe("PasswordLoginStrategy", () => { it("forces the user to update their master password on successful login when it does not meet master password policy requirements", async () => { passwordStrengthService.getPasswordStrength.mockReturnValue({ score: 0 } as any); policyService.evaluateMasterPassword.mockReturnValue(false); + tokenService.decodeAccessToken.mockResolvedValue({ sub: userId }); const result = await passwordLoginStrategy.logIn(credentials); @@ -213,6 +215,7 @@ describe("PasswordLoginStrategy", () => { it("forces the user to update their master password on successful 2FA login when it does not meet master password policy requirements", async () => { passwordStrengthService.getPasswordStrength.mockReturnValue({ score: 0 } as any); policyService.evaluateMasterPassword.mockReturnValue(false); + tokenService.decodeAccessToken.mockResolvedValue({ sub: userId }); const token2FAResponse = new IdentityTwoFactorResponse({ TwoFactorProviders: ["0"], diff --git a/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.spec.ts b/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.spec.ts index 16479f19ea5..ae1813d3d7b 100644 --- a/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.spec.ts +++ b/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.spec.ts @@ -65,6 +65,7 @@ describe("UserDecryptionOptionsService", () => { await fakeAccountService.addAccount(givenUser, { name: "Test User 1", email: "test1@email.com", + emailVerified: false, }); await fakeStateProvider.setUserState( USER_DECRYPTION_OPTIONS, diff --git a/libs/common/spec/fake-account-service.ts b/libs/common/spec/fake-account-service.ts index a8b09b7417f..649a158d757 100644 --- a/libs/common/spec/fake-account-service.ts +++ b/libs/common/spec/fake-account-service.ts @@ -1,5 +1,5 @@ import { mock } from "jest-mock-extended"; -import { ReplaySubject } from "rxjs"; +import { ReplaySubject, combineLatest, map } from "rxjs"; import { AccountInfo, AccountService } from "../src/auth/abstractions/account.service"; import { UserId } from "../src/types/guid"; @@ -7,15 +7,20 @@ import { UserId } from "../src/types/guid"; export function mockAccountServiceWith( userId: UserId, info: Partial = {}, + activity: Record = {}, ): FakeAccountService { const fullInfo: AccountInfo = { ...info, ...{ name: "name", email: "email", + emailVerified: true, }, }; - const service = new FakeAccountService({ [userId]: fullInfo }); + + const fullActivity = { [userId]: new Date(), ...activity }; + + const service = new FakeAccountService({ [userId]: fullInfo }, fullActivity); service.activeAccountSubject.next({ id: userId, ...fullInfo }); return service; } @@ -26,17 +31,46 @@ export class FakeAccountService implements AccountService { accountsSubject = new ReplaySubject>(1); // eslint-disable-next-line rxjs/no-exposed-subjects -- test class activeAccountSubject = new ReplaySubject<{ id: UserId } & AccountInfo>(1); + // eslint-disable-next-line rxjs/no-exposed-subjects -- test class + accountActivitySubject = new ReplaySubject>(1); private _activeUserId: UserId; get activeUserId() { return this._activeUserId; } accounts$ = this.accountsSubject.asObservable(); activeAccount$ = this.activeAccountSubject.asObservable(); + accountActivity$ = this.accountActivitySubject.asObservable(); + get sortedUserIds$() { + return this.accountActivity$.pipe( + map((activity) => { + return Object.entries(activity) + .map(([userId, lastActive]: [UserId, Date]) => ({ userId, lastActive })) + .sort((a, b) => a.lastActive.getTime() - b.lastActive.getTime()) + .map((a) => a.userId); + }), + ); + } + get nextUpAccount$() { + return combineLatest([this.accounts$, this.activeAccount$, this.sortedUserIds$]).pipe( + map(([accounts, activeAccount, sortedUserIds]) => { + const nextId = sortedUserIds.find((id) => id !== activeAccount?.id && accounts[id] != null); + return nextId ? { id: nextId, ...accounts[nextId] } : null; + }), + ); + } - constructor(initialData: Record) { + constructor(initialData: Record, accountActivity?: Record) { this.accountsSubject.next(initialData); this.activeAccountSubject.subscribe((data) => (this._activeUserId = data?.id)); this.activeAccountSubject.next(null); + this.accountActivitySubject.next(accountActivity); + } + setAccountActivity(userId: UserId, lastActivity: Date): Promise { + this.accountActivitySubject.next({ + ...this.accountActivitySubject["_buffer"][0], + [userId]: lastActivity, + }); + return this.mock.setAccountActivity(userId, lastActivity); } async addAccount(userId: UserId, accountData: AccountInfo): Promise { @@ -53,10 +87,27 @@ export class FakeAccountService implements AccountService { await this.mock.setAccountEmail(userId, email); } + async setAccountEmailVerified(userId: UserId, emailVerified: boolean): Promise { + await this.mock.setAccountEmailVerified(userId, emailVerified); + } + async switchAccount(userId: UserId): Promise { const next = userId == null ? null : { id: userId, ...this.accountsSubject["_buffer"]?.[0]?.[userId] }; this.activeAccountSubject.next(next); await this.mock.switchAccount(userId); } + + async clean(userId: UserId): Promise { + const current = this.accountsSubject["_buffer"][0] ?? {}; + const updated = { ...current, [userId]: loggedOutInfo }; + this.accountsSubject.next(updated); + await this.mock.clean(userId); + } } + +const loggedOutInfo: AccountInfo = { + name: undefined, + email: "", + emailVerified: false, +}; diff --git a/libs/common/src/auth/abstractions/account.service.ts b/libs/common/src/auth/abstractions/account.service.ts index fa9ad36378d..b7fd6d9bb93 100644 --- a/libs/common/src/auth/abstractions/account.service.ts +++ b/libs/common/src/auth/abstractions/account.service.ts @@ -8,18 +8,44 @@ import { UserId } from "../../types/guid"; */ export type AccountInfo = { email: string; + emailVerified: boolean; name: string | undefined; }; export function accountInfoEqual(a: AccountInfo, b: AccountInfo) { - return a?.email === b?.email && a?.name === b?.name; + if (a == null && b == null) { + return true; + } + + if (a == null || b == null) { + return false; + } + + const keys = new Set([...Object.keys(a), ...Object.keys(b)]) as Set; + for (const key of keys) { + if (a[key] !== b[key]) { + return false; + } + } + return true; } export abstract class AccountService { accounts$: Observable>; activeAccount$: Observable<{ id: UserId | undefined } & AccountInfo>; + + /** + * Observable of the last activity time for each account. + */ + accountActivity$: Observable>; + /** Account list in order of descending recency */ + sortedUserIds$: Observable; + /** Next account that is not the current active account */ + nextUpAccount$: Observable<{ id: UserId } & AccountInfo>; /** * Updates the `accounts$` observable with the new account data. + * + * @note Also sets the last active date of the account to `now`. * @param userId * @param accountData */ @@ -36,11 +62,30 @@ export abstract class AccountService { * @param email */ abstract setAccountEmail(userId: UserId, email: string): Promise; + /** + * updates the `accounts$` observable with the new email verification status for the account. + * @param userId + * @param emailVerified + */ + abstract setAccountEmailVerified(userId: UserId, emailVerified: boolean): Promise; /** * Updates the `activeAccount$` observable with the new active account. * @param userId */ abstract switchAccount(userId: UserId): Promise; + /** + * Cleans personal information for the given account from the `accounts$` observable. Does not remove the userId from the observable. + * + * @note Also sets the last active date of the account to `null`. + * @param userId + */ + abstract clean(userId: UserId): Promise; + /** + * Updates the given user's last activity time. + * @param userId + * @param lastActivity + */ + abstract setAccountActivity(userId: UserId, lastActivity: Date): Promise; } export abstract class InternalAccountService extends AccountService { diff --git a/libs/common/src/auth/services/account.service.spec.ts b/libs/common/src/auth/services/account.service.spec.ts index a9cec82c511..0ae14b0cc12 100644 --- a/libs/common/src/auth/services/account.service.spec.ts +++ b/libs/common/src/auth/services/account.service.spec.ts @@ -1,3 +1,8 @@ +/** + * need to update test environment so structuredClone works appropriately + * @jest-environment ../../libs/shared/test.environment.ts + */ + import { MockProxy, mock } from "jest-mock-extended"; import { firstValueFrom } from "rxjs"; @@ -6,15 +11,57 @@ import { FakeGlobalStateProvider } from "../../../spec/fake-state-provider"; import { trackEmissions } from "../../../spec/utils"; import { LogService } from "../../platform/abstractions/log.service"; import { MessagingService } from "../../platform/abstractions/messaging.service"; +import { Utils } from "../../platform/misc/utils"; import { UserId } from "../../types/guid"; -import { AccountInfo } from "../abstractions/account.service"; +import { AccountInfo, accountInfoEqual } from "../abstractions/account.service"; import { ACCOUNT_ACCOUNTS, ACCOUNT_ACTIVE_ACCOUNT_ID, + ACCOUNT_ACTIVITY, AccountServiceImplementation, } from "./account.service"; +describe("accountInfoEqual", () => { + const accountInfo: AccountInfo = { name: "name", email: "email", emailVerified: true }; + + it("compares nulls", () => { + expect(accountInfoEqual(null, null)).toBe(true); + expect(accountInfoEqual(null, accountInfo)).toBe(false); + expect(accountInfoEqual(accountInfo, null)).toBe(false); + }); + + it("compares all keys, not just those defined in AccountInfo", () => { + const different = { ...accountInfo, extra: "extra" }; + + expect(accountInfoEqual(accountInfo, different)).toBe(false); + }); + + it("compares name", () => { + const same = { ...accountInfo }; + const different = { ...accountInfo, name: "name2" }; + + expect(accountInfoEqual(accountInfo, same)).toBe(true); + expect(accountInfoEqual(accountInfo, different)).toBe(false); + }); + + it("compares email", () => { + const same = { ...accountInfo }; + const different = { ...accountInfo, email: "email2" }; + + expect(accountInfoEqual(accountInfo, same)).toBe(true); + expect(accountInfoEqual(accountInfo, different)).toBe(false); + }); + + it("compares emailVerified", () => { + const same = { ...accountInfo }; + const different = { ...accountInfo, emailVerified: false }; + + expect(accountInfoEqual(accountInfo, same)).toBe(true); + expect(accountInfoEqual(accountInfo, different)).toBe(false); + }); +}); + describe("accountService", () => { let messagingService: MockProxy; let logService: MockProxy; @@ -22,8 +69,8 @@ describe("accountService", () => { let sut: AccountServiceImplementation; let accountsState: FakeGlobalState>; let activeAccountIdState: FakeGlobalState; - const userId = "userId" as UserId; - const userInfo = { email: "email", name: "name" }; + const userId = Utils.newGuid() as UserId; + const userInfo = { email: "email", name: "name", emailVerified: true }; beforeEach(() => { messagingService = mock(); @@ -86,6 +133,25 @@ describe("accountService", () => { expect(currentValue).toEqual({ [userId]: userInfo }); }); + + it("sets the last active date of the account to now", async () => { + const state = globalStateProvider.getFake(ACCOUNT_ACTIVITY); + state.stateSubject.next({}); + await sut.addAccount(userId, userInfo); + + expect(state.nextMock).toHaveBeenCalledWith({ [userId]: expect.any(Date) }); + }); + + it.each([null, undefined, 123, "not a guid"])( + "does not set last active if the userId is not a valid guid", + async (userId) => { + const state = globalStateProvider.getFake(ACCOUNT_ACTIVITY); + state.stateSubject.next({}); + await expect(sut.addAccount(userId as UserId, userInfo)).rejects.toThrow( + "userId is required", + ); + }, + ); }); describe("setAccountName", () => { @@ -134,6 +200,58 @@ describe("accountService", () => { }); }); + describe("setAccountEmailVerified", () => { + const initialState = { [userId]: userInfo }; + initialState[userId].emailVerified = false; + beforeEach(() => { + accountsState.stateSubject.next(initialState); + }); + + it("should update the account", async () => { + await sut.setAccountEmailVerified(userId, true); + const currentState = await firstValueFrom(accountsState.state$); + + expect(currentState).toEqual({ + [userId]: { ...userInfo, emailVerified: true }, + }); + }); + + it("should not update if the email is the same", async () => { + await sut.setAccountEmailVerified(userId, false); + const currentState = await firstValueFrom(accountsState.state$); + + expect(currentState).toEqual(initialState); + }); + }); + + describe("clean", () => { + beforeEach(() => { + accountsState.stateSubject.next({ [userId]: userInfo }); + }); + + it("removes account info of the given user", async () => { + await sut.clean(userId); + const currentState = await firstValueFrom(accountsState.state$); + + expect(currentState).toEqual({ + [userId]: { + email: "", + emailVerified: false, + name: undefined, + }, + }); + }); + + it("removes account activity of the given user", async () => { + const state = globalStateProvider.getFake(ACCOUNT_ACTIVITY); + state.stateSubject.next({ [userId]: new Date() }); + + await sut.clean(userId); + + expect(state.nextMock).toHaveBeenCalledWith({}); + }); + }); + describe("switchAccount", () => { beforeEach(() => { accountsState.stateSubject.next({ [userId]: userInfo }); @@ -152,4 +270,83 @@ describe("accountService", () => { expect(sut.switchAccount("unknown" as UserId)).rejects.toThrowError("Account does not exist"); }); }); + + describe("account activity", () => { + let state: FakeGlobalState>; + + beforeEach(() => { + state = globalStateProvider.getFake(ACCOUNT_ACTIVITY); + }); + describe("accountActivity$", () => { + it("returns the account activity state", async () => { + state.stateSubject.next({ + [toId("user1")]: new Date(1), + [toId("user2")]: new Date(2), + }); + + await expect(firstValueFrom(sut.accountActivity$)).resolves.toEqual({ + [toId("user1")]: new Date(1), + [toId("user2")]: new Date(2), + }); + }); + + it("returns an empty object when account activity is null", async () => { + state.stateSubject.next(null); + + await expect(firstValueFrom(sut.accountActivity$)).resolves.toEqual({}); + }); + }); + + describe("sortedUserIds$", () => { + it("returns the sorted user ids by date with most recent first", async () => { + state.stateSubject.next({ + [toId("user1")]: new Date(3), + [toId("user2")]: new Date(2), + [toId("user3")]: new Date(1), + }); + + await expect(firstValueFrom(sut.sortedUserIds$)).resolves.toEqual([ + "user1" as UserId, + "user2" as UserId, + "user3" as UserId, + ]); + }); + + it("returns an empty array when account activity is null", async () => { + state.stateSubject.next(null); + + await expect(firstValueFrom(sut.sortedUserIds$)).resolves.toEqual([]); + }); + }); + + describe("setAccountActivity", () => { + const userId = Utils.newGuid() as UserId; + it("sets the account activity", async () => { + await sut.setAccountActivity(userId, new Date(1)); + + expect(state.nextMock).toHaveBeenCalledWith({ [userId]: new Date(1) }); + }); + + it("does not update if the activity is the same", async () => { + state.stateSubject.next({ [userId]: new Date(1) }); + + await sut.setAccountActivity(userId, new Date(1)); + + expect(state.nextMock).not.toHaveBeenCalled(); + }); + + it.each([null, undefined, 123, "not a guid"])( + "does not set last active if the userId is not a valid guid", + async (userId) => { + await sut.setAccountActivity(userId as UserId, new Date(1)); + + expect(state.nextMock).not.toHaveBeenCalled(); + }, + ); + }); + }); }); + +function toId(userId: string) { + return userId as UserId; +} diff --git a/libs/common/src/auth/services/account.service.ts b/libs/common/src/auth/services/account.service.ts index 77d61fae913..6740387ded8 100644 --- a/libs/common/src/auth/services/account.service.ts +++ b/libs/common/src/auth/services/account.service.ts @@ -1,4 +1,4 @@ -import { Subject, combineLatestWith, map, distinctUntilChanged, shareReplay } from "rxjs"; +import { combineLatestWith, map, distinctUntilChanged, shareReplay, combineLatest } from "rxjs"; import { AccountInfo, @@ -7,8 +7,9 @@ import { } from "../../auth/abstractions/account.service"; import { LogService } from "../../platform/abstractions/log.service"; import { MessagingService } from "../../platform/abstractions/messaging.service"; +import { Utils } from "../../platform/misc/utils"; import { - ACCOUNT_MEMORY, + ACCOUNT_DISK, GlobalState, GlobalStateProvider, KeyDefinition, @@ -16,25 +17,36 @@ import { import { UserId } from "../../types/guid"; export const ACCOUNT_ACCOUNTS = KeyDefinition.record( - ACCOUNT_MEMORY, + ACCOUNT_DISK, "accounts", { deserializer: (accountInfo) => accountInfo, }, ); -export const ACCOUNT_ACTIVE_ACCOUNT_ID = new KeyDefinition(ACCOUNT_MEMORY, "activeAccountId", { +export const ACCOUNT_ACTIVE_ACCOUNT_ID = new KeyDefinition(ACCOUNT_DISK, "activeAccountId", { deserializer: (id: UserId) => id, }); +export const ACCOUNT_ACTIVITY = KeyDefinition.record(ACCOUNT_DISK, "activity", { + deserializer: (activity) => new Date(activity), +}); + +const LOGGED_OUT_INFO: AccountInfo = { + email: "", + emailVerified: false, + name: undefined, +}; + export class AccountServiceImplementation implements InternalAccountService { - private lock = new Subject(); - private logout = new Subject(); private accountsState: GlobalState>; private activeAccountIdState: GlobalState; accounts$; activeAccount$; + accountActivity$; + sortedUserIds$; + nextUpAccount$; constructor( private messagingService: MessagingService, @@ -53,14 +65,40 @@ export class AccountServiceImplementation implements InternalAccountService { distinctUntilChanged((a, b) => a?.id === b?.id && accountInfoEqual(a, b)), shareReplay({ bufferSize: 1, refCount: false }), ); + this.accountActivity$ = this.globalStateProvider + .get(ACCOUNT_ACTIVITY) + .state$.pipe(map((activity) => activity ?? {})); + this.sortedUserIds$ = this.accountActivity$.pipe( + map((activity) => { + return Object.entries(activity) + .map(([userId, lastActive]: [UserId, Date]) => ({ userId, lastActive })) + .sort((a, b) => b.lastActive.getTime() - a.lastActive.getTime()) // later dates first + .map((a) => a.userId); + }), + ); + this.nextUpAccount$ = combineLatest([ + this.accounts$, + this.activeAccount$, + this.sortedUserIds$, + ]).pipe( + map(([accounts, activeAccount, sortedUserIds]) => { + const nextId = sortedUserIds.find((id) => id !== activeAccount?.id && accounts[id] != null); + return nextId ? { id: nextId, ...accounts[nextId] } : null; + }), + ); } async addAccount(userId: UserId, accountData: AccountInfo): Promise { + if (!Utils.isGuid(userId)) { + throw new Error("userId is required"); + } + await this.accountsState.update((accounts) => { accounts ||= {}; accounts[userId] = accountData; return accounts; }); + await this.setAccountActivity(userId, new Date()); } async setAccountName(userId: UserId, name: string): Promise { @@ -71,6 +109,15 @@ export class AccountServiceImplementation implements InternalAccountService { await this.setAccountInfo(userId, { email }); } + async setAccountEmailVerified(userId: UserId, emailVerified: boolean): Promise { + await this.setAccountInfo(userId, { emailVerified }); + } + + async clean(userId: UserId) { + await this.setAccountInfo(userId, LOGGED_OUT_INFO); + await this.removeAccountActivity(userId); + } + async switchAccount(userId: UserId): Promise { await this.activeAccountIdState.update( (_, accounts) => { @@ -94,6 +141,37 @@ export class AccountServiceImplementation implements InternalAccountService { ); } + async setAccountActivity(userId: UserId, lastActivity: Date): Promise { + if (!Utils.isGuid(userId)) { + // only store for valid userIds + return; + } + + await this.globalStateProvider.get(ACCOUNT_ACTIVITY).update( + (activity) => { + activity ||= {}; + activity[userId] = lastActivity; + return activity; + }, + { + shouldUpdate: (oldActivity) => oldActivity?.[userId]?.getTime() !== lastActivity?.getTime(), + }, + ); + } + + async removeAccountActivity(userId: UserId): Promise { + await this.globalStateProvider.get(ACCOUNT_ACTIVITY).update( + (activity) => { + if (activity == null) { + return activity; + } + delete activity[userId]; + return activity; + }, + { shouldUpdate: (oldActivity) => oldActivity?.[userId] != null }, + ); + } + // TODO: update to use our own account status settings. Requires inverting direction of state service accounts flow async delete(): Promise { try { diff --git a/libs/common/src/auth/services/auth.service.spec.ts b/libs/common/src/auth/services/auth.service.spec.ts index 3bdf85d3e15..9a93a4207b7 100644 --- a/libs/common/src/auth/services/auth.service.spec.ts +++ b/libs/common/src/auth/services/auth.service.spec.ts @@ -56,6 +56,7 @@ describe("AuthService", () => { status: AuthenticationStatus.Unlocked, id: userId, email: "email", + emailVerified: false, name: "name", }; @@ -109,6 +110,7 @@ describe("AuthService", () => { status: AuthenticationStatus.Unlocked, id: Utils.newGuid() as UserId, email: "email2", + emailVerified: false, name: "name2", }; @@ -126,7 +128,11 @@ describe("AuthService", () => { it("requests auth status for all known users", async () => { const userId2 = Utils.newGuid() as UserId; - await accountService.addAccount(userId2, { email: "email2", name: "name2" }); + await accountService.addAccount(userId2, { + email: "email2", + emailVerified: false, + name: "name2", + }); const mockFn = jest.fn().mockReturnValue(of(AuthenticationStatus.Locked)); sut.authStatusFor$ = mockFn; @@ -147,11 +153,14 @@ describe("AuthService", () => { cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(undefined)); }); - it("emits LoggedOut when userId is null", async () => { - expect(await firstValueFrom(sut.authStatusFor$(null))).toEqual( - AuthenticationStatus.LoggedOut, - ); - }); + it.each([null, undefined, "not a userId"])( + "emits LoggedOut when userId is invalid (%s)", + async () => { + expect(await firstValueFrom(sut.authStatusFor$(null))).toEqual( + AuthenticationStatus.LoggedOut, + ); + }, + ); it("emits LoggedOut when there is no access token", async () => { tokenService.hasAccessToken$.mockReturnValue(of(false)); diff --git a/libs/common/src/auth/services/auth.service.ts b/libs/common/src/auth/services/auth.service.ts index c9e711b4cc5..a4529084a2a 100644 --- a/libs/common/src/auth/services/auth.service.ts +++ b/libs/common/src/auth/services/auth.service.ts @@ -2,6 +2,7 @@ import { Observable, combineLatest, distinctUntilChanged, + firstValueFrom, map, of, shareReplay, @@ -12,6 +13,7 @@ import { ApiService } from "../../abstractions/api.service"; import { CryptoService } from "../../platform/abstractions/crypto.service"; import { MessagingService } from "../../platform/abstractions/messaging.service"; import { StateService } from "../../platform/abstractions/state.service"; +import { Utils } from "../../platform/misc/utils"; import { UserId } from "../../types/guid"; import { AccountService } from "../abstractions/account.service"; import { AuthService as AuthServiceAbstraction } from "../abstractions/auth.service"; @@ -39,13 +41,16 @@ export class AuthService implements AuthServiceAbstraction { this.authStatuses$ = this.accountService.accounts$.pipe( map((accounts) => Object.keys(accounts) as UserId[]), - switchMap((entries) => - combineLatest( + switchMap((entries) => { + if (entries.length === 0) { + return of([] as { userId: UserId; status: AuthenticationStatus }[]); + } + return combineLatest( entries.map((userId) => this.authStatusFor$(userId).pipe(map((status) => ({ userId, status }))), ), - ), - ), + ); + }), map((statuses) => { return statuses.reduce( (acc, { userId, status }) => { @@ -59,7 +64,7 @@ export class AuthService implements AuthServiceAbstraction { } authStatusFor$(userId: UserId): Observable { - if (userId == null) { + if (!Utils.isGuid(userId)) { return of(AuthenticationStatus.LoggedOut); } @@ -84,17 +89,8 @@ export class AuthService implements AuthServiceAbstraction { } async getAuthStatus(userId?: string): Promise { - // If we don't have an access token or userId, we're logged out - const isAuthenticated = await this.stateService.getIsAuthenticated({ userId: userId }); - if (!isAuthenticated) { - return AuthenticationStatus.LoggedOut; - } - - // Note: since we aggresively set the auto user key to memory if it exists on app init (see InitService) - // we only need to check if the user key is in memory. - const hasUserKey = await this.cryptoService.hasUserKeyInMemory(userId as UserId); - - return hasUserKey ? AuthenticationStatus.Unlocked : AuthenticationStatus.Locked; + userId ??= await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); + return await firstValueFrom(this.authStatusFor$(userId as UserId)); } logOut(callback: () => void) { diff --git a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts index fc5060af5fd..19b29f05932 100644 --- a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts +++ b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts @@ -90,6 +90,7 @@ describe("PasswordResetEnrollmentServiceImplementation", () => { const user1AccountInfo: AccountInfo = { name: "Test User 1", email: "test1@email.com", + emailVerified: true, }; activeAccountSubject.next(Object.assign(user1AccountInfo, { id: "userId" as UserId })); diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index 13c33305d1f..5ca604b5260 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -25,11 +25,10 @@ export type InitOptions = { export abstract class StateService { accounts$: Observable<{ [userId: string]: T }>; - activeAccount$: Observable; addAccount: (account: T) => Promise; - setActiveUser: (userId: string) => Promise; - clean: (options?: StorageOptions) => Promise; + clearDecryptedData: (userId: UserId) => Promise; + clean: (options?: StorageOptions) => Promise; init: (initOptions?: InitOptions) => Promise; /** @@ -122,8 +121,6 @@ export abstract class StateService { setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise; getEmail: (options?: StorageOptions) => Promise; setEmail: (value: string, options?: StorageOptions) => Promise; - getEmailVerified: (options?: StorageOptions) => Promise; - setEmailVerified: (value: boolean, options?: StorageOptions) => Promise; getEnableBrowserIntegration: (options?: StorageOptions) => Promise; setEnableBrowserIntegration: (value: boolean, options?: StorageOptions) => Promise; getEnableBrowserIntegrationFingerprint: (options?: StorageOptions) => Promise; @@ -147,8 +144,6 @@ export abstract class StateService { */ setEncryptedPinProtected: (value: string, options?: StorageOptions) => Promise; getIsAuthenticated: (options?: StorageOptions) => Promise; - getLastActive: (options?: StorageOptions) => Promise; - setLastActive: (value: number, options?: StorageOptions) => Promise; getLastSync: (options?: StorageOptions) => Promise; setLastSync: (value: string, options?: StorageOptions) => Promise; getMinimizeOnCopyToClipboard: (options?: StorageOptions) => Promise; @@ -180,5 +175,4 @@ export abstract class StateService { setVaultTimeout: (value: number, options?: StorageOptions) => Promise; getVaultTimeoutAction: (options?: StorageOptions) => Promise; setVaultTimeoutAction: (value: string, options?: StorageOptions) => Promise; - nextUpActiveUser: () => Promise; } diff --git a/libs/common/src/platform/misc/utils.spec.ts b/libs/common/src/platform/misc/utils.spec.ts index a7a520a77c9..964a2a19413 100644 --- a/libs/common/src/platform/misc/utils.spec.ts +++ b/libs/common/src/platform/misc/utils.spec.ts @@ -3,6 +3,33 @@ import * as path from "path"; import { Utils } from "./utils"; describe("Utils Service", () => { + describe("isGuid", () => { + it("is false when null", () => { + expect(Utils.isGuid(null)).toBe(false); + }); + + it("is false when undefined", () => { + expect(Utils.isGuid(undefined)).toBe(false); + }); + + it("is false when empty", () => { + expect(Utils.isGuid("")).toBe(false); + }); + + it("is false when not a string", () => { + expect(Utils.isGuid(123 as any)).toBe(false); + }); + + it("is false when not a guid", () => { + expect(Utils.isGuid("not a guid")).toBe(false); + }); + + it("is true when a guid", () => { + // we use a limited guid scope in which all zeroes is invalid + expect(Utils.isGuid("00000000-0000-1000-8000-000000000000")).toBe(true); + }); + }); + describe("getDomain", () => { it("should fail for invalid urls", () => { expect(Utils.getDomain(null)).toBeNull(); diff --git a/libs/common/src/platform/models/domain/state.ts b/libs/common/src/platform/models/domain/state.ts index 95557e082a9..5dde49f99db 100644 --- a/libs/common/src/platform/models/domain/state.ts +++ b/libs/common/src/platform/models/domain/state.ts @@ -9,9 +9,6 @@ export class State< > { accounts: { [userId: string]: TAccount } = {}; globals: TGlobalState; - activeUserId: string; - authenticatedAccounts: string[] = []; - accountActivity: { [userId: string]: number } = {}; constructor(globals: TGlobalState) { this.globals = globals; diff --git a/libs/common/src/platform/services/default-environment.service.spec.ts b/libs/common/src/platform/services/default-environment.service.spec.ts index dd504dc3023..7d266e93fc3 100644 --- a/libs/common/src/platform/services/default-environment.service.spec.ts +++ b/libs/common/src/platform/services/default-environment.service.spec.ts @@ -31,10 +31,12 @@ describe("EnvironmentService", () => { [testUser]: { name: "name", email: "email", + emailVerified: false, }, [alternateTestUser]: { name: "name", email: "email", + emailVerified: false, }, }); stateProvider = new FakeStateProvider(accountService); @@ -47,6 +49,7 @@ describe("EnvironmentService", () => { id: userId, email: "test@example.com", name: `Test Name ${userId}`, + emailVerified: false, }); await awaitAsync(); }; diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index cab5768d2af..9479d647109 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -1,4 +1,4 @@ -import { BehaviorSubject } from "rxjs"; +import { BehaviorSubject, firstValueFrom, map } from "rxjs"; import { Jsonify, JsonValue } from "type-fest"; import { AccountService } from "../../auth/abstractions/account.service"; @@ -33,10 +33,7 @@ const keys = { state: "state", stateVersion: "stateVersion", global: "global", - authenticatedAccounts: "authenticatedAccounts", - activeUserId: "activeUserId", tempAccountSettings: "tempAccountSettings", // used to hold account specific settings (i.e clear clipboard) between initial migration and first account authentication - accountActivity: "accountActivity", }; const partialKeys = { @@ -58,9 +55,6 @@ export class StateService< protected accountsSubject = new BehaviorSubject<{ [userId: string]: TAccount }>({}); accounts$ = this.accountsSubject.asObservable(); - protected activeAccountSubject = new BehaviorSubject(null); - activeAccount$ = this.activeAccountSubject.asObservable(); - private hasBeenInited = false; protected isRecoveredSession = false; @@ -112,36 +106,16 @@ export class StateService< } // Get all likely authenticated accounts - const authenticatedAccounts = ( - (await this.storageService.get(keys.authenticatedAccounts)) ?? [] - ).filter((account) => account != null); + const authenticatedAccounts = await firstValueFrom( + this.accountService.accounts$.pipe(map((accounts) => Object.keys(accounts))), + ); await this.updateState(async (state) => { for (const i in authenticatedAccounts) { state = await this.syncAccountFromDisk(authenticatedAccounts[i]); } - // After all individual accounts have been added - state.authenticatedAccounts = authenticatedAccounts; - - const storedActiveUser = await this.storageService.get(keys.activeUserId); - if (storedActiveUser != null) { - state.activeUserId = storedActiveUser; - } await this.pushAccounts(); - this.activeAccountSubject.next(state.activeUserId); - // TODO: Temporary update to avoid routing all account status changes through account service for now. - // account service tracks logged out accounts, but State service does not, so we need to add the active account - // if it's not in the accounts list. - if (state.activeUserId != null && this.accountsSubject.value[state.activeUserId] == null) { - const activeDiskAccount = await this.getAccountFromDisk({ userId: state.activeUserId }); - await this.accountService.addAccount(state.activeUserId as UserId, { - name: activeDiskAccount.profile.name, - email: activeDiskAccount.profile.email, - }); - } - await this.accountService.switchAccount(state.activeUserId as UserId); - // End TODO return state; }); @@ -161,61 +135,25 @@ export class StateService< return state; }); - // TODO: Temporary update to avoid routing all account status changes through account service for now. - // The determination of state should be handled by the various services that control those values. - await this.accountService.addAccount(userId as UserId, { - name: diskAccount.profile.name, - email: diskAccount.profile.email, - }); - return state; } async addAccount(account: TAccount) { await this.environmentService.seedUserEnvironment(account.profile.userId as UserId); await this.updateState(async (state) => { - state.authenticatedAccounts.push(account.profile.userId); - await this.storageService.save(keys.authenticatedAccounts, state.authenticatedAccounts); state.accounts[account.profile.userId] = account; return state; }); await this.scaffoldNewAccountStorage(account); - await this.setLastActive(new Date().getTime(), { userId: account.profile.userId }); - // TODO: Temporary update to avoid routing all account status changes through account service for now. - await this.accountService.addAccount(account.profile.userId as UserId, { - name: account.profile.name, - email: account.profile.email, - }); - await this.setActiveUser(account.profile.userId); } - async setActiveUser(userId: string): Promise { - await this.clearDecryptedDataForActiveUser(); - await this.updateState(async (state) => { - state.activeUserId = userId; - await this.storageService.save(keys.activeUserId, userId); - this.activeAccountSubject.next(state.activeUserId); - // TODO: temporary update to avoid routing all account status changes through account service for now. - await this.accountService.switchAccount(userId as UserId); - - return state; - }); - - await this.pushAccounts(); - } - - async clean(options?: StorageOptions): Promise { + async clean(options?: StorageOptions): Promise { options = this.reconcileOptions(options, await this.defaultInMemoryOptions()); await this.deAuthenticateAccount(options.userId); - let currentUser = (await this.state())?.activeUserId; - if (options.userId === currentUser) { - currentUser = await this.dynamicallySetActiveUser(); - } await this.removeAccountFromDisk(options?.userId); await this.removeAccountFromMemory(options?.userId); await this.pushAccounts(); - return currentUser as UserId; } /** @@ -515,24 +453,6 @@ export class StateService< ); } - async getEmailVerified(options?: StorageOptions): Promise { - return ( - (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.profile.emailVerified ?? false - ); - } - - async setEmailVerified(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.profile.emailVerified = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - async getEnableBrowserIntegration(options?: StorageOptions): Promise { return ( (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) @@ -642,35 +562,6 @@ export class StateService< ); } - async getLastActive(options?: StorageOptions): Promise { - options = this.reconcileOptions(options, await this.defaultOnDiskOptions()); - - const accountActivity = await this.storageService.get<{ [userId: string]: number }>( - keys.accountActivity, - options, - ); - - if (accountActivity == null || Object.keys(accountActivity).length < 1) { - return null; - } - - return accountActivity[options.userId]; - } - - async setLastActive(value: number, options?: StorageOptions): Promise { - options = this.reconcileOptions(options, await this.defaultOnDiskOptions()); - if (options.userId == null) { - return; - } - const accountActivity = - (await this.storageService.get<{ [userId: string]: number }>( - keys.accountActivity, - options, - )) ?? {}; - accountActivity[options.userId] = value; - await this.storageService.save(keys.accountActivity, accountActivity, options); - } - async getLastSync(options?: StorageOptions): Promise { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())) @@ -910,24 +801,28 @@ export class StateService< } protected async getAccountFromMemory(options: StorageOptions): Promise { + const userId = + options.userId ?? + (await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + )); + return await this.state().then(async (state) => { if (state.accounts == null) { return null; } - return state.accounts[await this.getUserIdFromMemory(options)]; - }); - } - - protected async getUserIdFromMemory(options: StorageOptions): Promise { - return await this.state().then((state) => { - return options?.userId != null - ? state.accounts[options.userId]?.profile?.userId - : state.activeUserId; + return state.accounts[userId]; }); } protected async getAccountFromDisk(options: StorageOptions): Promise { - if (options?.userId == null && (await this.state())?.activeUserId == null) { + const userId = + options.userId ?? + (await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + )); + + if (userId == null) { return null; } @@ -1086,53 +981,76 @@ export class StateService< } protected async defaultInMemoryOptions(): Promise { + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + return { storageLocation: StorageLocation.Memory, - userId: (await this.state()).activeUserId, + userId, }; } protected async defaultOnDiskOptions(): Promise { + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + return { storageLocation: StorageLocation.Disk, htmlStorageLocation: HtmlStorageLocation.Session, - userId: (await this.state())?.activeUserId ?? (await this.getActiveUserIdFromStorage()), + userId, useSecureStorage: false, }; } protected async defaultOnDiskLocalOptions(): Promise { + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + return { storageLocation: StorageLocation.Disk, htmlStorageLocation: HtmlStorageLocation.Local, - userId: (await this.state())?.activeUserId ?? (await this.getActiveUserIdFromStorage()), + userId, useSecureStorage: false, }; } protected async defaultOnDiskMemoryOptions(): Promise { + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + return { storageLocation: StorageLocation.Disk, htmlStorageLocation: HtmlStorageLocation.Memory, - userId: (await this.state())?.activeUserId ?? (await this.getUserId()), + userId, useSecureStorage: false, }; } protected async defaultSecureStorageOptions(): Promise { + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + return { storageLocation: StorageLocation.Disk, useSecureStorage: true, - userId: (await this.state())?.activeUserId ?? (await this.getActiveUserIdFromStorage()), + userId, }; } protected async getActiveUserIdFromStorage(): Promise { - return await this.storageService.get(keys.activeUserId); + return await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); } protected async removeAccountFromLocalStorage(userId: string = null): Promise { - userId = userId ?? (await this.state())?.activeUserId; + userId ??= await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + const storedAccount = await this.getAccount( this.reconcileOptions({ userId: userId }, await this.defaultOnDiskLocalOptions()), ); @@ -1143,7 +1061,10 @@ export class StateService< } protected async removeAccountFromSessionStorage(userId: string = null): Promise { - userId = userId ?? (await this.state())?.activeUserId; + userId ??= await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + const storedAccount = await this.getAccount( this.reconcileOptions({ userId: userId }, await this.defaultOnDiskOptions()), ); @@ -1154,7 +1075,10 @@ export class StateService< } protected async removeAccountFromSecureStorage(userId: string = null): Promise { - userId = userId ?? (await this.state())?.activeUserId; + userId ??= await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + await this.setUserKeyAutoUnlock(null, { userId: userId }); await this.setUserKeyBiometric(null, { userId: userId }); await this.setCryptoMasterKeyAuto(null, { userId: userId }); @@ -1163,8 +1087,11 @@ export class StateService< } protected async removeAccountFromMemory(userId: string = null): Promise { + userId ??= await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + await this.updateState(async (state) => { - userId = userId ?? state.activeUserId; delete state.accounts[userId]; return state; }); @@ -1178,15 +1105,16 @@ export class StateService< return Object.assign(this.createAccount(), persistentAccountInformation); } - protected async clearDecryptedDataForActiveUser(): Promise { + async clearDecryptedData(userId: UserId): Promise { await this.updateState(async (state) => { - const userId = state?.activeUserId; if (userId != null && state?.accounts[userId]?.data != null) { state.accounts[userId].data = new AccountData(); } return state; }); + + await this.pushAccounts(); } protected createAccount(init: Partial = null): TAccount { @@ -1201,14 +1129,6 @@ export class StateService< // We must have a manual call to clear tokens as we can't leverage state provider to clean // up our data as we have secure storage in the mix. await this.tokenService.clearTokens(userId as UserId); - await this.setLastActive(null, { userId: userId }); - await this.updateState(async (state) => { - state.authenticatedAccounts = state.authenticatedAccounts.filter((id) => id !== userId); - - await this.storageService.save(keys.authenticatedAccounts, state.authenticatedAccounts); - - return state; - }); } protected async removeAccountFromDisk(userId: string) { @@ -1217,32 +1137,6 @@ export class StateService< await this.removeAccountFromSecureStorage(userId); } - async nextUpActiveUser() { - const accounts = (await this.state())?.accounts; - if (accounts == null || Object.keys(accounts).length < 1) { - return null; - } - - let newActiveUser; - for (const userId in accounts) { - if (userId == null) { - continue; - } - if (await this.getIsAuthenticated({ userId: userId })) { - newActiveUser = userId; - break; - } - newActiveUser = null; - } - return newActiveUser as UserId; - } - - protected async dynamicallySetActiveUser() { - const newActiveUser = await this.nextUpActiveUser(); - await this.setActiveUser(newActiveUser); - return newActiveUser; - } - protected async saveSecureStorageKey( key: string, value: T, diff --git a/libs/common/src/platform/services/system.service.ts b/libs/common/src/platform/services/system.service.ts index d19390c45e0..80053673d8a 100644 --- a/libs/common/src/platform/services/system.service.ts +++ b/libs/common/src/platform/services/system.service.ts @@ -1,10 +1,12 @@ -import { firstValueFrom, timeout } from "rxjs"; +import { firstValueFrom, map, timeout } from "rxjs"; import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; +import { AccountService } from "../../auth/abstractions/account.service"; import { AuthService } from "../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { AutofillSettingsServiceAbstraction } from "../../autofill/services/autofill-settings.service"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; +import { UserId } from "../../types/guid"; import { MessagingService } from "../abstractions/messaging.service"; import { PlatformUtilsService } from "../abstractions/platform-utils.service"; import { StateService } from "../abstractions/state.service"; @@ -25,15 +27,18 @@ export class SystemService implements SystemServiceAbstraction { private autofillSettingsService: AutofillSettingsServiceAbstraction, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, private biometricStateService: BiometricStateService, + private accountService: AccountService, ) {} async startProcessReload(authService: AuthService): Promise { - const accounts = await firstValueFrom(this.stateService.accounts$); + const accounts = await firstValueFrom(this.accountService.accounts$); if (accounts != null) { const keys = Object.keys(accounts); if (keys.length > 0) { for (const userId of keys) { - if ((await authService.getAuthStatus(userId)) === AuthenticationStatus.Unlocked) { + let status = await firstValueFrom(authService.authStatusFor$(userId as UserId)); + status = await authService.getAuthStatus(userId); + if (status === AuthenticationStatus.Unlocked) { return; } } @@ -63,15 +68,24 @@ export class SystemService implements SystemServiceAbstraction { clearInterval(this.reloadInterval); this.reloadInterval = null; - const currentUser = await firstValueFrom(this.stateService.activeAccount$.pipe(timeout(500))); + const currentUser = await firstValueFrom( + this.accountService.activeAccount$.pipe( + map((a) => a?.id), + timeout(500), + ), + ); // Replace current active user if they will be logged out on reload if (currentUser != null) { const timeoutAction = await firstValueFrom( this.vaultTimeoutSettingsService.vaultTimeoutAction$().pipe(timeout(500)), ); if (timeoutAction === VaultTimeoutAction.LogOut) { - const nextUser = await this.stateService.nextUpActiveUser(); - await this.stateService.setActiveUser(nextUser); + const nextUser = await firstValueFrom( + this.accountService.nextUpAccount$.pipe(map((account) => account?.id ?? null)), + ); + // Can be removed once we migrate password generation history to state providers + await this.stateService.clearDecryptedData(currentUser); + await this.accountService.switchAccount(nextUser); } } diff --git a/libs/common/src/platform/state/implementations/default-active-user-state.provider.spec.ts b/libs/common/src/platform/state/implementations/default-active-user-state.provider.spec.ts index c1cc15a176f..681963f8233 100644 --- a/libs/common/src/platform/state/implementations/default-active-user-state.provider.spec.ts +++ b/libs/common/src/platform/state/implementations/default-active-user-state.provider.spec.ts @@ -1,7 +1,6 @@ import { mock } from "jest-mock-extended"; import { mockAccountServiceWith, trackEmissions } from "../../../../spec"; -import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; import { UserId } from "../../../types/guid"; import { SingleUserStateProvider } from "../user-state.provider"; @@ -14,7 +13,7 @@ describe("DefaultActiveUserStateProvider", () => { id: userId, name: "name", email: "email", - status: AuthenticationStatus.Locked, + emailVerified: false, }; const accountService = mockAccountServiceWith(userId, accountInfo); let sut: DefaultActiveUserStateProvider; diff --git a/libs/common/src/platform/state/implementations/default-active-user-state.spec.ts b/libs/common/src/platform/state/implementations/default-active-user-state.spec.ts index 51a972a9dc6..c652136a0d1 100644 --- a/libs/common/src/platform/state/implementations/default-active-user-state.spec.ts +++ b/libs/common/src/platform/state/implementations/default-active-user-state.spec.ts @@ -82,6 +82,7 @@ describe("DefaultActiveUserState", () => { activeAccountSubject.next({ id: userId, email: `test${id}@example.com`, + emailVerified: false, name: `Test User ${id}`, }); await awaitAsync(); diff --git a/libs/common/src/platform/state/implementations/default-state.provider.spec.ts b/libs/common/src/platform/state/implementations/default-state.provider.spec.ts index 3243b53d670..98d423cf484 100644 --- a/libs/common/src/platform/state/implementations/default-state.provider.spec.ts +++ b/libs/common/src/platform/state/implementations/default-state.provider.spec.ts @@ -69,7 +69,12 @@ describe("DefaultStateProvider", () => { userId?: UserId, ) => Observable, ) => { - const accountInfo = { email: "email", name: "name", status: AuthenticationStatus.LoggedOut }; + const accountInfo = { + email: "email", + emailVerified: false, + name: "name", + status: AuthenticationStatus.LoggedOut, + }; const keyDefinition = new KeyDefinition(new StateDefinition("test", "disk"), "test", { deserializer: (s) => s, }); @@ -114,7 +119,12 @@ describe("DefaultStateProvider", () => { ); describe("getUserState$", () => { - const accountInfo = { email: "email", name: "name", status: AuthenticationStatus.LoggedOut }; + const accountInfo = { + email: "email", + emailVerified: false, + name: "name", + status: AuthenticationStatus.LoggedOut, + }; const keyDefinition = new KeyDefinition(new StateDefinition("test", "disk"), "test", { deserializer: (s) => s, }); diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index ee5005202fa..6b309ecfb94 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -38,6 +38,7 @@ export const BILLING_DISK = new StateDefinition("billing", "disk"); export const KDF_CONFIG_DISK = new StateDefinition("kdfConfig", "disk"); export const KEY_CONNECTOR_DISK = new StateDefinition("keyConnector", "disk"); export const ACCOUNT_MEMORY = new StateDefinition("account", "memory"); +export const ACCOUNT_DISK = new StateDefinition("account", "disk"); export const MASTER_PASSWORD_MEMORY = new StateDefinition("masterPassword", "memory"); export const MASTER_PASSWORD_DISK = new StateDefinition("masterPassword", "disk"); export const TWO_FACTOR_MEMORY = new StateDefinition("twoFactor", "memory"); diff --git a/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts b/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts index 5344093a25a..12c24dcdef3 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts @@ -1,9 +1,10 @@ import { MockProxy, any, mock } from "jest-mock-extended"; -import { BehaviorSubject } from "rxjs"; +import { BehaviorSubject, of } from "rxjs"; import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; import { SearchService } from "../../abstractions/search.service"; import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; +import { AccountInfo } from "../../auth/abstractions/account.service"; import { AuthService } from "../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { FakeMasterPasswordService } from "../../auth/services/master-password/fake-master-password.service"; @@ -13,7 +14,6 @@ import { MessagingService } from "../../platform/abstractions/messaging.service" import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service"; import { StateService } from "../../platform/abstractions/state.service"; import { Utils } from "../../platform/misc/utils"; -import { Account } from "../../platform/models/domain/account"; import { StateEventRunnerService } from "../../platform/state"; import { UserId } from "../../types/guid"; import { CipherService } from "../../vault/abstractions/cipher.service"; @@ -39,7 +39,6 @@ describe("VaultTimeoutService", () => { let lockedCallback: jest.Mock, [userId: string]>; let loggedOutCallback: jest.Mock, [expired: boolean, userId?: string]>; - let accountsSubject: BehaviorSubject>; let vaultTimeoutActionSubject: BehaviorSubject; let availableVaultTimeoutActionsSubject: BehaviorSubject; @@ -65,10 +64,6 @@ describe("VaultTimeoutService", () => { lockedCallback = jest.fn(); loggedOutCallback = jest.fn(); - accountsSubject = new BehaviorSubject(null); - - stateService.accounts$ = accountsSubject; - vaultTimeoutActionSubject = new BehaviorSubject(VaultTimeoutAction.Lock); vaultTimeoutSettingsService.vaultTimeoutAction$.mockReturnValue(vaultTimeoutActionSubject); @@ -127,21 +122,39 @@ describe("VaultTimeoutService", () => { return Promise.resolve(accounts[userId]?.vaultTimeout); }); - stateService.getLastActive.mockImplementation((options) => { - return Promise.resolve(accounts[options.userId]?.lastActive); - }); - stateService.getUserId.mockResolvedValue(globalSetups?.userId); - stateService.activeAccount$ = new BehaviorSubject(globalSetups?.userId); - + // Set desired user active and known users on accounts service : note the only thing that matters here is that the ID are set if (globalSetups?.userId) { accountService.activeAccountSubject.next({ id: globalSetups.userId as UserId, email: null, + emailVerified: false, name: null, }); } + accountService.accounts$ = of( + Object.entries(accounts).reduce( + (agg, [id]) => { + agg[id] = { + email: "", + emailVerified: true, + name: "", + }; + return agg; + }, + {} as Record, + ), + ); + accountService.accountActivity$ = of( + Object.entries(accounts).reduce( + (agg, [id, info]) => { + agg[id] = info.lastActive ? new Date(info.lastActive) : null; + return agg; + }, + {} as Record, + ), + ); platformUtilsService.isViewOpen.mockResolvedValue(globalSetups?.isViewOpen ?? false); @@ -158,16 +171,6 @@ describe("VaultTimeoutService", () => { ], ); }); - - const accountsSubjectValue: Record = Object.keys(accounts).reduce( - (agg, key) => { - const newPartial: Record = {}; - newPartial[key] = null; // No values actually matter on this other than the key - return Object.assign(agg, newPartial); - }, - {} as Record, - ); - accountsSubject.next(accountsSubjectValue); }; const expectUserToHaveLocked = (userId: string) => { diff --git a/libs/common/src/services/vault-timeout/vault-timeout.service.ts b/libs/common/src/services/vault-timeout/vault-timeout.service.ts index 8baf6c04c49..8e0978d07d0 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.ts @@ -1,4 +1,4 @@ -import { firstValueFrom, timeout } from "rxjs"; +import { combineLatest, firstValueFrom, switchMap } from "rxjs"; import { SearchService } from "../../abstractions/search.service"; import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; @@ -64,14 +64,25 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { // Get whether or not the view is open a single time so it can be compared for each user const isViewOpen = await this.platformUtilsService.isViewOpen(); - const activeUserId = await firstValueFrom(this.stateService.activeAccount$.pipe(timeout(500))); - - const accounts = await firstValueFrom(this.stateService.accounts$); - for (const userId in accounts) { - if (userId != null && (await this.shouldLock(userId, activeUserId, isViewOpen))) { - await this.executeTimeoutAction(userId); - } - } + await firstValueFrom( + combineLatest([ + this.accountService.activeAccount$, + this.accountService.accountActivity$, + ]).pipe( + switchMap(async ([activeAccount, accountActivity]) => { + const activeUserId = activeAccount?.id; + for (const userIdString in accountActivity) { + const userId = userIdString as UserId; + if ( + userId != null && + (await this.shouldLock(userId, accountActivity[userId], activeUserId, isViewOpen)) + ) { + await this.executeTimeoutAction(userId); + } + } + }), + ), + ); } async lock(userId?: string): Promise { @@ -123,6 +134,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { private async shouldLock( userId: string, + lastActive: Date, activeUserId: string, isViewOpen: boolean, ): Promise { @@ -146,13 +158,12 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { return false; } - const lastActive = await this.stateService.getLastActive({ userId: userId }); if (lastActive == null) { return false; } const vaultTimeoutSeconds = vaultTimeout * 60; - const diffSeconds = (new Date().getTime() - lastActive) / 1000; + const diffSeconds = (new Date().getTime() - lastActive.getTime()) / 1000; return diffSeconds >= vaultTimeoutSeconds; } diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index 31bc5460b43..0a1f4b1d110 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -57,13 +57,14 @@ import { CipherServiceMigrator } from "./migrations/57-move-cipher-service-to-st import { RemoveRefreshTokenMigratedFlagMigrator } from "./migrations/58-remove-refresh-token-migrated-state-provider-flag"; import { KdfConfigMigrator } from "./migrations/59-move-kdf-config-to-state-provider"; import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key"; +import { KnownAccountsMigrator } from "./migrations/60-known-accounts"; import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account"; import { MoveStateVersionMigrator } from "./migrations/8-move-state-version"; import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-settings-to-global"; import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 59; +export const CURRENT_VERSION = 60; export type MinVersion = typeof MIN_VERSION; export function createMigrationBuilder() { @@ -124,7 +125,8 @@ export function createMigrationBuilder() { .with(AuthRequestMigrator, 55, 56) .with(CipherServiceMigrator, 56, 57) .with(RemoveRefreshTokenMigratedFlagMigrator, 57, 58) - .with(KdfConfigMigrator, 58, CURRENT_VERSION); + .with(KdfConfigMigrator, 58, 59) + .with(KnownAccountsMigrator, 59, CURRENT_VERSION); } export async function currentVersion( diff --git a/libs/common/src/state-migrations/migration-helper.spec.ts b/libs/common/src/state-migrations/migration-helper.spec.ts index 5f366f25972..162fac2fabc 100644 --- a/libs/common/src/state-migrations/migration-helper.spec.ts +++ b/libs/common/src/state-migrations/migration-helper.spec.ts @@ -27,6 +27,14 @@ const exampleJSON = { }, global_serviceName_key: "global_serviceName_key", user_userId_serviceName_key: "user_userId_serviceName_key", + global_account_accounts: { + "c493ed01-4e08-4e88-abc7-332f380ca760": { + otherStuff: "otherStuff3", + }, + "23e61a5f-2ece-4f5e-b499-f0bc489482a9": { + otherStuff: "otherStuff4", + }, + }, }; describe("RemoveLegacyEtmKeyMigrator", () => { @@ -81,6 +89,41 @@ describe("RemoveLegacyEtmKeyMigrator", () => { const accounts = await sut.getAccounts(); expect(accounts).toEqual([]); }); + + it("handles global scoped known accounts for version 60 and after", async () => { + sut.currentVersion = 60; + const accounts = await sut.getAccounts(); + expect(accounts).toEqual([ + // Note, still gets values stored in state service objects, just grabs user ids from global + { + userId: "c493ed01-4e08-4e88-abc7-332f380ca760", + account: { otherStuff: "otherStuff1" }, + }, + { + userId: "23e61a5f-2ece-4f5e-b499-f0bc489482a9", + account: { otherStuff: "otherStuff2" }, + }, + ]); + }); + }); + + describe("getKnownUserIds", () => { + it("returns all user ids", async () => { + const userIds = await sut.getKnownUserIds(); + expect(userIds).toEqual([ + "c493ed01-4e08-4e88-abc7-332f380ca760", + "23e61a5f-2ece-4f5e-b499-f0bc489482a9", + ]); + }); + + it("returns all user ids when version is 60 or greater", async () => { + sut.currentVersion = 60; + const userIds = await sut.getKnownUserIds(); + expect(userIds).toEqual([ + "c493ed01-4e08-4e88-abc7-332f380ca760", + "23e61a5f-2ece-4f5e-b499-f0bc489482a9", + ]); + }); }); describe("getFromGlobal", () => { diff --git a/libs/common/src/state-migrations/migration-helper.ts b/libs/common/src/state-migrations/migration-helper.ts index 2505e2b264a..5d1de8dd49e 100644 --- a/libs/common/src/state-migrations/migration-helper.ts +++ b/libs/common/src/state-migrations/migration-helper.ts @@ -162,7 +162,7 @@ export class MigrationHelper { async getAccounts(): Promise< { userId: string; account: ExpectedAccountType }[] > { - const userIds = (await this.get("authenticatedAccounts")) ?? []; + const userIds = await this.getKnownUserIds(); return Promise.all( userIds.map(async (userId) => ({ userId, @@ -171,6 +171,17 @@ export class MigrationHelper { ); } + /** + * Helper method to read known users ids. + */ + async getKnownUserIds(): Promise { + if (this.currentVersion < 61) { + return knownAccountUserIdsBuilderPre61(this.storageService); + } else { + return knownAccountUserIdsBuilder(this.storageService); + } + } + /** * Builds a user storage key appropriate for the current version. * @@ -233,3 +244,18 @@ function globalKeyBuilder(keyDefinition: KeyDefinitionLike): string { function globalKeyBuilderPre9(): string { throw Error("No key builder should be used for versions prior to 9."); } + +async function knownAccountUserIdsBuilderPre61( + storageService: AbstractStorageService, +): Promise { + return (await storageService.get("authenticatedAccounts")) ?? []; +} + +async function knownAccountUserIdsBuilder( + storageService: AbstractStorageService, +): Promise { + const accounts = await storageService.get>( + globalKeyBuilder({ stateDefinition: { name: "account" }, key: "accounts" }), + ); + return Object.keys(accounts ?? {}); +} diff --git a/libs/common/src/state-migrations/migrations/60-known-accounts.spec.ts b/libs/common/src/state-migrations/migrations/60-known-accounts.spec.ts new file mode 100644 index 00000000000..28dedb3c390 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/60-known-accounts.spec.ts @@ -0,0 +1,145 @@ +import { MockProxy } from "jest-mock-extended"; + +import { MigrationHelper } from "../migration-helper"; +import { mockMigrationHelper } from "../migration-helper.spec"; + +import { + ACCOUNT_ACCOUNTS, + ACCOUNT_ACTIVE_ACCOUNT_ID, + ACCOUNT_ACTIVITY, + KnownAccountsMigrator, +} from "./60-known-accounts"; + +const migrateJson = () => { + return { + authenticatedAccounts: ["user1", "user2"], + activeUserId: "user1", + user1: { + profile: { + email: "user1", + name: "User 1", + emailVerified: true, + }, + }, + user2: { + profile: { + email: "", + emailVerified: false, + }, + }, + accountActivity: { + user1: 1609459200000, // 2021-01-01 + user2: 1609545600000, // 2021-01-02 + }, + }; +}; + +const rollbackJson = () => { + return { + user1: { + profile: { + email: "user1", + name: "User 1", + emailVerified: true, + }, + }, + user2: { + profile: { + email: "", + emailVerified: false, + }, + }, + global_account_accounts: { + user1: { + profile: { + email: "user1", + name: "User 1", + emailVerified: true, + }, + }, + user2: { + profile: { + email: "", + emailVerified: false, + }, + }, + }, + global_account_activeAccountId: "user1", + global_account_activity: { + user1: "2021-01-01T00:00:00.000Z", + user2: "2021-01-02T00:00:00.000Z", + }, + }; +}; + +describe("ReplicateKnownAccounts", () => { + let helper: MockProxy; + let sut: KnownAccountsMigrator; + + describe("migrate", () => { + beforeEach(() => { + helper = mockMigrationHelper(migrateJson(), 59); + sut = new KnownAccountsMigrator(59, 60); + }); + + it("migrates accounts", async () => { + await sut.migrate(helper); + expect(helper.setToGlobal).toHaveBeenCalledWith(ACCOUNT_ACCOUNTS, { + user1: { + email: "user1", + name: "User 1", + emailVerified: true, + }, + user2: { + email: "", + emailVerified: false, + name: undefined, + }, + }); + expect(helper.remove).toHaveBeenCalledWith("authenticatedAccounts"); + }); + + it("migrates active account it", async () => { + await sut.migrate(helper); + expect(helper.setToGlobal).toHaveBeenCalledWith(ACCOUNT_ACTIVE_ACCOUNT_ID, "user1"); + expect(helper.remove).toHaveBeenCalledWith("activeUserId"); + }); + + it("migrates account activity", async () => { + await sut.migrate(helper); + expect(helper.setToGlobal).toHaveBeenCalledWith(ACCOUNT_ACTIVITY, { + user1: '"2021-01-01T00:00:00.000Z"', + user2: '"2021-01-02T00:00:00.000Z"', + }); + expect(helper.remove).toHaveBeenCalledWith("accountActivity"); + }); + }); + + describe("rollback", () => { + beforeEach(() => { + helper = mockMigrationHelper(rollbackJson(), 60); + sut = new KnownAccountsMigrator(59, 60); + }); + + it("rolls back authenticated accounts", async () => { + await sut.rollback(helper); + expect(helper.set).toHaveBeenCalledWith("authenticatedAccounts", ["user1", "user2"]); + expect(helper.removeFromGlobal).toHaveBeenCalledWith(ACCOUNT_ACCOUNTS); + }); + + it("rolls back active account id", async () => { + await sut.rollback(helper); + expect(helper.set).toHaveBeenCalledWith("activeUserId", "user1"); + expect(helper.removeFromGlobal).toHaveBeenCalledWith(ACCOUNT_ACTIVE_ACCOUNT_ID); + }); + + it("rolls back account activity", async () => { + await sut.rollback(helper); + expect(helper.set).toHaveBeenCalledWith("accountActivity", { + user1: 1609459200000, + user2: 1609545600000, + }); + expect(helper.removeFromGlobal).toHaveBeenCalledWith(ACCOUNT_ACTIVITY); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/60-known-accounts.ts b/libs/common/src/state-migrations/migrations/60-known-accounts.ts new file mode 100644 index 00000000000..75117da5b47 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/60-known-accounts.ts @@ -0,0 +1,111 @@ +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { Migrator } from "../migrator"; + +export const ACCOUNT_ACCOUNTS: KeyDefinitionLike = { + stateDefinition: { + name: "account", + }, + key: "accounts", +}; + +export const ACCOUNT_ACTIVE_ACCOUNT_ID: KeyDefinitionLike = { + stateDefinition: { + name: "account", + }, + key: "activeAccountId", +}; + +export const ACCOUNT_ACTIVITY: KeyDefinitionLike = { + stateDefinition: { + name: "account", + }, + key: "activity", +}; + +type ExpectedAccountType = { + profile?: { + email?: string; + name?: string; + emailVerified?: boolean; + }; +}; + +export class KnownAccountsMigrator extends Migrator<59, 60> { + async migrate(helper: MigrationHelper): Promise { + await this.migrateAuthenticatedAccounts(helper); + await this.migrateActiveAccountId(helper); + await this.migrateAccountActivity(helper); + } + async rollback(helper: MigrationHelper): Promise { + // authenticated account are removed, but the accounts record also contains logged out accounts. Best we can do is to add them all back + const accounts = (await helper.getFromGlobal>(ACCOUNT_ACCOUNTS)) ?? {}; + await helper.set("authenticatedAccounts", Object.keys(accounts)); + await helper.removeFromGlobal(ACCOUNT_ACCOUNTS); + + // Active Account Id + const activeAccountId = await helper.getFromGlobal(ACCOUNT_ACTIVE_ACCOUNT_ID); + if (activeAccountId) { + await helper.set("activeUserId", activeAccountId); + } + await helper.removeFromGlobal(ACCOUNT_ACTIVE_ACCOUNT_ID); + + // Account Activity + const accountActivity = await helper.getFromGlobal>(ACCOUNT_ACTIVITY); + if (accountActivity) { + const toStore = Object.entries(accountActivity).reduce( + (agg, [userId, dateString]) => { + agg[userId] = new Date(dateString).getTime(); + return agg; + }, + {} as Record, + ); + await helper.set("accountActivity", toStore); + } + await helper.removeFromGlobal(ACCOUNT_ACTIVITY); + } + + private async migrateAuthenticatedAccounts(helper: MigrationHelper) { + const authenticatedAccounts = (await helper.get("authenticatedAccounts")) ?? []; + const accounts = await Promise.all( + authenticatedAccounts.map(async (userId) => { + const account = await helper.get(userId); + return { userId, account }; + }), + ); + const accountsToStore = accounts.reduce( + (agg, { userId, account }) => { + if (account?.profile) { + agg[userId] = { + email: account.profile.email ?? "", + emailVerified: account.profile.emailVerified ?? false, + name: account.profile.name, + }; + } + return agg; + }, + {} as Record, + ); + + await helper.setToGlobal(ACCOUNT_ACCOUNTS, accountsToStore); + await helper.remove("authenticatedAccounts"); + } + + private async migrateAccountActivity(helper: MigrationHelper) { + const stored = await helper.get>("accountActivity"); + const accountActivity = Object.entries(stored ?? {}).reduce( + (agg, [userId, dateMs]) => { + agg[userId] = JSON.stringify(new Date(dateMs)); + return agg; + }, + {} as Record, + ); + await helper.setToGlobal(ACCOUNT_ACTIVITY, accountActivity); + await helper.remove("accountActivity"); + } + + private async migrateActiveAccountId(helper: MigrationHelper) { + const activeAccountId = await helper.get("activeUserId"); + await helper.setToGlobal(ACCOUNT_ACTIVE_ACCOUNT_ID, activeAccountId); + await helper.remove("activeUserId"); + } +} diff --git a/libs/common/src/tools/send/services/send.service.spec.ts b/libs/common/src/tools/send/services/send.service.spec.ts index 41183c42af0..2f0f50c6168 100644 --- a/libs/common/src/tools/send/services/send.service.spec.ts +++ b/libs/common/src/tools/send/services/send.service.spec.ts @@ -62,6 +62,7 @@ describe("SendService", () => { accountService.activeAccountSubject.next({ id: mockUserId, email: "email", + emailVerified: false, name: "name", }); diff --git a/libs/common/src/vault/services/sync/sync.service.ts b/libs/common/src/vault/services/sync/sync.service.ts index 73869ff488e..995ab7319b2 100644 --- a/libs/common/src/vault/services/sync/sync.service.ts +++ b/libs/common/src/vault/services/sync/sync.service.ts @@ -326,7 +326,10 @@ export class SyncService implements SyncServiceAbstraction { await this.cryptoService.setOrgKeys(response.organizations, response.providerOrganizations); await this.avatarService.setSyncAvatarColor(response.id as UserId, response.avatarColor); await this.tokenService.setSecurityStamp(response.securityStamp, response.id as UserId); - await this.stateService.setEmailVerified(response.emailVerified); + await this.accountService.setAccountEmailVerified( + response.id as UserId, + response.emailVerified, + ); await this.billingAccountProfileStateService.setHasPremium( response.premiumPersonally, From e7416384dcb1b1b242a4672366bf5a6e81d1ee09 Mon Sep 17 00:00:00 2001 From: Will Martin Date: Tue, 30 Apr 2024 10:27:47 -0400 Subject: [PATCH 104/110] [CL-220] item components (#8870) --- .../popup/layout/popup-layout.stories.ts | 41 ++- .../src/a11y/a11y-cell.directive.ts | 33 ++ .../src/a11y/a11y-grid.directive.ts | 145 ++++++++ .../components/src/a11y/a11y-row.directive.ts | 31 ++ libs/components/src/badge/badge.directive.ts | 9 +- .../src/icon-button/icon-button.component.ts | 16 +- libs/components/src/index.ts | 1 + .../src/input/autofocus.directive.ts | 9 +- libs/components/src/item/index.ts | 1 + .../src/item/item-action.component.ts | 12 + .../src/item/item-content.component.html | 16 + .../src/item/item-content.component.ts | 15 + .../src/item/item-group.component.ts | 13 + libs/components/src/item/item.component.html | 21 ++ libs/components/src/item/item.component.ts | 29 ++ libs/components/src/item/item.mdx | 141 ++++++++ libs/components/src/item/item.module.ts | 12 + libs/components/src/item/item.stories.ts | 326 ++++++++++++++++++ .../components/src/search/search.component.ts | 6 +- .../src/shared/focusable-element.ts | 8 + libs/components/src/styles.scss | 2 +- 21 files changed, 858 insertions(+), 29 deletions(-) create mode 100644 libs/components/src/a11y/a11y-cell.directive.ts create mode 100644 libs/components/src/a11y/a11y-grid.directive.ts create mode 100644 libs/components/src/a11y/a11y-row.directive.ts create mode 100644 libs/components/src/item/index.ts create mode 100644 libs/components/src/item/item-action.component.ts create mode 100644 libs/components/src/item/item-content.component.html create mode 100644 libs/components/src/item/item-content.component.ts create mode 100644 libs/components/src/item/item-group.component.ts create mode 100644 libs/components/src/item/item.component.html create mode 100644 libs/components/src/item/item.component.ts create mode 100644 libs/components/src/item/item.mdx create mode 100644 libs/components/src/item/item.module.ts create mode 100644 libs/components/src/item/item.stories.ts create mode 100644 libs/components/src/shared/focusable-element.ts diff --git a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts index 1b10e50c0c2..77530d06e5a 100644 --- a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts +++ b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts @@ -6,9 +6,11 @@ import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/an import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { AvatarModule, + BadgeModule, ButtonModule, I18nMockService, IconButtonModule, + ItemModule, } from "@bitwarden/components"; import { PopupFooterComponent } from "./popup-footer.component"; @@ -30,23 +32,34 @@ class ExtensionContainerComponent {} @Component({ selector: "vault-placeholder", template: ` -
vault item
-
vault item
-
vault item
-
vault item
-
vault item
-
vault item
-
vault item
-
vault item
-
vault item
-
vault item
-
vault item
-
vault item
-
vault item last item
+ + + + + + + + + + + + + + + + + `, standalone: true, + imports: [CommonModule, ItemModule, BadgeModule, IconButtonModule], }) -class VaultComponent {} +class VaultComponent { + protected data = Array.from(Array(20).keys()); +} @Component({ selector: "generator-placeholder", diff --git a/libs/components/src/a11y/a11y-cell.directive.ts b/libs/components/src/a11y/a11y-cell.directive.ts new file mode 100644 index 00000000000..fdd75c076f9 --- /dev/null +++ b/libs/components/src/a11y/a11y-cell.directive.ts @@ -0,0 +1,33 @@ +import { ContentChild, Directive, ElementRef, HostBinding } from "@angular/core"; + +import { FocusableElement } from "../shared/focusable-element"; + +@Directive({ + selector: "bitA11yCell", + standalone: true, + providers: [{ provide: FocusableElement, useExisting: A11yCellDirective }], +}) +export class A11yCellDirective implements FocusableElement { + @HostBinding("attr.role") + role: "gridcell" | null; + + @ContentChild(FocusableElement) + private focusableChild: FocusableElement; + + getFocusTarget() { + let focusTarget: HTMLElement; + if (this.focusableChild) { + focusTarget = this.focusableChild.getFocusTarget(); + } else { + focusTarget = this.elementRef.nativeElement.querySelector("button, a"); + } + + if (!focusTarget) { + return this.elementRef.nativeElement; + } + + return focusTarget; + } + + constructor(private elementRef: ElementRef) {} +} diff --git a/libs/components/src/a11y/a11y-grid.directive.ts b/libs/components/src/a11y/a11y-grid.directive.ts new file mode 100644 index 00000000000..c632376f4fc --- /dev/null +++ b/libs/components/src/a11y/a11y-grid.directive.ts @@ -0,0 +1,145 @@ +import { + AfterViewInit, + ContentChildren, + Directive, + HostBinding, + HostListener, + Input, + QueryList, +} from "@angular/core"; + +import type { A11yCellDirective } from "./a11y-cell.directive"; +import { A11yRowDirective } from "./a11y-row.directive"; + +@Directive({ + selector: "bitA11yGrid", + standalone: true, +}) +export class A11yGridDirective implements AfterViewInit { + @HostBinding("attr.role") + role = "grid"; + + @ContentChildren(A11yRowDirective) + rows: QueryList; + + /** The number of pages to navigate on `PageUp` and `PageDown` */ + @Input() pageSize = 5; + + private grid: A11yCellDirective[][]; + + /** The row that currently has focus */ + private activeRow = 0; + + /** The cell that currently has focus */ + private activeCol = 0; + + @HostListener("keydown", ["$event"]) + onKeyDown(event: KeyboardEvent) { + switch (event.code) { + case "ArrowUp": + this.updateCellFocusByDelta(-1, 0); + break; + case "ArrowRight": + this.updateCellFocusByDelta(0, 1); + break; + case "ArrowDown": + this.updateCellFocusByDelta(1, 0); + break; + case "ArrowLeft": + this.updateCellFocusByDelta(0, -1); + break; + case "Home": + this.updateCellFocusByDelta(-this.activeRow, -this.activeCol); + break; + case "End": + this.updateCellFocusByDelta(this.grid.length, this.grid[this.grid.length - 1].length); + break; + case "PageUp": + this.updateCellFocusByDelta(-this.pageSize, 0); + break; + case "PageDown": + this.updateCellFocusByDelta(this.pageSize, 0); + break; + default: + return; + } + + /** Prevent default scrolling behavior */ + event.preventDefault(); + } + + ngAfterViewInit(): void { + this.initializeGrid(); + } + + private initializeGrid(): void { + try { + this.grid = this.rows.map((listItem) => { + listItem.role = "row"; + return [...listItem.cells]; + }); + this.grid.flat().forEach((cell) => { + cell.role = "gridcell"; + cell.getFocusTarget().tabIndex = -1; + }); + + this.getActiveCellContent().tabIndex = 0; + } catch (error) { + // eslint-disable-next-line no-console + console.error("Unable to initialize grid"); + } + } + + /** Get the focusable content of the active cell */ + private getActiveCellContent(): HTMLElement { + return this.grid[this.activeRow][this.activeCol].getFocusTarget(); + } + + /** Move focus via a delta against the currently active gridcell */ + private updateCellFocusByDelta(rowDelta: number, colDelta: number) { + const prevActive = this.getActiveCellContent(); + + this.activeCol += colDelta; + this.activeRow += rowDelta; + + // Row upper bound + if (this.activeRow >= this.grid.length) { + this.activeRow = this.grid.length - 1; + } + + // Row lower bound + if (this.activeRow < 0) { + this.activeRow = 0; + } + + // Column upper bound + if (this.activeCol >= this.grid[this.activeRow].length) { + if (this.activeRow < this.grid.length - 1) { + // Wrap to next row on right arrow + this.activeCol = 0; + this.activeRow += 1; + } else { + this.activeCol = this.grid[this.activeRow].length - 1; + } + } + + // Column lower bound + if (this.activeCol < 0) { + if (this.activeRow > 0) { + // Wrap to prev row on left arrow + this.activeRow -= 1; + this.activeCol = this.grid[this.activeRow].length - 1; + } else { + this.activeCol = 0; + } + } + + const nextActive = this.getActiveCellContent(); + nextActive.tabIndex = 0; + nextActive.focus(); + + if (nextActive !== prevActive) { + prevActive.tabIndex = -1; + } + } +} diff --git a/libs/components/src/a11y/a11y-row.directive.ts b/libs/components/src/a11y/a11y-row.directive.ts new file mode 100644 index 00000000000..e062eb2b5a9 --- /dev/null +++ b/libs/components/src/a11y/a11y-row.directive.ts @@ -0,0 +1,31 @@ +import { + AfterViewInit, + ContentChildren, + Directive, + HostBinding, + QueryList, + ViewChildren, +} from "@angular/core"; + +import { A11yCellDirective } from "./a11y-cell.directive"; + +@Directive({ + selector: "bitA11yRow", + standalone: true, +}) +export class A11yRowDirective implements AfterViewInit { + @HostBinding("attr.role") + role: "row" | null; + + cells: A11yCellDirective[]; + + @ViewChildren(A11yCellDirective) + private viewCells: QueryList; + + @ContentChildren(A11yCellDirective) + private contentCells: QueryList; + + ngAfterViewInit(): void { + this.cells = [...this.viewCells, ...this.contentCells]; + } +} diff --git a/libs/components/src/badge/badge.directive.ts b/libs/components/src/badge/badge.directive.ts index b81b9f80e27..acce4a18aad 100644 --- a/libs/components/src/badge/badge.directive.ts +++ b/libs/components/src/badge/badge.directive.ts @@ -1,5 +1,7 @@ import { Directive, ElementRef, HostBinding, Input } from "@angular/core"; +import { FocusableElement } from "../shared/focusable-element"; + export type BadgeVariant = "primary" | "secondary" | "success" | "danger" | "warning" | "info"; const styles: Record = { @@ -22,8 +24,9 @@ const hoverStyles: Record = { @Directive({ selector: "span[bitBadge], a[bitBadge], button[bitBadge]", + providers: [{ provide: FocusableElement, useExisting: BadgeDirective }], }) -export class BadgeDirective { +export class BadgeDirective implements FocusableElement { @HostBinding("class") get classList() { return [ "tw-inline-block", @@ -62,6 +65,10 @@ export class BadgeDirective { */ @Input() truncate = true; + getFocusTarget() { + return this.el.nativeElement; + } + private hasHoverEffects = false; constructor(private el: ElementRef) { diff --git a/libs/components/src/icon-button/icon-button.component.ts b/libs/components/src/icon-button/icon-button.component.ts index 53e80327956..54f6dfda963 100644 --- a/libs/components/src/icon-button/icon-button.component.ts +++ b/libs/components/src/icon-button/icon-button.component.ts @@ -1,6 +1,7 @@ -import { Component, HostBinding, Input } from "@angular/core"; +import { Component, ElementRef, HostBinding, Input } from "@angular/core"; import { ButtonLikeAbstraction, ButtonType } from "../shared/button-like.abstraction"; +import { FocusableElement } from "../shared/focusable-element"; export type IconButtonType = ButtonType | "contrast" | "main" | "muted" | "light"; @@ -123,9 +124,12 @@ const sizes: Record = { @Component({ selector: "button[bitIconButton]:not(button[bitButton])", templateUrl: "icon-button.component.html", - providers: [{ provide: ButtonLikeAbstraction, useExisting: BitIconButtonComponent }], + providers: [ + { provide: ButtonLikeAbstraction, useExisting: BitIconButtonComponent }, + { provide: FocusableElement, useExisting: BitIconButtonComponent }, + ], }) -export class BitIconButtonComponent implements ButtonLikeAbstraction { +export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableElement { @Input("bitIconButton") icon: string; @Input() buttonType: IconButtonType; @@ -162,4 +166,10 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction { setButtonType(value: "primary" | "secondary" | "danger" | "unstyled") { this.buttonType = value; } + + getFocusTarget() { + return this.elementRef.nativeElement; + } + + constructor(private elementRef: ElementRef) {} } diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index 36185911a6b..1e4a3a86ffe 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -16,6 +16,7 @@ export * from "./form-field"; export * from "./icon-button"; export * from "./icon"; export * from "./input"; +export * from "./item"; export * from "./layout"; export * from "./link"; export * from "./menu"; diff --git a/libs/components/src/input/autofocus.directive.ts b/libs/components/src/input/autofocus.directive.ts index f8161ee6e03..625e7fbc927 100644 --- a/libs/components/src/input/autofocus.directive.ts +++ b/libs/components/src/input/autofocus.directive.ts @@ -3,12 +3,7 @@ import { take } from "rxjs/operators"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -/** - * Interface for implementing focusable components. Used by the AutofocusDirective. - */ -export abstract class FocusableElement { - focus: () => void; -} +import { FocusableElement } from "../shared/focusable-element"; /** * Directive to focus an element. @@ -46,7 +41,7 @@ export class AutofocusDirective { private focus() { if (this.focusableElement) { - this.focusableElement.focus(); + this.focusableElement.getFocusTarget().focus(); } else { this.el.nativeElement.focus(); } diff --git a/libs/components/src/item/index.ts b/libs/components/src/item/index.ts new file mode 100644 index 00000000000..56896cdc3c6 --- /dev/null +++ b/libs/components/src/item/index.ts @@ -0,0 +1 @@ +export * from "./item.module"; diff --git a/libs/components/src/item/item-action.component.ts b/libs/components/src/item/item-action.component.ts new file mode 100644 index 00000000000..8cabf5c5c23 --- /dev/null +++ b/libs/components/src/item/item-action.component.ts @@ -0,0 +1,12 @@ +import { Component } from "@angular/core"; + +import { A11yCellDirective } from "../a11y/a11y-cell.directive"; + +@Component({ + selector: "bit-item-action", + standalone: true, + imports: [], + template: ``, + providers: [{ provide: A11yCellDirective, useExisting: ItemActionComponent }], +}) +export class ItemActionComponent extends A11yCellDirective {} diff --git a/libs/components/src/item/item-content.component.html b/libs/components/src/item/item-content.component.html new file mode 100644 index 00000000000..d034a4a0017 --- /dev/null +++ b/libs/components/src/item/item-content.component.html @@ -0,0 +1,16 @@ +
+ + +
+
+ +
+
+ +
+
+
+ +
+ +
diff --git a/libs/components/src/item/item-content.component.ts b/libs/components/src/item/item-content.component.ts new file mode 100644 index 00000000000..58a11985127 --- /dev/null +++ b/libs/components/src/item/item-content.component.ts @@ -0,0 +1,15 @@ +import { CommonModule } from "@angular/common"; +import { ChangeDetectionStrategy, Component } from "@angular/core"; + +@Component({ + selector: "bit-item-content, [bit-item-content]", + standalone: true, + imports: [CommonModule], + templateUrl: `item-content.component.html`, + host: { + class: + "fvw-target tw-outline-none tw-text-main hover:tw-text-main hover:tw-no-underline tw-text-base tw-py-2 tw-px-4 tw-bg-transparent tw-w-full tw-border-none tw-flex tw-gap-4 tw-items-center tw-justify-between", + }, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ItemContentComponent {} diff --git a/libs/components/src/item/item-group.component.ts b/libs/components/src/item/item-group.component.ts new file mode 100644 index 00000000000..2a9a8275cc6 --- /dev/null +++ b/libs/components/src/item/item-group.component.ts @@ -0,0 +1,13 @@ +import { ChangeDetectionStrategy, Component } from "@angular/core"; + +@Component({ + selector: "bit-item-group", + standalone: true, + imports: [], + template: ``, + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + class: "tw-block", + }, +}) +export class ItemGroupComponent {} diff --git a/libs/components/src/item/item.component.html b/libs/components/src/item/item.component.html new file mode 100644 index 00000000000..0c91c6848e9 --- /dev/null +++ b/libs/components/src/item/item.component.html @@ -0,0 +1,21 @@ + +
+ + + + +
+ +
+
diff --git a/libs/components/src/item/item.component.ts b/libs/components/src/item/item.component.ts new file mode 100644 index 00000000000..4b7b57fa9f3 --- /dev/null +++ b/libs/components/src/item/item.component.ts @@ -0,0 +1,29 @@ +import { CommonModule } from "@angular/common"; +import { ChangeDetectionStrategy, Component, HostListener, signal } from "@angular/core"; + +import { A11yRowDirective } from "../a11y/a11y-row.directive"; + +import { ItemActionComponent } from "./item-action.component"; + +@Component({ + selector: "bit-item", + standalone: true, + imports: [CommonModule, ItemActionComponent], + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: "item.component.html", + providers: [{ provide: A11yRowDirective, useExisting: ItemComponent }], +}) +export class ItemComponent extends A11yRowDirective { + /** + * We have `:focus-within` and `:focus-visible` but no `:focus-visible-within` + */ + protected focusVisibleWithin = signal(false); + @HostListener("focusin", ["$event.target"]) + onFocusIn(target: HTMLElement) { + this.focusVisibleWithin.set(target.matches(".fvw-target:focus-visible")); + } + @HostListener("focusout") + onFocusOut() { + this.focusVisibleWithin.set(false); + } +} diff --git a/libs/components/src/item/item.mdx b/libs/components/src/item/item.mdx new file mode 100644 index 00000000000..8506de72bb9 --- /dev/null +++ b/libs/components/src/item/item.mdx @@ -0,0 +1,141 @@ +import { Meta, Story, Primary, Controls, Canvas } from "@storybook/addon-docs"; + +import * as stories from "./item.stories"; + + + +```ts +import { ItemModule } from "@bitwarden/components"; +``` + +# Item + +`` is a horizontal card that contains one or more interactive actions. + +It is a generic container that can be used for either standalone content, an alternative to tables, +or to list nav links. + + + + + +## Primary Content + +The primary content of an item is supplied by `bit-item-content`. + +### Content Types + +The content can be a button, anchor, or static container. + +```html + + Hi, I am a link. + + + + + + + + I'm just static :( + +``` + + + + + +### Content Slots + +`bit-item-content` contains the following slots to help position the content: + +| Slot | Description | +| ------------------ | --------------------------------------------------- | +| default | primary text or arbitrary content; fan favorite | +| `slot="secondary"` | supporting text; under the default slot | +| `slot="start"` | commonly an icon or avatar; before the default slot | +| `slot="end"` | commonly an icon; after the default slot | + +- Note: There is also an `end` slot within `bit-item` itself. Place + [interactive secondary actions](#secondary-actions) there, and place non-interactive content (such + as icons) in `bit-item-content` + +```html + + + +``` + + + + + +## Secondary Actions + +Secondary interactive actions can be placed in the item through the `"end"` slot, outside of +`bit-item-content`. + +Each action must be wrapped by ``. + +Actions are commonly icon buttons or badge buttons. + +```html + + + + + + + + + + + + + + + +``` + +## Item Groups + +Groups of items can be associated by wrapping them in the ``. + + + + + + + + + +### A11y + +Keyboard nav is currently disabled due to a bug when used within a virtual scroll viewport. + +Item groups utilize arrow-based keyboard navigation +([further reading here](https://www.w3.org/WAI/ARIA/apg/patterns/grid/examples/layout-grids/#kbd_label)). + +Use `aria-label` or `aria-labelledby` to give groups an accessible name. + +```html + + ... + ... + ... + +``` + +### Virtual Scrolling + + + + diff --git a/libs/components/src/item/item.module.ts b/libs/components/src/item/item.module.ts new file mode 100644 index 00000000000..226fed11d80 --- /dev/null +++ b/libs/components/src/item/item.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from "@angular/core"; + +import { ItemActionComponent } from "./item-action.component"; +import { ItemContentComponent } from "./item-content.component"; +import { ItemGroupComponent } from "./item-group.component"; +import { ItemComponent } from "./item.component"; + +@NgModule({ + imports: [ItemComponent, ItemContentComponent, ItemActionComponent, ItemGroupComponent], + exports: [ItemComponent, ItemContentComponent, ItemActionComponent, ItemGroupComponent], +}) +export class ItemModule {} diff --git a/libs/components/src/item/item.stories.ts b/libs/components/src/item/item.stories.ts new file mode 100644 index 00000000000..b9d8d6cc2e8 --- /dev/null +++ b/libs/components/src/item/item.stories.ts @@ -0,0 +1,326 @@ +import { ScrollingModule } from "@angular/cdk/scrolling"; +import { CommonModule } from "@angular/common"; +import { Meta, StoryObj, componentWrapperDecorator, moduleMetadata } from "@storybook/angular"; + +import { A11yGridDirective } from "../a11y/a11y-grid.directive"; +import { AvatarModule } from "../avatar"; +import { BadgeModule } from "../badge"; +import { IconButtonModule } from "../icon-button"; +import { TypographyModule } from "../typography"; + +import { ItemActionComponent } from "./item-action.component"; +import { ItemContentComponent } from "./item-content.component"; +import { ItemGroupComponent } from "./item-group.component"; +import { ItemComponent } from "./item.component"; + +export default { + title: "Component Library/Item", + component: ItemComponent, + decorators: [ + moduleMetadata({ + imports: [ + CommonModule, + ItemGroupComponent, + AvatarModule, + IconButtonModule, + BadgeModule, + TypographyModule, + ItemActionComponent, + ItemContentComponent, + A11yGridDirective, + ScrollingModule, + ], + }), + componentWrapperDecorator((story) => `
${story}
`), + ], +} as Meta; + +type Story = StoryObj; + +export const Default: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + + + + + + + + + + + + + + + + `, + }), +}; + +export const ContentSlots: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + + + + `, + }), +}; + +export const ContentTypes: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + + + Hi, I am a link. + + + + + + + + I'm just static :( + + + `, + }), +}; + +export const TextOverflow: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` +
TODO: Fix truncation
+ + + Helloooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo + + + `, + }), +}; + +export const MultipleActionList: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + `, + }), +}; + +export const SingleActionList: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + + + + Foobar + + + + + + Foobar + + + + + + Foobar + + + + + + Foobar + + + + + + Foobar + + + + + + Foobar + + + + + `, + }), +}; + +export const VirtualScrolling: Story = { + render: (_args) => ({ + props: { + data: Array.from(Array(100000).keys()), + }, + template: /*html*/ ` + + + + + + + + + + + + + + + + + + + + `, + }), +}; diff --git a/libs/components/src/search/search.component.ts b/libs/components/src/search/search.component.ts index a0f3eb363f0..27170d5d7b8 100644 --- a/libs/components/src/search/search.component.ts +++ b/libs/components/src/search/search.component.ts @@ -1,7 +1,7 @@ import { Component, ElementRef, Input, ViewChild } from "@angular/core"; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms"; -import { FocusableElement } from "../input/autofocus.directive"; +import { FocusableElement } from "../shared/focusable-element"; let nextId = 0; @@ -32,8 +32,8 @@ export class SearchComponent implements ControlValueAccessor, FocusableElement { @Input() disabled: boolean; @Input() placeholder: string; - focus() { - this.input.nativeElement.focus(); + getFocusTarget() { + return this.input.nativeElement; } onChange(searchText: string) { diff --git a/libs/components/src/shared/focusable-element.ts b/libs/components/src/shared/focusable-element.ts new file mode 100644 index 00000000000..1ea422aa6f2 --- /dev/null +++ b/libs/components/src/shared/focusable-element.ts @@ -0,0 +1,8 @@ +/** + * Interface for implementing focusable components. + * + * Used by the `AutofocusDirective` and `A11yGridDirective`. + */ +export abstract class FocusableElement { + getFocusTarget: () => HTMLElement; +} diff --git a/libs/components/src/styles.scss b/libs/components/src/styles.scss index ae97838e09f..7ddcb1b64b9 100644 --- a/libs/components/src/styles.scss +++ b/libs/components/src/styles.scss @@ -49,6 +49,6 @@ $card-icons-base: "../../src/billing/images/cards/"; @import "multi-select/scss/bw.theme.scss"; // Workaround for https://bitwarden.atlassian.net/browse/CL-110 -#storybook-docs pre.prismjs { +.sbdocs-preview pre.prismjs { color: white; } From 418d4642da81e09de6a4b445042a26592a017c98 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Tue, 30 Apr 2024 10:55:00 -0400 Subject: [PATCH 105/110] Hide grace period note when in self-serve trial (#8768) --- ...organization-subscription-selfhost.component.html | 5 ++++- .../self-hosted-organization-subscription.view.ts | 12 ++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.html b/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.html index 6d6691f336b..b4c1224db94 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.html +++ b/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.html @@ -42,7 +42,10 @@ : subscription.expirationWithGracePeriod ) | date: "mediumDate" }} -
+
{{ "selfHostGracePeriodHelp" | i18n: (subscription.expirationWithGracePeriod | date: "mediumDate") diff --git a/libs/common/src/billing/models/view/self-hosted-organization-subscription.view.ts b/libs/common/src/billing/models/view/self-hosted-organization-subscription.view.ts index c1f5640207e..7b496882948 100644 --- a/libs/common/src/billing/models/view/self-hosted-organization-subscription.view.ts +++ b/libs/common/src/billing/models/view/self-hosted-organization-subscription.view.ts @@ -58,4 +58,16 @@ export class SelfHostedOrganizationSubscriptionView implements View { get isExpiredAndOutsideGracePeriod() { return this.hasExpiration && this.expirationWithGracePeriod < new Date(); } + + /** + * In the case of a trial, where there is no grace period, the expirationWithGracePeriod and expirationWithoutGracePeriod will + * be exactly the same. This can be used to hide the grace period note. + */ + get isInTrial() { + return ( + this.expirationWithGracePeriod && + this.expirationWithoutGracePeriod && + this.expirationWithGracePeriod.getTime() === this.expirationWithoutGracePeriod.getTime() + ); + } } From 04decd1c09a69bde07c389c481264badba3355e9 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Tue, 30 Apr 2024 16:35:39 +0100 Subject: [PATCH 106/110] [AC-2265] As a Provider Admin, I shouldn't be able to use my client organizations' billing pages (#8981) * initial commit * add the feature flag * Resolve pr comments --- .../icons/manage-billing.icon.ts | 25 +++++++++++++++++++ ...nization-subscription-cloud.component.html | 12 ++++++++- ...ganization-subscription-cloud.component.ts | 16 +++++++++++- apps/web/src/locales/en/messages.json | 3 +++ 4 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/app/billing/organizations/icons/manage-billing.icon.ts diff --git a/apps/web/src/app/billing/organizations/icons/manage-billing.icon.ts b/apps/web/src/app/billing/organizations/icons/manage-billing.icon.ts new file mode 100644 index 00000000000..6f583bf2e81 --- /dev/null +++ b/apps/web/src/app/billing/organizations/icons/manage-billing.icon.ts @@ -0,0 +1,25 @@ +import { svgIcon } from "@bitwarden/components"; + +export const ManageBilling = svgIcon` + + + + + + + + + + + + + + + + + + + + + + `; diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html index 16641c0d526..38903bab19e 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html @@ -1,6 +1,6 @@ - + {{ "loading" | i18n }} @@ -256,3 +256,13 @@ + +
+ + {{ + "manageBillingFromProviderPortalMessage" | i18n + }} +
+
diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts index 477032debae..7acb1088085 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts @@ -5,7 +5,7 @@ import { concatMap, firstValueFrom, lastValueFrom, Observable, Subject, takeUnti import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { OrganizationApiKeyType } from "@bitwarden/common/admin-console/enums"; +import { OrganizationApiKeyType, ProviderType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { PlanType } from "@bitwarden/common/billing/enums"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; @@ -28,6 +28,7 @@ import { } from "../shared/offboarding-survey.component"; import { BillingSyncApiKeyComponent } from "./billing-sync-api-key.component"; +import { ManageBilling } from "./icons/manage-billing.icon"; import { SecretsManagerSubscriptionOptions } from "./sm-adjust-subscription.component"; @Component({ @@ -47,11 +48,17 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy loading: boolean; locale: string; showUpdatedSubscriptionStatusSection$: Observable; + manageBillingFromProviderPortal = ManageBilling; + IsProviderManaged = false; protected readonly teamsStarter = ProductType.TeamsStarter; private destroy$ = new Subject(); + protected enableConsolidatedBilling$ = this.configService.getFeatureFlag$( + FeatureFlag.EnableConsolidatedBilling, + ); + constructor( private apiService: ApiService, private platformUtilsService: PlatformUtilsService, @@ -99,6 +106,13 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy this.loading = true; this.locale = await firstValueFrom(this.i18nService.locale$); this.userOrg = await this.organizationService.get(this.organizationId); + const enableConsolidatedBilling = await firstValueFrom(this.enableConsolidatedBilling$); + this.IsProviderManaged = + this.userOrg.hasProvider && + this.userOrg.providerType == ProviderType.Msp && + enableConsolidatedBilling + ? true + : false; if (this.userOrg.canViewSubscription) { this.sub = await this.organizationApiService.getSubscription(this.organizationId); this.lineItems = this.sub?.subscription?.items; diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 63069a83de8..3a590622b81 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -8049,5 +8049,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } From be50a174def9763b88086c60552cc31320c34f78 Mon Sep 17 00:00:00 2001 From: Colton Hurst Date: Tue, 30 Apr 2024 12:31:09 -0400 Subject: [PATCH 107/110] SM-1196: Update export file name (#8865) --- .../event-logs/service-accounts-events.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts index 554e7fa37d7..0547c4fcbad 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts @@ -17,7 +17,7 @@ import { ServiceAccountEventLogApiService } from "./service-account-event-log-ap templateUrl: "./service-accounts-events.component.html", }) export class ServiceAccountEventsComponent extends BaseEventsComponent implements OnDestroy { - exportFileName = "service-account-events"; + exportFileName = "machine-account-events"; private destroy$ = new Subject(); private serviceAccountId: string; From 3acbffa0726d8b4bda5c410d2ad98cf226a8ab7a Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Tue, 30 Apr 2024 12:35:36 -0400 Subject: [PATCH 108/110] [PM-6144] Basic auth autofill in Manifest v3 (#8975) * Add Support for autofilling Basic Auth to MV3 * Remove `any` --- .../background/web-request.background.ts | 34 ++++++------------- .../browser/src/background/main.background.ts | 7 ++-- apps/browser/src/manifest.v3.json | 4 ++- 3 files changed, 16 insertions(+), 29 deletions(-) diff --git a/apps/browser/src/autofill/background/web-request.background.ts b/apps/browser/src/autofill/background/web-request.background.ts index 8cdfa0f0276..2eb976529f4 100644 --- a/apps/browser/src/autofill/background/web-request.background.ts +++ b/apps/browser/src/autofill/background/web-request.background.ts @@ -4,40 +4,29 @@ import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { BrowserApi } from "../../platform/browser/browser-api"; - export default class WebRequestBackground { - private pendingAuthRequests: any[] = []; - private webRequest: any; + private pendingAuthRequests: Set = new Set([]); private isFirefox: boolean; constructor( platformUtilsService: PlatformUtilsService, private cipherService: CipherService, private authService: AuthService, + private readonly webRequest: typeof chrome.webRequest, ) { - if (BrowserApi.isManifestVersion(2)) { - this.webRequest = chrome.webRequest; - } this.isFirefox = platformUtilsService.isFirefox(); } - async init() { - if (!this.webRequest || !this.webRequest.onAuthRequired) { - return; - } - + startListening() { this.webRequest.onAuthRequired.addListener( - async (details: any, callback: any) => { - if (!details.url || this.pendingAuthRequests.indexOf(details.requestId) !== -1) { + async (details, callback) => { + if (!details.url || this.pendingAuthRequests.has(details.requestId)) { if (callback) { - callback(); + callback(null); } return; } - - this.pendingAuthRequests.push(details.requestId); - + this.pendingAuthRequests.add(details.requestId); if (this.isFirefox) { // eslint-disable-next-line return new Promise(async (resolve, reject) => { @@ -51,7 +40,7 @@ export default class WebRequestBackground { [this.isFirefox ? "blocking" : "asyncBlocking"], ); - this.webRequest.onCompleted.addListener((details: any) => this.completeAuthRequest(details), { + this.webRequest.onCompleted.addListener((details) => this.completeAuthRequest(details), { urls: ["http://*/*"], }); this.webRequest.onErrorOccurred.addListener( @@ -91,10 +80,7 @@ export default class WebRequestBackground { } } - private completeAuthRequest(details: any) { - const i = this.pendingAuthRequests.indexOf(details.requestId); - if (i > -1) { - this.pendingAuthRequests.splice(i, 1); - } + private completeAuthRequest(details: chrome.webRequest.WebResponseCacheDetails) { + this.pendingAuthRequests.delete(details.requestId); } } diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 01e325ad516..adcb4a21ba4 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1056,11 +1056,12 @@ export default class MainBackground { this.cipherService, ); - if (BrowserApi.isManifestVersion(2)) { + if (chrome.webRequest != null && chrome.webRequest.onAuthRequired != null) { this.webRequestBackground = new WebRequestBackground( this.platformUtilsService, this.cipherService, this.authService, + chrome.webRequest, ); } } @@ -1106,9 +1107,7 @@ export default class MainBackground { await this.tabsBackground.init(); this.contextMenusBackground?.init(); await this.idleBackground.init(); - if (BrowserApi.isManifestVersion(2)) { - await this.webRequestBackground.init(); - } + this.webRequestBackground?.startListening(); if (this.platformUtilsService.isFirefox() && !this.isPrivateMode) { // Set Private Mode windows to the default icon - they do not share state with the background page diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index c0c88706b80..b7aaba2e0e0 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -60,7 +60,9 @@ "clipboardWrite", "idle", "scripting", - "offscreen" + "offscreen", + "webRequest", + "webRequestAuthProvider" ], "optional_permissions": ["nativeMessaging", "privacy"], "host_permissions": [""], From 200b0f75341dd38186f1ea05dab399ab12c4d771 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Tue, 30 Apr 2024 12:46:01 -0400 Subject: [PATCH 109/110] Correct and test changeover point for userId source in storage migration (#8990) --- .../src/state-migrations/migration-helper.spec.ts | 5 +++++ .../src/state-migrations/migration-helper.ts | 6 +++--- .../migrations/60-known-accounts.spec.ts | 14 +++++--------- .../migrations/60-known-accounts.ts | 4 ++-- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/libs/common/src/state-migrations/migration-helper.spec.ts b/libs/common/src/state-migrations/migration-helper.spec.ts index 162fac2fabc..21c5c72a18b 100644 --- a/libs/common/src/state-migrations/migration-helper.spec.ts +++ b/libs/common/src/state-migrations/migration-helper.spec.ts @@ -235,6 +235,11 @@ export function mockMigrationHelper( helper.setToUser(userId, keyDefinition, value), ); mockHelper.getAccounts.mockImplementation(() => helper.getAccounts()); + mockHelper.getKnownUserIds.mockImplementation(() => helper.getKnownUserIds()); + mockHelper.removeFromGlobal.mockImplementation((keyDefinition) => + helper.removeFromGlobal(keyDefinition), + ); + mockHelper.remove.mockImplementation((key) => helper.remove(key)); mockHelper.type = helper.type; diff --git a/libs/common/src/state-migrations/migration-helper.ts b/libs/common/src/state-migrations/migration-helper.ts index 5d1de8dd49e..b377df8ef9d 100644 --- a/libs/common/src/state-migrations/migration-helper.ts +++ b/libs/common/src/state-migrations/migration-helper.ts @@ -175,8 +175,8 @@ export class MigrationHelper { * Helper method to read known users ids. */ async getKnownUserIds(): Promise { - if (this.currentVersion < 61) { - return knownAccountUserIdsBuilderPre61(this.storageService); + if (this.currentVersion < 60) { + return knownAccountUserIdsBuilderPre60(this.storageService); } else { return knownAccountUserIdsBuilder(this.storageService); } @@ -245,7 +245,7 @@ function globalKeyBuilderPre9(): string { throw Error("No key builder should be used for versions prior to 9."); } -async function knownAccountUserIdsBuilderPre61( +async function knownAccountUserIdsBuilderPre60( storageService: AbstractStorageService, ): Promise { return (await storageService.get("authenticatedAccounts")) ?? []; diff --git a/libs/common/src/state-migrations/migrations/60-known-accounts.spec.ts b/libs/common/src/state-migrations/migrations/60-known-accounts.spec.ts index 28dedb3c390..01be4adb6a7 100644 --- a/libs/common/src/state-migrations/migrations/60-known-accounts.spec.ts +++ b/libs/common/src/state-migrations/migrations/60-known-accounts.spec.ts @@ -51,17 +51,13 @@ const rollbackJson = () => { }, global_account_accounts: { user1: { - profile: { - email: "user1", - name: "User 1", - emailVerified: true, - }, + email: "user1", + name: "User 1", + emailVerified: true, }, user2: { - profile: { - email: "", - emailVerified: false, - }, + email: "", + emailVerified: false, }, }, global_account_activeAccountId: "user1", diff --git a/libs/common/src/state-migrations/migrations/60-known-accounts.ts b/libs/common/src/state-migrations/migrations/60-known-accounts.ts index 75117da5b47..3b02a5acc4d 100644 --- a/libs/common/src/state-migrations/migrations/60-known-accounts.ts +++ b/libs/common/src/state-migrations/migrations/60-known-accounts.ts @@ -38,8 +38,8 @@ export class KnownAccountsMigrator extends Migrator<59, 60> { } async rollback(helper: MigrationHelper): Promise { // authenticated account are removed, but the accounts record also contains logged out accounts. Best we can do is to add them all back - const accounts = (await helper.getFromGlobal>(ACCOUNT_ACCOUNTS)) ?? {}; - await helper.set("authenticatedAccounts", Object.keys(accounts)); + const userIds = (await helper.getKnownUserIds()) ?? []; + await helper.set("authenticatedAccounts", userIds); await helper.removeFromGlobal(ACCOUNT_ACCOUNTS); // Active Account Id From b4631b0dd164ee34de9f5dff43a1bf559880ebd0 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Tue, 30 Apr 2024 12:58:16 -0400 Subject: [PATCH 110/110] Ps/improve-log-service (#8989) * Match console method signatures in logService abstraction * Add a few usages of improved signature * Remove reality check test * Improve electron logging --- .../src/background/runtime.background.ts | 3 +- .../offscreen-document.spec.ts | 4 +- .../offscreen-document/offscreen-document.ts | 2 +- .../services/console-log.service.spec.ts | 36 ++++++------ .../platform/services/console-log.service.ts | 6 +- apps/desktop/src/platform/preload.ts | 3 +- .../services/electron-log.main.service.ts | 14 ++--- .../services/electron-log.renderer.service.ts | 14 +++-- .../services/logging-error-handler.ts | 2 +- libs/common/spec/intercept-console.ts | 23 +++----- .../src/platform/abstractions/log.service.ts | 10 ++-- .../services/console-log.service.spec.ts | 57 ++++++++++--------- .../platform/services/console-log.service.ts | 26 ++++----- 13 files changed, 104 insertions(+), 96 deletions(-) diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index 98b1df9c80a..d8f3cf840fd 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -76,7 +76,8 @@ export default class RuntimeBackground { void this.processMessageWithSender(msg, sender).catch((err) => this.logService.error( - `Error while processing message in RuntimeBackground '${msg?.command}'. Error: ${err?.message ?? "Unknown Error"}`, + `Error while processing message in RuntimeBackground '${msg?.command}'.`, + err, ), ); return false; diff --git a/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts b/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts index 1cbcc7a94c8..933cd08c2ea 100644 --- a/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts +++ b/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts @@ -28,6 +28,7 @@ describe("OffscreenDocument", () => { }); it("shows a console message if the handler throws an error", async () => { + const error = new Error("test error"); browserClipboardServiceCopySpy.mockRejectedValueOnce(new Error("test error")); sendExtensionRuntimeMessage({ command: "offscreenCopyToClipboard", text: "test" }); @@ -35,7 +36,8 @@ describe("OffscreenDocument", () => { expect(browserClipboardServiceCopySpy).toHaveBeenCalled(); expect(consoleErrorSpy).toHaveBeenCalledWith( - "Error resolving extension message response: Error: test error", + "Error resolving extension message response", + error, ); }); diff --git a/apps/browser/src/platform/offscreen-document/offscreen-document.ts b/apps/browser/src/platform/offscreen-document/offscreen-document.ts index 627036b80bd..4994a6e9ba8 100644 --- a/apps/browser/src/platform/offscreen-document/offscreen-document.ts +++ b/apps/browser/src/platform/offscreen-document/offscreen-document.ts @@ -71,7 +71,7 @@ class OffscreenDocument implements OffscreenDocumentInterface { Promise.resolve(messageResponse) .then((response) => sendResponse(response)) .catch((error) => - this.consoleLogService.error(`Error resolving extension message response: ${error}`), + this.consoleLogService.error("Error resolving extension message response", error), ); return true; }; diff --git a/apps/cli/src/platform/services/console-log.service.spec.ts b/apps/cli/src/platform/services/console-log.service.spec.ts index 10a0ad8cca8..03598b16e66 100644 --- a/apps/cli/src/platform/services/console-log.service.spec.ts +++ b/apps/cli/src/platform/services/console-log.service.spec.ts @@ -2,13 +2,18 @@ import { interceptConsole, restoreConsole } from "@bitwarden/common/spec"; import { ConsoleLogService } from "./console-log.service"; -let caughtMessage: any = {}; - describe("CLI Console log service", () => { + const error = new Error("this is an error"); + const obj = { a: 1, b: 2 }; let logService: ConsoleLogService; + let consoleSpy: { + log: jest.Mock; + warn: jest.Mock; + error: jest.Mock; + }; + beforeEach(() => { - caughtMessage = {}; - interceptConsole(caughtMessage); + consoleSpy = interceptConsole(); logService = new ConsoleLogService(true); }); @@ -19,24 +24,21 @@ describe("CLI Console log service", () => { it("should redirect all console to error if BW_RESPONSE env is true", () => { process.env.BW_RESPONSE = "true"; - logService.debug("this is a debug message"); - expect(caughtMessage).toMatchObject({ - error: { 0: "this is a debug message" }, - }); + logService.debug("this is a debug message", error, obj); + expect(consoleSpy.error).toHaveBeenCalledWith("this is a debug message", error, obj); }); it("should not redirect console to error if BW_RESPONSE != true", () => { process.env.BW_RESPONSE = "false"; - logService.debug("debug"); - logService.info("info"); - logService.warning("warning"); - logService.error("error"); + logService.debug("debug", error, obj); + logService.info("info", error, obj); + logService.warning("warning", error, obj); + logService.error("error", error, obj); - expect(caughtMessage).toMatchObject({ - log: { 0: "info" }, - warn: { 0: "warning" }, - error: { 0: "error" }, - }); + expect(consoleSpy.log).toHaveBeenCalledWith("debug", error, obj); + expect(consoleSpy.log).toHaveBeenCalledWith("info", error, obj); + expect(consoleSpy.warn).toHaveBeenCalledWith("warning", error, obj); + expect(consoleSpy.error).toHaveBeenCalledWith("error", error, obj); }); }); diff --git a/apps/cli/src/platform/services/console-log.service.ts b/apps/cli/src/platform/services/console-log.service.ts index a35dae71fcc..5bdc0b40152 100644 --- a/apps/cli/src/platform/services/console-log.service.ts +++ b/apps/cli/src/platform/services/console-log.service.ts @@ -6,17 +6,17 @@ export class ConsoleLogService extends BaseConsoleLogService { super(isDev, filter); } - write(level: LogLevelType, message: string) { + write(level: LogLevelType, message?: any, ...optionalParams: any[]) { if (this.filter != null && this.filter(level)) { return; } if (process.env.BW_RESPONSE === "true") { // eslint-disable-next-line - console.error(message); + console.error(message, ...optionalParams); return; } - super.write(level, message); + super.write(level, message, ...optionalParams); } } diff --git a/apps/desktop/src/platform/preload.ts b/apps/desktop/src/platform/preload.ts index 771d25ef0a8..d81d6476526 100644 --- a/apps/desktop/src/platform/preload.ts +++ b/apps/desktop/src/platform/preload.ts @@ -103,7 +103,8 @@ export default { isMacAppStore: isMacAppStore(), isWindowsStore: isWindowsStore(), reloadProcess: () => ipcRenderer.send("reload-process"), - log: (level: LogLevelType, message: string) => ipcRenderer.invoke("ipc.log", { level, message }), + log: (level: LogLevelType, message?: any, ...optionalParams: any[]) => + ipcRenderer.invoke("ipc.log", { level, message, optionalParams }), openContextMenu: ( menu: { diff --git a/apps/desktop/src/platform/services/electron-log.main.service.ts b/apps/desktop/src/platform/services/electron-log.main.service.ts index 832365785ce..0725de3dc9f 100644 --- a/apps/desktop/src/platform/services/electron-log.main.service.ts +++ b/apps/desktop/src/platform/services/electron-log.main.service.ts @@ -25,28 +25,28 @@ export class ElectronLogMainService extends BaseLogService { } log.initialize(); - ipcMain.handle("ipc.log", (_event, { level, message }) => { - this.write(level, message); + ipcMain.handle("ipc.log", (_event, { level, message, optionalParams }) => { + this.write(level, message, ...optionalParams); }); } - write(level: LogLevelType, message: string) { + write(level: LogLevelType, message?: any, ...optionalParams: any[]) { if (this.filter != null && this.filter(level)) { return; } switch (level) { case LogLevelType.Debug: - log.debug(message); + log.debug(message, ...optionalParams); break; case LogLevelType.Info: - log.info(message); + log.info(message, ...optionalParams); break; case LogLevelType.Warning: - log.warn(message); + log.warn(message, ...optionalParams); break; case LogLevelType.Error: - log.error(message); + log.error(message, ...optionalParams); break; default: break; diff --git a/apps/desktop/src/platform/services/electron-log.renderer.service.ts b/apps/desktop/src/platform/services/electron-log.renderer.service.ts index e0e0757e6ad..cea939f1609 100644 --- a/apps/desktop/src/platform/services/electron-log.renderer.service.ts +++ b/apps/desktop/src/platform/services/electron-log.renderer.service.ts @@ -6,27 +6,29 @@ export class ElectronLogRendererService extends BaseLogService { super(ipc.platform.isDev, filter); } - write(level: LogLevelType, message: string) { + write(level: LogLevelType, message?: any, ...optionalParams: any[]) { if (this.filter != null && this.filter(level)) { return; } /* eslint-disable no-console */ - ipc.platform.log(level, message).catch((e) => console.log("Error logging", e)); + ipc.platform + .log(level, message, ...optionalParams) + .catch((e) => console.log("Error logging", e)); /* eslint-disable no-console */ switch (level) { case LogLevelType.Debug: - console.debug(message); + console.debug(message, ...optionalParams); break; case LogLevelType.Info: - console.info(message); + console.info(message, ...optionalParams); break; case LogLevelType.Warning: - console.warn(message); + console.warn(message, ...optionalParams); break; case LogLevelType.Error: - console.error(message); + console.error(message, ...optionalParams); break; default: break; diff --git a/libs/angular/src/platform/services/logging-error-handler.ts b/libs/angular/src/platform/services/logging-error-handler.ts index 522412dd288..5644272d35b 100644 --- a/libs/angular/src/platform/services/logging-error-handler.ts +++ b/libs/angular/src/platform/services/logging-error-handler.ts @@ -14,7 +14,7 @@ export class LoggingErrorHandler extends ErrorHandler { override handleError(error: any): void { try { const logService = this.injector.get(LogService, null); - logService.error(error); + logService.error("Unhandled error in angular", error); } catch { super.handleError(error); } diff --git a/libs/common/spec/intercept-console.ts b/libs/common/spec/intercept-console.ts index 01c4063e7aa..565d475cae1 100644 --- a/libs/common/spec/intercept-console.ts +++ b/libs/common/spec/intercept-console.ts @@ -2,22 +2,17 @@ const originalConsole = console; declare let console: any; -export function interceptConsole(interceptions: any): object { +export function interceptConsole(): { + log: jest.Mock; + warn: jest.Mock; + error: jest.Mock; +} { console = { - log: function () { - // eslint-disable-next-line - interceptions.log = arguments; - }, - warn: function () { - // eslint-disable-next-line - interceptions.warn = arguments; - }, - error: function () { - // eslint-disable-next-line - interceptions.error = arguments; - }, + log: jest.fn(), + warn: jest.fn(), + error: jest.fn(), }; - return interceptions; + return console; } export function restoreConsole() { diff --git a/libs/common/src/platform/abstractions/log.service.ts b/libs/common/src/platform/abstractions/log.service.ts index dffa3ca8d3e..d77a4f69906 100644 --- a/libs/common/src/platform/abstractions/log.service.ts +++ b/libs/common/src/platform/abstractions/log.service.ts @@ -1,9 +1,9 @@ import { LogLevelType } from "../enums/log-level-type.enum"; export abstract class LogService { - abstract debug(message: string): void; - abstract info(message: string): void; - abstract warning(message: string): void; - abstract error(message: string): void; - abstract write(level: LogLevelType, message: string): void; + abstract debug(message?: any, ...optionalParams: any[]): void; + abstract info(message?: any, ...optionalParams: any[]): void; + abstract warning(message?: any, ...optionalParams: any[]): void; + abstract error(message?: any, ...optionalParams: any[]): void; + abstract write(level: LogLevelType, message?: any, ...optionalParams: any[]): void; } diff --git a/libs/common/src/platform/services/console-log.service.spec.ts b/libs/common/src/platform/services/console-log.service.spec.ts index 129969bbc4f..508ca4eb327 100644 --- a/libs/common/src/platform/services/console-log.service.spec.ts +++ b/libs/common/src/platform/services/console-log.service.spec.ts @@ -2,13 +2,18 @@ import { interceptConsole, restoreConsole } from "../../../spec"; import { ConsoleLogService } from "./console-log.service"; -let caughtMessage: any; - describe("ConsoleLogService", () => { + const error = new Error("this is an error"); + const obj = { a: 1, b: 2 }; + let consoleSpy: { + log: jest.Mock; + warn: jest.Mock; + error: jest.Mock; + }; let logService: ConsoleLogService; + beforeEach(() => { - caughtMessage = {}; - interceptConsole(caughtMessage); + consoleSpy = interceptConsole(); logService = new ConsoleLogService(true); }); @@ -18,41 +23,41 @@ describe("ConsoleLogService", () => { it("filters messages below the set threshold", () => { logService = new ConsoleLogService(true, () => true); - logService.debug("debug"); - logService.info("info"); - logService.warning("warning"); - logService.error("error"); + logService.debug("debug", error, obj); + logService.info("info", error, obj); + logService.warning("warning", error, obj); + logService.error("error", error, obj); - expect(caughtMessage).toEqual({}); + expect(consoleSpy.log).not.toHaveBeenCalled(); + expect(consoleSpy.warn).not.toHaveBeenCalled(); + expect(consoleSpy.error).not.toHaveBeenCalled(); }); + it("only writes debug messages in dev mode", () => { logService = new ConsoleLogService(false); logService.debug("debug message"); - expect(caughtMessage.log).toBeUndefined(); + expect(consoleSpy.log).not.toHaveBeenCalled(); }); it("writes debug/info messages to console.log", () => { - logService.debug("this is a debug message"); - expect(caughtMessage).toMatchObject({ - log: { "0": "this is a debug message" }, - }); + logService.debug("this is a debug message", error, obj); + logService.info("this is an info message", error, obj); - logService.info("this is an info message"); - expect(caughtMessage).toMatchObject({ - log: { "0": "this is an info message" }, - }); + expect(consoleSpy.log).toHaveBeenCalledTimes(2); + expect(consoleSpy.log).toHaveBeenCalledWith("this is a debug message", error, obj); + expect(consoleSpy.log).toHaveBeenCalledWith("this is an info message", error, obj); }); + it("writes warning messages to console.warn", () => { - logService.warning("this is a warning message"); - expect(caughtMessage).toMatchObject({ - warn: { 0: "this is a warning message" }, - }); + logService.warning("this is a warning message", error, obj); + + expect(consoleSpy.warn).toHaveBeenCalledWith("this is a warning message", error, obj); }); + it("writes error messages to console.error", () => { - logService.error("this is an error message"); - expect(caughtMessage).toMatchObject({ - error: { 0: "this is an error message" }, - }); + logService.error("this is an error message", error, obj); + + expect(consoleSpy.error).toHaveBeenCalledWith("this is an error message", error, obj); }); }); diff --git a/libs/common/src/platform/services/console-log.service.ts b/libs/common/src/platform/services/console-log.service.ts index 3eb3ad1881d..a1480a0c267 100644 --- a/libs/common/src/platform/services/console-log.service.ts +++ b/libs/common/src/platform/services/console-log.service.ts @@ -9,26 +9,26 @@ export class ConsoleLogService implements LogServiceAbstraction { protected filter: (level: LogLevelType) => boolean = null, ) {} - debug(message: string) { + debug(message?: any, ...optionalParams: any[]) { if (!this.isDev) { return; } - this.write(LogLevelType.Debug, message); + this.write(LogLevelType.Debug, message, ...optionalParams); } - info(message: string) { - this.write(LogLevelType.Info, message); + info(message?: any, ...optionalParams: any[]) { + this.write(LogLevelType.Info, message, ...optionalParams); } - warning(message: string) { - this.write(LogLevelType.Warning, message); + warning(message?: any, ...optionalParams: any[]) { + this.write(LogLevelType.Warning, message, ...optionalParams); } - error(message: string) { - this.write(LogLevelType.Error, message); + error(message?: any, ...optionalParams: any[]) { + this.write(LogLevelType.Error, message, ...optionalParams); } - write(level: LogLevelType, message: string) { + write(level: LogLevelType, message?: any, ...optionalParams: any[]) { if (this.filter != null && this.filter(level)) { return; } @@ -36,19 +36,19 @@ export class ConsoleLogService implements LogServiceAbstraction { switch (level) { case LogLevelType.Debug: // eslint-disable-next-line - console.log(message); + console.log(message, ...optionalParams); break; case LogLevelType.Info: // eslint-disable-next-line - console.log(message); + console.log(message, ...optionalParams); break; case LogLevelType.Warning: // eslint-disable-next-line - console.warn(message); + console.warn(message, ...optionalParams); break; case LogLevelType.Error: // eslint-disable-next-line - console.error(message); + console.error(message, ...optionalParams); break; default: break;