From c8c314dd35b8e832bab9e36a79ca6f2e411ab5eb Mon Sep 17 00:00:00 2001 From: Danielle Flinn <43477473+danielleflinn@users.noreply.github.com> Date: Tue, 29 Aug 2023 13:42:56 -0700 Subject: [PATCH 01/46] [PM-2866] - Update color variables for better contrast (#6078) * Update variables.scss * update toast text color to have better WCAG contrast * added toastcolor variables * Update window.main.ts * Tweaked styles * darkened backgroundAlt2 and button background * lightened button border * lightened button backgroundColor * Update window.main.ts * updated brand colors and added toastTextColor variable * lightened solarize danger variable to meet WCAG contrast with dark text * updated browser solarize variable to match tw-theme.css --- apps/browser/src/popup/scss/variables.scss | 10 +++++----- apps/desktop/src/scss/variables.scss | 8 ++++---- apps/web/src/scss/variables.scss | 6 +++--- libs/components/src/tw-theme.css | 2 +- libs/components/src/variables.scss | 8 ++++---- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/apps/browser/src/popup/scss/variables.scss b/apps/browser/src/popup/scss/variables.scss index 05843a9b351..2cdc49cd9ef 100644 --- a/apps/browser/src/popup/scss/variables.scss +++ b/apps/browser/src/popup/scss/variables.scss @@ -24,10 +24,10 @@ $gray-light: #777; $text-muted: $gray-light; $brand-primary: #175ddc; -$brand-danger: #dd4b39; -$brand-success: #00a65a; +$brand-danger: #c83522; +$brand-success: #017e45; $brand-info: #555555; -$brand-warning: #bf7e16; +$brand-warning: #8b6609; $brand-primary-accent: #1252a3; $background-color: #f0f0f0; @@ -237,7 +237,7 @@ $themes: ( passwordCountText: $nord5, calloutBorderColor: $nord0, calloutBackgroundColor: $nord2, - toastTextColor: #ffffff, + toastTextColor: #000000, svgSuffix: "-dark.svg", transparentColor: rgba(0, 0, 0, 0), dateInputColorScheme: dark, @@ -299,7 +299,7 @@ $themes: ( passwordCountText: $solarizedDarkBase2, calloutBorderColor: $solarizedDarkBase03, calloutBackgroundColor: $solarizedDarkBase01, - toastTextColor: #ffffff, + toastTextColor: #000000, svgSuffix: "-solarized.svg", transparentColor: rgba(0, 0, 0, 0), dateInputColorScheme: dark, diff --git a/apps/desktop/src/scss/variables.scss b/apps/desktop/src/scss/variables.scss index b99881134d5..3ad4c0f0754 100644 --- a/apps/desktop/src/scss/variables.scss +++ b/apps/desktop/src/scss/variables.scss @@ -20,10 +20,10 @@ $gray-light: #777; $text-muted: $gray-light; $brand-primary: #175ddc; -$brand-danger: #dd4b39; -$brand-success: #00a65a; +$brand-danger: #c83522; +$brand-success: #017e45; $brand-info: #555555; -$brand-warning: #bf7e16; +$brand-warning: #8b6609; $brand-primary-accent: #1252a3; $background-color: white; @@ -211,7 +211,7 @@ $themes: ( passwordCountText: $nord5, calloutBorderColor: $nord1, calloutBackgroundColor: $nord2, - toastTextColor: #ffffff, + toastTextColor: #000000, accountSwitcherBackgroundColor: $nord0, accountSwitcherTextColor: $nord5, svgSuffix: "-dark.svg", diff --git a/apps/web/src/scss/variables.scss b/apps/web/src/scss/variables.scss index 222c43fed19..719f403e385 100644 --- a/apps/web/src/scss/variables.scss +++ b/apps/web/src/scss/variables.scss @@ -4,10 +4,10 @@ $primary: #175ddc; $primary-accent: #1252a3; $secondary: #ced4da; $secondary-alt: #1a3b66; -$success: #00a65a; +$success: #017e45; $info: #555555; -$warning: #bf7e16; -$danger: #dd4b39; +$warning: #8b6609; +$danger: #c83522; $white: #ffffff; // Bootstrap Variable Overrides diff --git a/libs/components/src/tw-theme.css b/libs/components/src/tw-theme.css index 420b2cfd483..1ff2064fdbd 100644 --- a/libs/components/src/tw-theme.css +++ b/libs/components/src/tw-theme.css @@ -151,7 +151,7 @@ --color-text-main: 253 246 227; --color-text-muted: 147 161 161; - --color-text-contrast: 0 43 54; + --color-text-contrast: 0 0 0; --color-text-alt2: 255 255 255; --color-text-code: 240 141 199; diff --git a/libs/components/src/variables.scss b/libs/components/src/variables.scss index 1abfd645fd2..88e3cba5c3c 100644 --- a/libs/components/src/variables.scss +++ b/libs/components/src/variables.scss @@ -4,10 +4,10 @@ $primary: #175ddc; $primary-accent: #1252a3; $secondary: #ced4da; $secondary-alt: #1a3b66; -$success: #00a65a; +$success: #017e45; $info: #555555; -$warning: #bf7e16; -$danger: #dd4b39; +$warning: #8b6609; +$danger: #c83522; $white: #ffffff; // Bootstrap Variable Overrides @@ -85,7 +85,7 @@ $mfaTypes: 0, 2, 3, 4, 6; // Theme Variables // Light -$lightDangerHover: #c43421; +$lightDangerHover: #c83522; $lightInputColor: #465057; $lightInputPlaceholderColor: #b6b8b8; From 3930189c4e90408eff4a2f60499e842e5011c982 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Tue, 29 Aug 2023 17:08:02 -0400 Subject: [PATCH 02/46] PM-3632 - Vault Timeout Input comp (all clients) - resolve bug where logic which was supposed to initialize the custom vault timeout timeframe if coming from a previously selected numeric timeout was not working properly. (#6102) --- .../components/settings/vault-timeout-input.component.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/libs/angular/src/components/settings/vault-timeout-input.component.ts b/libs/angular/src/components/settings/vault-timeout-input.component.ts index b0dec5affb7..e01a01fdd1d 100644 --- a/libs/angular/src/components/settings/vault-timeout-input.component.ts +++ b/libs/angular/src/components/settings/vault-timeout-input.component.ts @@ -76,14 +76,17 @@ export class VaultTimeoutInputComponent } }); - // Assign the previous value to the custom fields + // Assign the current value to the custom fields + // so that if the user goes from a numeric value to custom + // we can initialize the custom fields with the current value + // ex: user picks 5 min, goes to custom, we want to show 0 hr, 5 min in the custom fields this.form.controls.vaultTimeout.valueChanges .pipe( filter((value) => value !== VaultTimeoutInputComponent.CUSTOM_VALUE), takeUntil(this.destroy$) ) - .subscribe((_) => { - const current = Math.max(this.form.value.vaultTimeout, 0); + .subscribe((value) => { + const current = Math.max(value, 0); this.form.patchValue({ custom: { hours: Math.floor(current / 60), From 9288367bc8925581c0620c94be9ccc94eee220cb Mon Sep 17 00:00:00 2001 From: Vince Grassia <593223+vgrassia@users.noreply.github.com> Date: Wed, 30 Aug 2023 10:46:10 -0400 Subject: [PATCH 03/46] Fix logic in workflow (#6147) --- .github/workflows/release-desktop.yml | 40 +++++++++++++-------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index 99119725a3e..22b76f46408 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -56,7 +56,7 @@ jobs: uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - name: Branch check - if: ${{ github.event.inputs.release_type != 'Dry Run' }} + if: ${{ inputs.release_type != 'Dry Run' }} run: | if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc-desktop" ]]; then echo "===================================" @@ -69,7 +69,7 @@ jobs: id: version uses: bitwarden/gh-actions/release-version-check@67ab95d7a466bcefdedf3f93cbc10bcff436edfe with: - release-type: ${{ github.event.inputs.release_type }} + release-type: ${{ inputs.release_type }} project-type: ts file: apps/desktop/src/package.json monorepo: true @@ -93,7 +93,7 @@ jobs: esac - name: Create GitHub deployment - if: ${{ github.event.inputs.release_type != 'Dry Run' }} + if: ${{ inputs.release_type != 'Dry Run' }} uses: chrnorm/deployment-action@d42cde7132fcec920de534fffc3be83794335c00 # v2.0.5 id: deployment with: @@ -122,7 +122,7 @@ jobs: cf-prod-account" - name: Download all artifacts - if: ${{ github.event.inputs.release_type != 'Dry Run' }} + if: ${{ inputs.release_type != 'Dry Run' }} uses: bitwarden/gh-actions/download-artifacts@67ab95d7a466bcefdedf3f93cbc10bcff436edfe with: workflow: build-desktop.yml @@ -131,7 +131,7 @@ jobs: path: apps/desktop/artifacts - name: Dry Run - Download all artifacts - if: ${{ github.event.inputs.release_type == 'Dry Run' }} + if: ${{ inputs.release_type == 'Dry Run' }} uses: bitwarden/gh-actions/download-artifacts@67ab95d7a466bcefdedf3f93cbc10bcff436edfe with: workflow: build-desktop.yml @@ -146,17 +146,17 @@ jobs: run: mv Bitwarden-${{ env.PKG_VERSION }}-universal.pkg Bitwarden-${{ env.PKG_VERSION }}-universal.pkg.archive - name: Set staged rollout percentage - if: ${{ github.event.inputs.electron_publish }} + if: ${{ inputs.electron_publish == 'true' }} env: RELEASE_CHANNEL: ${{ steps.release-channel.outputs.channel }} - ROLLOUT_PCT: ${{ github.event.inputs.rollout_percentage }} + ROLLOUT_PCT: ${{ inputs.rollout_percentage }} run: | echo "stagingPercentage: ${ROLLOUT_PCT}" >> apps/desktop/artifacts/${RELEASE_CHANNEL}.yml echo "stagingPercentage: ${ROLLOUT_PCT}" >> apps/desktop/artifacts/${RELEASE_CHANNEL}-linux.yml echo "stagingPercentage: ${ROLLOUT_PCT}" >> apps/desktop/artifacts/${RELEASE_CHANNEL}-mac.yml - name: Publish artifacts to S3 - if: ${{ github.event.inputs.release_type != 'Dry Run' && github.event.inputs.electron_publish }} + if: ${{ inputs.release_type != 'Dry Run' && inputs.electron_publish == 'true' }} env: AWS_ACCESS_KEY_ID: ${{ steps.retrieve-secrets.outputs.aws-electron-access-id }} AWS_SECRET_ACCESS_KEY: ${{ steps.retrieve-secrets.outputs.aws-electron-access-key }} @@ -170,7 +170,7 @@ jobs: --quiet - name: Publish artifacts to R2 - if: ${{ github.event.inputs.release_type != 'Dry Run' && github.event.inputs.electron_publish }} + if: ${{ inputs.release_type != 'Dry Run' && 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 }} @@ -192,7 +192,7 @@ jobs: - name: Create Release uses: ncipollo/release-action@a2e71bdd4e7dab70ca26a852f29600c98b33153e # v1.12.0 - if: ${{ steps.release-channel.outputs.channel == 'latest' && github.event.inputs.release_type != 'Dry Run' && inputs.github_release }} + if: ${{ steps.release-channel.outputs.channel == 'latest' && inputs.release_type != 'Dry Run' && inputs.github_release == 'true' }} env: PKG_VERSION: ${{ steps.version.outputs.version }} RELEASE_CHANNEL: ${{ steps.release-channel.outputs.channel }} @@ -230,7 +230,7 @@ jobs: draft: true - name: Update deployment status to Success - if: ${{ github.event.inputs.release_type != 'Dry Run' && success() }} + if: ${{ inputs.release_type != 'Dry Run' && success() }} uses: chrnorm/deployment-status@2afb7d27101260f4a764219439564d954d10b5b0 # v2.0.1 with: token: '${{ secrets.GITHUB_TOKEN }}' @@ -238,7 +238,7 @@ jobs: deployment-id: ${{ steps.deployment.outputs.deployment_id }} - name: Update deployment status to Failure - if: ${{ github.event.inputs.release_type != 'Dry Run' && failure() }} + if: ${{ inputs.release_type != 'Dry Run' && failure() }} uses: chrnorm/deployment-status@2afb7d27101260f4a764219439564d954d10b5b0 # v2.0.1 with: token: '${{ secrets.GITHUB_TOKEN }}' @@ -249,7 +249,7 @@ jobs: name: Deploy Snap runs-on: ubuntu-22.04 needs: setup - if: inputs.snap_publish + if: ${{ inputs.snap_publish == 'true' }} env: _PKG_VERSION: ${{ needs.setup.outputs.release-version }} steps: @@ -278,7 +278,7 @@ jobs: working-directory: apps/desktop - name: Download Snap artifact - if: ${{ github.event.inputs.release_type != 'Dry Run' }} + if: ${{ inputs.release_type != 'Dry Run' }} uses: bitwarden/gh-actions/download-artifacts@67ab95d7a466bcefdedf3f93cbc10bcff436edfe with: workflow: build-desktop.yml @@ -288,7 +288,7 @@ jobs: path: apps/desktop/dist - name: Dry Run - Download Snap artifact - if: ${{ github.event.inputs.release_type == 'Dry Run' }} + if: ${{ inputs.release_type == 'Dry Run' }} uses: bitwarden/gh-actions/download-artifacts@67ab95d7a466bcefdedf3f93cbc10bcff436edfe with: workflow: build-desktop.yml @@ -298,7 +298,7 @@ jobs: path: apps/desktop/dist - name: Deploy to Snap Store - if: ${{ github.event.inputs.release_type != 'Dry Run' }} + if: ${{ inputs.release_type != 'Dry Run' }} env: SNAPCRAFT_STORE_CREDENTIALS: ${{ steps.retrieve-secrets.outputs.snapcraft-store-token }} run: | @@ -310,7 +310,7 @@ jobs: name: Deploy Choco runs-on: windows-2019 needs: setup - if: inputs.choco_publish + if: ${{ inputs.choco_publish == 'true' }} env: _PKG_VERSION: ${{ needs.setup.outputs.release-version }} steps: @@ -346,7 +346,7 @@ jobs: working-directory: apps/desktop - name: Download choco artifact - if: ${{ github.event.inputs.release_type != 'Dry Run' }} + if: ${{ inputs.release_type != 'Dry Run' }} uses: bitwarden/gh-actions/download-artifacts@67ab95d7a466bcefdedf3f93cbc10bcff436edfe with: workflow: build-desktop.yml @@ -356,7 +356,7 @@ jobs: path: apps/desktop/dist - name: Dry Run - Download choco artifact - if: ${{ github.event.inputs.release_type == 'Dry Run' }} + if: ${{ inputs.release_type == 'Dry Run' }} uses: bitwarden/gh-actions/download-artifacts@67ab95d7a466bcefdedf3f93cbc10bcff436edfe with: workflow: build-desktop.yml @@ -366,7 +366,7 @@ jobs: path: apps/desktop/dist - name: Push to Chocolatey - if: ${{ github.event.inputs.release_type != 'Dry Run' }} + if: ${{ inputs.release_type != 'Dry Run' }} shell: pwsh run: choco push --source=https://push.chocolatey.org/ working-directory: apps/desktop/dist From b444eed0b5b61fb66eb89d1c90ee0ded89a3f6d9 Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez Date: Wed, 30 Aug 2023 10:18:20 -0500 Subject: [PATCH 04/46] [PM-3589] Context Menu No Longer Shows Autofill Ciphers (#6085) * [PM-3589] Context Menu No Longer Shows Autofill Ciphers * [PM-3589] Ensuring that passwordless users can also access ciphers that require reprompt * [PM-3589] Fixing jest test * [PM-3589] Fixing issue where context menu autofill does not allow filling when passwordless setup is in place --- .../cipher-context-menu-handler.spec.ts | 57 +------------------ .../browser/cipher-context-menu-handler.ts | 14 +---- .../context-menu-clicked-handler.spec.ts | 5 +- .../browser/context-menu-clicked-handler.ts | 21 +++++-- .../browser/src/background/main.background.ts | 6 +- .../popup/components/vault/view.component.ts | 10 +--- 6 files changed, 32 insertions(+), 81 deletions(-) diff --git a/apps/browser/src/autofill/browser/cipher-context-menu-handler.spec.ts b/apps/browser/src/autofill/browser/cipher-context-menu-handler.spec.ts index dbe391ce4ab..d7cac8d44b2 100644 --- a/apps/browser/src/autofill/browser/cipher-context-menu-handler.spec.ts +++ b/apps/browser/src/autofill/browser/cipher-context-menu-handler.spec.ts @@ -1,7 +1,6 @@ import { mock, MockProxy } from "jest-mock-extended"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; @@ -14,7 +13,6 @@ describe("CipherContextMenuHandler", () => { let mainContextMenuHandler: MockProxy; let authService: MockProxy; let cipherService: MockProxy; - let userVerificationService: MockProxy; let sut: CipherContextMenuHandler; @@ -22,17 +20,10 @@ describe("CipherContextMenuHandler", () => { mainContextMenuHandler = mock(); authService = mock(); cipherService = mock(); - userVerificationService = mock(); - userVerificationService.hasMasterPassword.mockResolvedValue(true); jest.spyOn(MainContextMenuHandler, "removeAll").mockResolvedValue(); - sut = new CipherContextMenuHandler( - mainContextMenuHandler, - authService, - cipherService, - userVerificationService - ); + sut = new CipherContextMenuHandler(mainContextMenuHandler, authService, cipherService); }); afterEach(() => jest.resetAllMocks()); @@ -78,7 +69,7 @@ describe("CipherContextMenuHandler", () => { expect(mainContextMenuHandler.noLogins).toHaveBeenCalledTimes(1); }); - it("only adds valid ciphers", async () => { + it("only adds login ciphers including ciphers that require reprompt", async () => { authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.Unlocked); mainContextMenuHandler.init.mockResolvedValue(true); @@ -90,47 +81,6 @@ describe("CipherContextMenuHandler", () => { name: "Test Cipher", login: { username: "Test Username" }, }; - - cipherService.getAllDecryptedForUrl.mockResolvedValue([ - null, // invalid cipher - undefined, // invalid cipher - { type: CipherType.Card }, // invalid cipher - { type: CipherType.Login, reprompt: CipherRepromptType.Password }, // invalid cipher - realCipher, // valid cipher - ] as any[]); - - await sut.update("https://test.com"); - - expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledTimes(1); - - expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith("https://test.com"); - - expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledTimes(2); - - expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledWith( - "Test Cipher (Test Username)", - "5", - "https://test.com", - realCipher - ); - }); - - it("adds ciphers with master password reprompt if the user does not have a master password", async () => { - authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.Unlocked); - - // User does not have a master password, or has one but hasn't logged in with it (key connector user or TDE user) - userVerificationService.hasMasterPasswordAndMasterKeyHash.mockResolvedValue(false); - - mainContextMenuHandler.init.mockResolvedValue(true); - - const realCipher = { - id: "5", - type: CipherType.Login, - reprompt: CipherRepromptType.None, - name: "Test Cipher", - login: { username: "Test Username" }, - }; - const repromptCipher = { id: "6", type: CipherType.Login, @@ -143,8 +93,8 @@ describe("CipherContextMenuHandler", () => { null, // invalid cipher undefined, // invalid cipher { type: CipherType.Card }, // invalid cipher - repromptCipher, // valid cipher realCipher, // valid cipher + repromptCipher, ] as any[]); await sut.update("https://test.com"); @@ -153,7 +103,6 @@ describe("CipherContextMenuHandler", () => { expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith("https://test.com"); - // Should call this twice, once for each valid cipher expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledTimes(2); expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledWith( diff --git a/apps/browser/src/autofill/browser/cipher-context-menu-handler.ts b/apps/browser/src/autofill/browser/cipher-context-menu-handler.ts index 1d1be8f8386..fe6479aae51 100644 --- a/apps/browser/src/autofill/browser/cipher-context-menu-handler.ts +++ b/apps/browser/src/autofill/browser/cipher-context-menu-handler.ts @@ -1,5 +1,4 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -12,7 +11,6 @@ import { authServiceFactory, AuthServiceInitOptions, } from "../../auth/background/service-factories/auth-service.factory"; -import { userVerificationServiceFactory } from "../../auth/background/service-factories/user-verification-service.factory"; import { Account } from "../../models/account"; import { CachedServices } from "../../platform/background/service-factories/factory-options"; import { BrowserApi } from "../../platform/browser/browser-api"; @@ -39,8 +37,7 @@ export class CipherContextMenuHandler { constructor( private mainContextMenuHandler: MainContextMenuHandler, private authService: AuthService, - private cipherService: CipherService, - private userVerificationService: UserVerificationService + private cipherService: CipherService ) {} static async create(cachedServices: CachedServices) { @@ -79,8 +76,7 @@ export class CipherContextMenuHandler { return new CipherContextMenuHandler( await MainContextMenuHandler.mv3Create(cachedServices), await authServiceFactory(cachedServices, serviceOptions), - await cipherServiceFactory(cachedServices, serviceOptions), - await userVerificationServiceFactory(cachedServices, serviceOptions) + await cipherServiceFactory(cachedServices, serviceOptions) ); } @@ -180,11 +176,7 @@ export class CipherContextMenuHandler { } private async updateForCipher(url: string, cipher: CipherView) { - if ( - cipher == null || - cipher.type !== CipherType.Login || - (await this.userVerificationService.hasMasterPasswordAndMasterKeyHash()) - ) { + if (cipher == null || cipher.type !== CipherType.Login) { return; } diff --git a/apps/browser/src/autofill/browser/context-menu-clicked-handler.spec.ts b/apps/browser/src/autofill/browser/context-menu-clicked-handler.spec.ts index a9dbcbaacc5..021d15df89e 100644 --- a/apps/browser/src/autofill/browser/context-menu-clicked-handler.spec.ts +++ b/apps/browser/src/autofill/browser/context-menu-clicked-handler.spec.ts @@ -3,6 +3,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { TotpService } from "@bitwarden/common/abstractions/totp.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherType } from "@bitwarden/common/vault/enums/cipher-type"; @@ -63,6 +64,7 @@ describe("ContextMenuClickedHandler", () => { let cipherService: MockProxy; let totpService: MockProxy; let eventCollectionService: MockProxy; + let userVerificationService: MockProxy; let sut: ContextMenuClickedHandler; @@ -82,7 +84,8 @@ describe("ContextMenuClickedHandler", () => { authService, cipherService, totpService, - eventCollectionService + eventCollectionService, + userVerificationService ); }); diff --git a/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts b/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts index 38e605abe70..9a14ea06da0 100644 --- a/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts +++ b/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts @@ -1,6 +1,7 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { TotpService } from "@bitwarden/common/abstractions/totp.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { EventType } from "@bitwarden/common/enums"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; @@ -14,6 +15,7 @@ import { AuthServiceInitOptions, } from "../../auth/background/service-factories/auth-service.factory"; import { totpServiceFactory } from "../../auth/background/service-factories/totp-service.factory"; +import { userVerificationServiceFactory } from "../../auth/background/service-factories/user-verification-service.factory"; import LockedVaultPendingNotificationsItem from "../../background/models/lockedVaultPendingNotificationsItem"; import { eventCollectionServiceFactory } from "../../background/service-factories/event-collection-service.factory"; import { Account } from "../../models/account"; @@ -56,7 +58,8 @@ export class ContextMenuClickedHandler { private authService: AuthService, private cipherService: CipherService, private totpService: TotpService, - private eventCollectionService: EventCollectionService + private eventCollectionService: EventCollectionService, + private userVerificationService: UserVerificationService ) {} static async mv3Create(cachedServices: CachedServices) { @@ -109,7 +112,8 @@ export class ContextMenuClickedHandler { await authServiceFactory(cachedServices, serviceOptions), await cipherServiceFactory(cachedServices, serviceOptions), await totpServiceFactory(cachedServices, serviceOptions), - await eventCollectionServiceFactory(cachedServices, serviceOptions) + await eventCollectionServiceFactory(cachedServices, serviceOptions), + await userVerificationServiceFactory(cachedServices, serviceOptions) ); } @@ -204,7 +208,7 @@ export class ContextMenuClickedHandler { return; } - if (cipher.reprompt !== CipherRepromptType.None) { + if (await this.isPasswordRepromptRequired(cipher)) { await BrowserApi.tabSendMessageData(tab, "passwordReprompt", { cipherId: cipher.id, action: AUTOFILL_ID, @@ -218,7 +222,7 @@ export class ContextMenuClickedHandler { this.copyToClipboard({ text: cipher.login.username, tab: tab }); break; case COPY_PASSWORD_ID: - if (cipher.reprompt !== CipherRepromptType.None) { + if (await this.isPasswordRepromptRequired(cipher)) { await BrowserApi.tabSendMessageData(tab, "passwordReprompt", { cipherId: cipher.id, action: COPY_PASSWORD_ID, @@ -230,7 +234,7 @@ export class ContextMenuClickedHandler { break; case COPY_VERIFICATIONCODE_ID: - if (cipher.reprompt !== CipherRepromptType.None) { + if (await this.isPasswordRepromptRequired(cipher)) { await BrowserApi.tabSendMessageData(tab, "passwordReprompt", { cipherId: cipher.id, action: COPY_VERIFICATIONCODE_ID, @@ -246,6 +250,13 @@ export class ContextMenuClickedHandler { } } + private async isPasswordRepromptRequired(cipher: CipherView): Promise { + return ( + cipher.reprompt === CipherRepromptType.Password && + (await this.userVerificationService.hasMasterPasswordAndMasterKeyHash()) + ); + } + private async getIdentifier(tab: chrome.tabs.Tab, info: chrome.contextMenus.OnClickData) { return new Promise((resolve, reject) => { BrowserApi.sendTabsMessage( diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 617acc2bf78..c65a8697631 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -635,7 +635,8 @@ export default class MainBackground { this.authService, this.cipherService, this.totpService, - this.eventCollectionService + this.eventCollectionService, + this.userVerificationService ); this.contextMenusBackground = new ContextMenusBackground(contextMenuClickedHandler); @@ -670,8 +671,7 @@ export default class MainBackground { this.cipherContextMenuHandler = new CipherContextMenuHandler( this.mainContextMenuHandler, this.authService, - this.cipherService, - this.userVerificationService + this.cipherService ); } } diff --git a/apps/browser/src/vault/popup/components/vault/view.component.ts b/apps/browser/src/vault/popup/components/vault/view.component.ts index a70a11475ca..8f45547737a 100644 --- a/apps/browser/src/vault/popup/components/vault/view.component.ts +++ b/apps/browser/src/vault/popup/components/vault/view.component.ts @@ -170,8 +170,8 @@ export class ViewComponent extends BaseViewComponent { switch (this.loadAction) { case AUTOFILL_ID: - this.fillCipher(); - return; + await this.fillCipher(); + break; case COPY_USERNAME_ID: await this.copy(this.cipher.login.username, "username", "Username"); break; @@ -186,7 +186,7 @@ export class ViewComponent extends BaseViewComponent { } if (this.inPopout && this.loadAction) { - this.close(); + setTimeout(() => this.close(), 1000); } } @@ -238,10 +238,6 @@ export class ViewComponent extends BaseViewComponent { const didAutofill = await this.doAutofill(); if (didAutofill) { this.platformUtilsService.showToast("success", null, this.i18nService.t("autoFillSuccess")); - - if (this.inPopout) { - this.close(); - } } } From 3340af8084514ffb1edc300a2dae8d7af95798b1 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Wed, 30 Aug 2023 12:57:20 -0500 Subject: [PATCH 05/46] PM-3585 Improve state migrations (#5009) * WIP: safer state migrations Co-authored-by: Justin Baur * Add min version check and remove old migrations Co-authored-by: Oscar Hinton * Add rollback and version checking * Add state version move migration * Expand tests and improve typing for Migrations * Remove StateMigration Service * Rewrite version 5 and 6 migrations * Add all but initial migration to supported migrations * Handle stateVersion location in migrator update versions * Move to unique migrations directory * Disallow imports outside of state-migrations * Lint and test fixes * Do not run migrations if we cannot determine state * Fix desktop background StateService build * Document Migration builder class * Add debug logging to migrations * Comment on migrator overrides * Use specific property names * `npm run prettier` :robot: * Insert new migration * Set stateVersion when creating new globals object * PR comments * Fix migrate imports * Move migration building into `migrate` function * Export current version from migration definitions * Move file version concerns to migrator * Update migrate spec to reflect new version requirements * Fix import paths * Prefer unique state data * Remove unnecessary async * Prefer to not use `any` --------- Co-authored-by: Justin Baur Co-authored-by: Oscar Hinton --- .../browser/cipher-context-menu-handler.ts | 3 - .../browser/context-menu-clicked-handler.ts | 3 - .../browser/main-context-menu-handler.ts | 3 - .../browser/src/background/main.background.ts | 8 - .../state-migration-service.factory.ts | 40 -- .../state-service.factory.ts | 8 +- .../platform/listeners/on-command-listener.ts | 6 - .../platform/listeners/on-install-listener.ts | 3 - .../src/platform/listeners/update-badge.ts | 3 - .../services/browser-state.service.spec.ts | 4 - .../services/browser-state.service.ts | 3 - .../src/popup/services/services.module.ts | 18 +- apps/cli/src/bw.ts | 9 - .../src/app/services/services.module.ts | 2 - apps/desktop/src/main.ts | 1 - apps/web/src/app/core/core.module.ts | 7 - .../src/app/core/state-migration.service.ts | 13 - apps/web/src/app/core/state/state.service.ts | 3 - .../src/services/jslib-services.module.ts | 8 - libs/common/src/enums/index.ts | 1 - libs/common/src/enums/state-version.enum.ts | 10 - .../abstractions/state-migration.service.ts | 4 - .../platform/abstractions/state.service.ts | 2 - .../platform/models/domain/global-state.ts | 3 +- .../services/state-migration.service.spec.ts | 216 ------- .../services/state-migration.service.ts | 587 ------------------ .../src/platform/services/state.service.ts | 24 +- .../src/state-migrations/.eslintrc.json | 24 + libs/common/src/state-migrations/index.ts | 1 + .../src/state-migrations/migrate.spec.ts | 67 ++ libs/common/src/state-migrations/migrate.ts | 60 ++ .../migration-builder.spec.ts | 117 ++++ .../src/state-migrations/migration-builder.ts | 106 ++++ .../state-migrations/migration-helper.spec.ts | 84 +++ .../src/state-migrations/migration-helper.ts | 37 ++ .../migrations/3-fix-premium.spec.ts | 111 ++++ .../migrations/3-fix-premium.ts | 48 ++ .../4-remove-ever-been-unlocked.spec.ts | 75 +++ .../migrations/4-remove-ever-been-unlocked.ts | 32 + .../5-add-key-type-to-org-keys.spec.ts | 141 +++++ .../migrations/5-add-key-type-to-org-keys.ts | 67 ++ .../6-remove-legacy-etm-key.spec.ts | 80 +++ .../migrations/6-remove-legacy-etm-key.ts | 32 + ...e-biometric-auto-prompt-to-account.spec.ts | 102 +++ ...7-move-biometric-auto-prompt-to-account.ts | 45 ++ .../migrations/8-move-state-version.spec.ts | 90 +++ .../migrations/8-move-state-version.ts | 37 ++ .../migrations/min-version.spec.ts | 29 + .../migrations/min-version.ts | 26 + .../src/state-migrations/migrator.spec.ts | 75 +++ libs/common/src/state-migrations/migrator.ts | 40 ++ 51 files changed, 1538 insertions(+), 980 deletions(-) delete mode 100644 apps/browser/src/platform/background/service-factories/state-migration-service.factory.ts delete mode 100644 apps/web/src/app/core/state-migration.service.ts delete mode 100644 libs/common/src/enums/state-version.enum.ts delete mode 100644 libs/common/src/platform/abstractions/state-migration.service.ts delete mode 100644 libs/common/src/platform/services/state-migration.service.spec.ts delete mode 100644 libs/common/src/platform/services/state-migration.service.ts create mode 100644 libs/common/src/state-migrations/.eslintrc.json create mode 100644 libs/common/src/state-migrations/index.ts create mode 100644 libs/common/src/state-migrations/migrate.spec.ts create mode 100644 libs/common/src/state-migrations/migrate.ts create mode 100644 libs/common/src/state-migrations/migration-builder.spec.ts create mode 100644 libs/common/src/state-migrations/migration-builder.ts create mode 100644 libs/common/src/state-migrations/migration-helper.spec.ts create mode 100644 libs/common/src/state-migrations/migration-helper.ts create mode 100644 libs/common/src/state-migrations/migrations/3-fix-premium.spec.ts create mode 100644 libs/common/src/state-migrations/migrations/3-fix-premium.ts create mode 100644 libs/common/src/state-migrations/migrations/4-remove-ever-been-unlocked.spec.ts create mode 100644 libs/common/src/state-migrations/migrations/4-remove-ever-been-unlocked.ts create mode 100644 libs/common/src/state-migrations/migrations/5-add-key-type-to-org-keys.spec.ts create mode 100644 libs/common/src/state-migrations/migrations/5-add-key-type-to-org-keys.ts create mode 100644 libs/common/src/state-migrations/migrations/6-remove-legacy-etm-key.spec.ts create mode 100644 libs/common/src/state-migrations/migrations/6-remove-legacy-etm-key.ts create mode 100644 libs/common/src/state-migrations/migrations/7-move-biometric-auto-prompt-to-account.spec.ts create mode 100644 libs/common/src/state-migrations/migrations/7-move-biometric-auto-prompt-to-account.ts create mode 100644 libs/common/src/state-migrations/migrations/8-move-state-version.spec.ts create mode 100644 libs/common/src/state-migrations/migrations/8-move-state-version.ts create mode 100644 libs/common/src/state-migrations/migrations/min-version.spec.ts create mode 100644 libs/common/src/state-migrations/migrations/min-version.ts create mode 100644 libs/common/src/state-migrations/migrator.spec.ts create mode 100644 libs/common/src/state-migrations/migrator.ts diff --git a/apps/browser/src/autofill/browser/cipher-context-menu-handler.ts b/apps/browser/src/autofill/browser/cipher-context-menu-handler.ts index fe6479aae51..6140db260f5 100644 --- a/apps/browser/src/autofill/browser/cipher-context-menu-handler.ts +++ b/apps/browser/src/autofill/browser/cipher-context-menu-handler.ts @@ -66,9 +66,6 @@ export class CipherContextMenuHandler { clipboardWriteCallback: NOT_IMPLEMENTED, win: self, }, - stateMigrationServiceOptions: { - stateFactory: stateFactory, - }, stateServiceOptions: { stateFactory: stateFactory, }, diff --git a/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts b/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts index 9a14ea06da0..a6bff50a195 100644 --- a/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts +++ b/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts @@ -88,9 +88,6 @@ export class ContextMenuClickedHandler { clipboardWriteCallback: NOT_IMPLEMENTED, win: self, }, - stateMigrationServiceOptions: { - stateFactory: stateFactory, - }, stateServiceOptions: { stateFactory: stateFactory, }, diff --git a/apps/browser/src/autofill/browser/main-context-menu-handler.ts b/apps/browser/src/autofill/browser/main-context-menu-handler.ts index 9b16aa266db..b9af3dd191f 100644 --- a/apps/browser/src/autofill/browser/main-context-menu-handler.ts +++ b/apps/browser/src/autofill/browser/main-context-menu-handler.ts @@ -79,9 +79,6 @@ export class MainContextMenuHandler { logServiceOptions: { isDev: false, }, - stateMigrationServiceOptions: { - stateFactory: stateFactory, - }, stateServiceOptions: { stateFactory: stateFactory, }, diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index c65a8697631..31e81c198f6 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -59,7 +59,6 @@ import { EncryptServiceImplementation } from "@bitwarden/common/platform/service import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation"; import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; -import { StateMigrationService } from "@bitwarden/common/platform/services/state-migration.service"; import { SystemService } from "@bitwarden/common/platform/services/system.service"; import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service"; import { AvatarUpdateService } from "@bitwarden/common/services/account/avatar-update.service"; @@ -177,7 +176,6 @@ export default class MainBackground { searchService: SearchServiceAbstraction; notificationsService: NotificationsServiceAbstraction; stateService: StateServiceAbstraction; - stateMigrationService: StateMigrationService; systemService: SystemServiceAbstraction; eventCollectionService: EventCollectionServiceAbstraction; eventUploadService: EventUploadServiceAbstraction; @@ -262,17 +260,11 @@ export default class MainBackground { new KeyGenerationService(this.cryptoFunctionService) ) : new MemoryStorageService(); - this.stateMigrationService = new StateMigrationService( - this.storageService, - this.secureStorageService, - new StateFactory(GlobalState, Account) - ); this.stateService = new BrowserStateService( this.storageService, this.secureStorageService, this.memoryStorageService, this.logService, - this.stateMigrationService, new StateFactory(GlobalState, Account) ); this.platformUtilsService = new BrowserPlatformUtilsService( diff --git a/apps/browser/src/platform/background/service-factories/state-migration-service.factory.ts b/apps/browser/src/platform/background/service-factories/state-migration-service.factory.ts deleted file mode 100644 index 8d4ee969583..00000000000 --- a/apps/browser/src/platform/background/service-factories/state-migration-service.factory.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; -import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; -import { StateMigrationService } from "@bitwarden/common/platform/services/state-migration.service"; - -import { Account } from "../../../models/account"; - -import { CachedServices, factory, FactoryOptions } from "./factory-options"; -import { - diskStorageServiceFactory, - DiskStorageServiceInitOptions, - secureStorageServiceFactory, - SecureStorageServiceInitOptions, -} from "./storage-service.factory"; - -type StateMigrationServiceFactoryOptions = FactoryOptions & { - stateMigrationServiceOptions: { - stateFactory: StateFactory; - }; -}; - -export type StateMigrationServiceInitOptions = StateMigrationServiceFactoryOptions & - DiskStorageServiceInitOptions & - SecureStorageServiceInitOptions; - -export function stateMigrationServiceFactory( - cache: { stateMigrationService?: StateMigrationService } & CachedServices, - opts: StateMigrationServiceInitOptions -): Promise { - return factory( - cache, - "stateMigrationService", - opts, - async () => - new StateMigrationService( - await diskStorageServiceFactory(cache, opts), - await secureStorageServiceFactory(cache, opts), - opts.stateMigrationServiceOptions.stateFactory - ) - ); -} 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 f926d428890..7d3aaf9b6f3 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 @@ -6,10 +6,6 @@ import { BrowserStateService } from "../../services/browser-state.service"; import { CachedServices, factory, FactoryOptions } from "./factory-options"; import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory"; -import { - stateMigrationServiceFactory, - StateMigrationServiceInitOptions, -} from "./state-migration-service.factory"; import { diskStorageServiceFactory, secureStorageServiceFactory, @@ -30,8 +26,7 @@ export type StateServiceInitOptions = StateServiceFactoryOptions & DiskStorageServiceInitOptions & SecureStorageServiceInitOptions & MemoryStorageServiceInitOptions & - LogServiceInitOptions & - StateMigrationServiceInitOptions; + LogServiceInitOptions; export async function stateServiceFactory( cache: { stateService?: BrowserStateService } & CachedServices, @@ -47,7 +42,6 @@ export async function stateServiceFactory( await secureStorageServiceFactory(cache, opts), await memoryStorageServiceFactory(cache, opts), await logServiceFactory(cache, opts), - await stateMigrationServiceFactory(cache, opts), opts.stateServiceOptions.stateFactory, opts.stateServiceOptions.useAccountCache ) diff --git a/apps/browser/src/platform/listeners/on-command-listener.ts b/apps/browser/src/platform/listeners/on-command-listener.ts index 65af31e173c..0e2cf03828d 100644 --- a/apps/browser/src/platform/listeners/on-command-listener.ts +++ b/apps/browser/src/platform/listeners/on-command-listener.ts @@ -47,9 +47,6 @@ const doAutoFillLogin = async (tab: chrome.tabs.Tab): Promise => { stateServiceOptions: { stateFactory: new StateFactory(GlobalState, Account), }, - stateMigrationServiceOptions: { - stateFactory: new StateFactory(GlobalState, Account), - }, apiServiceOptions: { logoutCallback: () => Promise.resolve(), }, @@ -94,9 +91,6 @@ const doGeneratePasswordToClipboard = async (tab: chrome.tabs.Tab): Promise Promise.resolve(), win: self, }, - stateMigrationServiceOptions: { - stateFactory: stateFactory, - }, stateServiceOptions: { stateFactory: stateFactory, }, diff --git a/apps/browser/src/platform/listeners/on-install-listener.ts b/apps/browser/src/platform/listeners/on-install-listener.ts index 480e811fd26..0394941e283 100644 --- a/apps/browser/src/platform/listeners/on-install-listener.ts +++ b/apps/browser/src/platform/listeners/on-install-listener.ts @@ -23,9 +23,6 @@ export async function onInstallListener(details: chrome.runtime.InstalledDetails stateServiceOptions: { stateFactory: new StateFactory(GlobalState, Account), }, - stateMigrationServiceOptions: { - stateFactory: new StateFactory(GlobalState, Account), - }, }; const environmentService = await environmentServiceFactory(cache, opts); diff --git a/apps/browser/src/platform/listeners/update-badge.ts b/apps/browser/src/platform/listeners/update-badge.ts index 89b620ad6fe..1b692eb9b97 100644 --- a/apps/browser/src/platform/listeners/update-badge.ts +++ b/apps/browser/src/platform/listeners/update-badge.ts @@ -272,9 +272,6 @@ export class UpdateBadge { stateServiceOptions: { stateFactory: new StateFactory(GlobalState, Account), }, - stateMigrationServiceOptions: { - stateFactory: new StateFactory(GlobalState, Account), - }, apiServiceOptions: { logoutCallback: () => Promise.reject("not implemented"), }, 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 d6bb83f7fb5..0712416172c 100644 --- a/apps/browser/src/platform/services/browser-state.service.spec.ts +++ b/apps/browser/src/platform/services/browser-state.service.spec.ts @@ -8,7 +8,6 @@ import { import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { State } from "@bitwarden/common/platform/models/domain/state"; -import { StateMigrationService } from "@bitwarden/common/platform/services/state-migration.service"; import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; @@ -26,7 +25,6 @@ describe("Browser State Service", () => { let secureStorageService: MockProxy; let diskStorageService: MockProxy; let logService: MockProxy; - let stateMigrationService: MockProxy; let stateFactory: MockProxy>; let useAccountCache: boolean; @@ -39,7 +37,6 @@ describe("Browser State Service", () => { secureStorageService = mock(); diskStorageService = mock(); logService = mock(); - stateMigrationService = mock(); stateFactory = mock(); // turn off account cache for tests useAccountCache = false; @@ -64,7 +61,6 @@ describe("Browser State Service", () => { secureStorageService, memoryStorageService, logService, - stateMigrationService, stateFactory, useAccountCache ); diff --git a/apps/browser/src/platform/services/browser-state.service.ts b/apps/browser/src/platform/services/browser-state.service.ts index 34fa1a1d0f3..5e356e7fbe8 100644 --- a/apps/browser/src/platform/services/browser-state.service.ts +++ b/apps/browser/src/platform/services/browser-state.service.ts @@ -1,7 +1,6 @@ import { BehaviorSubject } from "rxjs"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { StateMigrationService } from "@bitwarden/common/platform/abstractions/state-migration.service"; import { AbstractStorageService, AbstractMemoryStorageService, @@ -41,7 +40,6 @@ export class BrowserStateService secureStorageService: AbstractStorageService, memoryStorageService: AbstractMemoryStorageService, logService: LogService, - stateMigrationService: StateMigrationService, stateFactory: StateFactory, useAccountCache = true ) { @@ -50,7 +48,6 @@ export class BrowserStateService secureStorageService, memoryStorageService, logService, - stateMigrationService, stateFactory, useAccountCache ); diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 191e2c78060..261f6abe37d 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -47,7 +47,6 @@ import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platfor import { LogService as LogServiceAbstraction } 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 { StateMigrationService } from "@bitwarden/common/platform/abstractions/state-migration.service"; import { StateService as BaseStateServiceAbstraction, StateService, @@ -442,36 +441,23 @@ function getBgService(service: keyof MainBackground) { provide: MEMORY_STORAGE, useFactory: getBgService("memoryStorageService"), }, - { - provide: StateMigrationService, - useFactory: getBgService("stateMigrationService"), - deps: [], - }, { provide: StateServiceAbstraction, useFactory: ( storageService: AbstractStorageService, secureStorageService: AbstractStorageService, memoryStorageService: AbstractMemoryStorageService, - logService: LogServiceAbstraction, - stateMigrationService: StateMigrationService + logService: LogServiceAbstraction ) => { return new BrowserStateService( storageService, secureStorageService, memoryStorageService, logService, - stateMigrationService, new StateFactory(GlobalState, Account) ); }, - deps: [ - AbstractStorageService, - SECURE_STORAGE, - MEMORY_STORAGE, - LogServiceAbstraction, - StateMigrationService, - ], + deps: [AbstractStorageService, SECURE_STORAGE, MEMORY_STORAGE, LogServiceAbstraction], }, { provide: UsernameGenerationServiceAbstraction, diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 1bcaa1a2acf..42ba158ee72 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -37,7 +37,6 @@ import { EnvironmentService } from "@bitwarden/common/platform/services/environm import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { NoopMessagingService } from "@bitwarden/common/platform/services/noop-messaging.service"; -import { StateMigrationService } from "@bitwarden/common/platform/services/state-migration.service"; import { StateService } from "@bitwarden/common/platform/services/state.service"; import { AuditService } from "@bitwarden/common/services/audit.service"; import { OrganizationUserServiceImplementation } from "@bitwarden/common/services/organization-user/organization-user.service.implementation"; @@ -136,7 +135,6 @@ export class Main { keyConnectorService: KeyConnectorService; userVerificationService: UserVerificationService; stateService: StateService; - stateMigrationService: StateMigrationService; organizationService: OrganizationService; providerService: ProviderService; twoFactorService: TwoFactorService; @@ -188,18 +186,11 @@ export class Main { this.memoryStorageService = new MemoryStorageService(); - this.stateMigrationService = new StateMigrationService( - this.storageService, - this.secureStorageService, - new StateFactory(GlobalState, Account) - ); - this.stateService = new StateService( this.storageService, this.secureStorageService, this.memoryStorageService, this.logService, - this.stateMigrationService, new StateFactory(GlobalState, Account) ); diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index ded0366dc16..42208077c33 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -28,7 +28,6 @@ import { } 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 { StateMigrationService as StateMigrationServiceAbstraction } from "@bitwarden/common/platform/abstractions/state-migration.service"; import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/platform/abstractions/system.service"; @@ -134,7 +133,6 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK"); SECURE_STORAGE, MEMORY_STORAGE, LogService, - StateMigrationServiceAbstraction, STATE_FACTORY, STATE_SERVICE_USE_CACHE, ], diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 9f15d0d24d9..5107d31b1c5 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -90,7 +90,6 @@ export class Main { null, this.memoryStorageService, this.logService, - null, new StateFactory(GlobalState, Account), false // Do not use disk caching because this will get out of sync with the renderer service ); diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index 03f20ad2955..b2e44d7e3db 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -17,7 +17,6 @@ import { FileDownloadService } from "@bitwarden/common/platform/abstractions/fil import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.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 { StateMigrationService as StateMigrationServiceAbstraction } from "@bitwarden/common/platform/abstractions/state-migration.service"; import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; @@ -27,7 +26,6 @@ import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "@ import { PolicyListService } from "../admin-console/core/policy-list.service"; import { HtmlStorageService } from "../core/html-storage.service"; import { I18nService } from "../core/i18n.service"; -import { StateMigrationService } from "../core/state-migration.service"; import { CollectionAdminService } from "../vault/core/collection-admin.service"; import { PasswordRepromptService } from "../vault/core/password-reprompt.service"; @@ -84,11 +82,6 @@ import { WebPlatformUtilsService } from "./web-platform-utils.service"; }, { provide: MessagingServiceAbstraction, useClass: BroadcasterMessagingService }, { provide: ModalServiceAbstraction, useClass: ModalService }, - { - provide: StateMigrationServiceAbstraction, - useClass: StateMigrationService, - deps: [AbstractStorageService, SECURE_STORAGE, STATE_FACTORY], - }, StateService, { provide: BaseStateServiceAbstraction, diff --git a/apps/web/src/app/core/state-migration.service.ts b/apps/web/src/app/core/state-migration.service.ts deleted file mode 100644 index c1d6e2ded5d..00000000000 --- a/apps/web/src/app/core/state-migration.service.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { StateMigrationService as BaseStateMigrationService } from "@bitwarden/common/platform/services/state-migration.service"; - -import { Account } from "./state/account"; -import { GlobalState } from "./state/global-state"; - -export class StateMigrationService extends BaseStateMigrationService { - protected async migrationStateFrom1To2(): Promise { - await super.migrateStateFrom1To2(); - const globals = (await this.get("global")) ?? this.stateFactory.createGlobal(null); - globals.rememberEmail = (await this.get("rememberEmail")) ?? globals.rememberEmail; - await this.set("global", globals); - } -} diff --git a/apps/web/src/app/core/state/state.service.ts b/apps/web/src/app/core/state/state.service.ts index 60f09ceae36..c95077bfbcc 100644 --- a/apps/web/src/app/core/state/state.service.ts +++ b/apps/web/src/app/core/state/state.service.ts @@ -7,7 +7,6 @@ import { STATE_SERVICE_USE_CACHE, } from "@bitwarden/angular/services/injection-tokens"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { StateMigrationService } from "@bitwarden/common/platform/abstractions/state-migration.service"; import { AbstractMemoryStorageService, AbstractStorageService, @@ -30,7 +29,6 @@ export class StateService extends BaseStateService { @Inject(SECURE_STORAGE) secureStorageService: AbstractStorageService, @Inject(MEMORY_STORAGE) memoryStorageService: AbstractMemoryStorageService, logService: LogService, - stateMigrationService: StateMigrationService, @Inject(STATE_FACTORY) stateFactory: StateFactory, @Inject(STATE_SERVICE_USE_CACHE) useAccountCache = true ) { @@ -39,7 +37,6 @@ export class StateService extends BaseStateService { secureStorageService, memoryStorageService, logService, - stateMigrationService, stateFactory, useAccountCache ); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 14b26ca43da..df64c25c914 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -77,7 +77,6 @@ import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platfor 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 { StateMigrationService as StateMigrationServiceAbstraction } from "@bitwarden/common/platform/abstractions/state-migration.service"; import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { ValidationService as ValidationServiceAbstraction } from "@bitwarden/common/platform/abstractions/validation.service"; @@ -94,7 +93,6 @@ import { EncryptServiceImplementation } from "@bitwarden/common/platform/service import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation"; import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service"; import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service"; -import { StateMigrationService } from "@bitwarden/common/platform/services/state-migration.service"; import { StateService } from "@bitwarden/common/platform/services/state.service"; import { ValidationService } from "@bitwarden/common/platform/services/validation.service"; import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service"; @@ -480,16 +478,10 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction"; SECURE_STORAGE, MEMORY_STORAGE, LogService, - StateMigrationServiceAbstraction, STATE_FACTORY, STATE_SERVICE_USE_CACHE, ], }, - { - provide: StateMigrationServiceAbstraction, - useClass: StateMigrationService, - deps: [AbstractStorageService, SECURE_STORAGE, STATE_FACTORY], - }, { provide: VaultExportServiceAbstraction, useClass: VaultExportService, diff --git a/libs/common/src/enums/index.ts b/libs/common/src/enums/index.ts index 87a688b856e..b62b3ecfa81 100644 --- a/libs/common/src/enums/index.ts +++ b/libs/common/src/enums/index.ts @@ -18,7 +18,6 @@ export * from "./notification-type.enum"; export * from "./product-type.enum"; export * from "./provider-type.enum"; export * from "./secure-note-type.enum"; -export * from "./state-version.enum"; export * from "./storage-location.enum"; export * from "./theme-type.enum"; export * from "./uri-match-type.enum"; diff --git a/libs/common/src/enums/state-version.enum.ts b/libs/common/src/enums/state-version.enum.ts deleted file mode 100644 index 927ce3a1105..00000000000 --- a/libs/common/src/enums/state-version.enum.ts +++ /dev/null @@ -1,10 +0,0 @@ -export enum StateVersion { - One = 1, // Original flat key/value pair store - Two = 2, // Move to a typed State object - Three = 3, // Fix migration of users' premium status - Four = 4, // Fix 'Never Lock' option by removing stale data - Five = 5, // Migrate to new storage of encrypted organization keys - Six = 6, // Delete account.keys.legacyEtmKey property - Seven = 7, // Remove global desktop auto prompt setting, move to account - Latest = Seven, -} diff --git a/libs/common/src/platform/abstractions/state-migration.service.ts b/libs/common/src/platform/abstractions/state-migration.service.ts deleted file mode 100644 index f16777a159f..00000000000 --- a/libs/common/src/platform/abstractions/state-migration.service.ts +++ /dev/null @@ -1,4 +0,0 @@ -export abstract class StateMigrationService { - needsMigration: () => Promise; - migrate: () => Promise; -} diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index 4a2b515b74a..82813718de3 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -495,8 +495,6 @@ export abstract class StateService { setVaultTimeoutAction: (value: string, options?: StorageOptions) => Promise; getApproveLoginRequests: (options?: StorageOptions) => Promise; setApproveLoginRequests: (value: boolean, options?: StorageOptions) => Promise; - getStateVersion: () => Promise; - setStateVersion: (value: number) => Promise; getWindow: () => Promise; setWindow: (value: WindowState) => Promise; /** diff --git a/libs/common/src/platform/models/domain/global-state.ts b/libs/common/src/platform/models/domain/global-state.ts index dfe3c6c417f..30ad32124cf 100644 --- a/libs/common/src/platform/models/domain/global-state.ts +++ b/libs/common/src/platform/models/domain/global-state.ts @@ -1,5 +1,5 @@ import { EnvironmentUrls } from "../../../auth/models/domain/environment-urls"; -import { StateVersion, ThemeType } from "../../../enums"; +import { ThemeType } from "../../../enums"; import { WindowState } from "../../../models/domain/window-state"; export class GlobalState { @@ -25,7 +25,6 @@ export class GlobalState { enableBiometrics?: boolean; biometricText?: string; noAutoPromptBiometricsText?: string; - stateVersion: StateVersion = StateVersion.One; environmentUrls: EnvironmentUrls = new EnvironmentUrls(); enableTray?: boolean; enableMinimizeToTray?: boolean; diff --git a/libs/common/src/platform/services/state-migration.service.spec.ts b/libs/common/src/platform/services/state-migration.service.spec.ts deleted file mode 100644 index 7bbd19106d5..00000000000 --- a/libs/common/src/platform/services/state-migration.service.spec.ts +++ /dev/null @@ -1,216 +0,0 @@ -// eslint-disable-next-line no-restricted-imports -import { Substitute, SubstituteOf } from "@fluffy-spoon/substitute"; -import { MockProxy, any, mock } from "jest-mock-extended"; - -import { StateVersion } from "../../enums"; -import { AbstractStorageService } from "../abstractions/storage.service"; -import { StateFactory } from "../factories/state-factory"; -import { Account } from "../models/domain/account"; -import { GlobalState } from "../models/domain/global-state"; - -import { StateMigrationService } from "./state-migration.service"; - -const userId = "USER_ID"; - -// Note: each test calls the private migration method for that migration, -// so that we don't accidentally run all following migrations as well - -describe("State Migration Service", () => { - let storageService: MockProxy; - let secureStorageService: SubstituteOf; - let stateFactory: SubstituteOf; - - let stateMigrationService: StateMigrationService; - - beforeEach(() => { - storageService = mock(); - secureStorageService = Substitute.for(); - stateFactory = Substitute.for(); - - stateMigrationService = new StateMigrationService( - storageService, - secureStorageService, - stateFactory - ); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - describe("StateVersion 3 to 4 migration", () => { - beforeEach(() => { - const globalVersion3: Partial = { - stateVersion: StateVersion.Three, - }; - - storageService.get.calledWith("global", any()).mockResolvedValue(globalVersion3); - storageService.get.calledWith("authenticatedAccounts", any()).mockResolvedValue([userId]); - }); - - it("clears everBeenUnlocked", async () => { - const accountVersion3: Account = { - profile: { - apiKeyClientId: null, - convertAccountToKeyConnector: null, - email: "EMAIL", - emailVerified: true, - everBeenUnlocked: true, - hasPremiumPersonally: false, - kdfIterations: 100000, - kdfType: 0, - keyHash: "KEY_HASH", - lastSync: "LAST_SYNC", - userId: userId, - usesKeyConnector: false, - forcePasswordResetReason: null, - }, - }; - - const expectedAccountVersion4: Account = { - profile: { - ...accountVersion3.profile, - }, - }; - delete expectedAccountVersion4.profile.everBeenUnlocked; - - storageService.get.calledWith(userId, any()).mockResolvedValue(accountVersion3); - - await (stateMigrationService as any).migrateStateFrom3To4(); - - expect(storageService.save).toHaveBeenCalledTimes(2); - expect(storageService.save).toHaveBeenCalledWith(userId, expectedAccountVersion4, any()); - }); - - it("updates StateVersion number", async () => { - await (stateMigrationService as any).migrateStateFrom3To4(); - - expect(storageService.save).toHaveBeenCalledWith( - "global", - { stateVersion: StateVersion.Four }, - any() - ); - expect(storageService.save).toHaveBeenCalledTimes(1); - }); - }); - - describe("StateVersion 4 to 5 migration", () => { - it("migrates organization keys to new format", async () => { - const accountVersion4 = new Account({ - keys: { - organizationKeys: { - encrypted: { - orgOneId: "orgOneEncKey", - orgTwoId: "orgTwoEncKey", - orgThreeId: "orgThreeEncKey", - }, - }, - }, - } as any); - - const expectedAccount = new Account({ - keys: { - organizationKeys: { - encrypted: { - orgOneId: { - type: "organization", - key: "orgOneEncKey", - }, - orgTwoId: { - type: "organization", - key: "orgTwoEncKey", - }, - orgThreeId: { - type: "organization", - key: "orgThreeEncKey", - }, - }, - } as any, - } as any, - }); - - const migratedAccount = await (stateMigrationService as any).migrateAccountFrom4To5( - accountVersion4 - ); - - expect(migratedAccount).toEqual(expectedAccount); - }); - }); - - describe("StateVersion 5 to 6 migration", () => { - it("deletes account.keys.legacyEtmKey value", async () => { - const accountVersion5 = new Account({ - keys: { - legacyEtmKey: "legacy key", - }, - } as any); - - const migratedAccount = await (stateMigrationService as any).migrateAccountFrom5To6( - accountVersion5 - ); - - expect(migratedAccount.keys.legacyEtmKey).toBeUndefined(); - }); - }); - - describe("StateVersion 6 to 7 migration", () => { - it("should delete global.noAutoPromptBiometrics value", async () => { - storageService.get - .calledWith("global", any()) - .mockResolvedValue({ stateVersion: StateVersion.Six, noAutoPromptBiometrics: true }); - storageService.get.calledWith("authenticatedAccounts", any()).mockResolvedValue([]); - - await stateMigrationService.migrate(); - - expect(storageService.save).toHaveBeenCalledWith( - "global", - { - stateVersion: StateVersion.Seven, - }, - any() - ); - }); - - it("should call migrateStateFrom6To7 on each account", async () => { - const accountVersion6 = new Account({ - otherStuff: "other stuff", - } as any); - - storageService.get - .calledWith("global", any()) - .mockResolvedValue({ stateVersion: StateVersion.Six, noAutoPromptBiometrics: true }); - storageService.get.calledWith("authenticatedAccounts", any()).mockResolvedValue([userId]); - storageService.get.calledWith(userId, any()).mockResolvedValue(accountVersion6); - - const migrateSpy = jest.fn(); - (stateMigrationService as any).migrateAccountFrom6To7 = migrateSpy; - - await stateMigrationService.migrate(); - - expect(migrateSpy).toHaveBeenCalledWith(true, accountVersion6); - }); - - it("should update account.settings.disableAutoBiometricsPrompt value if global is no prompt", async () => { - const result = await (stateMigrationService as any).migrateAccountFrom6To7(true, { - otherStuff: "other stuff", - }); - - expect(result).toEqual({ - otherStuff: "other stuff", - settings: { - disableAutoBiometricsPrompt: true, - }, - }); - }); - - it("should not update account.settings.disableAutoBiometricsPrompt value if global auto prompt is enabled", async () => { - const result = await (stateMigrationService as any).migrateAccountFrom6To7(false, { - otherStuff: "other stuff", - }); - - expect(result).toEqual({ - otherStuff: "other stuff", - }); - }); - }); -}); diff --git a/libs/common/src/platform/services/state-migration.service.ts b/libs/common/src/platform/services/state-migration.service.ts deleted file mode 100644 index 234d1b2bff8..00000000000 --- a/libs/common/src/platform/services/state-migration.service.ts +++ /dev/null @@ -1,587 +0,0 @@ -import { OrganizationData } from "../../admin-console/models/data/organization.data"; -import { PolicyData } from "../../admin-console/models/data/policy.data"; -import { ProviderData } from "../../admin-console/models/data/provider.data"; -import { EnvironmentUrls } from "../../auth/models/domain/environment-urls"; -import { TokenService } from "../../auth/services/token.service"; -import { StateVersion, ThemeType, KdfType, HtmlStorageLocation } from "../../enums"; -import { EventData } from "../../models/data/event.data"; -import { GeneratedPasswordHistory } from "../../tools/generator/password"; -import { SendData } from "../../tools/send/models/data/send.data"; -import { CipherData } from "../../vault/models/data/cipher.data"; -import { CollectionData } from "../../vault/models/data/collection.data"; -import { FolderData } from "../../vault/models/data/folder.data"; -import { AbstractStorageService } from "../abstractions/storage.service"; -import { StateFactory } from "../factories/state-factory"; -import { - Account, - AccountSettings, - EncryptionPair, - AccountSettingsSettings, -} from "../models/domain/account"; -import { EncString } from "../models/domain/enc-string"; -import { GlobalState } from "../models/domain/global-state"; -import { StorageOptions } from "../models/domain/storage-options"; - -// Originally (before January 2022) storage was handled as a flat key/value pair store. -// With the move to a typed object for state storage these keys should no longer be in use anywhere outside of this migration. -const v1Keys: { [key: string]: string } = { - accessToken: "accessToken", - alwaysShowDock: "alwaysShowDock", - autoConfirmFingerprints: "autoConfirmFingerprints", - autoFillOnPageLoadDefault: "autoFillOnPageLoadDefault", - biometricAwaitingAcceptance: "biometricAwaitingAcceptance", - biometricFingerprintValidated: "biometricFingerprintValidated", - biometricText: "biometricText", - biometricUnlock: "biometric", - clearClipboard: "clearClipboardKey", - clientId: "apikey_clientId", - clientSecret: "apikey_clientSecret", - collapsedGroupings: "collapsedGroupings", - convertAccountToKeyConnector: "convertAccountToKeyConnector", - defaultUriMatch: "defaultUriMatch", - disableAddLoginNotification: "disableAddLoginNotification", - disableAutoBiometricsPrompt: "noAutoPromptBiometrics", - disableAutoTotpCopy: "disableAutoTotpCopy", - disableBadgeCounter: "disableBadgeCounter", - disableChangedPasswordNotification: "disableChangedPasswordNotification", - disableContextMenuItem: "disableContextMenuItem", - disableFavicon: "disableFavicon", - disableGa: "disableGa", - dontShowCardsCurrentTab: "dontShowCardsCurrentTab", - dontShowIdentitiesCurrentTab: "dontShowIdentitiesCurrentTab", - emailVerified: "emailVerified", - enableAlwaysOnTop: "enableAlwaysOnTopKey", - enableAutoFillOnPageLoad: "enableAutoFillOnPageLoad", - enableBiometric: "enabledBiometric", - enableBrowserIntegration: "enableBrowserIntegration", - enableBrowserIntegrationFingerprint: "enableBrowserIntegrationFingerprint", - enableCloseToTray: "enableCloseToTray", - enableFullWidth: "enableFullWidth", - enableMinimizeToTray: "enableMinimizeToTray", - enableStartToTray: "enableStartToTrayKey", - enableTray: "enableTray", - encKey: "encKey", // Generated Symmetric Key - encOrgKeys: "encOrgKeys", - encPrivate: "encPrivateKey", - encProviderKeys: "encProviderKeys", - entityId: "entityId", - entityType: "entityType", - environmentUrls: "environmentUrls", - equivalentDomains: "equivalentDomains", - eventCollection: "eventCollection", - forcePasswordReset: "forcePasswordReset", - history: "generatedPasswordHistory", - installedVersion: "installedVersion", - kdf: "kdf", - kdfIterations: "kdfIterations", - key: "key", // Master Key - keyHash: "keyHash", - lastActive: "lastActive", - localData: "sitesLocalData", - locale: "locale", - mainWindowSize: "mainWindowSize", - minimizeOnCopyToClipboard: "minimizeOnCopyToClipboardKey", - neverDomains: "neverDomains", - noAutoPromptBiometricsText: "noAutoPromptBiometricsText", - openAtLogin: "openAtLogin", - passwordGenerationOptions: "passwordGenerationOptions", - pinProtected: "pinProtectedKey", - protectedPin: "protectedPin", - refreshToken: "refreshToken", - ssoCodeVerifier: "ssoCodeVerifier", - ssoIdentifier: "ssoOrgIdentifier", - ssoState: "ssoState", - stamp: "securityStamp", - theme: "theme", - userEmail: "userEmail", - userId: "userId", - usesConnector: "usesKeyConnector", - vaultTimeoutAction: "vaultTimeoutAction", - vaultTimeout: "lockOption", - rememberedEmail: "rememberedEmail", -}; - -const v1KeyPrefixes: { [key: string]: string } = { - ciphers: "ciphers_", - collections: "collections_", - folders: "folders_", - lastSync: "lastSync_", - policies: "policies_", - twoFactorToken: "twoFactorToken_", - organizations: "organizations_", - providers: "providers_", - sends: "sends_", - settings: "settings_", -}; - -const keys = { - 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 = { - autoKey: "_masterkey_auto", - biometricKey: "_masterkey_biometric", - masterKey: "_masterkey", -}; - -export class StateMigrationService< - TGlobalState extends GlobalState = GlobalState, - TAccount extends Account = Account -> { - constructor( - protected storageService: AbstractStorageService, - protected secureStorageService: AbstractStorageService, - protected stateFactory: StateFactory - ) {} - - async needsMigration(): Promise { - const currentStateVersion = await this.getCurrentStateVersion(); - return currentStateVersion == null || currentStateVersion < StateVersion.Latest; - } - - async migrate(): Promise { - let currentStateVersion = await this.getCurrentStateVersion(); - while (currentStateVersion < StateVersion.Latest) { - switch (currentStateVersion) { - case StateVersion.One: - await this.migrateStateFrom1To2(); - break; - case StateVersion.Two: - await this.migrateStateFrom2To3(); - break; - case StateVersion.Three: - await this.migrateStateFrom3To4(); - break; - case StateVersion.Four: { - const authenticatedAccounts = await this.getAuthenticatedAccounts(); - for (const account of authenticatedAccounts) { - const migratedAccount = await this.migrateAccountFrom4To5(account); - await this.set(account.profile.userId, migratedAccount); - } - await this.setCurrentStateVersion(StateVersion.Five); - break; - } - case StateVersion.Five: { - const authenticatedAccounts = await this.getAuthenticatedAccounts(); - for (const account of authenticatedAccounts) { - const migratedAccount = await this.migrateAccountFrom5To6(account); - await this.set(account.profile.userId, migratedAccount); - } - await this.setCurrentStateVersion(StateVersion.Six); - break; - } - case StateVersion.Six: { - const authenticatedAccounts = await this.getAuthenticatedAccounts(); - const globals = (await this.getGlobals()) as any; - for (const account of authenticatedAccounts) { - const migratedAccount = await this.migrateAccountFrom6To7( - globals?.noAutoPromptBiometrics, - account - ); - await this.set(account.profile.userId, migratedAccount); - } - if (globals) { - delete globals.noAutoPromptBiometrics; - } - await this.set(keys.global, globals); - await this.setCurrentStateVersion(StateVersion.Seven); - } - } - - currentStateVersion += 1; - } - } - - protected async migrateStateFrom1To2(): Promise { - const clearV1Keys = async (clearingUserId?: string) => { - for (const key in v1Keys) { - if (key == null) { - continue; - } - await this.set(v1Keys[key], null); - } - if (clearingUserId != null) { - for (const keyPrefix in v1KeyPrefixes) { - if (keyPrefix == null) { - continue; - } - await this.set(v1KeyPrefixes[keyPrefix] + userId, null); - } - } - }; - - // Some processes, like biometrics, may have already defined a value before migrations are run. - // We don't want to null out those values if they don't exist in the old storage scheme (like for new installs) - // So, the OOO for migration is that we: - // 1. Check for an existing storage value from the old storage structure OR - // 2. Check for a value already set by processes that run before migration OR - // 3. Assign the default value - const globals: any = - (await this.get(keys.global)) ?? this.stateFactory.createGlobal(null); - globals.stateVersion = StateVersion.Two; - globals.environmentUrls = - (await this.get(v1Keys.environmentUrls)) ?? globals.environmentUrls; - globals.locale = (await this.get(v1Keys.locale)) ?? globals.locale; - globals.noAutoPromptBiometrics = - (await this.get(v1Keys.disableAutoBiometricsPrompt)) ?? - globals.noAutoPromptBiometrics; - globals.noAutoPromptBiometricsText = - (await this.get(v1Keys.noAutoPromptBiometricsText)) ?? - globals.noAutoPromptBiometricsText; - globals.ssoCodeVerifier = - (await this.get(v1Keys.ssoCodeVerifier)) ?? globals.ssoCodeVerifier; - globals.ssoOrganizationIdentifier = - (await this.get(v1Keys.ssoIdentifier)) ?? globals.ssoOrganizationIdentifier; - globals.ssoState = (await this.get(v1Keys.ssoState)) ?? globals.ssoState; - globals.rememberedEmail = - (await this.get(v1Keys.rememberedEmail)) ?? globals.rememberedEmail; - globals.theme = (await this.get(v1Keys.theme)) ?? globals.theme; - globals.vaultTimeout = (await this.get(v1Keys.vaultTimeout)) ?? globals.vaultTimeout; - globals.vaultTimeoutAction = - (await this.get(v1Keys.vaultTimeoutAction)) ?? globals.vaultTimeoutAction; - globals.window = (await this.get(v1Keys.mainWindowSize)) ?? globals.window; - globals.enableTray = (await this.get(v1Keys.enableTray)) ?? globals.enableTray; - globals.enableMinimizeToTray = - (await this.get(v1Keys.enableMinimizeToTray)) ?? globals.enableMinimizeToTray; - globals.enableCloseToTray = - (await this.get(v1Keys.enableCloseToTray)) ?? globals.enableCloseToTray; - globals.enableStartToTray = - (await this.get(v1Keys.enableStartToTray)) ?? globals.enableStartToTray; - globals.openAtLogin = (await this.get(v1Keys.openAtLogin)) ?? globals.openAtLogin; - globals.alwaysShowDock = - (await this.get(v1Keys.alwaysShowDock)) ?? globals.alwaysShowDock; - globals.enableBrowserIntegration = - (await this.get(v1Keys.enableBrowserIntegration)) ?? - globals.enableBrowserIntegration; - globals.enableBrowserIntegrationFingerprint = - (await this.get(v1Keys.enableBrowserIntegrationFingerprint)) ?? - globals.enableBrowserIntegrationFingerprint; - - const userId = - (await this.get(v1Keys.userId)) ?? (await this.get(v1Keys.entityId)); - - const defaultAccount = this.stateFactory.createAccount(null); - const accountSettings: AccountSettings = { - autoConfirmFingerPrints: - (await this.get(v1Keys.autoConfirmFingerprints)) ?? - defaultAccount.settings.autoConfirmFingerPrints, - autoFillOnPageLoadDefault: - (await this.get(v1Keys.autoFillOnPageLoadDefault)) ?? - defaultAccount.settings.autoFillOnPageLoadDefault, - biometricUnlock: - (await this.get(v1Keys.biometricUnlock)) ?? - defaultAccount.settings.biometricUnlock, - clearClipboard: - (await this.get(v1Keys.clearClipboard)) ?? defaultAccount.settings.clearClipboard, - defaultUriMatch: - (await this.get(v1Keys.defaultUriMatch)) ?? defaultAccount.settings.defaultUriMatch, - disableAddLoginNotification: - (await this.get(v1Keys.disableAddLoginNotification)) ?? - defaultAccount.settings.disableAddLoginNotification, - disableAutoBiometricsPrompt: - (await this.get(v1Keys.disableAutoBiometricsPrompt)) ?? - defaultAccount.settings.disableAutoBiometricsPrompt, - disableAutoTotpCopy: - (await this.get(v1Keys.disableAutoTotpCopy)) ?? - defaultAccount.settings.disableAutoTotpCopy, - disableBadgeCounter: - (await this.get(v1Keys.disableBadgeCounter)) ?? - defaultAccount.settings.disableBadgeCounter, - disableChangedPasswordNotification: - (await this.get(v1Keys.disableChangedPasswordNotification)) ?? - defaultAccount.settings.disableChangedPasswordNotification, - disableContextMenuItem: - (await this.get(v1Keys.disableContextMenuItem)) ?? - defaultAccount.settings.disableContextMenuItem, - disableGa: (await this.get(v1Keys.disableGa)) ?? defaultAccount.settings.disableGa, - dontShowCardsCurrentTab: - (await this.get(v1Keys.dontShowCardsCurrentTab)) ?? - defaultAccount.settings.dontShowCardsCurrentTab, - dontShowIdentitiesCurrentTab: - (await this.get(v1Keys.dontShowIdentitiesCurrentTab)) ?? - defaultAccount.settings.dontShowIdentitiesCurrentTab, - enableAlwaysOnTop: - (await this.get(v1Keys.enableAlwaysOnTop)) ?? - defaultAccount.settings.enableAlwaysOnTop, - enableAutoFillOnPageLoad: - (await this.get(v1Keys.enableAutoFillOnPageLoad)) ?? - defaultAccount.settings.enableAutoFillOnPageLoad, - enableBiometric: - (await this.get(v1Keys.enableBiometric)) ?? - defaultAccount.settings.enableBiometric, - enableFullWidth: - (await this.get(v1Keys.enableFullWidth)) ?? - defaultAccount.settings.enableFullWidth, - environmentUrls: globals.environmentUrls ?? defaultAccount.settings.environmentUrls, - equivalentDomains: - (await this.get(v1Keys.equivalentDomains)) ?? - defaultAccount.settings.equivalentDomains, - minimizeOnCopyToClipboard: - (await this.get(v1Keys.minimizeOnCopyToClipboard)) ?? - defaultAccount.settings.minimizeOnCopyToClipboard, - neverDomains: - (await this.get(v1Keys.neverDomains)) ?? defaultAccount.settings.neverDomains, - passwordGenerationOptions: - (await this.get(v1Keys.passwordGenerationOptions)) ?? - defaultAccount.settings.passwordGenerationOptions, - pinProtected: Object.assign(new EncryptionPair(), { - decrypted: null, - encrypted: await this.get(v1Keys.pinProtected), - }), - protectedPin: await this.get(v1Keys.protectedPin), - settings: - userId == null - ? null - : await this.get(v1KeyPrefixes.settings + userId), - vaultTimeout: - (await this.get(v1Keys.vaultTimeout)) ?? defaultAccount.settings.vaultTimeout, - vaultTimeoutAction: - (await this.get(v1Keys.vaultTimeoutAction)) ?? - defaultAccount.settings.vaultTimeoutAction, - }; - - // (userId == null) = no logged in user (so no known userId) and we need to temporarily store account specific settings in state to migrate on first auth - // (userId != null) = we have a currently authed user (so known userId) with encrypted data and other key settings we can move, no need to temporarily store account settings - if (userId == null) { - await this.set(keys.tempAccountSettings, accountSettings); - await this.set(keys.global, globals); - await this.set(keys.authenticatedAccounts, []); - await this.set(keys.activeUserId, null); - await clearV1Keys(); - return; - } - - globals.twoFactorToken = await this.get(v1KeyPrefixes.twoFactorToken + userId); - await this.set(keys.global, globals); - await this.set(userId, { - data: { - addEditCipherInfo: null, - ciphers: { - decrypted: null, - encrypted: await this.get<{ [id: string]: CipherData }>(v1KeyPrefixes.ciphers + userId), - }, - collapsedGroupings: null, - collections: { - decrypted: null, - encrypted: await this.get<{ [id: string]: CollectionData }>( - v1KeyPrefixes.collections + userId - ), - }, - eventCollection: await this.get(v1Keys.eventCollection), - folders: { - decrypted: null, - encrypted: await this.get<{ [id: string]: FolderData }>(v1KeyPrefixes.folders + userId), - }, - localData: null, - organizations: await this.get<{ [id: string]: OrganizationData }>( - v1KeyPrefixes.organizations + userId - ), - passwordGenerationHistory: { - decrypted: null, - encrypted: await this.get(v1Keys.history), - }, - policies: { - decrypted: null, - encrypted: await this.get<{ [id: string]: PolicyData }>(v1KeyPrefixes.policies + userId), - }, - providers: await this.get<{ [id: string]: ProviderData }>(v1KeyPrefixes.providers + userId), - sends: { - decrypted: null, - encrypted: await this.get<{ [id: string]: SendData }>(v1KeyPrefixes.sends + userId), - }, - }, - keys: { - apiKeyClientSecret: await this.get(v1Keys.clientSecret), - cryptoMasterKey: null, - cryptoMasterKeyAuto: null, - cryptoMasterKeyB64: null, - cryptoMasterKeyBiometric: null, - cryptoSymmetricKey: { - encrypted: await this.get(v1Keys.encKey), - decrypted: null, - }, - legacyEtmKey: null, - organizationKeys: { - decrypted: null, - encrypted: await this.get(v1Keys.encOrgKeys), - }, - privateKey: { - decrypted: null, - encrypted: await this.get(v1Keys.encPrivate), - }, - providerKeys: { - decrypted: null, - encrypted: await this.get(v1Keys.encProviderKeys), - }, - publicKey: null, - }, - profile: { - apiKeyClientId: await this.get(v1Keys.clientId), - authenticationStatus: null, - convertAccountToKeyConnector: await this.get(v1Keys.convertAccountToKeyConnector), - email: await this.get(v1Keys.userEmail), - emailVerified: await this.get(v1Keys.emailVerified), - entityId: null, - entityType: null, - everBeenUnlocked: null, - forcePasswordReset: null, - hasPremiumPersonally: null, - kdfIterations: await this.get(v1Keys.kdfIterations), - kdfType: await this.get(v1Keys.kdf), - keyHash: await this.get(v1Keys.keyHash), - lastSync: null, - userId: userId, - usesKeyConnector: null, - }, - settings: accountSettings, - tokens: { - accessToken: await this.get(v1Keys.accessToken), - decodedToken: null, - refreshToken: await this.get(v1Keys.refreshToken), - securityStamp: null, - }, - }); - - await this.set(keys.authenticatedAccounts, [userId]); - await this.set(keys.activeUserId, userId); - - const accountActivity: { [userId: string]: number } = { - [userId]: await this.get(v1Keys.lastActive), - }; - accountActivity[userId] = await this.get(v1Keys.lastActive); - await this.set(keys.accountActivity, accountActivity); - - await clearV1Keys(userId); - - if (await this.secureStorageService.has(v1Keys.key, { keySuffix: "biometric" })) { - await this.secureStorageService.save( - `${userId}${partialKeys.biometricKey}`, - await this.secureStorageService.get(v1Keys.key, { keySuffix: "biometric" }), - { keySuffix: "biometric" } - ); - await this.secureStorageService.remove(v1Keys.key, { keySuffix: "biometric" }); - } - - if (await this.secureStorageService.has(v1Keys.key, { keySuffix: "auto" })) { - await this.secureStorageService.save( - `${userId}${partialKeys.autoKey}`, - await this.secureStorageService.get(v1Keys.key, { keySuffix: "auto" }), - { keySuffix: "auto" } - ); - await this.secureStorageService.remove(v1Keys.key, { keySuffix: "auto" }); - } - - if (await this.secureStorageService.has(v1Keys.key)) { - await this.secureStorageService.save( - `${userId}${partialKeys.masterKey}`, - await this.secureStorageService.get(v1Keys.key) - ); - await this.secureStorageService.remove(v1Keys.key); - } - } - - protected async migrateStateFrom2To3(): Promise { - const authenticatedUserIds = await this.get(keys.authenticatedAccounts); - await Promise.all( - authenticatedUserIds.map(async (userId) => { - const account = await this.get(userId); - if ( - account?.profile?.hasPremiumPersonally === null && - account.tokens?.accessToken != null - ) { - const decodedToken = await TokenService.decodeToken(account.tokens.accessToken); - account.profile.hasPremiumPersonally = decodedToken.premium; - await this.set(userId, account); - } - }) - ); - - const globals = await this.getGlobals(); - globals.stateVersion = StateVersion.Three; - await this.set(keys.global, globals); - } - - protected async migrateStateFrom3To4(): Promise { - const authenticatedUserIds = await this.get(keys.authenticatedAccounts); - await Promise.all( - authenticatedUserIds.map(async (userId) => { - const account = await this.get(userId); - if (account?.profile?.everBeenUnlocked != null) { - delete account.profile.everBeenUnlocked; - return this.set(userId, account); - } - }) - ); - - const globals = await this.getGlobals(); - globals.stateVersion = StateVersion.Four; - await this.set(keys.global, globals); - } - - protected async migrateAccountFrom4To5(account: TAccount): Promise { - const encryptedOrgKeys = account.keys?.organizationKeys?.encrypted; - if (encryptedOrgKeys != null) { - for (const [orgId, encKey] of Object.entries(encryptedOrgKeys)) { - encryptedOrgKeys[orgId] = { - type: "organization", - key: encKey as unknown as string, // Account v4 does not reflect the current account model so we have to cast - }; - } - } - - return account; - } - - protected async migrateAccountFrom5To6(account: TAccount): Promise { - delete (account as any).keys?.legacyEtmKey; - return account; - } - - protected async migrateAccountFrom6To7( - globalSetting: boolean, - account: TAccount - ): Promise { - if (globalSetting) { - account.settings = Object.assign({}, account.settings, { disableAutoBiometricsPrompt: true }); - } - return account; - } - - protected get options(): StorageOptions { - return { htmlStorageLocation: HtmlStorageLocation.Local }; - } - - protected get(key: string): Promise { - return this.storageService.get(key, this.options); - } - - protected set(key: string, value: any): Promise { - if (value == null) { - return this.storageService.remove(key, this.options); - } - return this.storageService.save(key, value, this.options); - } - - protected async getGlobals(): Promise { - return await this.get(keys.global); - } - - protected async getCurrentStateVersion(): Promise { - return (await this.getGlobals())?.stateVersion ?? StateVersion.One; - } - - protected async setCurrentStateVersion(newVersion: StateVersion): Promise { - const globals = await this.getGlobals(); - globals.stateVersion = newVersion; - await this.set(keys.global, globals); - } - - protected async getAuthenticatedAccounts(): Promise { - const authenticatedUserIds = await this.get(keys.authenticatedAccounts); - return Promise.all(authenticatedUserIds.map((id) => this.get(id))); - } -} diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 14ba4abfd39..5fdf40e8458 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -21,6 +21,7 @@ import { import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; import { EventData } from "../../models/data/event.data"; import { WindowState } from "../../models/domain/window-state"; +import { migrate } from "../../state-migrations"; import { GeneratedPasswordHistory } from "../../tools/generator/password"; import { SendData } from "../../tools/send/models/data/send.data"; import { SendView } from "../../tools/send/models/view/send.view"; @@ -32,7 +33,6 @@ import { CipherView } from "../../vault/models/view/cipher.view"; import { CollectionView } from "../../vault/models/view/collection.view"; import { AddEditCipherInfo } from "../../vault/types/add-edit-cipher-info"; import { LogService } from "../abstractions/log.service"; -import { StateMigrationService } from "../abstractions/state-migration.service"; import { StateService as StateServiceAbstraction } from "../abstractions/state.service"; import { AbstractMemoryStorageService, @@ -61,6 +61,7 @@ import { const keys = { state: "state", + stateVersion: "stateVersion", global: "global", authenticatedAccounts: "authenticatedAccounts", activeUserId: "activeUserId", @@ -106,7 +107,6 @@ export class StateService< protected secureStorageService: AbstractStorageService, protected memoryStorageService: AbstractMemoryStorageService, protected logService: LogService, - protected stateMigrationService: StateMigrationService, protected stateFactory: StateFactory, protected useAccountCache: boolean = true ) { @@ -133,9 +133,7 @@ export class StateService< return; } - if (await this.stateMigrationService.needsMigration()) { - await this.stateMigrationService.migrate(); - } + await migrate(this.storageService, this.logService); await this.state().then(async (state) => { if (state == null) { @@ -2724,16 +2722,6 @@ export class StateService< ); } - async getStateVersion(): Promise { - return (await this.getGlobals(await this.defaultOnDiskLocalOptions())).stateVersion ?? 1; - } - - async setStateVersion(value: number): Promise { - const globals = await this.getGlobals(await this.defaultOnDiskOptions()); - globals.stateVersion = value; - await this.saveGlobals(globals, await this.defaultOnDiskOptions()); - } - async getWindow(): Promise { const globals = await this.getGlobals(await this.defaultOnDiskOptions()); return globals?.window != null && Object.keys(globals.window).length > 0 @@ -2838,7 +2826,11 @@ export class StateService< globals = await this.getGlobalsFromDisk(options); } - return globals ?? this.createGlobals(); + if (globals == null) { + globals = this.createGlobals(); + } + + return globals; } protected async saveGlobals(globals: TGlobalState, options: StorageOptions) { diff --git a/libs/common/src/state-migrations/.eslintrc.json b/libs/common/src/state-migrations/.eslintrc.json new file mode 100644 index 00000000000..4b66f0a32fa --- /dev/null +++ b/libs/common/src/state-migrations/.eslintrc.json @@ -0,0 +1,24 @@ +{ + "overrides": [ + { + "files": ["*"], + "rules": { + "import/no-restricted-paths": [ + "error", + { + "basePath": "libs/common/src/state-migrations", + "zones": [ + { + "target": "./", + "from": "../", + // Relative to from, not basePath + "except": ["state-migrations"], + "message": "State migrations should rarely import from the greater codebase. If you need to import from another location, take into account the likelihood of change in that code and consider copying to the migration instead." + } + ] + } + ] + } + } + ] +} diff --git a/libs/common/src/state-migrations/index.ts b/libs/common/src/state-migrations/index.ts new file mode 100644 index 00000000000..c883b1ca811 --- /dev/null +++ b/libs/common/src/state-migrations/index.ts @@ -0,0 +1 @@ +export { migrate, CURRENT_VERSION } from "./migrate"; diff --git a/libs/common/src/state-migrations/migrate.spec.ts b/libs/common/src/state-migrations/migrate.spec.ts new file mode 100644 index 00000000000..ade3d261f69 --- /dev/null +++ b/libs/common/src/state-migrations/migrate.spec.ts @@ -0,0 +1,67 @@ +import { mock, MockProxy } from "jest-mock-extended"; + +// eslint-disable-next-line import/no-restricted-paths -- Needed to print log messages +import { LogService } from "../platform/abstractions/log.service"; +// eslint-disable-next-line import/no-restricted-paths -- Needed to interface with storage locations +import { AbstractStorageService } from "../platform/abstractions/storage.service"; + +import { CURRENT_VERSION, currentVersion, migrate } from "./migrate"; +import { MigrationBuilder } from "./migration-builder"; + +jest.mock("./migration-builder", () => { + return { + MigrationBuilder: { + create: jest.fn().mockReturnThis(), + }, + }; +}); + +describe("migrate", () => { + it("should not run migrations if state is empty", async () => { + const storage = mock(); + const logService = mock(); + storage.get.mockReturnValueOnce(null); + await migrate(storage, logService); + expect(MigrationBuilder.create).not.toHaveBeenCalled(); + }); + + it("should set to current version if state is empty", async () => { + const storage = mock(); + const logService = mock(); + storage.get.mockReturnValueOnce(null); + await migrate(storage, logService); + expect(storage.save).toHaveBeenCalledWith("stateVersion", CURRENT_VERSION); + }); +}); + +describe("currentVersion", () => { + let storage: MockProxy; + let logService: MockProxy; + + beforeEach(() => { + storage = mock(); + logService = mock(); + }); + + it("should return -1 if no version", async () => { + storage.get.mockReturnValueOnce(null); + expect(await currentVersion(storage, logService)).toEqual(-1); + }); + + it("should return version", async () => { + storage.get.calledWith("stateVersion").mockReturnValueOnce(1 as any); + expect(await currentVersion(storage, logService)).toEqual(1); + }); + + it("should return version from global", async () => { + storage.get.calledWith("stateVersion").mockReturnValueOnce(null); + storage.get.calledWith("global").mockReturnValueOnce({ stateVersion: 1 } as any); + expect(await currentVersion(storage, logService)).toEqual(1); + }); + + it("should prefer root version to global", async () => { + storage.get.calledWith("stateVersion").mockReturnValue(1 as any); + storage.get.calledWith("global").mockReturnValue({ stateVersion: 2 } as any); + expect(await currentVersion(storage, logService)).toEqual(1); + }); +}); diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts new file mode 100644 index 00000000000..483c4f2e8eb --- /dev/null +++ b/libs/common/src/state-migrations/migrate.ts @@ -0,0 +1,60 @@ +// eslint-disable-next-line import/no-restricted-paths -- Needed to print log messages +import { LogService } from "../platform/abstractions/log.service"; +// eslint-disable-next-line import/no-restricted-paths -- Needed to interface with storage locations +import { AbstractStorageService } from "../platform/abstractions/storage.service"; + +import { MigrationBuilder } from "./migration-builder"; +import { MigrationHelper } from "./migration-helper"; +import { FixPremiumMigrator } from "./migrations/3-fix-premium"; +import { RemoveEverBeenUnlockedMigrator } from "./migrations/4-remove-ever-been-unlocked"; +import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys"; +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"; +import { MinVersionMigrator } from "./migrations/min-version"; + +export const MIN_VERSION = 2; +export const CURRENT_VERSION = 8; +export type MinVersion = typeof MIN_VERSION; + +export async function migrate( + storageService: AbstractStorageService, + logService: LogService +): Promise { + const migrationHelper = new MigrationHelper( + await currentVersion(storageService, logService), + storageService, + logService + ); + if (migrationHelper.currentVersion < 0) { + // Cannot determine state, assuming empty so we don't repeatedly apply a migration. + await storageService.save("stateVersion", CURRENT_VERSION); + return; + } + MigrationBuilder.create() + .with(MinVersionMigrator) + .with(FixPremiumMigrator, 2, 3) + .with(RemoveEverBeenUnlockedMigrator, 3, 4) + .with(AddKeyTypeToOrgKeysMigrator, 4, 5) + .with(RemoveLegacyEtmKeyMigrator, 5, 6) + .with(MoveBiometricAutoPromptToAccount, 6, 7) + .with(MoveStateVersionMigrator, 7, CURRENT_VERSION) + .migrate(migrationHelper); +} + +export async function currentVersion( + storageService: AbstractStorageService, + logService: LogService +) { + let state = await storageService.get("stateVersion"); + if (state == null) { + // Pre v8 + state = (await storageService.get<{ stateVersion: number }>("global"))?.stateVersion; + } + if (state == null) { + logService.info("No state version found, assuming empty state."); + return -1; + } + logService.info(`State version: ${state}`); + return state; +} diff --git a/libs/common/src/state-migrations/migration-builder.spec.ts b/libs/common/src/state-migrations/migration-builder.spec.ts new file mode 100644 index 00000000000..fa53544f133 --- /dev/null +++ b/libs/common/src/state-migrations/migration-builder.spec.ts @@ -0,0 +1,117 @@ +import { mock } from "jest-mock-extended"; + +import { MigrationBuilder } from "./migration-builder"; +import { MigrationHelper } from "./migration-helper"; +import { Migrator } from "./migrator"; + +describe("MigrationBuilder", () => { + class TestMigrator extends Migrator<0, 1> { + async migrate(helper: MigrationHelper): Promise { + await helper.set("test", "test"); + return; + } + + async rollback(helper: MigrationHelper): Promise { + await helper.set("test", "rollback"); + return; + } + } + + let sut: MigrationBuilder; + + beforeEach(() => { + sut = MigrationBuilder.create(); + }); + + class TestBadMigrator extends Migrator<1, 0> { + async migrate(helper: MigrationHelper): Promise { + await helper.set("test", "test"); + } + + async rollback(helper: MigrationHelper): Promise { + await helper.set("test", "rollback"); + } + } + + it("should throw if instantiated incorrectly", () => { + expect(() => MigrationBuilder.create().with(TestMigrator, null, null)).toThrow(); + expect(() => + MigrationBuilder.create().with(TestMigrator, 0, 1).with(TestBadMigrator, 1, 0) + ).toThrow(); + }); + + it("should be able to create a new MigrationBuilder", () => { + expect(sut).toBeInstanceOf(MigrationBuilder); + }); + + it("should be able to add a migrator", () => { + const newBuilder = sut.with(TestMigrator, 0, 1); + const migrations = newBuilder["migrations"]; + expect(migrations.length).toBe(1); + expect(migrations[0]).toMatchObject({ migrator: expect.any(TestMigrator), direction: "up" }); + }); + + it("should be able to add a rollback", () => { + const newBuilder = sut.with(TestMigrator, 0, 1).rollback(TestMigrator, 1, 0); + const migrations = newBuilder["migrations"]; + expect(migrations.length).toBe(2); + expect(migrations[1]).toMatchObject({ migrator: expect.any(TestMigrator), direction: "down" }); + }); + + describe("migrate", () => { + let migrator: TestMigrator; + let rollback_migrator: TestMigrator; + + beforeEach(() => { + sut = sut.with(TestMigrator, 0, 1).rollback(TestMigrator, 1, 0); + migrator = (sut as any).migrations[0].migrator; + rollback_migrator = (sut as any).migrations[1].migrator; + }); + + it("should migrate", async () => { + const helper = new MigrationHelper(0, mock(), mock()); + const spy = jest.spyOn(migrator, "migrate"); + await sut.migrate(helper); + expect(spy).toBeCalledWith(helper); + }); + + it("should rollback", async () => { + const helper = new MigrationHelper(1, mock(), mock()); + const spy = jest.spyOn(rollback_migrator, "rollback"); + await sut.migrate(helper); + expect(spy).toBeCalledWith(helper); + }); + + it("should update version on migrate", async () => { + const helper = new MigrationHelper(0, mock(), mock()); + const spy = jest.spyOn(migrator, "updateVersion"); + await sut.migrate(helper); + expect(spy).toBeCalledWith(helper, "up"); + }); + + it("should update version on rollback", async () => { + const helper = new MigrationHelper(1, mock(), mock()); + const spy = jest.spyOn(rollback_migrator, "updateVersion"); + await sut.migrate(helper); + expect(spy).toBeCalledWith(helper, "down"); + }); + + it("should not run the migrator if the current version does not match the from version", async () => { + const helper = new MigrationHelper(3, mock(), mock()); + const migrate = jest.spyOn(migrator, "migrate"); + const rollback = jest.spyOn(rollback_migrator, "rollback"); + await sut.migrate(helper); + expect(migrate).not.toBeCalled(); + expect(rollback).not.toBeCalled(); + }); + + it("should not update version if the current version does not match the from version", async () => { + const helper = new MigrationHelper(3, mock(), mock()); + const migrate = jest.spyOn(migrator, "updateVersion"); + const rollback = jest.spyOn(rollback_migrator, "updateVersion"); + await sut.migrate(helper); + expect(migrate).not.toBeCalled(); + expect(rollback).not.toBeCalled(); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migration-builder.ts b/libs/common/src/state-migrations/migration-builder.ts new file mode 100644 index 00000000000..776295a6b8f --- /dev/null +++ b/libs/common/src/state-migrations/migration-builder.ts @@ -0,0 +1,106 @@ +import { MigrationHelper } from "./migration-helper"; +import { Direction, Migrator, VersionFrom, VersionTo } from "./migrator"; + +export class MigrationBuilder { + /** Create a new MigrationBuilder with an empty buffer of migrations to perform. + * + * Add migrations to the buffer with {@link with} and {@link rollback}. + * @returns A new MigrationBuilder. + */ + static create(): MigrationBuilder<0> { + return new MigrationBuilder([]); + } + + private constructor( + private migrations: readonly { migrator: Migrator; direction: Direction }[] + ) {} + + /** Add a migrator to the MigrationBuilder. Types are updated such that the chained MigrationBuilder must currently be + * at state version equal to the from version of the migrator. Return as MigrationBuilder where TTo is the to + * version of the migrator, so that the next migrator can be chained. + * + * @param migrate A migrator class or a tuple of a migrator class, the from version, and the to version. A tuple is + * required to instantiate version numbers unless a default constructor is defined. + * @returns A new MigrationBuilder with the to version of the migrator as the current version. + */ + with< + TMigrator extends Migrator, + TFrom extends VersionFrom & TCurrent, + TTo extends VersionTo + >( + ...migrate: [new () => TMigrator] | [new (from: TFrom, to: TTo) => TMigrator, TFrom, TTo] + ): MigrationBuilder { + return this.addMigrator(migrate, "up"); + } + + /** Add a migrator to rollback on the MigrationBuilder's list of migrations. As with {@link with}, types of + * MigrationBuilder and Migrator must align. However, this time the migration is reversed so TCurrent of the + * MigrationBuilder must be equal to the to version of the migrator. Return as MigrationBuilder where TFrom + * is the from version of the migrator, so that the next migrator can be chained. + * + * @param migrate A migrator class or a tuple of a migrator class, the from version, and the to version. A tuple is + * required to instantiate version numbers unless a default constructor is defined. + * @returns A new MigrationBuilder with the from version of the migrator as the current version. + */ + rollback< + TMigrator extends Migrator, + TFrom extends VersionFrom, + TTo extends VersionTo & TCurrent + >( + ...migrate: [new () => TMigrator] | [new (from: TFrom, to: TTo) => TMigrator, TTo, TFrom] + ): MigrationBuilder { + if (migrate.length === 3) { + migrate = [migrate[0], migrate[2], migrate[1]]; + } + return this.addMigrator(migrate, "down"); + } + + /** Execute the migrations as defined in the MigrationBuilder's migrator buffer */ + migrate(helper: MigrationHelper): Promise { + return this.migrations.reduce( + (promise, migrator) => + promise.then(async () => { + await this.runMigrator(migrator.migrator, helper, migrator.direction); + }), + Promise.resolve() + ); + } + + private addMigrator< + TMigrator extends Migrator, + TFrom extends VersionFrom & TCurrent, + TTo extends VersionTo + >( + migrate: [new () => TMigrator] | [new (from: TFrom, to: TTo) => TMigrator, TFrom, TTo], + direction: Direction = "up" + ) { + const newMigration = + migrate.length === 1 + ? { migrator: new migrate[0](), direction } + : { migrator: new migrate[0](migrate[1], migrate[2]), direction }; + + return new MigrationBuilder([...this.migrations, newMigration]); + } + + private async runMigrator( + migrator: Migrator, + helper: MigrationHelper, + direction: Direction + ): Promise { + const shouldMigrate = await migrator.shouldMigrate(helper, direction); + helper.info( + `Migrator ${migrator.constructor.name} (to version ${migrator.toVersion}) should migrate: ${shouldMigrate} - ${direction}` + ); + if (shouldMigrate) { + const method = direction === "up" ? migrator.migrate : migrator.rollback; + await method(helper); + helper.info( + `Migrator ${migrator.constructor.name} (to version ${migrator.toVersion}) migrated - ${direction}` + ); + await migrator.updateVersion(helper, direction); + helper.info( + `Migrator ${migrator.constructor.name} (to version ${migrator.toVersion}) updated version - ${direction}` + ); + } + } +} diff --git a/libs/common/src/state-migrations/migration-helper.spec.ts b/libs/common/src/state-migrations/migration-helper.spec.ts new file mode 100644 index 00000000000..5b8a0f2eb4f --- /dev/null +++ b/libs/common/src/state-migrations/migration-helper.spec.ts @@ -0,0 +1,84 @@ +import { MockProxy, mock } from "jest-mock-extended"; + +// eslint-disable-next-line import/no-restricted-paths -- Needed to print log messages +import { LogService } from "../platform/abstractions/log.service"; +// eslint-disable-next-line import/no-restricted-paths -- Needed to interface with storage locations +import { AbstractStorageService } from "../platform/abstractions/storage.service"; + +import { MigrationHelper } from "./migration-helper"; + +const exampleJSON = { + authenticatedAccounts: [ + "c493ed01-4e08-4e88-abc7-332f380ca760", + "23e61a5f-2ece-4f5e-b499-f0bc489482a9", + ], + "c493ed01-4e08-4e88-abc7-332f380ca760": { + otherStuff: "otherStuff1", + }, + "23e61a5f-2ece-4f5e-b499-f0bc489482a9": { + otherStuff: "otherStuff2", + }, +}; + +describe("RemoveLegacyEtmKeyMigrator", () => { + let storage: MockProxy; + let logService: MockProxy; + let sut: MigrationHelper; + + beforeEach(() => { + logService = mock(); + storage = mock(); + storage.get.mockImplementation((key) => (exampleJSON as any)[key]); + + sut = new MigrationHelper(0, storage, logService); + }); + + describe("get", () => { + it("should delegate to storage.get", async () => { + await sut.get("key"); + expect(storage.get).toHaveBeenCalledWith("key"); + }); + }); + + describe("set", () => { + it("should delegate to storage.save", async () => { + await sut.set("key", "value"); + expect(storage.save).toHaveBeenCalledWith("key", "value"); + }); + }); + + describe("getAccounts", () => { + it("should return all accounts", async () => { + const accounts = await sut.getAccounts(); + expect(accounts).toEqual([ + { userId: "c493ed01-4e08-4e88-abc7-332f380ca760", account: { otherStuff: "otherStuff1" } }, + { userId: "23e61a5f-2ece-4f5e-b499-f0bc489482a9", account: { otherStuff: "otherStuff2" } }, + ]); + }); + + it("should handle missing authenticatedAccounts", async () => { + storage.get.mockImplementation((key) => + key === "authenticatedAccounts" ? undefined : (exampleJSON as any)[key] + ); + const accounts = await sut.getAccounts(); + expect(accounts).toEqual([]); + }); + }); +}); + +/** Helper to create well-mocked migration helpers in migration tests */ +export function mockMigrationHelper(storageJson: any): MockProxy { + const logService: MockProxy = mock(); + const storage: MockProxy = mock(); + storage.get.mockImplementation((key) => (storageJson as any)[key]); + storage.save.mockImplementation(async (key, value) => { + (storageJson as any)[key] = value; + }); + const helper = new MigrationHelper(0, storage, logService); + + const mockHelper = mock(); + mockHelper.get.mockImplementation((key) => helper.get(key)); + mockHelper.set.mockImplementation((key, value) => helper.set(key, value)); + mockHelper.getAccounts.mockImplementation(() => helper.getAccounts()); + return mockHelper; +} diff --git a/libs/common/src/state-migrations/migration-helper.ts b/libs/common/src/state-migrations/migration-helper.ts new file mode 100644 index 00000000000..a185aa69a99 --- /dev/null +++ b/libs/common/src/state-migrations/migration-helper.ts @@ -0,0 +1,37 @@ +// eslint-disable-next-line import/no-restricted-paths -- Needed to print log messages +import { LogService } from "../platform/abstractions/log.service"; +// eslint-disable-next-line import/no-restricted-paths -- Needed to interface with storage locations +import { AbstractStorageService } from "../platform/abstractions/storage.service"; + +export class MigrationHelper { + constructor( + public currentVersion: number, + private storageService: AbstractStorageService, + public logService: LogService + ) {} + + get(key: string): Promise { + return this.storageService.get(key); + } + + set(key: string, value: T): Promise { + this.logService.info(`Setting ${key}`); + return this.storageService.save(key, value); + } + + info(message: string): void { + this.logService.info(message); + } + + async getAccounts(): Promise< + { userId: string; account: ExpectedAccountType }[] + > { + const userIds = (await this.get("authenticatedAccounts")) ?? []; + return Promise.all( + userIds.map(async (userId) => ({ + userId, + account: await this.get(userId), + })) + ); + } +} diff --git a/libs/common/src/state-migrations/migrations/3-fix-premium.spec.ts b/libs/common/src/state-migrations/migrations/3-fix-premium.spec.ts new file mode 100644 index 00000000000..1ef910d4569 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/3-fix-premium.spec.ts @@ -0,0 +1,111 @@ +import { MockProxy } from "jest-mock-extended"; + +// eslint-disable-next-line import/no-restricted-paths -- Used for testing migration, which requires import +import { TokenService } from "../../auth/services/token.service"; +import { MigrationHelper } from "../migration-helper"; +import { mockMigrationHelper } from "../migration-helper.spec"; + +import { FixPremiumMigrator } from "./3-fix-premium"; + +function migrateExampleJSON() { + return { + global: { + stateVersion: 2, + otherStuff: "otherStuff1", + }, + authenticatedAccounts: [ + "c493ed01-4e08-4e88-abc7-332f380ca760", + "23e61a5f-2ece-4f5e-b499-f0bc489482a9", + ], + "c493ed01-4e08-4e88-abc7-332f380ca760": { + profile: { + otherStuff: "otherStuff2", + hasPremiumPersonally: null as boolean, + }, + tokens: { + otherStuff: "otherStuff3", + accessToken: "accessToken", + }, + otherStuff: "otherStuff4", + }, + "23e61a5f-2ece-4f5e-b499-f0bc489482a9": { + profile: { + otherStuff: "otherStuff5", + hasPremiumPersonally: true, + }, + tokens: { + otherStuff: "otherStuff6", + accessToken: "accessToken", + }, + otherStuff: "otherStuff7", + }, + otherStuff: "otherStuff8", + }; +} + +jest.mock("../../auth/services/token.service", () => ({ + TokenService: { + decodeToken: jest.fn(), + }, +})); + +describe("FixPremiumMigrator", () => { + let helper: MockProxy; + let sut: FixPremiumMigrator; + const decodeTokenSpy = TokenService.decodeToken as jest.Mock; + + beforeEach(() => { + helper = mockMigrationHelper(migrateExampleJSON()); + sut = new FixPremiumMigrator(2, 3); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe("migrate", () => { + it("should migrate hasPremiumPersonally", async () => { + decodeTokenSpy.mockResolvedValueOnce({ premium: true }); + await sut.migrate(helper); + + expect(helper.set).toHaveBeenCalledTimes(1); + expect(helper.set).toHaveBeenCalledWith("c493ed01-4e08-4e88-abc7-332f380ca760", { + profile: { + otherStuff: "otherStuff2", + hasPremiumPersonally: true, + }, + tokens: { + otherStuff: "otherStuff3", + accessToken: "accessToken", + }, + otherStuff: "otherStuff4", + }); + }); + + it("should not migrate if decode throws", async () => { + decodeTokenSpy.mockRejectedValueOnce(new Error("test")); + await sut.migrate(helper); + + expect(helper.set).not.toHaveBeenCalled(); + }); + + it("should not migrate if decode returns null", async () => { + decodeTokenSpy.mockResolvedValueOnce(null); + await sut.migrate(helper); + + expect(helper.set).not.toHaveBeenCalled(); + }); + }); + + describe("updateVersion", () => { + it("should update version", async () => { + await sut.updateVersion(helper, "up"); + + expect(helper.set).toHaveBeenCalledTimes(1); + expect(helper.set).toHaveBeenCalledWith("global", { + stateVersion: 3, + otherStuff: "otherStuff1", + }); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/3-fix-premium.ts b/libs/common/src/state-migrations/migrations/3-fix-premium.ts new file mode 100644 index 00000000000..b6c69a99168 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/3-fix-premium.ts @@ -0,0 +1,48 @@ +// eslint-disable-next-line import/no-restricted-paths -- Used for token decoding, which are valid for days. We want the latest +import { TokenService } from "../../auth/services/token.service"; +import { MigrationHelper } from "../migration-helper"; +import { Migrator, IRREVERSIBLE, Direction } from "../migrator"; + +type ExpectedAccountType = { + profile?: { hasPremiumPersonally?: boolean }; + tokens?: { accessToken?: string }; +}; + +export class FixPremiumMigrator extends Migrator<2, 3> { + async migrate(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + + async function fixPremium(userId: string, account: ExpectedAccountType) { + if (account?.profile?.hasPremiumPersonally === null && account.tokens?.accessToken != null) { + let decodedToken: { premium: boolean }; + try { + decodedToken = await TokenService.decodeToken(account.tokens.accessToken); + } catch { + return; + } + + if (decodedToken?.premium == null) { + return; + } + + account.profile.hasPremiumPersonally = decodedToken?.premium; + return helper.set(userId, account); + } + } + + await Promise.all(accounts.map(({ userId, account }) => fixPremium(userId, account))); + } + + rollback(helper: MigrationHelper): Promise { + throw IRREVERSIBLE; + } + + // Override is necessary because default implementation assumes `stateVersion` at the root, but for this version + // it is nested inside a global object. + override async updateVersion(helper: MigrationHelper, direction: Direction): Promise { + const endVersion = direction === "up" ? this.toVersion : this.fromVersion; + helper.currentVersion = endVersion; + const global: Record = (await helper.get("global")) || {}; + await helper.set("global", { ...global, stateVersion: endVersion }); + } +} diff --git a/libs/common/src/state-migrations/migrations/4-remove-ever-been-unlocked.spec.ts b/libs/common/src/state-migrations/migrations/4-remove-ever-been-unlocked.spec.ts new file mode 100644 index 00000000000..1701762118d --- /dev/null +++ b/libs/common/src/state-migrations/migrations/4-remove-ever-been-unlocked.spec.ts @@ -0,0 +1,75 @@ +import { MockProxy } from "jest-mock-extended"; + +import { MigrationHelper } from "../migration-helper"; +import { mockMigrationHelper } from "../migration-helper.spec"; + +import { RemoveEverBeenUnlockedMigrator } from "./4-remove-ever-been-unlocked"; + +function migrateExampleJSON() { + return { + global: { + stateVersion: 3, + otherStuff: "otherStuff1", + }, + authenticatedAccounts: [ + "c493ed01-4e08-4e88-abc7-332f380ca760", + "23e61a5f-2ece-4f5e-b499-f0bc489482a9", + ], + "c493ed01-4e08-4e88-abc7-332f380ca760": { + profile: { + otherStuff: "otherStuff2", + everBeenUnlocked: true, + }, + otherStuff: "otherStuff3", + }, + "23e61a5f-2ece-4f5e-b499-f0bc489482a9": { + profile: { + otherStuff: "otherStuff4", + everBeenUnlocked: false, + }, + otherStuff: "otherStuff5", + }, + otherStuff: "otherStuff6", + }; +} + +describe("RemoveEverBeenUnlockedMigrator", () => { + let helper: MockProxy; + let sut: RemoveEverBeenUnlockedMigrator; + + beforeEach(() => { + helper = mockMigrationHelper(migrateExampleJSON()); + sut = new RemoveEverBeenUnlockedMigrator(3, 4); + }); + + describe("migrate", () => { + it("should remove everBeenUnlocked from profile", async () => { + await sut.migrate(helper); + expect(helper.set).toHaveBeenCalledTimes(2); + expect(helper.set).toHaveBeenCalledWith("c493ed01-4e08-4e88-abc7-332f380ca760", { + profile: { + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }); + expect(helper.set).toHaveBeenCalledWith("23e61a5f-2ece-4f5e-b499-f0bc489482a9", { + profile: { + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }); + }); + }); + + describe("updateVersion", () => { + it("should update version up", async () => { + await sut.updateVersion(helper, "up"); + + expect(helper.set).toHaveBeenCalledTimes(1); + expect(helper.set).toHaveBeenCalledWith("global", { + stateVersion: 4, + otherStuff: "otherStuff1", + }); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/4-remove-ever-been-unlocked.ts b/libs/common/src/state-migrations/migrations/4-remove-ever-been-unlocked.ts new file mode 100644 index 00000000000..cfa45958d06 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/4-remove-ever-been-unlocked.ts @@ -0,0 +1,32 @@ +import { MigrationHelper } from "../migration-helper"; +import { Direction, IRREVERSIBLE, Migrator } from "../migrator"; + +type ExpectedAccountType = { profile?: { everBeenUnlocked?: boolean } }; + +export class RemoveEverBeenUnlockedMigrator extends Migrator<3, 4> { + async migrate(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + + async function removeEverBeenUnlocked(userId: string, account: ExpectedAccountType) { + if (account?.profile?.everBeenUnlocked != null) { + delete account.profile.everBeenUnlocked; + return helper.set(userId, account); + } + } + + Promise.all(accounts.map(({ userId, account }) => removeEverBeenUnlocked(userId, account))); + } + + rollback(helper: MigrationHelper): Promise { + throw IRREVERSIBLE; + } + + // Override is necessary because default implementation assumes `stateVersion` at the root, but for this version + // it is nested inside a global object. + override async updateVersion(helper: MigrationHelper, direction: Direction): Promise { + const endVersion = direction === "up" ? this.toVersion : this.fromVersion; + helper.currentVersion = endVersion; + const global: { stateVersion: number } = (await helper.get("global")) || ({} as any); + await helper.set("global", { ...global, stateVersion: endVersion }); + } +} diff --git a/libs/common/src/state-migrations/migrations/5-add-key-type-to-org-keys.spec.ts b/libs/common/src/state-migrations/migrations/5-add-key-type-to-org-keys.spec.ts new file mode 100644 index 00000000000..028a0b879b1 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/5-add-key-type-to-org-keys.spec.ts @@ -0,0 +1,141 @@ +import { MockProxy } from "jest-mock-extended"; + +import { MigrationHelper } from "../migration-helper"; +import { mockMigrationHelper } from "../migration-helper.spec"; + +import { AddKeyTypeToOrgKeysMigrator } from "./5-add-key-type-to-org-keys"; + +function migrateExampleJSON() { + return { + global: { + stateVersion: 4, + otherStuff: "otherStuff1", + }, + authenticatedAccounts: [ + "c493ed01-4e08-4e88-abc7-332f380ca760", + "23e61a5f-2ece-4f5e-b499-f0bc489482a9", + ], + "c493ed01-4e08-4e88-abc7-332f380ca760": { + keys: { + organizationKeys: { + encrypted: { + orgOneId: "orgOneEncKey", + orgTwoId: "orgTwoEncKey", + }, + }, + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }, + }; +} + +function rollbackExampleJSON() { + return { + global: { + stateVersion: 5, + otherStuff: "otherStuff1", + }, + authenticatedAccounts: [ + "c493ed01-4e08-4e88-abc7-332f380ca760", + "23e61a5f-2ece-4f5e-b499-f0bc489482a9", + ], + "c493ed01-4e08-4e88-abc7-332f380ca760": { + keys: { + organizationKeys: { + encrypted: { + orgOneId: { + type: "organization", + key: "orgOneEncKey", + }, + orgTwoId: { + type: "organization", + key: "orgTwoEncKey", + }, + }, + }, + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }, + }; +} + +describe("AddKeyTypeToOrgKeysMigrator", () => { + let helper: MockProxy; + let sut: AddKeyTypeToOrgKeysMigrator; + + describe("migrate", () => { + beforeEach(() => { + helper = mockMigrationHelper(migrateExampleJSON()); + sut = new AddKeyTypeToOrgKeysMigrator(4, 5); + }); + + it("should add organization type to organization keys", async () => { + await sut.migrate(helper); + + expect(helper.set).toHaveBeenCalledWith("c493ed01-4e08-4e88-abc7-332f380ca760", { + keys: { + organizationKeys: { + encrypted: { + orgOneId: { + type: "organization", + key: "orgOneEncKey", + }, + orgTwoId: { + type: "organization", + key: "orgTwoEncKey", + }, + }, + }, + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }); + }); + + it("should update version", async () => { + await sut.updateVersion(helper, "up"); + + expect(helper.set).toHaveBeenCalledTimes(1); + expect(helper.set).toHaveBeenCalledWith("global", { + stateVersion: 5, + otherStuff: "otherStuff1", + }); + }); + }); + + describe("rollback", () => { + beforeEach(() => { + helper = mockMigrationHelper(rollbackExampleJSON()); + sut = new AddKeyTypeToOrgKeysMigrator(4, 5); + }); + + it("should remove type from orgainzation keys", async () => { + await sut.rollback(helper); + + expect(helper.set).toHaveBeenCalledWith("c493ed01-4e08-4e88-abc7-332f380ca760", { + keys: { + organizationKeys: { + encrypted: { + orgOneId: "orgOneEncKey", + orgTwoId: "orgTwoEncKey", + }, + }, + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }); + }); + + it("should update version down", async () => { + await sut.updateVersion(helper, "down"); + + expect(helper.set).toHaveBeenCalledTimes(1); + expect(helper.set).toHaveBeenCalledWith("global", { + stateVersion: 4, + otherStuff: "otherStuff1", + }); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/5-add-key-type-to-org-keys.ts b/libs/common/src/state-migrations/migrations/5-add-key-type-to-org-keys.ts new file mode 100644 index 00000000000..ab1550c52e3 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/5-add-key-type-to-org-keys.ts @@ -0,0 +1,67 @@ +import { MigrationHelper } from "../migration-helper"; +import { Direction, Migrator } from "../migrator"; + +type ExpectedAccountType = { keys?: { organizationKeys?: { encrypted: Record } } }; +type NewAccountType = { + keys?: { + organizationKeys?: { encrypted: Record }; + }; +}; + +export class AddKeyTypeToOrgKeysMigrator extends Migrator<4, 5> { + async migrate(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + + async function updateOrgKey(userId: string, account: ExpectedAccountType) { + const encryptedOrgKeys = account?.keys?.organizationKeys?.encrypted; + if (encryptedOrgKeys == null) { + return; + } + + const newOrgKeys: Record = {}; + + Object.entries(encryptedOrgKeys).forEach(([orgId, encKey]) => { + newOrgKeys[orgId] = { + type: "organization", + key: encKey, + }; + }); + (account as any).keys.organizationKeys.encrypted = newOrgKeys; + + await helper.set(userId, account); + } + + Promise.all(accounts.map(({ userId, account }) => updateOrgKey(userId, account))); + } + + async rollback(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + + async function updateOrgKey(userId: string, account: NewAccountType) { + const encryptedOrgKeys = account?.keys?.organizationKeys?.encrypted; + if (encryptedOrgKeys == null) { + return; + } + + const newOrgKeys: Record = {}; + + Object.entries(encryptedOrgKeys).forEach(([orgId, encKey]) => { + newOrgKeys[orgId] = encKey.key; + }); + (account as any).keys.organizationKeys.encrypted = newOrgKeys; + + await helper.set(userId, account); + } + + Promise.all(accounts.map(async ({ userId, account }) => updateOrgKey(userId, account))); + } + + // Override is necessary because default implementation assumes `stateVersion` at the root, but for this version + // it is nested inside a global object. + override async updateVersion(helper: MigrationHelper, direction: Direction): Promise { + const endVersion = direction === "up" ? this.toVersion : this.fromVersion; + helper.currentVersion = endVersion; + const global: { stateVersion: number } = (await helper.get("global")) || ({} as any); + await helper.set("global", { ...global, stateVersion: endVersion }); + } +} diff --git a/libs/common/src/state-migrations/migrations/6-remove-legacy-etm-key.spec.ts b/libs/common/src/state-migrations/migrations/6-remove-legacy-etm-key.spec.ts new file mode 100644 index 00000000000..bc7b862f6cf --- /dev/null +++ b/libs/common/src/state-migrations/migrations/6-remove-legacy-etm-key.spec.ts @@ -0,0 +1,80 @@ +import { MockProxy } from "jest-mock-extended"; + +import { MigrationHelper } from "../migration-helper"; +import { mockMigrationHelper } from "../migration-helper.spec"; + +import { RemoveLegacyEtmKeyMigrator } from "./6-remove-legacy-etm-key"; + +function exampleJSON() { + return { + global: { + stateVersion: 5, + otherStuff: "otherStuff1", + }, + authenticatedAccounts: [ + "c493ed01-4e08-4e88-abc7-332f380ca760", + "23e61a5f-2ece-4f5e-b499-f0bc489482a9", + "fd005ea6-a16a-45ef-ba4a-a194269bfd73", + ], + "c493ed01-4e08-4e88-abc7-332f380ca760": { + keys: { + legacyEtmKey: "legacyEtmKey", + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }, + "23e61a5f-2ece-4f5e-b499-f0bc489482a9": { + keys: { + legacyEtmKey: "legacyEtmKey", + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }, + }; +} + +describe("RemoveLegacyEtmKeyMigrator", () => { + let helper: MockProxy; + let sut: RemoveLegacyEtmKeyMigrator; + + beforeEach(() => { + helper = mockMigrationHelper(exampleJSON()); + sut = new RemoveLegacyEtmKeyMigrator(5, 6); + }); + + describe("migrate", () => { + it("should remove legacyEtmKey from all accounts", async () => { + await sut.migrate(helper); + expect(helper.set).toHaveBeenCalledWith("c493ed01-4e08-4e88-abc7-332f380ca760", { + keys: { + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }); + expect(helper.set).toHaveBeenCalledWith("23e61a5f-2ece-4f5e-b499-f0bc489482a9", { + keys: { + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }); + }); + }); + + describe("rollback", () => { + it("should throw", async () => { + await expect(sut.rollback(helper)).rejects.toThrow(); + }); + }); + + describe("updateVersion", () => { + it("should update version up", async () => { + await sut.updateVersion(helper, "up"); + + expect(helper.set).toHaveBeenCalledTimes(1); + expect(helper.set).toHaveBeenCalledWith("global", { + stateVersion: 6, + otherStuff: "otherStuff1", + }); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/6-remove-legacy-etm-key.ts b/libs/common/src/state-migrations/migrations/6-remove-legacy-etm-key.ts new file mode 100644 index 00000000000..2a06916ea33 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/6-remove-legacy-etm-key.ts @@ -0,0 +1,32 @@ +import { MigrationHelper } from "../migration-helper"; +import { Direction, IRREVERSIBLE, Migrator } from "../migrator"; + +type ExpectedAccountType = { keys?: { legacyEtmKey?: string } }; + +export class RemoveLegacyEtmKeyMigrator extends Migrator<5, 6> { + async migrate(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + + async function updateAccount(userId: string, account: ExpectedAccountType) { + if (account?.keys?.legacyEtmKey) { + delete account.keys.legacyEtmKey; + await helper.set(userId, account); + } + } + + await Promise.all(accounts.map(({ userId, account }) => updateAccount(userId, account))); + } + + async rollback(helper: MigrationHelper): Promise { + throw IRREVERSIBLE; + } + + // Override is necessary because default implementation assumes `stateVersion` at the root, but for this version + // it is nested inside a global object. + override async updateVersion(helper: MigrationHelper, direction: Direction): Promise { + const endVersion = direction === "up" ? this.toVersion : this.fromVersion; + helper.currentVersion = endVersion; + const global: { stateVersion: number } = (await helper.get("global")) || ({} as any); + await helper.set("global", { ...global, stateVersion: endVersion }); + } +} diff --git a/libs/common/src/state-migrations/migrations/7-move-biometric-auto-prompt-to-account.spec.ts b/libs/common/src/state-migrations/migrations/7-move-biometric-auto-prompt-to-account.spec.ts new file mode 100644 index 00000000000..fe73f8a9bc4 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/7-move-biometric-auto-prompt-to-account.spec.ts @@ -0,0 +1,102 @@ +import { MockProxy, any, matches } from "jest-mock-extended"; + +import { MigrationHelper } from "../migration-helper"; +import { mockMigrationHelper } from "../migration-helper.spec"; + +import { MoveBiometricAutoPromptToAccount } from "./7-move-biometric-auto-prompt-to-account"; + +function exampleJSON() { + return { + global: { + stateVersion: 6, + noAutoPromptBiometrics: true, + otherStuff: "otherStuff1", + }, + authenticatedAccounts: [ + "c493ed01-4e08-4e88-abc7-332f380ca760", + "23e61a5f-2ece-4f5e-b499-f0bc489482a9", + "fd005ea6-a16a-45ef-ba4a-a194269bfd73", + ], + "c493ed01-4e08-4e88-abc7-332f380ca760": { + settings: { + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }, + "23e61a5f-2ece-4f5e-b499-f0bc489482a9": { + settings: { + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }, + }; +} + +describe("RemoveLegacyEtmKeyMigrator", () => { + let helper: MockProxy; + let sut: MoveBiometricAutoPromptToAccount; + + beforeEach(() => { + helper = mockMigrationHelper(exampleJSON()); + sut = new MoveBiometricAutoPromptToAccount(6, 7); + }); + + describe("migrate", () => { + it("should remove noAutoPromptBiometrics from global", async () => { + await sut.migrate(helper); + expect(helper.set).toHaveBeenCalledWith("global", { + otherStuff: "otherStuff1", + stateVersion: 6, + }); + }); + + it("should set disableAutoBiometricsPrompt to true on all accounts", async () => { + await sut.migrate(helper); + expect(helper.set).toHaveBeenCalledWith("c493ed01-4e08-4e88-abc7-332f380ca760", { + settings: { + disableAutoBiometricsPrompt: true, + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }); + expect(helper.set).toHaveBeenCalledWith("23e61a5f-2ece-4f5e-b499-f0bc489482a9", { + settings: { + disableAutoBiometricsPrompt: true, + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }); + }); + + it("should not set disableAutoBiometricsPrompt to true on accounts if noAutoPromptBiometrics is false", async () => { + const json = exampleJSON(); + json.global.noAutoPromptBiometrics = false; + helper = mockMigrationHelper(json); + await sut.migrate(helper); + expect(helper.set).not.toHaveBeenCalledWith( + matches((s) => s != "global"), + any() + ); + }); + }); + + describe("rollback", () => { + it("should throw", async () => { + await expect(sut.rollback(helper)).rejects.toThrow(); + }); + }); + + describe("updateVersion", () => { + it("should update version up", async () => { + await sut.updateVersion(helper, "up"); + + expect(helper.set).toHaveBeenCalledTimes(1); + expect(helper.set).toHaveBeenCalledWith( + "global", + Object.assign({}, exampleJSON().global, { + stateVersion: 7, + }) + ); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/7-move-biometric-auto-prompt-to-account.ts b/libs/common/src/state-migrations/migrations/7-move-biometric-auto-prompt-to-account.ts new file mode 100644 index 00000000000..0ac065d60c1 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/7-move-biometric-auto-prompt-to-account.ts @@ -0,0 +1,45 @@ +import { MigrationHelper } from "../migration-helper"; +import { Direction, IRREVERSIBLE, Migrator } from "../migrator"; + +type ExpectedAccountType = { settings?: { disableAutoBiometricsPrompt?: boolean } }; + +export class MoveBiometricAutoPromptToAccount extends Migrator<6, 7> { + async migrate(helper: MigrationHelper): Promise { + const global = await helper.get<{ noAutoPromptBiometrics?: boolean }>("global"); + const noAutoPromptBiometrics = global?.noAutoPromptBiometrics ?? false; + + const accounts = await helper.getAccounts(); + async function updateAccount(userId: string, account: ExpectedAccountType) { + if (account == null) { + return; + } + + if (noAutoPromptBiometrics) { + account.settings = Object.assign(account?.settings ?? {}, { + disableAutoBiometricsPrompt: true, + }); + await helper.set(userId, account); + } + } + + delete global.noAutoPromptBiometrics; + + await Promise.all([ + ...accounts.map(({ userId, account }) => updateAccount(userId, account)), + helper.set("global", global), + ]); + } + + async rollback(helper: MigrationHelper): Promise { + throw IRREVERSIBLE; + } + + // Override is necessary because default implementation assumes `stateVersion` at the root, but for this version + // it is nested inside a global object. + override async updateVersion(helper: MigrationHelper, direction: Direction): Promise { + const endVersion = direction === "up" ? this.toVersion : this.fromVersion; + helper.currentVersion = endVersion; + const global: { stateVersion: number } = (await helper.get("global")) || ({} as any); + await helper.set("global", { ...global, stateVersion: endVersion }); + } +} diff --git a/libs/common/src/state-migrations/migrations/8-move-state-version.spec.ts b/libs/common/src/state-migrations/migrations/8-move-state-version.spec.ts new file mode 100644 index 00000000000..8c84fd642ea --- /dev/null +++ b/libs/common/src/state-migrations/migrations/8-move-state-version.spec.ts @@ -0,0 +1,90 @@ +import { MockProxy } from "jest-mock-extended"; + +import { MigrationHelper } from "../migration-helper"; +import { mockMigrationHelper } from "../migration-helper.spec"; + +import { MoveStateVersionMigrator } from "./8-move-state-version"; + +function migrateExampleJSON() { + return { + global: { + stateVersion: 6, + otherStuff: "otherStuff1", + }, + otherStuff: "otherStuff2", + }; +} + +function rollbackExampleJSON() { + return { + global: { + otherStuff: "otherStuff1", + }, + stateVersion: 7, + otherStuff: "otherStuff2", + }; +} + +describe("moveStateVersion", () => { + let helper: MockProxy; + let sut: MoveStateVersionMigrator; + + describe("migrate", () => { + beforeEach(() => { + helper = mockMigrationHelper(migrateExampleJSON()); + sut = new MoveStateVersionMigrator(7, 8); + }); + + it("should move state version to root", async () => { + await sut.migrate(helper); + expect(helper.set).toHaveBeenCalledWith("stateVersion", 6); + }); + + it("should remove state version from global", async () => { + await sut.migrate(helper); + expect(helper.set).toHaveBeenCalledWith("global", { + otherStuff: "otherStuff1", + }); + }); + + it("should throw if state version not found", async () => { + helper.get.mockReturnValue({ otherStuff: "otherStuff1" } as any); + await expect(sut.migrate(helper)).rejects.toThrow( + "Migration failed, state version not found" + ); + }); + + it("should update version up", async () => { + await sut.updateVersion(helper, "up"); + + expect(helper.set).toHaveBeenCalledTimes(1); + expect(helper.set).toHaveBeenCalledWith("stateVersion", 8); + }); + }); + + describe("rollback", () => { + beforeEach(() => { + helper = mockMigrationHelper(rollbackExampleJSON()); + sut = new MoveStateVersionMigrator(7, 8); + }); + + it("should move state version to global", async () => { + await sut.rollback(helper); + expect(helper.set).toHaveBeenCalledWith("global", { + stateVersion: 7, + otherStuff: "otherStuff1", + }); + expect(helper.set).toHaveBeenCalledWith("stateVersion", undefined); + }); + + it("should update version down", async () => { + await sut.updateVersion(helper, "down"); + + expect(helper.set).toHaveBeenCalledTimes(1); + expect(helper.set).toHaveBeenCalledWith("global", { + stateVersion: 7, + otherStuff: "otherStuff1", + }); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/8-move-state-version.ts b/libs/common/src/state-migrations/migrations/8-move-state-version.ts new file mode 100644 index 00000000000..cbcdf423843 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/8-move-state-version.ts @@ -0,0 +1,37 @@ +import { JsonObject } from "type-fest"; + +import { MigrationHelper } from "../migration-helper"; +import { Direction, Migrator } from "../migrator"; + +export class MoveStateVersionMigrator extends Migrator<7, 8> { + async migrate(helper: MigrationHelper): Promise { + const global = await helper.get<{ stateVersion: number }>("global"); + if (global.stateVersion) { + await helper.set("stateVersion", global.stateVersion); + delete global.stateVersion; + await helper.set("global", global); + } else { + throw new Error("Migration failed, state version not found"); + } + } + + async rollback(helper: MigrationHelper): Promise { + const version = await helper.get("stateVersion"); + const global = await helper.get("global"); + await helper.set("global", { ...global, stateVersion: version }); + await helper.set("stateVersion", undefined); + } + + // Override is necessary because default implementation assumes `stateVersion` at the root, but this migration moves + // it from a `global` object to root.This makes for unique rollback versioning. + override async updateVersion(helper: MigrationHelper, direction: Direction): Promise { + const endVersion = direction === "up" ? this.toVersion : this.fromVersion; + helper.currentVersion = endVersion; + if (direction === "up") { + await helper.set("stateVersion", endVersion); + } else { + const global: { stateVersion: number } = (await helper.get("global")) || ({} as any); + await helper.set("global", { ...global, stateVersion: endVersion }); + } + } +} diff --git a/libs/common/src/state-migrations/migrations/min-version.spec.ts b/libs/common/src/state-migrations/migrations/min-version.spec.ts new file mode 100644 index 00000000000..26e106c19a9 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/min-version.spec.ts @@ -0,0 +1,29 @@ +import { MockProxy } from "jest-mock-extended"; + +import { MIN_VERSION } from "../migrate"; +import { MigrationHelper } from "../migration-helper"; +import { mockMigrationHelper } from "../migration-helper.spec"; + +import { MinVersionMigrator } from "./min-version"; + +describe("MinVersionMigrator", () => { + let helper: MockProxy; + let sut: MinVersionMigrator; + + beforeEach(() => { + helper = mockMigrationHelper(null); + sut = new MinVersionMigrator(); + }); + + describe("shouldMigrate", () => { + it("should return true if current version is less than min version", async () => { + helper.currentVersion = MIN_VERSION - 1; + expect(await sut.shouldMigrate(helper)).toBe(true); + }); + + it("should return false if current version is greater than min version", async () => { + helper.currentVersion = MIN_VERSION + 1; + expect(await sut.shouldMigrate(helper)).toBe(false); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/min-version.ts b/libs/common/src/state-migrations/migrations/min-version.ts new file mode 100644 index 00000000000..a417cc51a3c --- /dev/null +++ b/libs/common/src/state-migrations/migrations/min-version.ts @@ -0,0 +1,26 @@ +import { MinVersion, MIN_VERSION } from "../migrate"; +import { MigrationHelper } from "../migration-helper"; +import { IRREVERSIBLE, Migrator } from "../migrator"; + +export function minVersionError(current: number) { + return `Your local data is too old to be migrated. Your current state version is ${current}, but minimum version is ${MIN_VERSION}.`; +} + +export class MinVersionMigrator extends Migrator<0, MinVersion> { + constructor() { + super(0, MIN_VERSION); + } + + // Overrides the default implementation to catch any version that may be passed in. + override shouldMigrate(helper: MigrationHelper): Promise { + return Promise.resolve(helper.currentVersion < MIN_VERSION); + } + async migrate(helper: MigrationHelper): Promise { + if (helper.currentVersion < MIN_VERSION) { + throw new Error(minVersionError(helper.currentVersion)); + } + } + async rollback(helper: MigrationHelper): Promise { + throw IRREVERSIBLE; + } +} diff --git a/libs/common/src/state-migrations/migrator.spec.ts b/libs/common/src/state-migrations/migrator.spec.ts new file mode 100644 index 00000000000..3abaa277273 --- /dev/null +++ b/libs/common/src/state-migrations/migrator.spec.ts @@ -0,0 +1,75 @@ +import { mock, MockProxy } from "jest-mock-extended"; + +// eslint-disable-next-line import/no-restricted-paths -- Needed to print log messages +import { LogService } from "../platform/abstractions/log.service"; +// eslint-disable-next-line import/no-restricted-paths -- Needed to interface with storage locations +import { AbstractStorageService } from "../platform/abstractions/storage.service"; + +import { MigrationHelper } from "./migration-helper"; +import { Migrator } from "./migrator"; + +describe("migrator default methods", () => { + class TestMigrator extends Migrator<0, 1> { + async migrate(helper: MigrationHelper): Promise { + await helper.set("test", "test"); + } + async rollback(helper: MigrationHelper): Promise { + await helper.set("test", "rollback"); + } + } + + let storage: MockProxy; + let logService: MockProxy; + let helper: MigrationHelper; + let sut: TestMigrator; + + beforeEach(() => { + storage = mock(); + logService = mock(); + helper = new MigrationHelper(0, storage, logService); + sut = new TestMigrator(0, 1); + }); + + describe("shouldMigrate", () => { + describe("up", () => { + it("should return true if the current version equals the from version", async () => { + expect(await sut.shouldMigrate(helper, "up")).toBe(true); + }); + + it("should return false if the current version does not equal the from version", async () => { + helper.currentVersion = 1; + expect(await sut.shouldMigrate(helper, "up")).toBe(false); + }); + }); + + describe("down", () => { + it("should return true if the current version equals the to version", async () => { + helper.currentVersion = 1; + expect(await sut.shouldMigrate(helper, "down")).toBe(true); + }); + + it("should return false if the current version does not equal the to version", async () => { + expect(await sut.shouldMigrate(helper, "down")).toBe(false); + }); + }); + }); + + describe("updateVersion", () => { + describe("up", () => { + it("should update the version", async () => { + await sut.updateVersion(helper, "up"); + expect(storage.save).toBeCalledWith("stateVersion", 1); + expect(helper.currentVersion).toBe(1); + }); + }); + + describe("down", () => { + it("should update the version", async () => { + helper.currentVersion = 1; + await sut.updateVersion(helper, "down"); + expect(storage.save).toBeCalledWith("stateVersion", 0); + expect(helper.currentVersion).toBe(0); + }); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrator.ts b/libs/common/src/state-migrations/migrator.ts new file mode 100644 index 00000000000..aba81372d49 --- /dev/null +++ b/libs/common/src/state-migrations/migrator.ts @@ -0,0 +1,40 @@ +import { NonNegativeInteger } from "type-fest"; + +import { MigrationHelper } from "./migration-helper"; + +export const IRREVERSIBLE = new Error("Irreversible migration"); + +export type VersionFrom = T extends Migrator + ? TFrom extends NonNegativeInteger + ? TFrom + : never + : never; +export type VersionTo = T extends Migrator + ? TTo extends NonNegativeInteger + ? TTo + : never + : never; +export type Direction = "up" | "down"; + +export abstract class Migrator { + constructor(public fromVersion: TFrom, public toVersion: TTo) { + if (fromVersion == null || toVersion == null) { + throw new Error("Invalid migration"); + } + if (fromVersion > toVersion) { + throw new Error("Invalid migration"); + } + } + + shouldMigrate(helper: MigrationHelper, direction: Direction): Promise { + const startVersion = direction === "up" ? this.fromVersion : this.toVersion; + return Promise.resolve(helper.currentVersion === startVersion); + } + abstract migrate(helper: MigrationHelper): Promise; + abstract rollback(helper: MigrationHelper): Promise; + async updateVersion(helper: MigrationHelper, direction: Direction): Promise { + const endVersion = direction === "up" ? this.toVersion : this.fromVersion; + helper.currentVersion = endVersion; + await helper.set("stateVersion", endVersion); + } +} From f61793b10b1b026c23a169bcba26eaac12c2d313 Mon Sep 17 00:00:00 2001 From: Jonathan Prusik Date: Wed, 30 Aug 2023 15:08:41 -0400 Subject: [PATCH 06/46] [PM-3588] Close duplicate single-action windows (#6091) * close duplicate single-action windows * Use longform conditional Co-authored-by: Cesar Gonzalez --------- Co-authored-by: Cesar Gonzalez --- .../platform/popup/browser-popout-window.service.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/apps/browser/src/platform/popup/browser-popout-window.service.ts b/apps/browser/src/platform/popup/browser-popout-window.service.ts index 95be15cc20d..ee03e3a2ec4 100644 --- a/apps/browser/src/platform/popup/browser-popout-window.service.ts +++ b/apps/browser/src/platform/popup/browser-popout-window.service.ts @@ -12,7 +12,6 @@ class BrowserPopoutWindowService implements BrowserPopupWindowServiceInterface { }; async openUnlockPrompt(senderWindowId: number) { - await this.closeUnlockPrompt(); await this.openSingleActionPopout( senderWindowId, "popup/index.html?uilocation=popout", @@ -36,8 +35,6 @@ class BrowserPopoutWindowService implements BrowserPopupWindowServiceInterface { action: string; } ) { - await this.closePasswordRepromptPrompt(); - const promptWindowPath = "popup/index.html#/view-cipher" + "?uilocation=popout" + @@ -73,18 +70,16 @@ class BrowserPopoutWindowService implements BrowserPopupWindowServiceInterface { const popupWindow = await BrowserApi.createWindow(windowOptions); - if (!singleActionPopoutKey) { - return; - } + await this.closeSingleActionPopout(singleActionPopoutKey); this.singleActionPopoutTabIds[singleActionPopoutKey] = popupWindow?.tabs[0].id; } private async closeSingleActionPopout(popoutKey: string) { const tabId = this.singleActionPopoutTabIds[popoutKey]; - if (!tabId) { - return; + + if (tabId) { + await BrowserApi.removeTab(tabId); } - await BrowserApi.removeTab(tabId); this.singleActionPopoutTabIds[popoutKey] = null; } } From 066056bd4564f76e1731b3018f585a82fc196554 Mon Sep 17 00:00:00 2001 From: aj-rosado <109146700+aj-rosado@users.noreply.github.com> Date: Wed, 30 Aug 2023 21:27:26 +0100 Subject: [PATCH 07/46] [PM-3226] Adding session to ReferenceEventRequest (#6114) * Adding session to ReferenceEventRequest * Added comment to regex --- .../auth/trial-initiation/trial-initiation.component.ts | 7 +++++++ libs/common/src/models/request/reference-event.request.ts | 1 + 2 files changed, 8 insertions(+) diff --git a/apps/web/src/app/auth/trial-initiation/trial-initiation.component.ts b/apps/web/src/app/auth/trial-initiation/trial-initiation.component.ts index e18c7548e03..183b57a90c6 100644 --- a/apps/web/src/app/auth/trial-initiation/trial-initiation.component.ts +++ b/apps/web/src/app/auth/trial-initiation/trial-initiation.component.ts @@ -94,6 +94,13 @@ export class TrialInitiationComponent implements OnInit, OnDestroy { if (this.referenceData.id === "") { this.referenceData.id = null; + } else { + // Matches "_ga_QBRN562QQQ=value1.value2.session" and captures values and session. + const regex = /_ga_QBRN562QQQ=([^.]+)\.([^.]+)\.(\d+)/; + const match = document.cookie.match(regex); + if (match) { + this.referenceData.session = match[3]; + } } } diff --git a/libs/common/src/models/request/reference-event.request.ts b/libs/common/src/models/request/reference-event.request.ts index 7a0b535a126..73a2532743a 100644 --- a/libs/common/src/models/request/reference-event.request.ts +++ b/libs/common/src/models/request/reference-event.request.ts @@ -1,5 +1,6 @@ export class ReferenceEventRequest { id: string; + session: string; layout: string; flow: string; } From 8669f81c1baac58cc16f7138498ea9fe04ce9fd0 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Thu, 31 Aug 2023 11:25:17 -0700 Subject: [PATCH 08/46] Make WebAuthn a Free Method (#6079) * remove webauthn premium badge * update premium two-stop options text for web clients --- apps/browser/src/_locales/en/messages.json | 4 ++-- apps/browser/src/popup/settings/premium.component.html | 2 +- apps/desktop/src/locales/en/messages.json | 4 ++-- apps/desktop/src/vault/app/accounts/premium.component.html | 2 +- apps/web/src/app/vault/settings/premium.component.html | 2 +- apps/web/src/locales/en/messages.json | 4 ++-- libs/common/src/auth/services/two-factor.service.ts | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 56640a8af8e..6aea5876eac 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "ppremiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." diff --git a/apps/browser/src/popup/settings/premium.component.html b/apps/browser/src/popup/settings/premium.component.html index a9a569ec176..2727ee405b9 100644 --- a/apps/browser/src/popup/settings/premium.component.html +++ b/apps/browser/src/popup/settings/premium.component.html @@ -22,7 +22,7 @@
  • - {{ "ppremiumSignUpTwoStep" | i18n }} + {{ "premiumSignUpTwoStepOptions" | i18n }}
  • diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index dd2d38e38b4..dbcb70d5e0c 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." diff --git a/apps/desktop/src/vault/app/accounts/premium.component.html b/apps/desktop/src/vault/app/accounts/premium.component.html index b91a021f9cc..b3a847dce12 100644 --- a/apps/desktop/src/vault/app/accounts/premium.component.html +++ b/apps/desktop/src/vault/app/accounts/premium.component.html @@ -17,7 +17,7 @@
  • - {{ "premiumSignUpTwoStep" | i18n }} + {{ "premiumSignUpTwoStepOptions" | i18n }}
  • diff --git a/apps/web/src/app/vault/settings/premium.component.html b/apps/web/src/app/vault/settings/premium.component.html index b47bfc1be2b..2c8c9d6146a 100644 --- a/apps/web/src/app/vault/settings/premium.component.html +++ b/apps/web/src/app/vault/settings/premium.component.html @@ -21,7 +21,7 @@
  • - {{ "premiumSignUpTwoStep" | i18n }} + {{ "premiumSignUpTwoStepOptions" | i18n }}
  • diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 1567bc65895..2e747f3cefd 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Emergency access" diff --git a/libs/common/src/auth/services/two-factor.service.ts b/libs/common/src/auth/services/two-factor.service.ts index b391b7d39e1..71687b675ee 100644 --- a/libs/common/src/auth/services/two-factor.service.ts +++ b/libs/common/src/auth/services/two-factor.service.ts @@ -55,7 +55,7 @@ export const TwoFactorProviders: Partial Date: Thu, 31 Aug 2023 16:53:51 -0400 Subject: [PATCH 09/46] PM-3623 - VaultTimeoutInputComp - resolve double value changes emission issue which was causing double dialogs to be shown when the user chose never as a vault timeout option by not emitting the setting of the custom hours & min values based on the user's non-custom timeout choice. + Add interface for form value to replace any. (#6098) --- .../settings/vault-timeout-input.component.ts | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/libs/angular/src/components/settings/vault-timeout-input.component.ts b/libs/angular/src/components/settings/vault-timeout-input.component.ts index e01a01fdd1d..d23fbe29367 100644 --- a/libs/angular/src/components/settings/vault-timeout-input.component.ts +++ b/libs/angular/src/components/settings/vault-timeout-input.component.ts @@ -15,6 +15,14 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +interface VaultTimeoutFormValue { + vaultTimeout: number | null; + custom: { + hours: number | null; + minutes: number | null; + }; +} + @Directive() export class VaultTimeoutInputComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy, OnChanges @@ -70,11 +78,13 @@ export class VaultTimeoutInputComponent this.applyVaultTimeoutPolicy(); }); - this.form.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((value) => { - if (this.onChange) { - this.onChange(this.getVaultTimeout(value)); - } - }); + this.form.valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe((value: VaultTimeoutFormValue) => { + if (this.onChange) { + this.onChange(this.getVaultTimeout(value)); + } + }); // Assign the current value to the custom fields // so that if the user goes from a numeric value to custom @@ -87,12 +97,19 @@ export class VaultTimeoutInputComponent ) .subscribe((value) => { const current = Math.max(value, 0); - this.form.patchValue({ - custom: { - hours: Math.floor(current / 60), - minutes: current % 60, + + // This cannot emit an event b/c it would cause form.valueChanges to fire again + // and we are already handling that above so just silently update + // custom fields when vaultTimeout changes to a non-custom value + this.form.patchValue( + { + custom: { + hours: Math.floor(current / 60), + minutes: current % 60, + }, }, - }); + { emitEvent: false } + ); }); this.canLockVault$ = this.vaultTimeoutSettingsService @@ -116,7 +133,7 @@ export class VaultTimeoutInputComponent } } - getVaultTimeout(value: any) { + getVaultTimeout(value: VaultTimeoutFormValue) { if (value.vaultTimeout !== VaultTimeoutInputComponent.CUSTOM_VALUE) { return value.vaultTimeout; } From ac1c7f9c8f8ad0979cf0a00a28212366ef0e9b00 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Thu, 31 Aug 2023 18:06:47 -0400 Subject: [PATCH 10/46] Auth - LoginComp - Focus logic bugfix - add null check to avoid error as focusInput was being called prematurely in some scenarios - confirmed the focus logic still works (#6095) --- apps/desktop/src/auth/login/login.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/auth/login/login.component.ts b/apps/desktop/src/auth/login/login.component.ts index 45b330f6dae..d1c6c88d14b 100644 --- a/apps/desktop/src/auth/login/login.component.ts +++ b/apps/desktop/src/auth/login/login.component.ts @@ -182,6 +182,6 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy { private focusInput() { const email = this.loggedEmail; - document.getElementById(email == null || email === "" ? "email" : "masterPassword").focus(); + document.getElementById(email == null || email === "" ? "email" : "masterPassword")?.focus(); } } From 1d7360bfdd36248f19aa048bd1012353e2c6f3b9 Mon Sep 17 00:00:00 2001 From: Dave Nicolson Date: Fri, 1 Sep 2023 12:06:49 +0200 Subject: [PATCH 11/46] [PS-1438] Prevent new line feed when selecting and copying passwords (#3460) * Prevent new line feed when selecting password * Prevent new line feed when copying password --- apps/desktop/src/scss/misc.scss | 1 - libs/angular/src/directives/copy-text.directive.ts | 6 +++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/scss/misc.scss b/apps/desktop/src/scss/misc.scss index a72b7754c13..8ed6a9b54be 100644 --- a/apps/desktop/src/scss/misc.scss +++ b/apps/desktop/src/scss/misc.scss @@ -561,7 +561,6 @@ h2, h3, label, a, -button, p, img, .box-header, diff --git a/libs/angular/src/directives/copy-text.directive.ts b/libs/angular/src/directives/copy-text.directive.ts index e3298c214c0..b595085e43b 100644 --- a/libs/angular/src/directives/copy-text.directive.ts +++ b/libs/angular/src/directives/copy-text.directive.ts @@ -1,5 +1,6 @@ import { Directive, ElementRef, HostListener, Input } from "@angular/core"; +import { ClientType } from "@bitwarden/common/enums"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @Directive({ @@ -15,6 +16,9 @@ export class CopyTextDirective { return; } - this.platformUtilsService.copyToClipboard(this.copyText, { window: window }); + const timeout = this.platformUtilsService.getClientType() === ClientType.Desktop ? 100 : 0; + setTimeout(() => { + this.platformUtilsService.copyToClipboard(this.copyText, { window: window }); + }, timeout); } } From 7d04974bd4cb2d37d36b028bd26828cd550568c7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 1 Sep 2023 12:43:17 +0000 Subject: [PATCH 12/46] Autosync the updated translations (#6167) 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 | 6 +- apps/browser/src/_locales/fil/messages.json | 4 +- apps/browser/src/_locales/fr/messages.json | 4 +- apps/browser/src/_locales/gl/messages.json | 4 +- apps/browser/src/_locales/he/messages.json | 4 +- apps/browser/src/_locales/hi/messages.json | 4 +- apps/browser/src/_locales/hr/messages.json | 4 +- apps/browser/src/_locales/hu/messages.json | 4 +- apps/browser/src/_locales/id/messages.json | 4 +- apps/browser/src/_locales/it/messages.json | 4 +- apps/browser/src/_locales/ja/messages.json | 4 +- apps/browser/src/_locales/ka/messages.json | 4 +- apps/browser/src/_locales/km/messages.json | 4 +- apps/browser/src/_locales/kn/messages.json | 4 +- apps/browser/src/_locales/ko/messages.json | 4 +- apps/browser/src/_locales/lt/messages.json | 4 +- apps/browser/src/_locales/lv/messages.json | 4 +- apps/browser/src/_locales/ml/messages.json | 4 +- apps/browser/src/_locales/mr/messages.json | 4 +- apps/browser/src/_locales/my/messages.json | 4 +- apps/browser/src/_locales/nb/messages.json | 4 +- apps/browser/src/_locales/ne/messages.json | 4 +- apps/browser/src/_locales/nl/messages.json | 4 +- apps/browser/src/_locales/nn/messages.json | 4 +- apps/browser/src/_locales/or/messages.json | 4 +- apps/browser/src/_locales/pl/messages.json | 4 +- apps/browser/src/_locales/pt_BR/messages.json | 4 +- apps/browser/src/_locales/pt_PT/messages.json | 4 +- apps/browser/src/_locales/ro/messages.json | 20 ++--- 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 | 82 +++++++++---------- 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 | 44 +++++----- apps/browser/src/_locales/vi/messages.json | 4 +- apps/browser/src/_locales/zh_CN/messages.json | 4 +- apps/browser/src/_locales/zh_TW/messages.json | 4 +- 60 files changed, 188 insertions(+), 188 deletions(-) diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 2b6c041a06d..cf6bf851dd0 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 جيغابايت وحدة تخزين مشفرة لمرفقات الملفات." }, - "ppremiumSignUpTwoStep": { - "message": "خيارات تسجيل الدخول الإضافية من خطوتين مثل YubiKey و FIDO U2F و Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "نظافة كلمة المرور، صحة الحساب، وتقارير خرق البيانات للحفاظ على سلامة خزنتك." diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 347fc449fa4..283b03a17a9 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "Fayl qoşmaları üçün 1 GB şifrələnmiş saxlama sahəsi" }, - "ppremiumSignUpTwoStep": { - "message": "YubiKey, FIDO U2F və Duo kimi iki mərhələli giriş seçimləri" + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "Anbarınızın təhlükəsiyini təmin etmək üçün parol gigiyenası, hesab sağlamlığı və verilənlərin pozulması hesabatları." diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 1df3d80b2e0..6aada06265d 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 ГБ зашыфраванага сховішча для далучаных файлаў." }, - "ppremiumSignUpTwoStep": { - "message": "Дадатковыя варыянты двухэтапнага ўваходу, такія як YubiKey, FIDO U2F і Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "Гігіена пароляў, здароўе ўліковага запісу і справаздачы аб уцечках даных для забеспячэння бяспекі вашага сховішча." diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index ee3049ffe22..cf9ec1b8ed9 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB пространство за файлове, които се шифрират." }, - "ppremiumSignUpTwoStep": { - "message": "Двустепенно удостоверяване чрез YubiKey, FIDO U2F и Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "Проверки в списъците с публикувани пароли, проверка на регистрациите и доклади за пробивите в сигурността, което спомага трезорът ви да е допълнително защитен." diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index a7b49de5ab3..7d7151b58f6 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "ফাইল সংযুক্তির জন্য ১ জিবি এনক্রিপ্টেড স্থান।" }, - "ppremiumSignUpTwoStep": { - "message": "YubiKey, FIDO U2F, ও Duo এর মতো অতিরিক্ত দ্বি-পদক্ষেপ লগইন বিকল্পগুলি।" + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "আপনার ভল্টটি সুরক্ষিত রাখতে পাসওয়ার্ড স্বাস্থ্যকরন, অ্যাকাউন্ট স্বাস্থ্য এবং ডেটা লঙ্ঘনের প্রতিবেদন।" diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index cf2b0fd8a52..657ce243606 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "ppremiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index f00775b993e..fbe9e33c65d 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB d'emmagatzematge xifrat per als fitxers adjunts." }, - "ppremiumSignUpTwoStep": { - "message": "Opcions addicionals d'inici de sessió en dues passes com ara YubiKey, FIDO U2F i Duo." + "premiumSignUpTwoStepOptions": { + "message": "Opcions propietàries de doble factor com ara YubiKey i Duo." }, "ppremiumSignUpReports": { "message": "Requisits d'higiene de la contrasenya, salut del compte i informe d'infraccions de dades per mantenir la seguretat de la vostra caixa forta." diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index 4d642f35640..a7bca0d78eb 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB šifrovaného úložiště pro přílohy." }, - "ppremiumSignUpTwoStep": { - "message": "Další možnosti dvoufázového přihlášení, jako je například YubiKey, FIDO U2F a Duo." + "premiumSignUpTwoStepOptions": { + "message": "Volby proprietálních dvoufázových přihlášení jako je YubiKey a Duo." }, "ppremiumSignUpReports": { "message": "Reporty o hygieně Vašich hesel, zdraví účtu a narušeních bezpečnosti." diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 6b278cbd93b..043a0fffead 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "ppremiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index 7d258e9a5d1..d2ddc3381b5 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB krypteret lager til vedhæftede filer." }, - "ppremiumSignUpTwoStep": { - "message": "Yderligere to-trins login muligheder såsom YubiKey, FIDO U2F og Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietære totrins-login muligheder, såsom YubiKey og Duo." }, "ppremiumSignUpReports": { "message": "Adgangskodehygiejne, kontosundhed og rapporter om datalæk til at holde din boks sikker." diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 8dfe56577e8..500329ab9ca 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB verschlüsselter Speicherplatz für Dateianhänge." }, - "ppremiumSignUpTwoStep": { - "message": "Zusätzliche Zweifaktor-Anmeldung über YubiKey, FIDO U2F, und Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietäre Optionen für die Zwei-Faktor Authentifizierung wie YubiKey und Duo." }, "ppremiumSignUpReports": { "message": "Berichte über Kennworthygiene, Kontostatus und Datenschutzverletzungen, um deinen Tresor sicher zu halten." diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index 5295936e303..e168fd93576 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB κρυπτογραφημένο αποθηκευτικό χώρο για συνημμένα αρχεία." }, - "ppremiumSignUpTwoStep": { - "message": "Πρόσθετες επιλογές σύνδεσης δύο βημάτων, όπως το YubiKey, το FIDO U2F και το Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "Ασφάλεια κωδικών, υγεία λογαριασμού και αναφορές παραβίασης δεδομένων για να διατηρήσετε ασφαλές το vault σας." diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index b10b53657a3..1632b31ee03 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "ppremiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index 8d46a4cbe26..b9f2a3784c3 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "ppremiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index 71764078cf4..e43102b483e 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB de espacio cifrado en disco para adjuntos." }, - "ppremiumSignUpTwoStep": { - "message": "Métodos de autenticación en dos pasos adicionales como YubiKey, FIDO U2F y Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "Higiene de contraseña, salud de la cuenta e informes de violaciones de datos para mantener su caja fuerte segura." diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index 24c15541572..5fc83e82676 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB ulatuses krüpteeritud salvestusruum." }, - "ppremiumSignUpTwoStep": { - "message": "Lisavõimalused kaheastmeliseks kinnitamiseks, näiteks YubiKey, FIDO U2F ja Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "Parooli hügieen, konto seisukord ja andmelekete raportid aitavad hoidlat turvalisena hoida." diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index 4e5a6857c4c..ac54d0c4ea6 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "Eranskinentzako 1GB-eko zifratutako biltegia." }, - "ppremiumSignUpTwoStep": { - "message": "YubiKey, FIDO U2F eta Duo bezalako bi urratseko saio hasierarako aukera gehigarriak." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "Pasahitzaren higienea, kontuaren egoera eta datu-bortxaketen txostenak, kutxa gotorra seguru mantentzeko." diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index b7aadc46e30..e1057038933 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "۱ گیگابایت فضای ذخیره سازی رمزگذاری شده برای پیوست های پرونده." }, - "ppremiumSignUpTwoStep": { - "message": "گزینه‌های ورود دو مرحله‌ای اضافی مانند YubiKey, FIDO U2F و Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "گزارش‌های بهداشت رمز عبور، سلامت حساب و نقض داده‌ها برای ایمن نگهداشتن گاوصندوق شما." diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 4ab409bfcb2..69492afd7f4 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 Gt salattua tallennustilaa tiedostoliitteille." }, - "ppremiumSignUpTwoStep": { - "message": "Muita kaksivaiheisen kirjautumisen todennusmenetelmiä kuten YubiKey, FIDO U2F ja Duo Security." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "Salasanahygienian, tilin terveyden ja tietovuotojen raportointitoiminnot pitävät holvisi turvassa." @@ -2310,7 +2310,7 @@ "message": "pakollinen" }, "search": { - "message": "Etsi" + "message": "Hae" }, "inputMinLength": { "message": "Syötteen tulee sisältää ainakin $COUNT$ merkkiä.", diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index 802d686c5af..be101c80b41 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB encrypted storage para sa mga file attachment." }, - "ppremiumSignUpTwoStep": { - "message": "Dagdag na dalawang hakbang na login option gaya ng YubiKey, FIDO U2F, at Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "Pasahod higiyena, kalusugan ng account, at mga ulat sa data breach upang panatilihing ligtas ang iyong vault." diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index 67c126dd9fc..b554c619a5f 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 Go de stockage chiffré pour les fichiers joints." }, - "ppremiumSignUpTwoStep": { - "message": "Options additionnelles d'identification à deux étapes telles que YubiKey, FIDO U2F et Duo." + "premiumSignUpTwoStepOptions": { + "message": "Options de connexion propriétaires à deux facteurs telles que YubiKey et Duo." }, "ppremiumSignUpReports": { "message": "Hygiène du mot de passe, santé du compte et rapports sur les brèches de données pour assurer la sécurité de votre coffre." diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index 56640a8af8e..6aea5876eac 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "ppremiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index ebdc30e269d..752935ce81a 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 ג'יגה של מקום אחסון עבור קבצים מצורפים." }, - "ppremiumSignUpTwoStep": { - "message": "אפשרויות כניסה דו שלבית מתקדמות כמו YubiKey, FIDO U2F, וגם Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "היגיינת סיסמאות, מצב בריאות החשבון, ודיווחים מעודכנים על פרצות חדשות בכדי לשמור על הכספת שלך בטוחה." diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 7d08de998db..d5c465e68bf 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB of encrypted file storage." }, - "ppremiumSignUpTwoStep": { - "message": "अतिरिक्त दो-चरण लॉगिन विकल्प जैसे YubiKey, FIDO U2F, और डुओ।" + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "अपनी वॉल्ट को सुरक्षित रखने के लिए पासवर्ड स्वच्छता, खाता स्वास्थ्य और डेटा उल्लंघन रिपोर्ट।" diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index 18f0d151312..db0fefbbffb 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB šifriranog prostora za pohranu podataka." }, - "ppremiumSignUpTwoStep": { - "message": "Dodatne mogućnosti za prijavu dvostrukom autentifikacijom kao što su YubiKey, FIDO U2F i Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "Higijenu lozinki, zdravlje računa i izvještaje o krađi podatak radi zaštite svojeg trezora." diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index e32509dbbc3..8bc89651d53 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB titkosított tárhely a fájlmellékleteknek." }, - "ppremiumSignUpTwoStep": { - "message": "További két lépcsős bejelentkezés lehetőségek, mint például YubiKey, FIDO U2F és Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "Jelszó higiénia, fiók biztonság és adatszivárgási jelentések a széf biztonsága érdekében." diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index b53f92ba44c..d8f6698f4ce 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB penyimpanan berkas yang dienkripsi." }, - "ppremiumSignUpTwoStep": { - "message": "Pilihan info masuk dua langkah tambahan seperti YubiKey, FIDO U2F, dan Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "Kebersihan kata sandi, kesehatan akun, dan laporan kebocoran data untuk tetap menjaga keamanan brankas Anda." diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 75154c2453f..e17ea019409 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB di spazio di archiviazione criptato per gli allegati." }, - "ppremiumSignUpTwoStep": { - "message": "Più opzioni di verifica in due passaggi come YubiKey, FIDO U2F, e Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "Sicurezza delle password, integrità dell'account, e rapporti su violazioni di dati per mantenere sicura la tua cassaforte." diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 93af04dd99f..e979237988c 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1GB の暗号化されたファイルストレージ" }, - "ppremiumSignUpTwoStep": { - "message": "YubiKey、FIDO U2F、Duoなどの追加の2段階認証ログインオプション" + "premiumSignUpTwoStepOptions": { + "message": "YubiKey、Duo などのプロプライエタリな2段階認証オプション。" }, "ppremiumSignUpReports": { "message": "保管庫を安全に保つための、パスワードやアカウントの健全性、データ侵害に関するレポート" diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index c8c379ec377..a619c47eaf4 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "ppremiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index 56640a8af8e..6aea5876eac 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "ppremiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index daaf3011f6f..fd01869139d 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "ಫೈಲ್ ಲಗತ್ತುಗಳಿಗಾಗಿ 1 ಜಿಬಿ ಎನ್‌ಕ್ರಿಪ್ಟ್ ಮಾಡಿದ ಸಂಗ್ರಹ." }, - "ppremiumSignUpTwoStep": { - "message": "ಹೆಚ್ಚುವರಿ ಎರಡು-ಹಂತದ ಲಾಗಿನ್ ಆಯ್ಕೆಗಳಾದ ಯೂಬಿಕೆ, ಎಫ್‌ಐಡಿಒ ಯು 2 ಎಫ್, ಮತ್ತು ಡ್ಯುವೋ." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "ನಿಮ್ಮ ವಾಲ್ಟ್ ಅನ್ನು ಸುರಕ್ಷಿತವಾಗಿರಿಸಲು ಪಾಸ್ವರ್ಡ್ ನೈರ್ಮಲ್ಯ, ಖಾತೆ ಆರೋಗ್ಯ ಮತ್ತು ಡೇಟಾ ಉಲ್ಲಂಘನೆ ವರದಿಗಳು." diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index 4d302bc5834..f982332a503 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1GB의 암호화된 파일 저장소." }, - "ppremiumSignUpTwoStep": { - "message": "YubiKey나 FIDO U2F, Duo 등의 추가적인 2단계 인증 옵션." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "보관함을 안전하게 유지하기 위한 암호 위생, 계정 상태, 데이터 유출 보고서" diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index 72748ba4a37..a3ba40c1fd9 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB užšifruotos vietos diske bylų prisegimams." }, - "ppremiumSignUpTwoStep": { - "message": "Papildomos dviejų žingsių prisijungimo opcijos, tokios kaip YubiKey, FIDO U2F ir Duo." + "premiumSignUpTwoStepOptions": { + "message": "Patentuotos dviejų žingsnių prisijungimo parinktys, tokios kaip YubiKey ir Duo." }, "ppremiumSignUpReports": { "message": "Slaptažodžio higiena, prieigos sveikata ir duomenų nutekinimo ataskaitos, kad tavo saugyklas būtų saugus." diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index ee46e51a6b2..cbf2c469124 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB šifrētas krātuves datņu pielikumiem." }, - "ppremiumSignUpTwoStep": { - "message": "Tādas papildu divpakāpju pieteikšanās iespējas kā YubiKey, FIDO U2F un Duo." + "premiumSignUpTwoStepOptions": { + "message": "Tādas slēgtā pirmavota divpakāpju pieteikšanās iespējas kā YubiKey un Duo." }, "ppremiumSignUpReports": { "message": "Paroļu higiēnas, konta veselības un datu noplūžu pārskati, lai uzturētu glabātavu drošu." diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index c7b3bd91b0d..416b35f78ec 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "ഫയൽ അറ്റാച്ചുമെന്റുകൾക്കായി 1 ജിബി എൻക്രിപ്റ്റുചെയ്‌ത സംഭരണം." }, - "ppremiumSignUpTwoStep": { - "message": "രണ്ട്-ഘട്ട പ്രവേശന ഓപ്ഷനുകളായ Yubikey, FIDO U2F, Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "നിങ്ങളുടെ വാൾട് സൂക്ഷിക്കുന്നതിന്. പാസ്‌വേഡ് ശുചിത്വം, അക്കൗണ്ട് ആരോഗ്യം, ഡാറ്റ ലംഘന റിപ്പോർട്ടുകൾ." diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index 5943fb9724e..e12aa7f8a65 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "ppremiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index 56640a8af8e..6aea5876eac 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "ppremiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 8b3889b799d..e806ea0a781 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB med kryptert fillagring for filvedlegg." }, - "ppremiumSignUpTwoStep": { - "message": "Ytterligere 2-trinnsinnloggingsmuligheter, slik som YubiKey, FIDO U2F, og Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "Passordhygiene, kontohelse, og databruddsrapporter som holder hvelvet ditt trygt." diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index 56640a8af8e..6aea5876eac 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "ppremiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index ed6f5394af3..f34016f70f0 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB versleutelde opslag voor bijlagen." }, - "ppremiumSignUpTwoStep": { - "message": "Extra opties voor tweestapsaanmelding zoals YubiKey, FIDO U2F en Duo." + "premiumSignUpTwoStepOptions": { + "message": "Eigen opties voor tweestapsaanmelding zoals YubiKey en Duo." }, "ppremiumSignUpReports": { "message": "Wachtwoordhygiëne, gezondheid van je account en datalekken om je kluis veilig te houden." diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index 56640a8af8e..6aea5876eac 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "ppremiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index 56640a8af8e..6aea5876eac 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "ppremiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index 37e5b701472..75f416b14e4 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB miejsca na zaszyfrowane załączniki." }, - "ppremiumSignUpTwoStep": { - "message": "Dodatkowe opcje logowania dwustopniowego, takie jak klucze YubiKey, FIDO U2F oraz Duo." + "premiumSignUpTwoStepOptions": { + "message": "Własnościowe opcje logowania dwuetapowego, takie jak YubiKey i Duo." }, "ppremiumSignUpReports": { "message": "Raporty bezpieczeństwa haseł, stanu konta i raporty wycieków danych, aby Twoje dane były bezpieczne." diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 0dd3ed1eee6..f8db10e49f8 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB de armazenamento de arquivos encriptados." }, - "ppremiumSignUpTwoStep": { - "message": "Opções de autenticação de duas etapas adicionais como YubiKey, FIDO U2F, e Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "Higiene de senha, saúde da conta, e relatórios sobre violação de dados para manter o seu cofre seguro." diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index 68a69fcf9c5..074ddb150e0 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB de armazenamento encriptado para anexos de ficheiros." }, - "ppremiumSignUpTwoStep": { - "message": "Opções adicionais de verificação de dois passos, como YubiKey, FIDO U2F e Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "Higiene de palavras-passe, saúde da conta e relatórios de violação de dados para manter o seu cofre seguro." diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 6f577a8da57..20c44ddcca2 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -196,13 +196,13 @@ "message": "Ajutor și feedback" }, "helpCenter": { - "message": "Bitwarden Help center" + "message": "Centrul de Ajutor Bitwarden" }, "communityForums": { - "message": "Explore Bitwarden community forums" + "message": "Explorează forumurile comunității Bitwarden" }, "contactSupport": { - "message": "Contact Bitwarden support" + "message": "Contactați asistența Bitwarden" }, "sync": { "message": "Sincronizare" @@ -442,7 +442,7 @@ "message": "Este necesară rescrierea parolei principale." }, "masterPasswordMinlength": { - "message": "Master password must be at least $VALUE$ characters long.", + "message": "Parola principală trebuie să aibă cel puțin $VALUE$ caractere.", "description": "The Master Password must be at least a specific number of characters long.", "placeholders": { "value": { @@ -634,10 +634,10 @@ "message": "Actualizare" }, "notificationUnlockDesc": { - "message": "Unlock your Bitwarden vault to complete the auto-fill request." + "message": "Deblochează seiful Bitwarden pentru a finaliza solicitarea de completare automată." }, "notificationUnlock": { - "message": "Unlock" + "message": "Deblocare" }, "enableContextMenuItem": { "message": "Afișați opțiunile meniului contextual" @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB spațiu de stocare criptat pentru atașamente de fișiere." }, - "ppremiumSignUpTwoStep": { - "message": "Opțiuni adiționale de conectare în două etape, cum ar fi YubiKey, FIDO U2F și Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "Rapoarte privind igiena parolelor, sănătatea contului și breșele de date pentru a vă păstra seiful în siguranță." @@ -985,7 +985,7 @@ "message": "Dacă se detectează un formular de autentificare, completați-l automat la încărcarea paginii web." }, "experimentalFeature": { - "message": "Compromised or untrusted websites can exploit auto-fill on page load." + "message": "Site-urile web compromise sau nesigure pot exploata funcția de autocompletare la încărcarea paginii." }, "learnMoreAboutAutofill": { "message": "Learn more about auto-fill" @@ -1468,7 +1468,7 @@ "message": "Articolul s-a completat automat " }, "insecurePageWarning": { - "message": "Warning: This is an unsecured HTTP page, and any information you submit can potentially be seen and changed by others. This Login was originally saved on a secure (HTTPS) page." + "message": "Avertisment: Aceasta este o pagină HTTP nesecurizată și orice informație pe care o trimiteți poate fi văzută și modificată de alte persoane. Această Parolă a fost salvată inițial pe o pagină securizată (HTTPS)." }, "insecurePageWarningFillPrompt": { "message": "Do you still wish to fill this login?" diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index d9614a63c2c..738193fb48a 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 ГБ зашифрованного хранилища для вложенных файлов." }, - "ppremiumSignUpTwoStep": { - "message": "Дополнительные варианты двухэтапной аутентификации, такие как YubiKey, FIDO U2F и Duo." + "premiumSignUpTwoStepOptions": { + "message": "Проприетарные варианты двухэтапной аутентификации, такие как YubiKey или Duo." }, "ppremiumSignUpReports": { "message": "Гигиена паролей, здоровье аккаунта и отчеты об утечках данных для обеспечения безопасности вашего хранилища." diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index 0d9a9648b7e..cf3ac370c6d 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "ගොනු ඇමුණුම් සඳහා 1 GB සංකේතාත්මක ගබඩා." }, - "ppremiumSignUpTwoStep": { - "message": "එවැනි YuBiKey, FIDO U2F, සහ Duo ලෙස අතිරේක පියවර දෙකක් පිවිසුම් විකල්ප." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "ඔබගේ සුරක්ෂිතාගාරය ආරක්ෂිතව තබා ගැනීම සඳහා මුරපදය සනීපාරක්ෂාව, ගිණුම් සෞඛ්යය සහ දත්ත උල්ලං ach නය වාර්තා කරයි." diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index c06ad279ec6..1b90bc94053 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB šifrovaného úložiska." }, - "ppremiumSignUpTwoStep": { - "message": "Ďalšie možnosti dvojstupňového prihlásenia ako YubiKey, FIDO U2F a Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "Správy o sile hesla, zabezpečení účtov a únikoch dát ktoré vám pomôžu udržať vaše kontá v bezpečí." diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index dba7c971bd7..cae2464dcb1 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB šifriranega prostora za shrambo podatkov." }, - "ppremiumSignUpTwoStep": { - "message": "Dodatne možnosti za prijavo v dveh korakih, n.pr. YubiKey, FIDO U2F in Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "Higiena gesel, zdravje računa in poročila o kraji podatkov, ki vam pomagajo ohraniti varnost vašega trezorja." diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index 155c5f6acc5..d4e5ee4f2a4 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -339,7 +339,7 @@ "message": "Остало" }, "unlockMethodNeededToChangeTimeoutActionDesc": { - "message": "Set up an unlock method to change your vault timeout action." + "message": "Подесите метод откључавања да бисте променили радњу временског ограничења сефа." }, "rateExtension": { "message": "Оцени овај додатак" @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1ГБ шифровано складиште за прилоге." }, - "ppremiumSignUpTwoStep": { - "message": "Додатне опције пријаве у два корака као што су YubiKey, FIDO U2F, и Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "Извештаји о хигијени лозинки, здравственом стању налога и кршењу података да бисте заштитили сеф." @@ -1606,10 +1606,10 @@ "message": "Биометрија прегледача није подржана на овом уређају." }, "biometricsFailedTitle": { - "message": "Biometrics failed" + "message": "Биометрија није успела" }, "biometricsFailedDesc": { - "message": "Biometrics cannot be completed, consider using a master password or logging out. If this persists, please contact Bitwarden support." + "message": "Биометрија се не може завршити, размислите о коришћењу главне лозинке или одјавите се. Ако се ово настави, контактирајте подршку Bitwarden-а." }, "nativeMessaginPermissionErrorTitle": { "message": "Дозвола није дата" @@ -2153,7 +2153,7 @@ "message": "Обавештење је послато на ваш уређај." }, "loginInitiated": { - "message": "Login initiated" + "message": "Пријава је покренута" }, "exposedMasterPassword": { "message": "Изложена главна лозинка" @@ -2240,25 +2240,25 @@ "message": "Отвара се у новом прозору" }, "deviceApprovalRequired": { - "message": "Device approval required. Select an approval option below:" + "message": "Потребно је одобрење уређаја. Изаберите опцију одобрења испод:" }, "rememberThisDevice": { - "message": "Remember this device" + "message": "Запамти овај уређај" }, "uncheckIfPublicDevice": { - "message": "Uncheck if using a public device" + "message": "Искључите ако се користи јавни уређај" }, "approveFromYourOtherDevice": { - "message": "Approve from your other device" + "message": "Одобри са мојим другим уређајем" }, "requestAdminApproval": { - "message": "Request admin approval" + "message": "Затражити одобрење администратора" }, "approveWithMasterPassword": { - "message": "Approve with master password" + "message": "Одобрити са главном лозинком" }, "ssoIdentifierRequired": { - "message": "Organization SSO identifier is required." + "message": "Потребан је SSO идентификатор организације." }, "eu": { "message": "EU", @@ -2280,40 +2280,40 @@ "message": "Приказ" }, "accountSuccessfullyCreated": { - "message": "Account successfully created!" + "message": "Налог је успешно креиран!" }, "adminApprovalRequested": { - "message": "Admin approval requested" + "message": "Захтевано је одобрење администратора" }, "adminApprovalRequestSentToAdmins": { - "message": "Your request has been sent to your admin." + "message": "Ваш захтев је послат вашем администратору." }, "youWillBeNotifiedOnceApproved": { - "message": "You will be notified once approved." + "message": "Бићете обавештени када буде одобрено." }, "troubleLoggingIn": { - "message": "Trouble logging in?" + "message": "Имате проблема са пријављивањем?" }, "loginApproved": { - "message": "Login approved" + "message": "Пријава је одобрена" }, "userEmailMissing": { - "message": "User email missing" + "message": "Недостаје имејл корисника" }, "deviceTrusted": { - "message": "Device trusted" + "message": "Уређај поуздан" }, "inputRequired": { - "message": "Input is required." + "message": "Унос је потребан." }, "required": { - "message": "required" + "message": "обавезно" }, "search": { - "message": "Search" + "message": "Тражи" }, "inputMinLength": { - "message": "Input must be at least $COUNT$ characters long.", + "message": "Унос трба имати најмање $COUNT$ слова.", "placeholders": { "count": { "content": "$1", @@ -2322,7 +2322,7 @@ } }, "inputMaxLength": { - "message": "Input must not exceed $COUNT$ characters in length.", + "message": "Унос не сме бити већи од $COUNT$ карактера.", "placeholders": { "count": { "content": "$1", @@ -2331,7 +2331,7 @@ } }, "inputForbiddenCharacters": { - "message": "The following characters are not allowed: $CHARACTERS$", + "message": "Следећи знакови нису дозвољени: $CHARACTERS$", "placeholders": { "characters": { "content": "$1", @@ -2340,7 +2340,7 @@ } }, "inputMinValue": { - "message": "Input value must be at least $MIN$.", + "message": "Вредност мора бити најмање $MIN$.", "placeholders": { "min": { "content": "$1", @@ -2349,7 +2349,7 @@ } }, "inputMaxValue": { - "message": "Input value must not exceed $MAX$.", + "message": "Вредност не сме бити већа од $MAX$.", "placeholders": { "max": { "content": "$1", @@ -2358,17 +2358,17 @@ } }, "multipleInputEmails": { - "message": "1 or more emails are invalid" + "message": "1 или више имејлова су неважећи" }, "inputTrimValidator": { - "message": "Input must not contain only whitespace.", + "message": "Унос не сме да садржи само размак.", "description": "Notification to inform the user that a form's input can't contain only whitespace." }, "inputEmail": { - "message": "Input is not an email address." + "message": "Унос није имејл." }, "fieldsNeedAttention": { - "message": "$COUNT$ field(s) above need your attention.", + "message": "$COUNT$ поље(а) изнад захтевај(у) вашу пажњу.", "placeholders": { "count": { "content": "$1", @@ -2377,22 +2377,22 @@ } }, "selectPlaceholder": { - "message": "-- Select --" + "message": "-- Одабрати --" }, "multiSelectPlaceholder": { - "message": "-- Type to filter --" + "message": "-- Тип за филтрирање --" }, "multiSelectLoading": { - "message": "Retrieving options..." + "message": "Преузимање опција..." }, "multiSelectNotFound": { - "message": "No items found" + "message": "Нема предмета" }, "multiSelectClearAll": { - "message": "Clear all" + "message": "Обриши све" }, "plusNMore": { - "message": "+ $QUANTITY$ more", + "message": "+ још $QUANTITY$", "placeholders": { "quantity": { "content": "$1", @@ -2401,10 +2401,10 @@ } }, "submenu": { - "message": "Submenu" + "message": "Под-мени" }, "toggleCollapse": { - "message": "Toggle collapse", + "message": "Промени проширење", "description": "Toggling an expand/collapse state." } } diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index f3275fce001..7ab058680c4 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB lagring av krypterade filer." }, - "ppremiumSignUpTwoStep": { - "message": "Ytterligare alternativ för tvåstegsverifiering såsom YubiKey, FIDO U2F och Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "Lösenordshygien, kontohälsa och dataintrångsrapporter för att hålla ditt valv säkert." diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index 56640a8af8e..6aea5876eac 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "ppremiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index 1cc5e9bc50c..7fcf7835119 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB of encrypted file storage." }, - "ppremiumSignUpTwoStep": { - "message": "ตัวเลือกการเข้าสู่ระบบแบบสองขั้นตอนเพิ่มเติม เช่น YubiKey, FIDO U2F และ Duo" + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "สุขอนามัยของรหัสผ่าน ความสมบูรณ์ของบัญชี และรายงานการละเมิดข้อมูลเพื่อให้ตู้นิรภัยของคุณปลอดภัย" diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 8bcdf1f6580..4134a27d265 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "Dosya ekleri için 1 GB şifrelenmiş depolama." }, - "ppremiumSignUpTwoStep": { - "message": "YubiKey, FIDO U2F ve Duo gibi iki aşamalı giriş seçenekleri." + "premiumSignUpTwoStepOptions": { + "message": "YubiKey ve Duo gibi marka bazlı iki aşamalı giriş seçenekleri." }, "ppremiumSignUpReports": { "message": "Kasanızı güvende tutmak için parola hijyeni, hesap sağlığı ve veri ihlali raporları." diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index dfa5ac4c7d3..2d1a0b66291 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 ГБ зашифрованого сховища для файлів." }, - "ppremiumSignUpTwoStep": { - "message": "Додаткові можливості двоетапної перевірки, наприклад, YubiKey, FIDO U2F та Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "Гігієна паролів, здоров'я облікового запису, а також звіти про вразливості даних, щоб зберігати ваше сховище в безпеці." @@ -2304,16 +2304,16 @@ "message": "Довірений пристрій" }, "inputRequired": { - "message": "Input is required." + "message": "Необхідно ввести дані." }, "required": { - "message": "required" + "message": "обов'язково" }, "search": { - "message": "Search" + "message": "Пошук" }, "inputMinLength": { - "message": "Input must be at least $COUNT$ characters long.", + "message": "Введені дані мають бути довжиною принаймні $COUNT$ символів.", "placeholders": { "count": { "content": "$1", @@ -2322,7 +2322,7 @@ } }, "inputMaxLength": { - "message": "Input must not exceed $COUNT$ characters in length.", + "message": "Вхідне значення не повинно перевищувати $COUNT$ символів.", "placeholders": { "count": { "content": "$1", @@ -2331,7 +2331,7 @@ } }, "inputForbiddenCharacters": { - "message": "The following characters are not allowed: $CHARACTERS$", + "message": "Вказані символи заборонені: $CHARACTERS$", "placeholders": { "characters": { "content": "$1", @@ -2340,7 +2340,7 @@ } }, "inputMinValue": { - "message": "Input value must be at least $MIN$.", + "message": "Значення має бути принаймні $MIN$.", "placeholders": { "min": { "content": "$1", @@ -2349,7 +2349,7 @@ } }, "inputMaxValue": { - "message": "Input value must not exceed $MAX$.", + "message": "Значення не може перевищувати $MAX$.", "placeholders": { "max": { "content": "$1", @@ -2358,17 +2358,17 @@ } }, "multipleInputEmails": { - "message": "1 or more emails are invalid" + "message": "1 або більше адрес е-пошти недійсні" }, "inputTrimValidator": { - "message": "Input must not contain only whitespace.", + "message": "Введене значення не повинно містити лише пробіл.", "description": "Notification to inform the user that a form's input can't contain only whitespace." }, "inputEmail": { - "message": "Input is not an email address." + "message": "Введені дані не є адресою е-пошти." }, "fieldsNeedAttention": { - "message": "$COUNT$ field(s) above need your attention.", + "message": "$COUNT$ поле (поля) вище потребують вашої уваги.", "placeholders": { "count": { "content": "$1", @@ -2377,22 +2377,22 @@ } }, "selectPlaceholder": { - "message": "-- Select --" + "message": "-- Оберіть--" }, "multiSelectPlaceholder": { - "message": "-- Type to filter --" + "message": "-- Введіть для фільтрування --" }, "multiSelectLoading": { - "message": "Retrieving options..." + "message": "Параметри отримання..." }, "multiSelectNotFound": { - "message": "No items found" + "message": "Нічого не знайдено" }, "multiSelectClearAll": { - "message": "Clear all" + "message": "Очистити все" }, "plusNMore": { - "message": "+ $QUANTITY$ more", + "message": "+ ще $QUANTITY$", "placeholders": { "quantity": { "content": "$1", @@ -2401,10 +2401,10 @@ } }, "submenu": { - "message": "Submenu" + "message": "Підменю" }, "toggleCollapse": { - "message": "Toggle collapse", + "message": "Згорнути/розгорнути", "description": "Toggling an expand/collapse state." } } diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index 7faabed46e0..5d71340db52 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1GB bộ nhớ lưu trữ tập tin được mã hóa." }, - "ppremiumSignUpTwoStep": { - "message": "Tuỳ chọn đăng nhập 2 bước bổ sung như YubiKey, FIDO U2F, và Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "Thanh lọc mật khẩu, kiểm tra an toàn tài khoản và các báo cáo rò rĩ dữ liệu là để giữ cho kho của bạn an toàn." diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index eb02a54f453..1fc419d0d16 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "1 GB 文件附件加密存储。" }, - "ppremiumSignUpTwoStep": { - "message": "额外的两步登录选项,如 YubiKey、FIDO U2F 和 Duo。" + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "密码健康、账户体检以及数据泄露报告,保障您的密码库安全。" diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index a0a2c9e3eb3..68eb917021e 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -795,8 +795,8 @@ "ppremiumSignUpStorage": { "message": "用於檔案附件的 1 GB 加密儲存空間。" }, - "ppremiumSignUpTwoStep": { - "message": "YubiKey、FIDO U2F 和 Duo 等額外的兩步驟登入選項。" + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "ppremiumSignUpReports": { "message": "密碼健康度檢查、提供帳戶體檢以及資料外洩報告,以保障您的密碼庫安全。" From 4e2f742aea8b9150518d045541ae46eb4dd1dafd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 1 Sep 2023 12:45:39 +0000 Subject: [PATCH 13/46] Autosync the updated translations (#6165) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/af/messages.json | 4 +- apps/desktop/src/locales/ar/messages.json | 4 +- apps/desktop/src/locales/az/messages.json | 4 +- apps/desktop/src/locales/be/messages.json | 4 +- apps/desktop/src/locales/bg/messages.json | 4 +- apps/desktop/src/locales/bn/messages.json | 4 +- apps/desktop/src/locales/bs/messages.json | 4 +- apps/desktop/src/locales/ca/messages.json | 4 +- apps/desktop/src/locales/cs/messages.json | 4 +- apps/desktop/src/locales/cy/messages.json | 4 +- apps/desktop/src/locales/da/messages.json | 4 +- apps/desktop/src/locales/de/messages.json | 4 +- apps/desktop/src/locales/el/messages.json | 4 +- apps/desktop/src/locales/en_GB/messages.json | 4 +- apps/desktop/src/locales/en_IN/messages.json | 4 +- apps/desktop/src/locales/eo/messages.json | 4 +- apps/desktop/src/locales/es/messages.json | 4 +- apps/desktop/src/locales/et/messages.json | 4 +- apps/desktop/src/locales/eu/messages.json | 4 +- apps/desktop/src/locales/fa/messages.json | 4 +- apps/desktop/src/locales/fi/messages.json | 4 +- apps/desktop/src/locales/fil/messages.json | 4 +- apps/desktop/src/locales/fr/messages.json | 4 +- apps/desktop/src/locales/gl/messages.json | 4 +- apps/desktop/src/locales/he/messages.json | 4 +- apps/desktop/src/locales/hi/messages.json | 4 +- apps/desktop/src/locales/hr/messages.json | 4 +- apps/desktop/src/locales/hu/messages.json | 4 +- apps/desktop/src/locales/id/messages.json | 4 +- apps/desktop/src/locales/it/messages.json | 4 +- apps/desktop/src/locales/ja/messages.json | 8 +- apps/desktop/src/locales/ka/messages.json | 4 +- apps/desktop/src/locales/km/messages.json | 4 +- apps/desktop/src/locales/kn/messages.json | 4 +- apps/desktop/src/locales/ko/messages.json | 4 +- apps/desktop/src/locales/lt/messages.json | 2419 ++++++++++++++++++ apps/desktop/src/locales/lv/messages.json | 4 +- apps/desktop/src/locales/me/messages.json | 4 +- apps/desktop/src/locales/ml/messages.json | 4 +- apps/desktop/src/locales/mr/messages.json | 4 +- apps/desktop/src/locales/my/messages.json | 4 +- apps/desktop/src/locales/nb/messages.json | 4 +- apps/desktop/src/locales/ne/messages.json | 4 +- apps/desktop/src/locales/nl/messages.json | 4 +- apps/desktop/src/locales/nn/messages.json | 4 +- apps/desktop/src/locales/or/messages.json | 4 +- apps/desktop/src/locales/pl/messages.json | 4 +- apps/desktop/src/locales/pt_BR/messages.json | 4 +- apps/desktop/src/locales/pt_PT/messages.json | 4 +- apps/desktop/src/locales/ro/messages.json | 4 +- apps/desktop/src/locales/ru/messages.json | 4 +- apps/desktop/src/locales/si/messages.json | 4 +- apps/desktop/src/locales/sk/messages.json | 4 +- apps/desktop/src/locales/sl/messages.json | 4 +- apps/desktop/src/locales/sr/messages.json | 78 +- apps/desktop/src/locales/sv/messages.json | 4 +- apps/desktop/src/locales/te/messages.json | 4 +- apps/desktop/src/locales/th/messages.json | 4 +- apps/desktop/src/locales/tr/messages.json | 4 +- apps/desktop/src/locales/uk/messages.json | 42 +- apps/desktop/src/locales/vi/messages.json | 4 +- apps/desktop/src/locales/zh_CN/messages.json | 4 +- apps/desktop/src/locales/zh_TW/messages.json | 28 +- 63 files changed, 2613 insertions(+), 194 deletions(-) create mode 100644 apps/desktop/src/locales/lt/messages.json diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json index 7e1ee03ea58..fa880624311 100644 --- a/apps/desktop/src/locales/af/messages.json +++ b/apps/desktop/src/locales/af/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GG geënkripteerde berging vir lêeraanhegsels." }, - "premiumSignUpTwoStep": { - "message": "Bykomende tweestapaantekenopsies soos YubiKey, FIDO U2F en Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Wagwoordhigiëne, rekeningwelstand en databreukverslae om u kluis veilig te hou." diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json index a10a5d6ee7b..914d2026267 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 جيغابايت وحدة تخزين مشفرة لمرفقات الملفات." }, - "premiumSignUpTwoStep": { - "message": "خيارات تسجيل الدخول الإضافية من خطوتين مثل YubiKey و FIDO U2F و Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "نظافة كلمة المرور، صحة الحساب، وتقارير خرق البيانات للحفاظ على سلامة خزنتك." diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index 44fd72128ca..3256e1198b2 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "Fayl qoşmaları üçün 1 GB şifrələnmiş saxlama sahəsi." }, - "premiumSignUpTwoStep": { - "message": "YubiKey, FIDO U2F və Duo kimi iki mərhələli giriş seçimləri." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Anbarınızın təhlükəsiyini təmin etmək üçün parol gigiyenası, hesab sağlamlığı və verilənlərin pozulması hesabatları." diff --git a/apps/desktop/src/locales/be/messages.json b/apps/desktop/src/locales/be/messages.json index 680a7599638..fc45db18b3f 100644 --- a/apps/desktop/src/locales/be/messages.json +++ b/apps/desktop/src/locales/be/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 ГБ зашыфраванага сховішча для далучаных файлаў." }, - "premiumSignUpTwoStep": { - "message": "Дадатковыя варыянты двухэтапнага ўваходу, такія як YubiKey, FIDO U2F і Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Гігіена пароляў, здароўе ўліковага запісу і справаздачы аб уцечках даных для забеспячэння бяспекі вашага сховішча." diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index ab6f93166ca..2283ccbde6e 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 ГБ пространство за файлове, които се шифроват." }, - "premiumSignUpTwoStep": { - "message": "Двустепенно удостоверяване чрез YubiKey, FIDO U2F и Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Проверки в списъците с публикувани пароли, проверка на регистрациите и доклади за пробивите в сигурността, което спомага трезорът ви да е допълнително защитен." diff --git a/apps/desktop/src/locales/bn/messages.json b/apps/desktop/src/locales/bn/messages.json index 5c155e80210..e59dd9de8b9 100644 --- a/apps/desktop/src/locales/bn/messages.json +++ b/apps/desktop/src/locales/bn/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "ফাইল সংযুক্তির জন্য ১ জিবি এনক্রিপ্টেড স্থান।" }, - "premiumSignUpTwoStep": { - "message": "YubiKey, FIDO U2F, ও Duo এর মতো অতিরিক্ত দ্বি-পদক্ষেপ লগইন বিকল্পগুলি।" + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "আপনার ভল্টটি সুরক্ষিত রাখতে পাসওয়ার্ড স্বাস্থ্যকরন, অ্যাকাউন্ট স্বাস্থ্য এবং ডেটা লঙ্ঘনের প্রতিবেদন।" diff --git a/apps/desktop/src/locales/bs/messages.json b/apps/desktop/src/locales/bs/messages.json index 3571229b42c..c88f7f392f1 100644 --- a/apps/desktop/src/locales/bs/messages.json +++ b/apps/desktop/src/locales/bs/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB šifriranog prostora za pohranu podataka." }, - "premiumSignUpTwoStep": { - "message": "Dodatne mogućnosti za prijavu u dva koraka kao što su YubiKey, FIDO U2F i Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Higijenu lozinki, zdravlje računa i izvještaje o krađi podataka radi zaštite svojeg trezora." diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index 0e11e49b8f4..9850e4b82a9 100644 --- a/apps/desktop/src/locales/ca/messages.json +++ b/apps/desktop/src/locales/ca/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB d'emmagatzematge xifrat per als fitxers adjunts." }, - "premiumSignUpTwoStep": { - "message": "Opcions addicionals d'inici de sessió en dues passes com ara YubiKey, FIDO U2F i Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Requisits d'higiene de la contrasenya, salut del compte i informe d'infraccions de dades per mantenir la seguretat de la vostra caixa forta." diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index a931ae1b14b..4ce336733f6 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB šifrovaného uložiště pro přílohy." }, - "premiumSignUpTwoStep": { - "message": "Další možnosti dvoufázového přihlášení, jako je například YubiKey, FIDO U2F a Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Reporty o hygieně Vašich hesel, zdraví účtu a narušeních bezpečnosti." diff --git a/apps/desktop/src/locales/cy/messages.json b/apps/desktop/src/locales/cy/messages.json index d2405f785d9..38e81a83bfd 100644 --- a/apps/desktop/src/locales/cy/messages.json +++ b/apps/desktop/src/locales/cy/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index e398565d90c..157e1313ff5 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB krypteret lagerplads til filvedhæftninger." }, - "premiumSignUpTwoStep": { - "message": "Yderligere totrins-loginmuligheder, såsom YubiKey, FIDO U2F og Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietære totrins-login muligheder, såsom YubiKey og Duo." }, "premiumSignUpReports": { "message": "Adgangskodehygiejne, kontosundhed og rapporter om datalæk til at holde din boks sikker." diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index b238af9b9d7..d16eb636284 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB verschlüsselter Speicherplatz für Dateianhänge." }, - "premiumSignUpTwoStep": { - "message": "Zusätzliche Zwei-Faktor-Anmeldung über YubiKey, FIDO U2F, und Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietäre Optionen für die Zwei-Faktor Authentifizierung wie YubiKey und Duo." }, "premiumSignUpReports": { "message": "Berichte über Kennworthygiene, Kontostatus und Datenschutzverletzungen, um Ihren Tresor sicher zu halten." diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index 10f77e91baa..fd800e8d713 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB κρυπτογραφημένο αποθηκευτικό χώρο για συνημμένα αρχεία." }, - "premiumSignUpTwoStep": { - "message": "Πρόσθετες επιλογές σύνδεσης δύο βημάτων, όπως το YubiKey, το FIDO U2F και το Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Ασφάλεια κωδικών, υγιής λογαριασμός και αναφορές παραβίασης δεδομένων για να διατηρήσετε ασφαλή τη λίστα σας." diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index 7af01bc7cc4..6bc9772eb88 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json index 811d706fbdd..363648da567 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." diff --git a/apps/desktop/src/locales/eo/messages.json b/apps/desktop/src/locales/eo/messages.json index 1d93cc95b66..66195d90739 100644 --- a/apps/desktop/src/locales/eo/messages.json +++ b/apps/desktop/src/locales/eo/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index e662e3455c5..b40c409d1b0 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1GB de espacio en disco cifrado." }, - "premiumSignUpTwoStep": { - "message": "Métodos de autenticación en dos pasos adicionales como YubiKey, FIDO U2F y Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Higiene de contraseña, salud de la cuenta e informes de violaciones de datos para mantener tu caja fuerte segura." diff --git a/apps/desktop/src/locales/et/messages.json b/apps/desktop/src/locales/et/messages.json index 64e3bc19e06..fb74084186c 100644 --- a/apps/desktop/src/locales/et/messages.json +++ b/apps/desktop/src/locales/et/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB ulatuses krüpteeritud salvestusruum." }, - "premiumSignUpTwoStep": { - "message": "Lisavõimalused kaheastmeliseks kinnitamiseks, näiteks YubiKey, FIDO U2F ja Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Parooli hügieen, konto seisukord ja andmelekete raportid aitavad hoidlat turvalisena hoida." diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json index c590b004d6f..4c3931490df 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "Eranskinentzako 1GB-eko zifratutako biltegia." }, - "premiumSignUpTwoStep": { - "message": "YubiKey, FIDO U2F eta Duo bezalako bi urratseko saio hasierarako aukera gehigarriak." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Pasahitzaren higienea, kontuaren egoera eta datu-bortxaketen txostenak, kutxa gotorra seguru mantentzeko." diff --git a/apps/desktop/src/locales/fa/messages.json b/apps/desktop/src/locales/fa/messages.json index 5661678b37a..9e5f6f8468c 100644 --- a/apps/desktop/src/locales/fa/messages.json +++ b/apps/desktop/src/locales/fa/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "۱ گیگابایت فضای ذخیره‌سازی رمزنگاری شده برای پرونده‌های پیوست." }, - "premiumSignUpTwoStep": { - "message": "گزینه‌های ورود دو مرحله‌ای اضافی مانند YubiKey, FIDO U2F و Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "گزارش‌های بهداشت رمز عبور، سلامت حساب و نقض داده‌ها برای ایمن نگهداشتن گاوصندوق شما." diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index c3e4de27aab..9025306a708 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 Gt salattua tallennustilaa tiedostoliitteille." }, - "premiumSignUpTwoStep": { - "message": "Muita kaksivaiheisen kirjautumisen todennusmenetelmiä kuten YubiKey, FIDO U2F ja Duo Security." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Salasanahygienian, tilin terveyden ja tietovuotojen raportointitoiminnot pitävät holvisi turvassa." diff --git a/apps/desktop/src/locales/fil/messages.json b/apps/desktop/src/locales/fil/messages.json index 996d1c28277..87b9bde0c46 100644 --- a/apps/desktop/src/locales/fil/messages.json +++ b/apps/desktop/src/locales/fil/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB naka encrypt na imbakan para sa mga attachment ng file." }, - "premiumSignUpTwoStep": { - "message": "Karagdagang dalawang hakbang na mga pagpipilian sa pag login tulad ng YubiKey, FIDO U2F, at Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Kalinisan ng password, kalusugan ng account, at mga ulat ng paglabag sa data upang mapanatiling ligtas ang iyong vault." diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index 6049b364fd0..3f249000bd5 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 Go de stockage chiffré pour les fichiers joints." }, - "premiumSignUpTwoStep": { - "message": "Options additionnelles d'identification à deux étapes telles que YubiKey, FIDO U2F et Duo." + "premiumSignUpTwoStepOptions": { + "message": "Options de connexion propriétaires à deux facteurs telles que YubiKey et Duo." }, "premiumSignUpReports": { "message": "Hygiène du mot de passe, santé du compte et rapports sur les brèches de données pour assurer la sécurité de votre coffre." diff --git a/apps/desktop/src/locales/gl/messages.json b/apps/desktop/src/locales/gl/messages.json index d2405f785d9..38e81a83bfd 100644 --- a/apps/desktop/src/locales/gl/messages.json +++ b/apps/desktop/src/locales/gl/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json index b06507da0c2..ad625991fa1 100644 --- a/apps/desktop/src/locales/he/messages.json +++ b/apps/desktop/src/locales/he/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 ג'יגה של מקום אחסון מוצפן עבור קבצים מצורפים." }, - "premiumSignUpTwoStep": { - "message": "אפשרויות כניסה דו שלבית מתקדמות כמו YubiKey, FIDO U2F, וDuo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "היגיינת סיסמאות, מצב בריאות החשבון, ודיווחים מעודכנים על פרצות חדשות בכדי לשמור על הכספת שלך בטוחה." diff --git a/apps/desktop/src/locales/hi/messages.json b/apps/desktop/src/locales/hi/messages.json index 024bcfb2dfe..156a431a2d2 100644 --- a/apps/desktop/src/locales/hi/messages.json +++ b/apps/desktop/src/locales/hi/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json index 6f1e9e08272..42966a51575 100644 --- a/apps/desktop/src/locales/hr/messages.json +++ b/apps/desktop/src/locales/hr/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB šifriranog prostora za pohranu podataka." }, - "premiumSignUpTwoStep": { - "message": "Dodatne mogućnosti za prijavu dvostrukom autentifikacijom kao što su YubiKey, FIDO U2F i Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Higijenu lozinki, zdravlje računa i izvještaje o krađi podatak radi zaštite svojeg trezora." diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index 283d30e0ee0..796da6fdece 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB titkosított fájlmelléklet tárhely." }, - "premiumSignUpTwoStep": { - "message": "További olyan kétlépcsős bejelentkezési opciók mint a YubiKey, FIDO U2F és Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Jelszó higiénia, felhasználói fiók biztonsága, és adatszivárgási jelentések a széf biztonsága érdekében." diff --git a/apps/desktop/src/locales/id/messages.json b/apps/desktop/src/locales/id/messages.json index 491ede25dc9..b976d1b8fd9 100644 --- a/apps/desktop/src/locales/id/messages.json +++ b/apps/desktop/src/locales/id/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB penyimpanan berkas yang dienkripsi." }, - "premiumSignUpTwoStep": { - "message": "Pilihan info masuk dua langkah tambahan seperti YubiKey, FIDO U2F, dan Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Kebersihan kata sandi, kesehatan akun, dan laporan pelanggaran data untuk menjaga brankas Anda tetap aman." diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index fc13e4220f7..f294888cc38 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB di spazio di archiviazione criptato per gli allegati." }, - "premiumSignUpTwoStep": { - "message": "Più opzioni di verifica in due passaggi come YubiKey, FIDO U2F, e Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Sicurezza delle password, integrità dell'account e rapporti sulle violazioni dei dati per mantenere la tua cassaforte sicura." diff --git a/apps/desktop/src/locales/ja/messages.json b/apps/desktop/src/locales/ja/messages.json index 393f203fb84..f8c11aca7e5 100644 --- a/apps/desktop/src/locales/ja/messages.json +++ b/apps/desktop/src/locales/ja/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1GB の暗号化されたファイルストレージ。" }, - "premiumSignUpTwoStep": { - "message": "YubiKey、FIDO U2F、Duoなどの追加の2段階認証ログインオプション" + "premiumSignUpTwoStepOptions": { + "message": "YubiKey、Duo などのプロプライエタリな2段階認証オプション。" }, "premiumSignUpReports": { "message": "保管庫を安全に保つための、パスワードやアカウントの健全性、データ侵害に関するレポート。" @@ -1681,7 +1681,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "myVault": { - "message": "保管庫" + "message": "自分の保管庫" }, "text": { "message": "テキスト" @@ -2049,7 +2049,7 @@ "message": "組織の検索" }, "searchMyVault": { - "message": "保管庫を検索" + "message": "自分の保管庫内を検索" }, "forwardedEmail": { "message": "転送されたメールエイリアス" diff --git a/apps/desktop/src/locales/ka/messages.json b/apps/desktop/src/locales/ka/messages.json index d2405f785d9..38e81a83bfd 100644 --- a/apps/desktop/src/locales/ka/messages.json +++ b/apps/desktop/src/locales/ka/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." diff --git a/apps/desktop/src/locales/km/messages.json b/apps/desktop/src/locales/km/messages.json index d2405f785d9..38e81a83bfd 100644 --- a/apps/desktop/src/locales/km/messages.json +++ b/apps/desktop/src/locales/km/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." diff --git a/apps/desktop/src/locales/kn/messages.json b/apps/desktop/src/locales/kn/messages.json index 4be76af8b3e..bfc29e570a1 100644 --- a/apps/desktop/src/locales/kn/messages.json +++ b/apps/desktop/src/locales/kn/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "ಫೈಲ್ ಲಗತ್ತುಗಳಿಗಾಗಿ 1 ಜಿಬಿ ಎನ್‌ಕ್ರಿಪ್ಟ್ ಮಾಡಿದ ಸಂಗ್ರಹ." }, - "premiumSignUpTwoStep": { - "message": "ಹೆಚ್ಚುವರಿ ಎರಡು-ಹಂತದ ಲಾಗಿನ್ ಆಯ್ಕೆಗಳಾದ ಯೂಬಿಕೆ, ಎಫ್‌ಐಡಿಒ ಯು 2 ಎಫ್, ಮತ್ತು ಡ್ಯುವೋ." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "ನಿಮ್ಮ ವಾಲ್ಟ್ ಅನ್ನು ಸುರಕ್ಷಿತವಾಗಿರಿಸಲು ಪಾಸ್ವರ್ಡ್ ನೈರ್ಮಲ್ಯ, ಖಾತೆ ಆರೋಗ್ಯ ಮತ್ತು ಡೇಟಾ ಉಲ್ಲಂಘನೆ ವರದಿಗಳು." diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json index 8948b61ee85..7bff705c8ec 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1GB의 암호화된 파일 저장소." }, - "premiumSignUpTwoStep": { - "message": "YubiKey나 FIDO U2F, Duo 등의 추가적인 2단계 인증 옵션." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "보관함을 안전하게 유지하기 위한 암호 위생, 계정 상태, 데이터 유출 보고서" diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json new file mode 100644 index 00000000000..38e81a83bfd --- /dev/null +++ b/apps/desktop/src/locales/lt/messages.json @@ -0,0 +1,2419 @@ +{ + "bitwarden": { + "message": "Bitwarden" + }, + "filters": { + "message": "Filters" + }, + "allItems": { + "message": "All items" + }, + "favorites": { + "message": "Favorites" + }, + "types": { + "message": "Types" + }, + "typeLogin": { + "message": "Login" + }, + "typeCard": { + "message": "Card" + }, + "typeIdentity": { + "message": "Identity" + }, + "typeSecureNote": { + "message": "Secure note" + }, + "folders": { + "message": "Folders" + }, + "collections": { + "message": "Collections" + }, + "searchVault": { + "message": "Search vault" + }, + "addItem": { + "message": "Add item" + }, + "shared": { + "message": "Shared" + }, + "share": { + "message": "Share" + }, + "moveToOrganization": { + "message": "Move to organization" + }, + "movedItemToOrg": { + "message": "$ITEMNAME$ moved to $ORGNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + }, + "orgname": { + "content": "$2", + "example": "Company Name" + } + } + }, + "moveToOrgDesc": { + "message": "Choose an organization that you wish to move this item to. Moving to an organization transfers ownership of the item to that organization. You will no longer be the direct owner of this item once it has been moved." + }, + "attachments": { + "message": "Attachments" + }, + "viewItem": { + "message": "View item" + }, + "name": { + "message": "Name" + }, + "uri": { + "message": "URI" + }, + "uriPosition": { + "message": "URI $POSITION$", + "description": "A listing of URIs. Ex: URI 1, URI 2, URI 3, etc.", + "placeholders": { + "position": { + "content": "$1", + "example": "2" + } + } + }, + "newUri": { + "message": "New URI" + }, + "username": { + "message": "Username" + }, + "password": { + "message": "Password" + }, + "passphrase": { + "message": "Passphrase" + }, + "editItem": { + "message": "Edit item" + }, + "emailAddress": { + "message": "Email address" + }, + "verificationCodeTotp": { + "message": "Verification code (TOTP)" + }, + "website": { + "message": "Website" + }, + "notes": { + "message": "Notes" + }, + "customFields": { + "message": "Custom fields" + }, + "launch": { + "message": "Launch" + }, + "copyValue": { + "message": "Copy value", + "description": "Copy value to clipboard" + }, + "minimizeOnCopyToClipboard": { + "message": "Minimize when copying to clipboard" + }, + "minimizeOnCopyToClipboardDesc": { + "message": "Minimize application when copying an item's data to the clipboard." + }, + "toggleVisibility": { + "message": "Toggle visibility" + }, + "toggleCollapse": { + "message": "Toggle collapse", + "description": "Toggling an expand/collapse state." + }, + "cardholderName": { + "message": "Cardholder name" + }, + "number": { + "message": "Number" + }, + "brand": { + "message": "Brand" + }, + "expiration": { + "message": "Expiration" + }, + "securityCode": { + "message": "Security code" + }, + "identityName": { + "message": "Identity name" + }, + "company": { + "message": "Company" + }, + "ssn": { + "message": "Social Security number" + }, + "passportNumber": { + "message": "Passport number" + }, + "licenseNumber": { + "message": "License number" + }, + "email": { + "message": "Email" + }, + "phone": { + "message": "Phone" + }, + "address": { + "message": "Address" + }, + "premiumRequired": { + "message": "Premium required" + }, + "premiumRequiredDesc": { + "message": "A Premium membership is required to use this feature." + }, + "errorOccurred": { + "message": "An error has occurred." + }, + "error": { + "message": "Error" + }, + "january": { + "message": "January" + }, + "february": { + "message": "February" + }, + "march": { + "message": "March" + }, + "april": { + "message": "April" + }, + "may": { + "message": "May" + }, + "june": { + "message": "June" + }, + "july": { + "message": "July" + }, + "august": { + "message": "August" + }, + "september": { + "message": "September" + }, + "october": { + "message": "October" + }, + "november": { + "message": "November" + }, + "december": { + "message": "December" + }, + "ex": { + "message": "ex.", + "description": "Short abbreviation for 'example'." + }, + "title": { + "message": "Title" + }, + "mr": { + "message": "Mr" + }, + "mrs": { + "message": "Mrs" + }, + "ms": { + "message": "Ms" + }, + "mx": { + "message": "Mx" + }, + "dr": { + "message": "Dr" + }, + "expirationMonth": { + "message": "Expiration month" + }, + "expirationYear": { + "message": "Expiration year" + }, + "select": { + "message": "Select" + }, + "other": { + "message": "Other" + }, + "generatePassword": { + "message": "Generate password" + }, + "type": { + "message": "Type" + }, + "firstName": { + "message": "First name" + }, + "middleName": { + "message": "Middle name" + }, + "lastName": { + "message": "Last name" + }, + "fullName": { + "message": "Full name" + }, + "address1": { + "message": "Address 1" + }, + "address2": { + "message": "Address 2" + }, + "address3": { + "message": "Address 3" + }, + "cityTown": { + "message": "City / Town" + }, + "stateProvince": { + "message": "State / Province" + }, + "zipPostalCode": { + "message": "Zip / Postal code" + }, + "country": { + "message": "Country" + }, + "save": { + "message": "Save" + }, + "cancel": { + "message": "Cancel" + }, + "delete": { + "message": "Delete" + }, + "favorite": { + "message": "Favorite" + }, + "edit": { + "message": "Edit" + }, + "authenticatorKeyTotp": { + "message": "Authenticator key (TOTP)" + }, + "folder": { + "message": "Folder" + }, + "newCustomField": { + "message": "New custom field" + }, + "value": { + "message": "Value" + }, + "dragToSort": { + "message": "Drag to sort" + }, + "cfTypeText": { + "message": "Text" + }, + "cfTypeHidden": { + "message": "Hidden" + }, + "cfTypeBoolean": { + "message": "Boolean" + }, + "cfTypeLinked": { + "message": "Linked", + "description": "This describes a field that is 'linked' (related) to another field." + }, + "linkedValue": { + "message": "Linked value", + "description": "This describes a value that is 'linked' (related) to another value." + }, + "remove": { + "message": "Remove" + }, + "nameRequired": { + "message": "Name is required." + }, + "addedItem": { + "message": "Item added" + }, + "editedItem": { + "message": "Item saved" + }, + "deleteItem": { + "message": "Delete item" + }, + "deleteFolder": { + "message": "Delete folder" + }, + "deleteAttachment": { + "message": "Delete attachment" + }, + "deleteItemConfirmation": { + "message": "Do you really want to send to the trash?" + }, + "deletedItem": { + "message": "Item sent to trash" + }, + "overwritePasswordConfirmation": { + "message": "Are you sure you want to overwrite the current password?" + }, + "overwriteUsername": { + "message": "Overwrite username" + }, + "overwriteUsernameConfirmation": { + "message": "Are you sure you want to overwrite the current username?" + }, + "noneFolder": { + "message": "No folder", + "description": "This is the folder for uncategorized items" + }, + "addFolder": { + "message": "Add folder" + }, + "editFolder": { + "message": "Edit folder" + }, + "regeneratePassword": { + "message": "Regenerate password" + }, + "copyPassword": { + "message": "Copy password" + }, + "copyUri": { + "message": "Copy URI" + }, + "copyVerificationCodeTotp": { + "message": "Copy verification code (TOTP)" + }, + "length": { + "message": "Length" + }, + "uppercase": { + "message": "Uppercase (A-Z)" + }, + "lowercase": { + "message": "Lowercase (a-z)" + }, + "numbers": { + "message": "Numbers (0-9)" + }, + "specialCharacters": { + "message": "Special characters (!@#$%^&*)" + }, + "numWords": { + "message": "Number of words" + }, + "wordSeparator": { + "message": "Word separator" + }, + "capitalize": { + "message": "Capitalize", + "description": "Make the first letter of a word uppercase." + }, + "includeNumber": { + "message": "Include number" + }, + "close": { + "message": "Close" + }, + "minNumbers": { + "message": "Minimum numbers" + }, + "minSpecial": { + "message": "Minimum special", + "description": "Minimum Special Characters" + }, + "ambiguous": { + "message": "Avoid ambiguous characters" + }, + "searchCollection": { + "message": "Search collection" + }, + "searchFolder": { + "message": "Search folder" + }, + "searchFavorites": { + "message": "Search favorites" + }, + "searchType": { + "message": "Search type", + "description": "Search item type" + }, + "newAttachment": { + "message": "Add new attachment" + }, + "deletedAttachment": { + "message": "Attachment deleted" + }, + "deleteAttachmentConfirmation": { + "message": "Are you sure you want to delete this attachment?" + }, + "attachmentSaved": { + "message": "Attachment saved" + }, + "file": { + "message": "File" + }, + "selectFile": { + "message": "Select a file" + }, + "maxFileSize": { + "message": "Maximum file size is 500 MB." + }, + "updateKey": { + "message": "You cannot use this feature until you update your encryption key." + }, + "editedFolder": { + "message": "Folder saved" + }, + "addedFolder": { + "message": "Folder added" + }, + "deleteFolderConfirmation": { + "message": "Are you sure you want to delete this folder?" + }, + "deletedFolder": { + "message": "Folder deleted" + }, + "loginOrCreateNewAccount": { + "message": "Log in or create a new account to access your secure vault." + }, + "createAccount": { + "message": "Create account" + }, + "logIn": { + "message": "Log in" + }, + "submit": { + "message": "Submit" + }, + "masterPass": { + "message": "Master password" + }, + "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." + }, + "masterPassHintDesc": { + "message": "A master password hint can help you remember your password if you forget it." + }, + "reTypeMasterPass": { + "message": "Re-type master password" + }, + "masterPassHint": { + "message": "Master password hint (optional)" + }, + "settings": { + "message": "Settings" + }, + "passwordHint": { + "message": "Password hint" + }, + "enterEmailToGetHint": { + "message": "Enter your account email address to receive your master password hint." + }, + "getMasterPasswordHint": { + "message": "Get master password hint" + }, + "emailRequired": { + "message": "Email address is required." + }, + "invalidEmail": { + "message": "Invalid email address." + }, + "masterPasswordRequired": { + "message": "Master password is required." + }, + "confirmMasterPasswordRequired": { + "message": "Master password retype is required." + }, + "masterPasswordMinlength": { + "message": "Master password must be at least $VALUE$ characters long.", + "description": "The Master Password must be at least a specific number of characters long.", + "placeholders": { + "value": { + "content": "$1", + "example": "8" + } + } + }, + "masterPassDoesntMatch": { + "message": "Master password confirmation does not match." + }, + "newAccountCreated": { + "message": "Your new account has been created! You may now log in." + }, + "masterPassSent": { + "message": "We've sent you an email with your master password hint." + }, + "unexpectedError": { + "message": "An unexpected error has occurred." + }, + "itemInformation": { + "message": "Item information" + }, + "noItemsInList": { + "message": "There are no items to list." + }, + "sendVerificationCode": { + "message": "Send a verification code to your email" + }, + "sendCode": { + "message": "Send code" + }, + "codeSent": { + "message": "Code sent" + }, + "verificationCode": { + "message": "Verification code" + }, + "confirmIdentity": { + "message": "Confirm your identity to continue." + }, + "verificationCodeRequired": { + "message": "Verification code is required." + }, + "invalidVerificationCode": { + "message": "Invalid verification code" + }, + "continue": { + "message": "Continue" + }, + "enterVerificationCodeApp": { + "message": "Enter the 6 digit verification code from your authenticator app." + }, + "enterVerificationCodeEmail": { + "message": "Enter the 6 digit verification code that was emailed to $EMAIL$.", + "placeholders": { + "email": { + "content": "$1", + "example": "example@gmail.com" + } + } + }, + "verificationCodeEmailSent": { + "message": "Verification email sent to $EMAIL$.", + "placeholders": { + "email": { + "content": "$1", + "example": "example@gmail.com" + } + } + }, + "rememberMe": { + "message": "Remember me" + }, + "sendVerificationCodeEmailAgain": { + "message": "Send verification code email again" + }, + "useAnotherTwoStepMethod": { + "message": "Use another two-step login method" + }, + "insertYubiKey": { + "message": "Insert your YubiKey into your computer's USB port, then touch its button." + }, + "insertU2f": { + "message": "Insert your security key into your computer's USB port. If it has a button, touch it." + }, + "recoveryCodeDesc": { + "message": "Lost access to all of your two-factor providers? Use your recovery code to turn off all two-factor providers on your account." + }, + "recoveryCodeTitle": { + "message": "Recovery code" + }, + "authenticatorAppTitle": { + "message": "Authenticator app" + }, + "authenticatorAppDesc": { + "message": "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes.", + "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + }, + "yubiKeyTitle": { + "message": "YubiKey OTP security key" + }, + "yubiKeyDesc": { + "message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices." + }, + "duoDesc": { + "message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.", + "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." + }, + "duoOrganizationDesc": { + "message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.", + "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." + }, + "webAuthnTitle": { + "message": "FIDO2 WebAuthn" + }, + "webAuthnDesc": { + "message": "Use any WebAuthn compatible security key to access your account." + }, + "emailTitle": { + "message": "Email" + }, + "emailDesc": { + "message": "Verification codes will be emailed to you." + }, + "loginUnavailable": { + "message": "Login unavailable" + }, + "noTwoStepProviders": { + "message": "This account has two-step login set up, however, none of the configured two-step providers are supported by this device." + }, + "noTwoStepProviders2": { + "message": "Please add additional providers that are better supported across devices (such as an authenticator app)." + }, + "twoStepOptions": { + "message": "Two-step login options" + }, + "selfHostedEnvironment": { + "message": "Self-hosted environment" + }, + "selfHostedEnvironmentFooter": { + "message": "Specify the base URL of your on-premises hosted Bitwarden installation." + }, + "customEnvironment": { + "message": "Custom environment" + }, + "customEnvironmentFooter": { + "message": "For advanced users. You can specify the base URL of each service independently." + }, + "baseUrl": { + "message": "Server URL" + }, + "apiUrl": { + "message": "API server URL" + }, + "webVaultUrl": { + "message": "Web vault server URL" + }, + "identityUrl": { + "message": "Identity server URL" + }, + "notificationsUrl": { + "message": "Notifications server URL" + }, + "iconsUrl": { + "message": "Icons server URL" + }, + "environmentSaved": { + "message": "Environment URLs saved" + }, + "ok": { + "message": "Ok" + }, + "yes": { + "message": "Yes" + }, + "no": { + "message": "No" + }, + "overwritePassword": { + "message": "Overwrite password" + }, + "learnMore": { + "message": "Learn more" + }, + "featureUnavailable": { + "message": "Feature unavailable" + }, + "loggedOut": { + "message": "Logged out" + }, + "loginExpired": { + "message": "Your login session has expired." + }, + "logOutConfirmation": { + "message": "Are you sure you want to log out?" + }, + "logOut": { + "message": "Log out" + }, + "addNewLogin": { + "message": "New login" + }, + "addNewItem": { + "message": "New item" + }, + "addNewFolder": { + "message": "New folder" + }, + "view": { + "message": "View" + }, + "account": { + "message": "Account" + }, + "loading": { + "message": "Loading..." + }, + "lockVault": { + "message": "Lock vault" + }, + "passwordGenerator": { + "message": "Password generator" + }, + "contactUs": { + "message": "Contact us" + }, + "helpAndFeedback": { + "message": "Help and feedback" + }, + "getHelp": { + "message": "Get help" + }, + "fileBugReport": { + "message": "File a bug report" + }, + "blog": { + "message": "Blog" + }, + "followUs": { + "message": "Follow us" + }, + "syncVault": { + "message": "Sync vault" + }, + "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?" + }, + "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." + }, + "yourAccountsFingerprint": { + "message": "Your account's 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." + }, + "goToWebVault": { + "message": "Go to web vault" + }, + "getMobileApp": { + "message": "Get mobile app" + }, + "getBrowserExtension": { + "message": "Get browser extension" + }, + "syncingComplete": { + "message": "Syncing complete" + }, + "syncingFailed": { + "message": "Syncing failed" + }, + "yourVaultIsLocked": { + "message": "Your vault is locked. Verify your identity to continue." + }, + "unlock": { + "message": "Unlock" + }, + "loggedInAsOn": { + "message": "Logged in as $EMAIL$ on $HOSTNAME$.", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "hostname": { + "content": "$2", + "example": "bitwarden.com" + } + } + }, + "invalidMasterPassword": { + "message": "Invalid master password" + }, + "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?" + }, + "twoStepLogin": { + "message": "Two-step login" + }, + "vaultTimeout": { + "message": "Vault timeout" + }, + "vaultTimeoutDesc": { + "message": "Choose when your vault will take the vault timeout action." + }, + "immediately": { + "message": "Immediately" + }, + "tenSeconds": { + "message": "10 seconds" + }, + "twentySeconds": { + "message": "20 seconds" + }, + "thirtySeconds": { + "message": "30 seconds" + }, + "oneMinute": { + "message": "1 minute" + }, + "twoMinutes": { + "message": "2 minutes" + }, + "fiveMinutes": { + "message": "5 minutes" + }, + "fifteenMinutes": { + "message": "15 minutes" + }, + "thirtyMinutes": { + "message": "30 minutes" + }, + "oneHour": { + "message": "1 hour" + }, + "fourHours": { + "message": "4 hours" + }, + "onIdle": { + "message": "On system idle" + }, + "onSleep": { + "message": "On system sleep" + }, + "onLocked": { + "message": "On system lock" + }, + "onRestart": { + "message": "On restart" + }, + "never": { + "message": "Never" + }, + "security": { + "message": "Security" + }, + "clearClipboard": { + "message": "Clear clipboard", + "description": "Clipboard is the operating system thing where you copy/paste data to on your device." + }, + "clearClipboardDesc": { + "message": "Automatically clear copied values from your clipboard.", + "description": "Clipboard is the operating system thing where you copy/paste data to on your device." + }, + "enableFavicon": { + "message": "Show website icons" + }, + "faviconDesc": { + "message": "Show a recognizable image next to each login." + }, + "enableMinToTray": { + "message": "Minimize to tray icon" + }, + "enableMinToTrayDesc": { + "message": "When minimizing the window, show an icon in the system tray instead." + }, + "enableMinToMenuBar": { + "message": "Minimize to menu bar" + }, + "enableMinToMenuBarDesc": { + "message": "When minimizing the window, show an icon in the menu bar instead." + }, + "enableCloseToTray": { + "message": "Close to tray icon" + }, + "enableCloseToTrayDesc": { + "message": "When closing the window, show an icon in the system tray instead." + }, + "enableCloseToMenuBar": { + "message": "Close to menu bar" + }, + "enableCloseToMenuBarDesc": { + "message": "When closing the window, show an icon in the menu bar instead." + }, + "enableTray": { + "message": "Show tray icon" + }, + "enableTrayDesc": { + "message": "Always show an icon in the system tray." + }, + "startToTray": { + "message": "Start to tray icon" + }, + "startToTrayDesc": { + "message": "When the application is first started, only show an icon in the system tray." + }, + "startToMenuBar": { + "message": "Start to menu bar" + }, + "startToMenuBarDesc": { + "message": "When the application is first started, only show an icon in the menu bar." + }, + "openAtLogin": { + "message": "Start automatically on login" + }, + "openAtLoginDesc": { + "message": "Start the Bitwarden desktop application automatically on login." + }, + "alwaysShowDock": { + "message": "Always show in the Dock" + }, + "alwaysShowDockDesc": { + "message": "Show the Bitwarden icon in the Dock even when minimized to the menu bar." + }, + "confirmTrayTitle": { + "message": "Confirm hiding tray" + }, + "confirmTrayDesc": { + "message": "Turning off this setting will also turn off all other tray related settings." + }, + "language": { + "message": "Language" + }, + "languageDesc": { + "message": "Change the language used by the application. Restart is required." + }, + "theme": { + "message": "Theme" + }, + "themeDesc": { + "message": "Change the application's color theme." + }, + "dark": { + "message": "Dark", + "description": "Dark color" + }, + "light": { + "message": "Light", + "description": "Light color" + }, + "copy": { + "message": "Copy", + "description": "Copy to clipboard" + }, + "checkForUpdates": { + "message": "Check for updates…" + }, + "version": { + "message": "Version $VERSION_NUM$", + "placeholders": { + "version_num": { + "content": "$1", + "example": "1.2.3" + } + } + }, + "restartToUpdate": { + "message": "Restart to update" + }, + "restartToUpdateDesc": { + "message": "Version $VERSION_NUM$ is ready to install. You must restart the application to complete the installation. Do you want to restart and update now?", + "placeholders": { + "version_num": { + "content": "$1", + "example": "1.2.3" + } + } + }, + "updateAvailable": { + "message": "Update available" + }, + "updateAvailableDesc": { + "message": "An update was found. Do you want to download it now?" + }, + "restart": { + "message": "Restart" + }, + "later": { + "message": "Later" + }, + "noUpdatesAvailable": { + "message": "No updates are currently available. You are using the latest version." + }, + "updateError": { + "message": "Update error" + }, + "unknown": { + "message": "Unknown" + }, + "copyUsername": { + "message": "Copy username" + }, + "copyNumber": { + "message": "Copy number", + "description": "Copy credit card number" + }, + "copySecurityCode": { + "message": "Copy security code", + "description": "Copy credit card security code (CVV)" + }, + "premiumMembership": { + "message": "Premium membership" + }, + "premiumManage": { + "message": "Manage membership" + }, + "premiumManageAlert": { + "message": "You can manage your membership on the bitwarden.com web vault. Do you want to visit the website now?" + }, + "premiumRefresh": { + "message": "Refresh membership" + }, + "premiumNotCurrentMember": { + "message": "You are not currently a Premium member." + }, + "premiumSignUpAndGet": { + "message": "Sign up for a Premium membership and get:" + }, + "premiumSignUpStorage": { + "message": "1 GB encrypted storage for file attachments." + }, + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." + }, + "premiumSignUpReports": { + "message": "Password hygiene, account health, and data breach reports to keep your vault safe." + }, + "premiumSignUpTotp": { + "message": "TOTP verification code (2FA) generator for logins in your vault." + }, + "premiumSignUpSupport": { + "message": "Priority customer support." + }, + "premiumSignUpFuture": { + "message": "All future premium features. More coming soon!" + }, + "premiumPurchase": { + "message": "Purchase Premium" + }, + "premiumPurchaseAlert": { + "message": "You can purchase premium membership on the bitwarden.com web vault. Do you want to visit the website now?" + }, + "premiumCurrentMember": { + "message": "You are a premium member!" + }, + "premiumCurrentMemberThanks": { + "message": "Thank you for supporting Bitwarden." + }, + "premiumPrice": { + "message": "All for just $PRICE$ /year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, + "refreshComplete": { + "message": "Refresh complete" + }, + "passwordHistory": { + "message": "Password history" + }, + "clear": { + "message": "Clear", + "description": "To clear something out. example: To clear browser history." + }, + "noPasswordsInList": { + "message": "There are no passwords to list." + }, + "undo": { + "message": "Undo" + }, + "redo": { + "message": "Redo" + }, + "cut": { + "message": "Cut", + "description": "Cut to clipboard" + }, + "paste": { + "message": "Paste", + "description": "Paste from clipboard" + }, + "selectAll": { + "message": "Select all" + }, + "zoomIn": { + "message": "Zoom in" + }, + "zoomOut": { + "message": "Zoom out" + }, + "resetZoom": { + "message": "Reset zoom" + }, + "toggleFullScreen": { + "message": "Toggle full screen" + }, + "reload": { + "message": "Reload" + }, + "toggleDevTools": { + "message": "Toggle developer tools" + }, + "minimize": { + "message": "Minimize", + "description": "Minimize window" + }, + "zoom": { + "message": "Zoom" + }, + "bringAllToFront": { + "message": "Bring all to front", + "description": "Bring all windows to front (foreground)" + }, + "aboutBitwarden": { + "message": "About Bitwarden" + }, + "services": { + "message": "Services" + }, + "hideBitwarden": { + "message": "Hide Bitwarden" + }, + "hideOthers": { + "message": "Hide others" + }, + "showAll": { + "message": "Show all" + }, + "quitBitwarden": { + "message": "Quit Bitwarden" + }, + "valueCopied": { + "message": "$VALUE$ copied", + "description": "Value has been copied to the clipboard.", + "placeholders": { + "value": { + "content": "$1", + "example": "Password" + } + } + }, + "help": { + "message": "Help" + }, + "window": { + "message": "Window" + }, + "checkPassword": { + "message": "Check if password has been exposed." + }, + "passwordExposed": { + "message": "This password has been exposed $VALUE$ time(s) in data breaches. You should change it.", + "placeholders": { + "value": { + "content": "$1", + "example": "2" + } + } + }, + "passwordSafe": { + "message": "This password was not found in any known data breaches. It should be safe to use." + }, + "baseDomain": { + "message": "Base domain", + "description": "Domain name. Ex. website.com" + }, + "domainName": { + "message": "Domain name", + "description": "Domain name. Ex. website.com" + }, + "host": { + "message": "Host", + "description": "A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'." + }, + "exact": { + "message": "Exact" + }, + "startsWith": { + "message": "Starts with" + }, + "regEx": { + "message": "Regular expression", + "description": "A programming term, also known as 'RegEx'." + }, + "matchDetection": { + "message": "Match detection", + "description": "URI match detection for auto-fill." + }, + "defaultMatchDetection": { + "message": "Default match detection", + "description": "Default URI match detection for auto-fill." + }, + "toggleOptions": { + "message": "Toggle options" + }, + "organization": { + "message": "Organization", + "description": "An entity of multiple related people (ex. a team or business organization)." + }, + "default": { + "message": "Default" + }, + "exit": { + "message": "Exit" + }, + "showHide": { + "message": "Show / Hide", + "description": "Text for a button that toggles the visibility of the window. Shows the window when it is hidden or hides the window if it is currently open." + }, + "hideToTray": { + "message": "Hide to tray" + }, + "alwaysOnTop": { + "message": "Always on top", + "description": "Application window should always stay on top of other windows" + }, + "dateUpdated": { + "message": "Updated", + "description": "ex. Date this item was updated" + }, + "dateCreated": { + "message": "Created", + "description": "ex. Date this item was created" + }, + "datePasswordUpdated": { + "message": "Password updated", + "description": "ex. Date this password was updated" + }, + "exportVault": { + "message": "Export vault" + }, + "fileFormat": { + "message": "File format" + }, + "hCaptchaUrl": { + "message": "hCaptcha Url", + "description": "hCaptcha is the name of a website, should not be translated" + }, + "loadAccessibilityCookie": { + "message": "Load accessibility cookie" + }, + "registerAccessibilityUser": { + "message": "Register as an accessibility user at", + "description": "ex. Register as an accessibility user at hcaptcha.com" + }, + "copyPasteLink": { + "message": "Copy and paste the link sent to your email below" + }, + "enterhCaptchaUrl": { + "message": "Enter URL to load accessibility cookie for hCaptcha", + "description": "hCaptcha is the name of a website, should not be translated" + }, + "hCaptchaUrlRequired": { + "message": "hCaptcha Url is required", + "description": "hCaptcha is the name of a website, should not be translated" + }, + "invalidUrl": { + "message": "Invalid Url" + }, + "done": { + "message": "Done" + }, + "accessibilityCookieSaved": { + "message": "Accessibility cookie saved!" + }, + "noAccessibilityCookieSaved": { + "message": "No accessibility cookie saved" + }, + "warning": { + "message": "WARNING", + "description": "WARNING (should stay in capitalized letters if the language permits)" + }, + "confirmVaultExport": { + "message": "Confirm vault export" + }, + "exportWarningDesc": { + "message": "This export contains your vault data in an unencrypted format. You should not store or send the exported file over unsecure channels (such as email). Delete it immediately after you are done using it." + }, + "encExportKeyWarningDesc": { + "message": "This export encrypts your data using your account's encryption key. If you ever rotate your account's encryption key you should export again since you will not be able to decrypt this export file." + }, + "encExportAccountWarningDesc": { + "message": "Account encryption keys are unique to each Bitwarden user account, so you can't import an encrypted export into a different account." + }, + "noOrganizationsList": { + "message": "You do not belong to any organizations. Organizations allow you to securely share items with other users." + }, + "noCollectionsInList": { + "message": "There are no collections to list." + }, + "ownership": { + "message": "Ownership" + }, + "whoOwnsThisItem": { + "message": "Who owns this item?" + }, + "strong": { + "message": "Strong", + "description": "ex. A strong password. Scale: Weak -> Good -> Strong" + }, + "good": { + "message": "Good", + "description": "ex. A good password. Scale: Weak -> Good -> Strong" + }, + "weak": { + "message": "Weak", + "description": "ex. A weak password. Scale: Weak -> Good -> Strong" + }, + "weakMasterPassword": { + "message": "Weak master password" + }, + "weakMasterPasswordDesc": { + "message": "The master password you have chosen is weak. You should use a strong master password (or a passphrase) to properly protect your Bitwarden account. Are you sure you want to use this master password?" + }, + "pin": { + "message": "PIN", + "description": "PIN code. Ex. The short code (often numeric) that you use to unlock a device." + }, + "unlockWithPin": { + "message": "Unlock with PIN" + }, + "setYourPinCode": { + "message": "Set your PIN code for unlocking Bitwarden. Your PIN settings will be reset if you ever fully log out of the application." + }, + "pinRequired": { + "message": "PIN code is required." + }, + "invalidPin": { + "message": "Invalid PIN code." + }, + "unlockWithWindowsHello": { + "message": "Unlock with Windows Hello" + }, + "additionalWindowsHelloSettings": { + "message": "Additional Windows Hello settings" + }, + "windowsHelloConsentMessage": { + "message": "Verify for Bitwarden." + }, + "unlockWithTouchId": { + "message": "Unlock with Touch ID" + }, + "additionalTouchIdSettings": { + "message": "Additional Touch ID settings" + }, + "touchIdConsentMessage": { + "message": "unlock your vault" + }, + "autoPromptWindowsHello": { + "message": "Ask for Windows Hello on app start" + }, + "autoPromptTouchId": { + "message": "Ask for Touch ID on app start" + }, + "requirePasswordOnStart": { + "message": "Require password or PIN on app start" + }, + "recommendedForSecurity": { + "message": "Recommended for security." + }, + "lockWithMasterPassOnRestart": { + "message": "Lock with master password on restart" + }, + "deleteAccount": { + "message": "Delete account" + }, + "deleteAccountDesc": { + "message": "Proceed below to delete your account and all vault data." + }, + "deleteAccountWarning": { + "message": "Deleting your account is permanent. It cannot be undone." + }, + "accountDeleted": { + "message": "Account deleted" + }, + "accountDeletedDesc": { + "message": "Your account has been closed and all associated data has been deleted." + }, + "preferences": { + "message": "Preferences" + }, + "enableMenuBar": { + "message": "Show menu bar icon" + }, + "enableMenuBarDesc": { + "message": "Always show an icon in the menu bar." + }, + "hideToMenuBar": { + "message": "Hide to menu bar" + }, + "selectOneCollection": { + "message": "You must select at least one collection." + }, + "premiumUpdated": { + "message": "You've upgraded to Premium." + }, + "restore": { + "message": "Restore" + }, + "premiumManageAlertAppStore": { + "message": "You can manage your subscription from the App Store. Do you want to visit the App Store now?" + }, + "legal": { + "message": "Legal", + "description": "Noun. As in 'legal documents', like our terms of service and privacy policy." + }, + "termsOfService": { + "message": "Terms of Service" + }, + "privacyPolicy": { + "message": "Privacy Policy" + }, + "unsavedChangesConfirmation": { + "message": "Are you sure you want to leave? If you leave now then your current information will not be saved." + }, + "unsavedChangesTitle": { + "message": "Unsaved changes" + }, + "clone": { + "message": "Clone" + }, + "passwordGeneratorPolicyInEffect": { + "message": "One or more organization policies are affecting your generator settings." + }, + "vaultTimeoutAction": { + "message": "Vault timeout action" + }, + "vaultTimeoutActionLockDesc": { + "message": "Master password or other unlock method is required to access your vault again." + }, + "vaultTimeoutActionLogOutDesc": { + "message": "Re-authentication is required to access your vault again." + }, + "unlockMethodNeededToChangeTimeoutActionDesc": { + "message": "Set up an unlock method to change your vault timeout action." + }, + "lock": { + "message": "Lock", + "description": "Verb form: to make secure or inaccesible by" + }, + "trash": { + "message": "Trash", + "description": "Noun: a special folder to hold deleted items" + }, + "searchTrash": { + "message": "Search trash" + }, + "permanentlyDeleteItem": { + "message": "Permanently delete item" + }, + "permanentlyDeleteItemConfirmation": { + "message": "Are you sure you want to permanently delete this item?" + }, + "permanentlyDeletedItem": { + "message": "Item permanently deleted" + }, + "restoredItem": { + "message": "Item restored" + }, + "permanentlyDelete": { + "message": "Permanently delete" + }, + "vaultTimeoutLogOutConfirmation": { + "message": "Logging out will remove all access to your vault and requires online authentication after the timeout period. Are you sure you want to use this setting?" + }, + "vaultTimeoutLogOutConfirmationTitle": { + "message": "Timeout action confirmation" + }, + "enterpriseSingleSignOn": { + "message": "Enterprise single sign-on" + }, + "setMasterPassword": { + "message": "Set master password" + }, + "ssoCompleteRegistration": { + "message": "In order to complete logging in with SSO, please set a master password to access and protect your vault." + }, + "currentMasterPass": { + "message": "Current master password" + }, + "newMasterPass": { + "message": "New master password" + }, + "confirmNewMasterPass": { + "message": "Confirm new master password" + }, + "masterPasswordPolicyInEffect": { + "message": "One or more organization policies require your master password to meet the following requirements:" + }, + "policyInEffectMinComplexity": { + "message": "Minimum complexity score of $SCORE$", + "placeholders": { + "score": { + "content": "$1", + "example": "4" + } + } + }, + "policyInEffectMinLength": { + "message": "Minimum length of $LENGTH$", + "placeholders": { + "length": { + "content": "$1", + "example": "14" + } + } + }, + "policyInEffectUppercase": { + "message": "Contain one or more uppercase characters" + }, + "policyInEffectLowercase": { + "message": "Contain one or more lowercase characters" + }, + "policyInEffectNumbers": { + "message": "Contain one or more numbers" + }, + "policyInEffectSpecial": { + "message": "Contain one or more of the following special characters $CHARS$", + "placeholders": { + "chars": { + "content": "$1", + "example": "!@#$%^&*" + } + } + }, + "masterPasswordPolicyRequirementsNotMet": { + "message": "Your new master password does not meet the policy requirements." + }, + "acceptPolicies": { + "message": "By checking this box you agree to the following:" + }, + "acceptPoliciesRequired": { + "message": "Terms of Service and Privacy Policy have not been acknowledged." + }, + "enableBrowserIntegration": { + "message": "Allow browser integration" + }, + "enableBrowserIntegrationDesc": { + "message": "Used for biometrics in browser." + }, + "enableDuckDuckGoBrowserIntegration": { + "message": "Allow DuckDuckGo browser integration" + }, + "enableDuckDuckGoBrowserIntegrationDesc": { + "message": "Use your Bitwarden vault when browsing with DuckDuckGo." + }, + "browserIntegrationUnsupportedTitle": { + "message": "Browser integration not supported" + }, + "browserIntegrationMasOnlyDesc": { + "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." + }, + "browserIntegrationWindowsStoreDesc": { + "message": "Unfortunately browser integration is currently not supported in the Microsoft Store version." + }, + "browserIntegrationLinuxDesc": { + "message": "Unfortunately browser integration is currently not supported in the linux version." + }, + "enableBrowserIntegrationFingerprint": { + "message": "Require verification for browser integration" + }, + "enableBrowserIntegrationFingerprintDesc": { + "message": "Add an additional layer of security by requiring fingerprint phrase confirmation when establishing a link between your desktop and browser. This requires user action and verification each time a connection is created." + }, + "approve": { + "message": "Approve" + }, + "verifyBrowserTitle": { + "message": "Verify browser connection" + }, + "verifyBrowserDesc": { + "message": "Please ensure the shown fingerprint is identical to the fingerprint showed in the browser extension." + }, + "verifyNativeMessagingConnectionTitle": { + "message": "$APPID$ wants to connect to Bitwarden", + "placeholders": { + "appid": { + "content": "$1", + "example": "My App" + } + } + }, + "verifyNativeMessagingConnectionDesc": { + "message": "Would you like to approve this request?" + }, + "verifyNativeMessagingConnectionWarning": { + "message": "If you did not initiate this request, do not approve it." + }, + "biometricsNotEnabledTitle": { + "message": "Biometrics not set up" + }, + "biometricsNotEnabledDesc": { + "message": "Browser biometrics requires desktop biometrics to be set up in the settings first." + }, + "personalOwnershipSubmitError": { + "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." + }, + "hintEqualsPassword": { + "message": "Your password hint cannot be the same as your password." + }, + "personalOwnershipPolicyInEffect": { + "message": "An organization policy is affecting your ownership options." + }, + "allSends": { + "message": "All Sends", + "description": "'Sends' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeFile": { + "message": "File" + }, + "sendTypeText": { + "message": "Text" + }, + "searchSends": { + "message": "Search Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "editSend": { + "message": "Edit Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "myVault": { + "message": "My vault" + }, + "text": { + "message": "Text" + }, + "deletionDate": { + "message": "Deletion date" + }, + "deletionDateDesc": { + "message": "The Send will be permanently deleted on the specified date and time.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "expirationDate": { + "message": "Expiration date" + }, + "expirationDateDesc": { + "message": "If set, access to this Send will expire on the specified date and time.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "maxAccessCount": { + "message": "Maximum access count", + "description": "This text will be displayed after a Send has been accessed the maximum amount of times." + }, + "maxAccessCountDesc": { + "message": "If set, users will no longer be able to access this Send once the maximum access count is reached.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "currentAccessCount": { + "message": "Current access count" + }, + "disableSend": { + "message": "Deactivate this Send so that no one can access it.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDesc": { + "message": "Optionally require a password for users to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendNotesDesc": { + "message": "Private notes about this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendLink": { + "message": "Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendLinkLabel": { + "message": "Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "textHiddenByDefault": { + "message": "When accessing the Send, hide the text by default", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "createdSend": { + "message": "Send added", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "editedSend": { + "message": "Send saved", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "deletedSend": { + "message": "Send deleted", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newPassword": { + "message": "New password" + }, + "whatTypeOfSend": { + "message": "What type of Send is this?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "createSend": { + "message": "New Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTextDesc": { + "message": "The text you want to send." + }, + "sendFileDesc": { + "message": "The file you want to send." + }, + "days": { + "message": "$DAYS$ days", + "placeholders": { + "days": { + "content": "$1", + "example": "1" + } + } + }, + "oneDay": { + "message": "1 day" + }, + "custom": { + "message": "Custom" + }, + "deleteSendConfirmation": { + "message": "Are you sure you want to delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "copySendLinkToClipboard": { + "message": "Copy Send link to clipboard", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "copySendLinkOnSave": { + "message": "Copy the link to share this Send to my clipboard upon save." + }, + "sendDisabled": { + "message": "Send removed", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendDisabledWarning": { + "message": "Due to an enterprise policy, you are only able to delete an existing Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "copyLink": { + "message": "Copy link" + }, + "disabled": { + "message": "Disabled" + }, + "removePassword": { + "message": "Remove password" + }, + "removedPassword": { + "message": "Password removed" + }, + "removePasswordConfirmation": { + "message": "Are you sure you want to remove the password?" + }, + "maxAccessCountReached": { + "message": "Max access count reached" + }, + "expired": { + "message": "Expired" + }, + "pendingDeletion": { + "message": "Pending deletion" + }, + "webAuthnAuthenticate": { + "message": "Authenticate WebAuthn" + }, + "hideEmail": { + "message": "Hide my email address from recipients." + }, + "sendOptionsPolicyInEffect": { + "message": "One or more organization policies are affecting your Send options." + }, + "emailVerificationRequired": { + "message": "Email verification required" + }, + "emailVerificationRequiredDesc": { + "message": "You must verify your email to use this feature." + }, + "passwordPrompt": { + "message": "Master password re-prompt" + }, + "passwordConfirmation": { + "message": "Master password confirmation" + }, + "passwordConfirmationDesc": { + "message": "This action is protected. To continue, please re-enter your master password to verify your identity." + }, + "updatedMasterPassword": { + "message": "Updated master password" + }, + "updateMasterPassword": { + "message": "Update master password" + }, + "updateMasterPasswordWarning": { + "message": "Your master password was recently changed by an administrator in your organization. In order to access the vault, you must update it now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." + }, + "updateWeakMasterPasswordWarning": { + "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." + }, + "hours": { + "message": "Hours" + }, + "minutes": { + "message": "Minutes" + }, + "vaultTimeoutPolicyInEffect": { + "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + } + } + }, + "vaultTimeoutPolicyWithActionInEffect": { + "message": "Your organization policies are affecting your vault timeout. Maximum allowed vault timeout is $HOURS$ hour(s) and $MINUTES$ minute(s). Your vault timeout action is set to $ACTION$.", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + }, + "minutes": { + "content": "$2", + "example": "5" + }, + "action": { + "content": "$3", + "example": "Lock" + } + } + }, + "vaultTimeoutActionPolicyInEffect": { + "message": "Your organization policies have set your vault timeout action to $ACTION$.", + "placeholders": { + "action": { + "content": "$1", + "example": "Lock" + } + } + }, + "vaultTimeoutTooLarge": { + "message": "Your vault timeout exceeds the restrictions set by your organization." + }, + "resetPasswordPolicyAutoEnroll": { + "message": "Automatic enrollment" + }, + "resetPasswordAutoEnrollInviteWarning": { + "message": "This organization has an enterprise policy that will automatically enroll you in password reset. Enrollment will allow organization administrators to change your master password." + }, + "vaultExportDisabled": { + "message": "Vault export removed" + }, + "personalVaultExportPolicyInEffect": { + "message": "One or more organization policies prevents you from exporting your personal vault." + }, + "addAccount": { + "message": "Add account" + }, + "removeMasterPassword": { + "message": "Remove master password" + }, + "removedMasterPassword": { + "message": "Master password removed" + }, + "convertOrganizationEncryptionDesc": { + "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", + "placeholders": { + "organization": { + "content": "$1", + "example": "My Org Name" + } + } + }, + "leaveOrganization": { + "message": "Leave organization" + }, + "leaveOrganizationConfirmation": { + "message": "Are you sure you want to leave this organization?" + }, + "leftOrganization": { + "message": "You have left the organization." + }, + "ssoKeyConnectorError": { + "message": "Key connector error: make sure key connector is available and working correctly." + }, + "lockAllVaults": { + "message": "Lock all vaults" + }, + "accountLimitReached": { + "message": "No more than 5 accounts may be logged in at the same time." + }, + "accountPreferences": { + "message": "Preferences" + }, + "appPreferences": { + "message": "App settings (all accounts)" + }, + "accountSwitcherLimitReached": { + "message": "Account limit reached. Log out of an account to add another." + }, + "settingsTitle": { + "message": "App settings for $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "jdoe@example.com" + } + } + }, + "switchAccount": { + "message": "Switch account" + }, + "options": { + "message": "Options" + }, + "sessionTimeout": { + "message": "Your session has timed out. Please go back and try logging in again." + }, + "exportingPersonalVaultTitle": { + "message": "Exporting individual vault" + }, + "exportingPersonalVaultDescription": { + "message": "Only the individual vault items associated with $EMAIL$ will be exported. Organization vault items will not be included.", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "locked": { + "message": "Locked" + }, + "unlocked": { + "message": "Unlocked" + }, + "generator": { + "message": "Generator" + }, + "whatWouldYouLikeToGenerate": { + "message": "What would you like to generate?" + }, + "passwordType": { + "message": "Password type" + }, + "regenerateUsername": { + "message": "Regenerate username" + }, + "generateUsername": { + "message": "Generate username" + }, + "usernameType": { + "message": "Username type" + }, + "plusAddressedEmail": { + "message": "Plus addressed email", + "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" + }, + "plusAddressedEmailDesc": { + "message": "Use your email provider's sub-addressing capabilities." + }, + "catchallEmail": { + "message": "Catch-all email" + }, + "catchallEmailDesc": { + "message": "Use your domain's configured catch-all inbox." + }, + "random": { + "message": "Random" + }, + "randomWord": { + "message": "Random word" + }, + "websiteName": { + "message": "Website name" + }, + "service": { + "message": "Service" + }, + "allVaults": { + "message": "All vaults" + }, + "searchOrganization": { + "message": "Search organization" + }, + "searchMyVault": { + "message": "Search my vault" + }, + "forwardedEmail": { + "message": "Forwarded email alias" + }, + "forwardedEmailDesc": { + "message": "Generate an email alias with an external forwarding service." + }, + "hostname": { + "message": "Hostname", + "description": "Part of a URL." + }, + "apiAccessToken": { + "message": "API Access Token" + }, + "apiKey": { + "message": "API key" + }, + "premiumSubcriptionRequired": { + "message": "Premium subscription required" + }, + "organizationIsDisabled": { + "message": "Organization suspended" + }, + "disabledOrganizationFilterError": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, + "neverLockWarning": { + "message": "Are you sure you want to use the \"Never\" option? Setting your lock options to \"Never\" stores your vault's encryption key on your device. If you use this option you should ensure that you keep your device properly protected." + }, + "vault": { + "message": "Vault" + }, + "loginWithMasterPassword": { + "message": "Log in with master password" + }, + "loggingInAs": { + "message": "Logging in as" + }, + "rememberEmail": { + "message": "Remember email" + }, + "notYou": { + "message": "Not you?" + }, + "newAroundHere": { + "message": "New around here?" + }, + "loggingInTo": { + "message": "Logging in to $DOMAIN$", + "placeholders": { + "domain": { + "content": "$1", + "example": "example.com" + } + } + }, + "logInWithAnotherDevice": { + "message": "Log in with another device" + }, + "loginInitiated": { + "message": "Login initiated" + }, + "notificationSentDevice": { + "message": "A notification has been sent to your device." + }, + "fingerprintMatchInfo": { + "message": "Please make sure your vault is unlocked and Fingerprint phrase matches the other device." + }, + "fingerprintPhraseHeader": { + "message": "Fingerprint phrase" + }, + "needAnotherOption": { + "message": "Log in with device must be set up in the settings of the Bitwarden app. Need another option?" + }, + "viewAllLoginOptions": { + "message": "View all login options" + }, + "resendNotification": { + "message": "Resend notification" + }, + "toggleCharacterCount": { + "message": "Toggle character count", + "description": "'Character count' describes a feature that displays a number next to each character of the password." + }, + "areYouTryingtoLogin": { + "message": "Are you trying to log in?" + }, + "logInAttemptBy": { + "message": "Login attempt by $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "deviceType": { + "message": "Device Type" + }, + "ipAddress": { + "message": "IP Address" + }, + "time": { + "message": "Time" + }, + "confirmLogIn": { + "message": "Confirm login" + }, + "denyLogIn": { + "message": "Deny login" + }, + "approveLoginRequests": { + "message": "Approve login requests" + }, + "logInConfirmedForEmailOnDevice": { + "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + }, + "device": { + "content": "$2", + "example": "iOS" + } + } + }, + "youDeniedALogInAttemptFromAnotherDevice": { + "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + }, + "justNow": { + "message": "Just now" + }, + "requestedXMinutesAgo": { + "message": "Requested $MINUTES$ minutes ago", + "placeholders": { + "minutes": { + "content": "$1", + "example": "5" + } + } + }, + "loginRequestHasAlreadyExpired": { + "message": "Login request has already expired." + }, + "thisRequestIsNoLongerValid": { + "message": "This request is no longer valid." + }, + "approveLoginRequestDesc": { + "message": "Use this device to approve login requests made from other devices." + }, + "confirmLoginAtemptForMail": { + "message": "Confirm login attempt for $EMAIL$", + "placeholders": { + "email": { + "content": "$1", + "example": "name@example.com" + } + } + }, + "logInRequested": { + "message": "Log in requested" + }, + "exposedMasterPassword": { + "message": "Exposed Master Password" + }, + "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?" + }, + "weakAndExposedMasterPassword": { + "message": "Weak and Exposed Master Password" + }, + "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?" + }, + "checkForBreaches": { + "message": "Check known data breaches for this password" + }, + "important": { + "message": "Important:" + }, + "masterPasswordHint": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "characterMinimum": { + "message": "$LENGTH$ character minimum", + "placeholders": { + "length": { + "content": "$1", + "example": "14" + } + } + }, + "windowsBiometricUpdateWarning": { + "message": "Bitwarden recommends updating your biometric settings to require your master password (or PIN) on the first unlock. Would you like to update your settings now?" + }, + "windowsBiometricUpdateWarningTitle": { + "message": "Recommended Settings Update" + }, + "deviceApprovalRequired": { + "message": "Device approval required. Select an approval option below:" + }, + "rememberThisDevice": { + "message": "Remember this device" + }, + "uncheckIfPublicDevice": { + "message": "Uncheck if using a public device" + }, + "approveFromYourOtherDevice": { + "message": "Approve from your other device" + }, + "requestAdminApproval": { + "message": "Request admin approval" + }, + "approveWithMasterPassword": { + "message": "Approve with master password" + }, + "region": { + "message": "Region" + }, + "ssoIdentifierRequired": { + "message": "Organization SSO identifier is required." + }, + "eu": { + "message": "EU", + "description": "European Union" + }, + "loggingInOn": { + "message": "Logging in on" + }, + "usDomain": { + "message": "bitwarden.com" + }, + "euDomain": { + "message": "bitwarden.eu" + }, + "selfHosted": { + "message": "Self-hosted" + }, + "accessDenied": { + "message": "Access denied. You do not have permission to view this page." + }, + "accountSuccessfullyCreated": { + "message": "Account successfully created!" + }, + "adminApprovalRequested": { + "message": "Admin approval requested" + }, + "adminApprovalRequestSentToAdmins": { + "message": "Your request has been sent to your admin." + }, + "youWillBeNotifiedOnceApproved": { + "message": "You will be notified once approved." + }, + "troubleLoggingIn": { + "message": "Trouble logging in?" + }, + "loginApproved": { + "message": "Login approved" + }, + "userEmailMissing": { + "message": "User email missing" + }, + "deviceTrusted": { + "message": "Device trusted" + }, + "inputRequired": { + "message": "Input is required." + }, + "required": { + "message": "required" + }, + "search": { + "message": "Search" + }, + "inputMinLength": { + "message": "Input must be at least $COUNT$ characters long.", + "placeholders": { + "count": { + "content": "$1", + "example": "8" + } + } + }, + "inputMaxLength": { + "message": "Input must not exceed $COUNT$ characters in length.", + "placeholders": { + "count": { + "content": "$1", + "example": "20" + } + } + }, + "inputForbiddenCharacters": { + "message": "The following characters are not allowed: $CHARACTERS$", + "placeholders": { + "characters": { + "content": "$1", + "example": "@, #, $, %" + } + } + }, + "inputMinValue": { + "message": "Input value must be at least $MIN$.", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + } + } + }, + "inputMaxValue": { + "message": "Input value must not exceed $MAX$.", + "placeholders": { + "max": { + "content": "$1", + "example": "100" + } + } + }, + "multipleInputEmails": { + "message": "1 or more emails are invalid" + }, + "inputTrimValidator": { + "message": "Input must not contain only whitespace.", + "description": "Notification to inform the user that a form's input can't contain only whitespace." + }, + "inputEmail": { + "message": "Input is not an email address." + }, + "fieldsNeedAttention": { + "message": "$COUNT$ field(s) above need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "4" + } + } + }, + "selectPlaceholder": { + "message": "-- Select --" + }, + "multiSelectPlaceholder": { + "message": "-- Type to filter --" + }, + "multiSelectLoading": { + "message": "Retrieving options..." + }, + "multiSelectNotFound": { + "message": "No items found" + }, + "multiSelectClearAll": { + "message": "Clear all" + }, + "plusNMore": { + "message": "+ $QUANTITY$ more", + "placeholders": { + "quantity": { + "content": "$1", + "example": "5" + } + } + }, + "submenu": { + "message": "Submenu" + } +} diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index 79b8ae6ab25..5d75ee2208d 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB šifrētas krātuves datņu pielikumiem." }, - "premiumSignUpTwoStep": { - "message": "Tādas papildu divpakāpju pieteikšanās iespējas kā YubiKey, FIDO U2F un Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Paroļu higiēnas, konta veselības un datu noplūžu pārskati, lai uzturētu glabātavu drošu." diff --git a/apps/desktop/src/locales/me/messages.json b/apps/desktop/src/locales/me/messages.json index e7c60b94ba7..20336258846 100644 --- a/apps/desktop/src/locales/me/messages.json +++ b/apps/desktop/src/locales/me/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB šifrovanog skladišta za priloge datoteka." }, - "premiumSignUpTwoStep": { - "message": "Dodatne opcije prijave u dva koraka kao što su YubiKey, FIDO U2F i Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Higijena lozinke, zdravlje računa i podaci o krađi podataka kako bi trezor bio siguran." diff --git a/apps/desktop/src/locales/ml/messages.json b/apps/desktop/src/locales/ml/messages.json index f3f435e22aa..18b448b1d1e 100644 --- a/apps/desktop/src/locales/ml/messages.json +++ b/apps/desktop/src/locales/ml/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "ഫയൽ അറ്റാച്ചുമെന്റുകൾക്കായി 1 GB എൻക്രിപ്റ്റുചെയ്‌ത സ്റ്റോറേജ്." }, - "premiumSignUpTwoStep": { - "message": "രണ്ട്-ഘട്ട പ്രവേശന ഓപ്ഷനുകളായ Yubikey, FIDO U2F, Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "നിങ്ങളുടെ വാൾട് സൂക്ഷിക്കുന്നതിന്. പാസ്‌വേഡ് ശുചിത്വം, അക്കൗണ്ട് ആരോഗ്യം, ഡാറ്റ ലംഘന റിപ്പോർട്ടുകൾ." diff --git a/apps/desktop/src/locales/mr/messages.json b/apps/desktop/src/locales/mr/messages.json index d2405f785d9..38e81a83bfd 100644 --- a/apps/desktop/src/locales/mr/messages.json +++ b/apps/desktop/src/locales/mr/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." diff --git a/apps/desktop/src/locales/my/messages.json b/apps/desktop/src/locales/my/messages.json index d0277bff90d..29d7954de21 100644 --- a/apps/desktop/src/locales/my/messages.json +++ b/apps/desktop/src/locales/my/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." diff --git a/apps/desktop/src/locales/nb/messages.json b/apps/desktop/src/locales/nb/messages.json index 900f615b204..f4f65b9f536 100644 --- a/apps/desktop/src/locales/nb/messages.json +++ b/apps/desktop/src/locales/nb/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB med kryptert fillagring." }, - "premiumSignUpTwoStep": { - "message": "Ytterligere 2-trinnsinnloggingsmuligheter, slik som YubiKey, FIDO U2F, og Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Passordhygiene, kontohelse, og databruddsrapporter som holder hvelvet ditt trygt." diff --git a/apps/desktop/src/locales/ne/messages.json b/apps/desktop/src/locales/ne/messages.json index d2405f785d9..38e81a83bfd 100644 --- a/apps/desktop/src/locales/ne/messages.json +++ b/apps/desktop/src/locales/ne/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index 088b2ca8281..a124cede5cf 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB versleutelde opslag voor bijlagen." }, - "premiumSignUpTwoStep": { - "message": "Extra opties voor tweestapsaanmelding zoals YubiKey, FIDO U2F en Duo." + "premiumSignUpTwoStepOptions": { + "message": "Eigen opties voor tweestapsaanmelding zoals YubiKey en Duo." }, "premiumSignUpReports": { "message": "Rapportage op wachtwoordhygiëne, gezondheid van je account en gegevensinbreuk om je kluis veilig te houden." diff --git a/apps/desktop/src/locales/nn/messages.json b/apps/desktop/src/locales/nn/messages.json index a936f9047e9..46dee838abf 100644 --- a/apps/desktop/src/locales/nn/messages.json +++ b/apps/desktop/src/locales/nn/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." diff --git a/apps/desktop/src/locales/or/messages.json b/apps/desktop/src/locales/or/messages.json index cec32d87b39..d6cf45a696e 100644 --- a/apps/desktop/src/locales/or/messages.json +++ b/apps/desktop/src/locales/or/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index 63d724e0b78..ce85ae771dd 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB miejsca na zaszyfrowane załączniki." }, - "premiumSignUpTwoStep": { - "message": "Dodatkowe opcje logowania dwustopniowego, takie jak klucze YubiKey, FIDO U2F oraz Duo." + "premiumSignUpTwoStepOptions": { + "message": "Własnościowe opcje logowania dwuetapowego, takie jak YubiKey i Duo." }, "premiumSignUpReports": { "message": "Raporty bezpieczeństwa haseł, stanu konta i raporty wycieków danych, aby Twoje dane były bezpieczne." diff --git a/apps/desktop/src/locales/pt_BR/messages.json b/apps/desktop/src/locales/pt_BR/messages.json index 781605752cc..8e3aac885c3 100644 --- a/apps/desktop/src/locales/pt_BR/messages.json +++ b/apps/desktop/src/locales/pt_BR/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB de armazenamento de arquivos encriptados." }, - "premiumSignUpTwoStep": { - "message": "Opções de autenticação em duas etapas adicionais como YubiKey, FIDO U2F, e Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Higiene de senha, saúde da conta, e relatórios sobre violação de dados para manter o seu cofre seguro." diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index 5a52982f763..52188383594 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB de armazenamento encriptado para anexos de ficheiros." }, - "premiumSignUpTwoStep": { - "message": "Opções adicionais de verificação de dois passos, como YubiKey, FIDO U2F e Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Higiene de palavras-passe, saúde da conta e relatórios de violação de dados para manter o seu cofre seguro." diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json index 5e9acf893ea..ca46af9d5d5 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB spațiu de stocare criptat pentru atașamente de fișiere." }, - "premiumSignUpTwoStep": { - "message": "Opțiuni adiționale de autentificare în două etape, cum ar fi YubiKey, FIDO U2F și Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Rapoarte privind igiena parolelor, sănătatea contului și breșele de date pentru a vă păstra seiful în siguranță." diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index 61944e41ecd..7785e84f1cf 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 ГБ зашифрованного хранилища для вложенных файлов." }, - "premiumSignUpTwoStep": { - "message": "Дополнительные варианты двухэтапной аутентификации, такие как YubiKey, FIDO U2F и Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Гигиена паролей, здоровье аккаунта и отчеты об утечках данных для обеспечения безопасности вашего хранилища." diff --git a/apps/desktop/src/locales/si/messages.json b/apps/desktop/src/locales/si/messages.json index 6ed26ccdc6f..e2e5347aeb9 100644 --- a/apps/desktop/src/locales/si/messages.json +++ b/apps/desktop/src/locales/si/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index af7a73301d8..41411606590 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB šifrovaného úložiska." }, - "premiumSignUpTwoStep": { - "message": "Ďalšie možnosti dvojstupňového prihlásenia ako YubiKey, FIDO U2F a Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Správy o sile hesla, zabezpečení účtov a únikoch dát ktoré vám pomôžu udržať vaše kontá v bezpečí." diff --git a/apps/desktop/src/locales/sl/messages.json b/apps/desktop/src/locales/sl/messages.json index 3da6f653bb5..089da060600 100644 --- a/apps/desktop/src/locales/sl/messages.json +++ b/apps/desktop/src/locales/sl/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index 246757c9f54..b5712de8625 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1ГБ шифровано складиште за прилоге." }, - "premiumSignUpTwoStep": { - "message": "Додатне опције пријаве у два корака као што су YubiKey, FIDO U2F, и Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Извештаји о хигијени лозинки, здравственом стању налога и кршењу података да бисте заштитили сеф." @@ -1493,7 +1493,7 @@ "message": "Одјављени сеф захтева да поново потврдите идентитет да бисте му поново приступили." }, "unlockMethodNeededToChangeTimeoutActionDesc": { - "message": "Set up an unlock method to change your vault timeout action." + "message": "Подесите метод откључавања да бисте променили радњу временског ограничења сефа." }, "lock": { "message": "Закључај", @@ -2110,7 +2110,7 @@ "message": "Пријавите се са другим уређајем" }, "loginInitiated": { - "message": "Login initiated" + "message": "Пријава је покренута" }, "notificationSentDevice": { "message": "Обавештење је послато на ваш уређај." @@ -2250,28 +2250,28 @@ "message": "Препоручено ажурирање поставки" }, "deviceApprovalRequired": { - "message": "Device approval required. Select an approval option below:" + "message": "Потребно је одобрење уређаја. Изаберите опцију одобрења испод:" }, "rememberThisDevice": { - "message": "Remember this device" + "message": "Запамти овај уређај" }, "uncheckIfPublicDevice": { - "message": "Uncheck if using a public device" + "message": "Искључите ако се користи јавни уређај" }, "approveFromYourOtherDevice": { - "message": "Approve from your other device" + "message": "Одобри са мојим другим уређајем" }, "requestAdminApproval": { - "message": "Request admin approval" + "message": "Затражити одобрење администратора" }, "approveWithMasterPassword": { - "message": "Approve with master password" + "message": "Одобрити са главном лозинком" }, "region": { - "message": "Region" + "message": "Регион" }, "ssoIdentifierRequired": { - "message": "Organization SSO identifier is required." + "message": "Потребан је SSO идентификатор организације." }, "eu": { "message": "EU", @@ -2293,40 +2293,40 @@ "message": "Одбијен приступ. Немате дозволу да видите ову страницу." }, "accountSuccessfullyCreated": { - "message": "Account successfully created!" + "message": "Налог је успешно креиран!" }, "adminApprovalRequested": { - "message": "Admin approval requested" + "message": "Захтевано је одобрење администратора" }, "adminApprovalRequestSentToAdmins": { - "message": "Your request has been sent to your admin." + "message": "Ваш захтев је послат вашем администратору." }, "youWillBeNotifiedOnceApproved": { - "message": "You will be notified once approved." + "message": "Бићете обавештени када буде одобрено." }, "troubleLoggingIn": { - "message": "Trouble logging in?" + "message": "Имате проблема са пријављивањем?" }, "loginApproved": { - "message": "Login approved" + "message": "Пријава је одобрена" }, "userEmailMissing": { - "message": "User email missing" + "message": "Недостаје имејл корисника" }, "deviceTrusted": { - "message": "Device trusted" + "message": "Уређај поуздан" }, "inputRequired": { - "message": "Input is required." + "message": "Унос је потребан." }, "required": { - "message": "required" + "message": "обавезно" }, "search": { - "message": "Search" + "message": "Тражи" }, "inputMinLength": { - "message": "Input must be at least $COUNT$ characters long.", + "message": "Унос трба имати најмање $COUNT$ слова.", "placeholders": { "count": { "content": "$1", @@ -2335,7 +2335,7 @@ } }, "inputMaxLength": { - "message": "Input must not exceed $COUNT$ characters in length.", + "message": "Унос не сме бити већи од $COUNT$ карактера.", "placeholders": { "count": { "content": "$1", @@ -2344,7 +2344,7 @@ } }, "inputForbiddenCharacters": { - "message": "The following characters are not allowed: $CHARACTERS$", + "message": "Следећи знакови нису дозвољени: $CHARACTERS$", "placeholders": { "characters": { "content": "$1", @@ -2353,7 +2353,7 @@ } }, "inputMinValue": { - "message": "Input value must be at least $MIN$.", + "message": "Вредност мора бити најмање $MIN$.", "placeholders": { "min": { "content": "$1", @@ -2362,7 +2362,7 @@ } }, "inputMaxValue": { - "message": "Input value must not exceed $MAX$.", + "message": "Вредност не сме бити већа од $MAX$.", "placeholders": { "max": { "content": "$1", @@ -2371,17 +2371,17 @@ } }, "multipleInputEmails": { - "message": "1 or more emails are invalid" + "message": "1 или више имејлова су неважећи" }, "inputTrimValidator": { - "message": "Input must not contain only whitespace.", + "message": "Унос не сме да садржи само размак.", "description": "Notification to inform the user that a form's input can't contain only whitespace." }, "inputEmail": { - "message": "Input is not an email address." + "message": "Унос није имејл." }, "fieldsNeedAttention": { - "message": "$COUNT$ field(s) above need your attention.", + "message": "$COUNT$ поље(а) изнад захтевај(у) вашу пажњу.", "placeholders": { "count": { "content": "$1", @@ -2390,22 +2390,22 @@ } }, "selectPlaceholder": { - "message": "-- Select --" + "message": "-- Одабрати --" }, "multiSelectPlaceholder": { - "message": "-- Type to filter --" + "message": "-- Тип за филтрирање --" }, "multiSelectLoading": { - "message": "Retrieving options..." + "message": "Преузимање опција..." }, "multiSelectNotFound": { - "message": "No items found" + "message": "Нема предмета" }, "multiSelectClearAll": { - "message": "Clear all" + "message": "Обриши све" }, "plusNMore": { - "message": "+ $QUANTITY$ more", + "message": "+ још $QUANTITY$", "placeholders": { "quantity": { "content": "$1", @@ -2414,6 +2414,6 @@ } }, "submenu": { - "message": "Submenu" + "message": "Под-мени" } } diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index 3101b8a7228..b224bb083c5 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB krypterad lagring." }, - "premiumSignUpTwoStep": { - "message": "Ytterligare alternativ för tvåstegsverifiering såsom YubiKey, FIDO U2F och Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Lösenordshygien, kontohälsa och dataintrångsrapporter för att skydda ditt valv." diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json index d2405f785d9..38e81a83bfd 100644 --- a/apps/desktop/src/locales/te/messages.json +++ b/apps/desktop/src/locales/te/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." diff --git a/apps/desktop/src/locales/th/messages.json b/apps/desktop/src/locales/th/messages.json index b2210cd5363..32f10e27404 100644 --- a/apps/desktop/src/locales/th/messages.json +++ b/apps/desktop/src/locales/th/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB of encrypted file storage." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index 7919a0f90f8..ff5185c7141 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "Dosya ekleri için 1 GB şifrelenmiş depolama." }, - "premiumSignUpTwoStep": { - "message": "YubiKey, FIDO U2F ve Duo gibi iki aşamalı giriş seçenekleri." + "premiumSignUpTwoStepOptions": { + "message": "YubiKey ve Duo gibi marka bazlı iki aşamalı giriş seçenekleri." }, "premiumSignUpReports": { "message": "Kasanızı güvende tutmak için parola hijyeni, hesap sağlığı ve veri ihlali raporları." diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index afe6b9a8ffd..09a7ec512bc 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 ГБ зашифрованого сховища для файлів." }, - "premiumSignUpTwoStep": { - "message": "Додаткові можливості двоетапної перевірки, наприклад, YubiKey, FIDO U2F та Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Гігієна паролів, здоров'я облікового запису, а також звіти про вразливості даних, щоб зберігати ваше сховище в безпеці." @@ -2317,16 +2317,16 @@ "message": "Довірений пристрій" }, "inputRequired": { - "message": "Input is required." + "message": "Необхідно ввести дані." }, "required": { - "message": "required" + "message": "обов'язково" }, "search": { - "message": "Search" + "message": "Пошук" }, "inputMinLength": { - "message": "Input must be at least $COUNT$ characters long.", + "message": "Введені дані мають бути довжиною принаймні $COUNT$ символів.", "placeholders": { "count": { "content": "$1", @@ -2335,7 +2335,7 @@ } }, "inputMaxLength": { - "message": "Input must not exceed $COUNT$ characters in length.", + "message": "Вхідне значення не повинно перевищувати $COUNT$ символів.", "placeholders": { "count": { "content": "$1", @@ -2344,7 +2344,7 @@ } }, "inputForbiddenCharacters": { - "message": "The following characters are not allowed: $CHARACTERS$", + "message": "Вказані символи заборонені: $CHARACTERS$", "placeholders": { "characters": { "content": "$1", @@ -2353,7 +2353,7 @@ } }, "inputMinValue": { - "message": "Input value must be at least $MIN$.", + "message": "Значення має бути принаймні $MIN$.", "placeholders": { "min": { "content": "$1", @@ -2362,7 +2362,7 @@ } }, "inputMaxValue": { - "message": "Input value must not exceed $MAX$.", + "message": "Значення не може перевищувати $MAX$.", "placeholders": { "max": { "content": "$1", @@ -2371,17 +2371,17 @@ } }, "multipleInputEmails": { - "message": "1 or more emails are invalid" + "message": "1 або більше адрес е-пошти недійсні" }, "inputTrimValidator": { - "message": "Input must not contain only whitespace.", + "message": "Введене значення не повинно містити лише пробіл.", "description": "Notification to inform the user that a form's input can't contain only whitespace." }, "inputEmail": { - "message": "Input is not an email address." + "message": "Введені дані не є адресою е-пошти." }, "fieldsNeedAttention": { - "message": "$COUNT$ field(s) above need your attention.", + "message": "$COUNT$ поле (поля) вище потребують вашої уваги.", "placeholders": { "count": { "content": "$1", @@ -2390,22 +2390,22 @@ } }, "selectPlaceholder": { - "message": "-- Select --" + "message": "-- Оберіть--" }, "multiSelectPlaceholder": { - "message": "-- Type to filter --" + "message": "-- Введіть для фільтрування --" }, "multiSelectLoading": { - "message": "Retrieving options..." + "message": "Параметри отримання..." }, "multiSelectNotFound": { - "message": "No items found" + "message": "Нічого не знайдено" }, "multiSelectClearAll": { - "message": "Clear all" + "message": "Очистити все" }, "plusNMore": { - "message": "+ $QUANTITY$ more", + "message": "+ ще $QUANTITY$", "placeholders": { "quantity": { "content": "$1", @@ -2414,6 +2414,6 @@ } }, "submenu": { - "message": "Submenu" + "message": "Підменю" } } diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json index 2cee99f327c..dcbedbe2938 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1GB bộ nhớ lưu trữ tập tin được mã hóa." }, - "premiumSignUpTwoStep": { - "message": "Các tùy chọn xác thực hai lớp bổ sung như YubiKey, FIDO U2F và Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "Thanh lọc mật khẩu, kiểm tra an toàn tài khoản và các báo cáo rò rĩ dữ liệu là để giữ cho kho của bạn an toàn." diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index 135104c0fc7..ff74b521eed 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "1 GB 文件附件加密存储。" }, - "premiumSignUpTwoStep": { - "message": "额外的两步登录选项,如 YubiKey、FIDO U2F 和 Duo。" + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "密码健康、账户体检以及数据泄露报告,保障您的密码库安全。" diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index be38a8605fe..1de910b1b44 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -1077,8 +1077,8 @@ "premiumSignUpStorage": { "message": "用於檔案附件的 1 GB 的加密檔案儲存空間。" }, - "premiumSignUpTwoStep": { - "message": "YubiKey、FIDO U2F 和 Duo 等額外的兩步驟登入選項。" + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpReports": { "message": "密碼健康度檢查、提供帳戶體檢以及資料外洩報告,以保障您的密碼庫安全。" @@ -1414,7 +1414,7 @@ "message": "啟動時詢問 Touch ID" }, "requirePasswordOnStart": { - "message": "Require password or PIN on app start" + "message": "需要在啟動應用程式時輸入密碼或 PIN 碼。" }, "recommendedForSecurity": { "message": "Recommended for security." @@ -2110,7 +2110,7 @@ "message": "使用其他裝置登入" }, "loginInitiated": { - "message": "Login initiated" + "message": "登入已發起" }, "notificationSentDevice": { "message": "已傳送通知至您的裝置。" @@ -2181,7 +2181,7 @@ "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." }, "justNow": { - "message": "Just now" + "message": "剛剛" }, "requestedXMinutesAgo": { "message": "Requested $MINUTES$ minutes ago", @@ -2193,7 +2193,7 @@ } }, "loginRequestHasAlreadyExpired": { - "message": "Login request has already expired." + "message": "登入要求已逾期。" }, "thisRequestIsNoLongerValid": { "message": "This request is no longer valid." @@ -2202,7 +2202,7 @@ "message": "Use this device to approve login requests made from other devices." }, "confirmLoginAtemptForMail": { - "message": "Confirm login attempt for $EMAIL$", + "message": "確認 $EMAIL$ 的登入嘗試", "placeholders": { "email": { "content": "$1", @@ -2214,13 +2214,13 @@ "message": "已要求登入" }, "exposedMasterPassword": { - "message": "Exposed Master Password" + "message": "已暴露的主密碼" }, "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?" }, "weakAndExposedMasterPassword": { - "message": "Weak and Exposed Master Password" + "message": "強度不足且已暴露的主密碼" }, "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?" @@ -2253,7 +2253,7 @@ "message": "Device approval required. Select an approval option below:" }, "rememberThisDevice": { - "message": "Remember this device" + "message": "記住這個裝置" }, "uncheckIfPublicDevice": { "message": "Uncheck if using a public device" @@ -2268,7 +2268,7 @@ "message": "Approve with master password" }, "region": { - "message": "Region" + "message": "區域" }, "ssoIdentifierRequired": { "message": "Organization SSO identifier is required." @@ -2293,10 +2293,10 @@ "message": "拒絕存取。您沒有檢視此頁面的權限。" }, "accountSuccessfullyCreated": { - "message": "Account successfully created!" + "message": "成功建立帳號!" }, "adminApprovalRequested": { - "message": "Admin approval requested" + "message": "需要管理員批准" }, "adminApprovalRequestSentToAdmins": { "message": "Your request has been sent to your admin." @@ -2305,7 +2305,7 @@ "message": "You will be notified once approved." }, "troubleLoggingIn": { - "message": "Trouble logging in?" + "message": "登入時遇到困難?" }, "loginApproved": { "message": "Login approved" From 326b24e6557e0584110d2be75e4b1eaf5c9deb7a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 1 Sep 2023 12:50:51 +0000 Subject: [PATCH 14/46] Autosync the updated translations (#6166) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/web/src/locales/af/messages.json | 7 +- apps/web/src/locales/ar/messages.json | 7 +- apps/web/src/locales/az/messages.json | 7 +- apps/web/src/locales/be/messages.json | 7 +- apps/web/src/locales/bg/messages.json | 13 ++-- apps/web/src/locales/bn/messages.json | 7 +- apps/web/src/locales/bs/messages.json | 7 +- apps/web/src/locales/ca/messages.json | 7 +- apps/web/src/locales/cs/messages.json | 7 +- apps/web/src/locales/cy/messages.json | 7 +- apps/web/src/locales/da/messages.json | 7 +- apps/web/src/locales/de/messages.json | 7 +- apps/web/src/locales/el/messages.json | 7 +- apps/web/src/locales/en_GB/messages.json | 7 +- apps/web/src/locales/en_IN/messages.json | 7 +- apps/web/src/locales/eo/messages.json | 7 +- apps/web/src/locales/es/messages.json | 7 +- apps/web/src/locales/et/messages.json | 7 +- apps/web/src/locales/eu/messages.json | 7 +- apps/web/src/locales/fa/messages.json | 7 +- apps/web/src/locales/fi/messages.json | 7 +- apps/web/src/locales/fil/messages.json | 7 +- apps/web/src/locales/fr/messages.json | 7 +- apps/web/src/locales/gl/messages.json | 7 +- apps/web/src/locales/he/messages.json | 7 +- apps/web/src/locales/hi/messages.json | 7 +- apps/web/src/locales/hr/messages.json | 7 +- apps/web/src/locales/hu/messages.json | 7 +- apps/web/src/locales/id/messages.json | 7 +- apps/web/src/locales/it/messages.json | 9 ++- apps/web/src/locales/ja/messages.json | 7 +- apps/web/src/locales/ka/messages.json | 7 +- apps/web/src/locales/km/messages.json | 7 +- apps/web/src/locales/kn/messages.json | 7 +- apps/web/src/locales/ko/messages.json | 7 +- apps/web/src/locales/lv/messages.json | 7 +- apps/web/src/locales/ml/messages.json | 7 +- apps/web/src/locales/mr/messages.json | 7 +- apps/web/src/locales/my/messages.json | 7 +- apps/web/src/locales/nb/messages.json | 7 +- apps/web/src/locales/ne/messages.json | 7 +- apps/web/src/locales/nl/messages.json | 7 +- apps/web/src/locales/nn/messages.json | 7 +- apps/web/src/locales/or/messages.json | 7 +- apps/web/src/locales/pl/messages.json | 7 +- apps/web/src/locales/pt_BR/messages.json | 7 +- apps/web/src/locales/pt_PT/messages.json | 7 +- apps/web/src/locales/ro/messages.json | 7 +- apps/web/src/locales/ru/messages.json | 7 +- apps/web/src/locales/si/messages.json | 7 +- apps/web/src/locales/sk/messages.json | 7 +- apps/web/src/locales/sl/messages.json | 7 +- apps/web/src/locales/sr/messages.json | 89 ++++++++++++------------ apps/web/src/locales/sr_CS/messages.json | 7 +- apps/web/src/locales/sv/messages.json | 7 +- apps/web/src/locales/te/messages.json | 7 +- apps/web/src/locales/th/messages.json | 7 +- apps/web/src/locales/tr/messages.json | 7 +- apps/web/src/locales/uk/messages.json | 7 +- apps/web/src/locales/vi/messages.json | 7 +- apps/web/src/locales/zh_CN/messages.json | 13 ++-- apps/web/src/locales/zh_TW/messages.json | 7 +- 62 files changed, 358 insertions(+), 172 deletions(-) diff --git a/apps/web/src/locales/af/messages.json b/apps/web/src/locales/af/messages.json index 7d68eb6f4c6..4b80c3c2119 100644 --- a/apps/web/src/locales/af/messages.json +++ b/apps/web/src/locales/af/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GG geënkripteerde berging vir lêeraanhegsels." }, - "premiumSignUpTwoStep": { - "message": "Bykomende tweestapaantekenopsies soos YubiKey, FIDO U2F en Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Noodtoegang" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "Your plan comes with a free 7 day trial. Your payment method will not be charged until the trial has ended. You may cancel at any time." }, diff --git a/apps/web/src/locales/ar/messages.json b/apps/web/src/locales/ar/messages.json index c5b8fb72e39..f43798b93c1 100644 --- a/apps/web/src/locales/ar/messages.json +++ b/apps/web/src/locales/ar/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "الوصول الطارئ" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "Your plan comes with a free 7 day trial. Your payment method will not be charged until the trial has ended. You may cancel at any time." }, diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index a51fb880f62..6e037753488 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "Fayl qoşmaları üçün 1 GB şifrələnmiş saxlama sahəsi." }, - "premiumSignUpTwoStep": { - "message": "YubiKey, FIDO U2F və Duo kimi iki mərhələli giriş seçimləri." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Fövqəladə vəziyyət müraciəti" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Ödənilməmiş abunəliklər üçün ödəniş üsulunuzdan ödəniş alınacaq." + }, "paymentChargedWithTrial": { "message": "Planınızda 7 günlük ödənişsiz sınaq var. Sınaq müddəti bitənə qədər ödəniş metodundan pul çıxılmayacaq. Faktura, hər $INTERVAL$ bir müntəzəm olaraq icra ediləcək. İstənilən vaxt imtina edə bilərsiniz." }, diff --git a/apps/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index 798ed6258c2..305dcf75092 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 ГБ зашыфраванага сховішча для далучаных файлаў." }, - "premiumSignUpTwoStep": { - "message": "Дадатковыя варыянты двухэтапнага ўваходу, такія як YubiKey, FIDO U2F і Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Экстранны доступ" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "У ваш тарыфны план уключаны выпрабавальны перыяд на 7 дзён. У вас не будзе спагнана плата згодна з выбраным спосабам аплаты пакуль не завяршыцца выпрабавальны перыяд. Вы можаце скасаваць яго ў любы момант." }, diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index c3530f9b835..9b32d828818 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB пространство за файлове, които се шифрират." }, - "premiumSignUpTwoStep": { - "message": "Двустепенно удостоверяване чрез YubiKey, FIDO U2F и Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Авариен достъп" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Сумата за всички неплатени абонаменти ще бъде изискана от Вашия метод за плащане." + }, "paymentChargedWithTrial": { "message": "Планът ви идва с пробен период от 7 дена. Плащането няма се изиска преди това, след което ще се повтаря всеки $INTERVAL$. Може да се откажете по всяко време." }, @@ -5170,10 +5173,10 @@ "message": "Премахване на спонсорирането" }, "removeSponsorshipConfirmation": { - "message": "After removing a sponsorship, you will be responsible for this subscription and related invoices. Are you sure you want to continue?" + "message": "След като премахнете спонсорството, Вие ще поемете отговорността за този абонамент и свързаните с него плащания. Наистина ли искате да продължите?" }, "sponsorshipCreated": { - "message": "Sponsorship created" + "message": "Спонсорството е създадено" }, "emailSent": { "message": "Писмото е изпратено" @@ -5290,7 +5293,7 @@ "message": "Migrated to Key Connector" }, "paymentSponsored": { - "message": "Please provide a payment method to associate with the organization. Don't worry, we won't charge you anything unless you select additional features or your sponsorship expires. " + "message": "Посочете метод за плащане, който да бъде свързан с организацията. Не се притеснявайте – от него няма да бъдат изискани никакви суми, докато не изберете допълнителни функционалности или докато не изтече периода на спонсорирането Ви. " }, "orgCreatedSponsorshipInvalid": { "message": "The sponsorship offer has expired. You may delete the organization you created to avoid a charge at the end of your 7 day trial. Otherwise you may close this prompt to keep the organization and assume billing responsibility." diff --git a/apps/web/src/locales/bn/messages.json b/apps/web/src/locales/bn/messages.json index 07f6478c2f6..c970aacffb7 100644 --- a/apps/web/src/locales/bn/messages.json +++ b/apps/web/src/locales/bn/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "ফাইল সংযুক্তির জন্য ১ জিবি এনক্রিপ্টেড স্থান।" }, - "premiumSignUpTwoStep": { - "message": "YubiKey, FIDO U2F, ও Duo এর মতো অতিরিক্ত দ্বি-পদক্ষেপ লগইন বিকল্পগুলি।" + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Emergency access" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "Your plan comes with a free 7 day trial. Your payment method will not be charged until the trial has ended. You may cancel at any time." }, diff --git a/apps/web/src/locales/bs/messages.json b/apps/web/src/locales/bs/messages.json index b1810d3616a..ea7b495a715 100644 --- a/apps/web/src/locales/bs/messages.json +++ b/apps/web/src/locales/bs/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Emergency access" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "Your plan comes with a free 7 day trial. Your payment method will not be charged until the trial has ended. You may cancel at any time." }, diff --git a/apps/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index bae96a1ffc2..498f1a56615 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB d'emmagatzematge xifrat per als fitxers adjunts." }, - "premiumSignUpTwoStep": { - "message": "Opcions addicionals d'inici de sessió en dues passes com ara YubiKey, FIDO U2F i Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Accés d’emergència" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "El vostre pla inclou una prova gratuïta de 7 dies. El mètode de pagament no es cobrarà fins que no s'haja acabat la prova. Podeu cancel·lar-ho en qualsevol moment." }, diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index 8f06a3acb29..c18a9fa2044 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB šifrovaného úložiště pro přílohy." }, - "premiumSignUpTwoStep": { - "message": "Další možnosti dvoufázového přihlášení, jako je například YubiKey, FIDO U2F a Duo." + "premiumSignUpTwoStepOptions": { + "message": "Volby proprietálních dvoufázových přihlášení jako je YubiKey a Duo." }, "premiumSignUpEmergency": { "message": "Nouzový přístup" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Za nezaplacené předplatné bude naúčtována částka z Vaší platební metody." + }, "paymentChargedWithTrial": { "message": "Vybraný plán obsahuje bezplatnou 7denní zkušební dobu. Částka z Vašeho účtu nebude stržena, dokud tato zkušební doba neuplyne. Předplatné můžete kdykoli zrušit." }, diff --git a/apps/web/src/locales/cy/messages.json b/apps/web/src/locales/cy/messages.json index e416094e183..a71551e7710 100644 --- a/apps/web/src/locales/cy/messages.json +++ b/apps/web/src/locales/cy/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Emergency access" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "Your plan comes with a free 7 day trial. Your payment method will not be charged until the trial has ended. You may cancel at any time." }, diff --git a/apps/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index 0124b315f95..3ed144bcaf5 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB krypteret lager til vedhæftede filer." }, - "premiumSignUpTwoStep": { - "message": "Yderligere totrins login-muligheder, såsom YubiKey, FIDO U2F og Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietære totrins-login muligheder, såsom YubiKey og Duo." }, "premiumSignUpEmergency": { "message": "Nødadgang" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Der vil ske opkrævning for evt. ubetalte abonnementer via betalingsmetoden." + }, "paymentChargedWithTrial": { "message": "Dit abonnement indeholder en gratis 7-dages prøveperiode. Din betalingsmetode vil ikke blive debiteret, før prøveperioden er slut. Du kan til enhver tid annullere." }, diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index 615f5397c95..07f5f1d5e7a 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB verschlüsselter Speicherplatz für Dateianhänge." }, - "premiumSignUpTwoStep": { - "message": "Zusätzliche Zwei-Faktor-Authentifizierungsmöglichkeiten wie z.B. YubiKey, FIDO U2F und Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Notfallzugriff" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Deine Zahlungsmethode wird für alle unbezahlten Abonnements belastet." + }, "paymentChargedWithTrial": { "message": "Dein Tarif beinhaltet eine kostenlose 7-Tage-Testversion. Deine Zahlungsart wird nicht belastet, bis die Testphase abgelaufen ist. Du erhältst pro $INTERVAL$ eine Rechnung. Eine Kündigung ist zu jeder Zeit möglich." }, diff --git a/apps/web/src/locales/el/messages.json b/apps/web/src/locales/el/messages.json index 834f96c5069..5103325a343 100644 --- a/apps/web/src/locales/el/messages.json +++ b/apps/web/src/locales/el/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB κρυπτογραφημένο αποθηκευτικό χώρο για συνημμένα αρχεία." }, - "premiumSignUpTwoStep": { - "message": "Πρόσθετες επιλογές σύνδεσης δύο παραγόντων, όπως το YubiKey, το FIDO U2F και το Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Πρόσβαση Έκτακτης Ανάγκης" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "Το πακέτο σας έρχεται με δωρεάν δοκιμή 7 ημερών. Ο τρόπος πληρωμής σας δεν θα χρεωθεί μέχρι να τελειώσει η δοκιμή. Η χρέωση θα πραγματοποιείται σε επαναλαμβανόμενη βάση κάθε $INTERVAL$. Μπορείτε να το ακυρώσετε οποιαδήποτε στιγμή." }, diff --git a/apps/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index c772bd2c33b..f342a7e81f3 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Emergency access" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "Your plan comes with a free 7 day trial. Your payment method will not be charged until the trial has ended. You may cancel at any time." }, diff --git a/apps/web/src/locales/en_IN/messages.json b/apps/web/src/locales/en_IN/messages.json index 4cfdf991cc0..5f31a22c729 100644 --- a/apps/web/src/locales/en_IN/messages.json +++ b/apps/web/src/locales/en_IN/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Emergency Access" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "Your plan comes with a free 7 day trial. Your payment method will not be charged until the trial has ended. Billing will occur on a recurring basis each $INTERVAL$. You may cancel at any time." }, diff --git a/apps/web/src/locales/eo/messages.json b/apps/web/src/locales/eo/messages.json index e4ab167381f..8034131dcc5 100644 --- a/apps/web/src/locales/eo/messages.json +++ b/apps/web/src/locales/eo/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB ĉifrita stokado por dosieraj aldonaĵoj." }, - "premiumSignUpTwoStep": { - "message": "Pliaj du-paŝaj ensalutaj opcioj kiel YubiKey, FIDO U2F kaj Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Kriza Aliro" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "Via plano venas kun senpaga 7-taga provado. Via pagmaniero ne estos ŝargita ĝis la proceso finiĝos. Fakturado okazos ĉiufoje $INTERVAL$. Vi rajtas nuligi iam ajn." }, diff --git a/apps/web/src/locales/es/messages.json b/apps/web/src/locales/es/messages.json index 7b59b2e8cd4..50e743cb655 100644 --- a/apps/web/src/locales/es/messages.json +++ b/apps/web/src/locales/es/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB de almacenamiento de archivos cifrados." }, - "premiumSignUpTwoStep": { - "message": "Opciones adicionales de inicio de sesión de dos pasos como YubiKey, Fido U2F y Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Acceso de emergencia" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "Your plan comes with a free 7 day trial. Your card will not be charged until the trial has ended and on a recurring basis each $INTERVAL$. You may cancel at any time." }, diff --git a/apps/web/src/locales/et/messages.json b/apps/web/src/locales/et/messages.json index 104b001178c..f31e0832366 100644 --- a/apps/web/src/locales/et/messages.json +++ b/apps/web/src/locales/et/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB ulatuses krüpteeritud salvestusruum." }, - "premiumSignUpTwoStep": { - "message": "Lisavõimalused kaheastmeliseks kinnitamiseks, näiteks YubiKey, FIDO U2F ja Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Hädaolukorra ligipääs" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "Valitud pakett sisaldab 7 päevast prooviperioodi. Krediitkaardilt ei võeta raha enne, kui prooviperiood läbi saab. Väljatoodud summa debiteeritakse iga $INTERVAL$. Tellimust on võimalik igal ajal tühistada." }, diff --git a/apps/web/src/locales/eu/messages.json b/apps/web/src/locales/eu/messages.json index e4f714fe225..793f4612ca2 100644 --- a/apps/web/src/locales/eu/messages.json +++ b/apps/web/src/locales/eu/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "Eranskinentzako 1GB-eko zifratutako biltegia." }, - "premiumSignUpTwoStep": { - "message": "YubiKey, FIDO U2F eta Duo bezalako bi urratseko saio hasierarako aukera gehigarriak." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Larrialdietarako sarbidea" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "Zure planak 7 eguneko doako probaldia du. Zure ordainketa ez da kobratuko probaldia amaitu arte. Edozein unetan utz dezakezu bertan behera." }, diff --git a/apps/web/src/locales/fa/messages.json b/apps/web/src/locales/fa/messages.json index 73f4c95644c..fe48e771fa0 100644 --- a/apps/web/src/locales/fa/messages.json +++ b/apps/web/src/locales/fa/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "۱ گیگابایت فضای ذخیره‌سازی رمزنگاری شده برای پرونده‌های پیوست." }, - "premiumSignUpTwoStep": { - "message": "گزینه‌های ورود دو مرحله‌ای اضافی مانند YubiKey, FIDO U2F و Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "دسترسی اضطراری" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "طرح شما با یک دوره آزمایشی رایگان ۷ روزه همراه است. تا پایان دوره آزمایشی از روش پرداخت شما هزینه ای کسر نمی‌شود. هر زمان که مایل بودید می‌توانید آن را لغو کنید." }, diff --git a/apps/web/src/locales/fi/messages.json b/apps/web/src/locales/fi/messages.json index 016f1ee256d..1606ca35ff6 100644 --- a/apps/web/src/locales/fi/messages.json +++ b/apps/web/src/locales/fi/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 Gt salattua tallennustilaa tiedostoliitteille." }, - "premiumSignUpTwoStep": { - "message": "Muita kaksivaiheisen kirjautumisen todentajia, kuten YubiKey, FIDO U2F ja Duo Security." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Varmuuskäyttö" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Maksutavaltasi veloitetaan kaikki maksamattomat tilauksesta." + }, "paymentChargedWithTrial": { "message": "Tilauksesi sisältää ilmaisen 7 päivän kokeilujakson. Maksutapaasi ei veloiteta ennen kokeilujakson päättymistä. Voit irtisanoa tilauksen koska tahansa." }, diff --git a/apps/web/src/locales/fil/messages.json b/apps/web/src/locales/fil/messages.json index 8cc5a44b3f1..1b11567b782 100644 --- a/apps/web/src/locales/fil/messages.json +++ b/apps/web/src/locales/fil/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1GB na naka-encrypt na storage para sa mga file attachment." }, - "premiumSignUpTwoStep": { - "message": "Karagdagang mga opsyon para sa dalawang-hakbang na pag-log in tulad ng YubiKey, FIDO U2F, at Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Emergency access" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "May libreng 7 araw na trial ang plano mo. Hindi sisingilin ang paraan mo sa pagbabayad hanggang sa matapos ang libreng trial. Makakapagkansela ka kailanman." }, diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index 62da8e84e12..6bc5b0a1454 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 Go de stockage chiffré pour les fichiers joints." }, - "premiumSignUpTwoStep": { - "message": "Options additionnelles d'authentification à deux facteurs telles que YubiKey, FIDO U2F et Duo." + "premiumSignUpTwoStepOptions": { + "message": "Options de connexion propriétaires à deux facteurs telles que YubiKey et Duo." }, "premiumSignUpEmergency": { "message": "Accès d'urgence" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Votre mode de paiement sera facturé pour tous les abonnements impayés." + }, "paymentChargedWithTrial": { "message": "Votre offre comprend un essai gratuit de 7 jours. Votre mode de paiement ne sera pas facturé avant la fin de la période d'essai. Vous pouvez annuler à tout moment." }, diff --git a/apps/web/src/locales/gl/messages.json b/apps/web/src/locales/gl/messages.json index e416094e183..a71551e7710 100644 --- a/apps/web/src/locales/gl/messages.json +++ b/apps/web/src/locales/gl/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Emergency access" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "Your plan comes with a free 7 day trial. Your payment method will not be charged until the trial has ended. You may cancel at any time." }, diff --git a/apps/web/src/locales/he/messages.json b/apps/web/src/locales/he/messages.json index dbb202a515e..716f2d46071 100644 --- a/apps/web/src/locales/he/messages.json +++ b/apps/web/src/locales/he/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 ג'יגה של מקום אחסון מוצפן עבור קבצים מצורפים." }, - "premiumSignUpTwoStep": { - "message": "אפשרויות כניסה דו שלבית מתקדמות כמו YubiKey, FIDO U2F, וגם Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Emergency access" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "התוכנית שבחרת מגיעה עם 7 ימי נסיון חינמי. שיטת התשלום שבחרת לא תחויב עד לתום תקופת הנסיון. ביצוע החשבון יתבצע על בסיס מתחדש בכל $INTERVAL$. באפשרותך לבטל בכל עת." }, diff --git a/apps/web/src/locales/hi/messages.json b/apps/web/src/locales/hi/messages.json index 53f6e802ee9..cb737f84f44 100644 --- a/apps/web/src/locales/hi/messages.json +++ b/apps/web/src/locales/hi/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Emergency access" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "Your plan comes with a free 7 day trial. Your payment method will not be charged until the trial has ended. You may cancel at any time." }, diff --git a/apps/web/src/locales/hr/messages.json b/apps/web/src/locales/hr/messages.json index d1682ac5301..3d8d176514c 100644 --- a/apps/web/src/locales/hr/messages.json +++ b/apps/web/src/locales/hr/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB šifriranog prostora za pohranu podataka." }, - "premiumSignUpTwoStep": { - "message": "Dodatne mogućnosti za prijavu dvostrukom autentifikacijom kao što su YubiKey, FIDO U2F i Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Pristup u nuždi" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "Plan dolazi s besplatnom probnom verzijom od 7 dana. Tvoj način plaćanja neće biti terećen dok ne završi probno razdoblje. Možeš otkazati u bilo kojem trenutku." }, diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index ddd4f5872bb..c8c8439626f 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB titkosított tárhely a fájlmellékleteknek." }, - "premiumSignUpTwoStep": { - "message": "További olyan kétlépcsős bejelentkezési opciók mint a YubiKey, FIDO U2F és Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Sürgősségi hozzáférés" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "A fizetési mód minden ki nem fizetett előfizetésért megterhelésre kerül." + }, "paymentChargedWithTrial": { "message": "A jelenlegi csomag 7 napos ingyenes próbaidőszakot tartalmaz. A fizetési módot az időszak végéig nem terheljük. A csomag bármikor lemondható." }, diff --git a/apps/web/src/locales/id/messages.json b/apps/web/src/locales/id/messages.json index 344f13e2df0..a88799f1adc 100644 --- a/apps/web/src/locales/id/messages.json +++ b/apps/web/src/locales/id/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "Penyimpanan terenkripsi 1 GB untuk lampiran file." }, - "premiumSignUpTwoStep": { - "message": "Opsi login dua langkah tambahan seperti YubiKey, FIDO U2F, dan Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Akses darurat" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "Paket Anda dilengkapi dengan uji coba gratis selama 7 hari. Metode pembayaran Anda tidak akan ditagih hingga uji coba berakhir. Penagihan akan dilakukan secara berulang setiap $INTERVAL$. Anda dapat membatalkannya kapan saja." }, diff --git a/apps/web/src/locales/it/messages.json b/apps/web/src/locales/it/messages.json index 4900c68ae7c..c082b3dd9ca 100644 --- a/apps/web/src/locales/it/messages.json +++ b/apps/web/src/locales/it/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB di spazio di archiviazione criptato per gli allegati." }, - "premiumSignUpTwoStep": { - "message": "Più opzioni di verifica in due passaggi come YubiKey, FIDO U2F, e Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Accesso di emergenza" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Eventuali abbonamenti non pagati saranno addebitati sul tuo metodo di pagamento." + }, "paymentChargedWithTrial": { "message": "Il tuo piano include una prova gratis di 7 giorni. Il tuo metodo di pagamento non sarà addebitato fino alla fine del periodo di prova. Puoi cancellarlo in qualsiasi momento." }, @@ -7160,7 +7163,7 @@ "message": "Accesso effettuato!" }, "smBetaEndedDesc": { - "message": "La beta del Gestore dei Segreti è terminata in $BETA_ENDING_DATE$. Ti rimangono $DAYS$ giorni per aggiungere il Gestore dei Segreti al tuo abbonamento a pagamento e mantenere l'accesso ai dati del Gestore dei Segreti. Contatta il Successo del Cliente per aggiungere il Gestore dei Segreti al tuo abbonamento.", + "message": "La beta del Gestore dei Segreti è terminata in $BETA_ENDING_DATE$. Ti rimangono $DAYS$ giorni per aggiungere il Gestore dei Segreti al tuo abbonamento a pagamento e mantenere l'accesso ai dati del Gestore dei Segreti. Contatta l'assistenza per aggiungere il Gestore dei Segreti al tuo abbonamento.", "placeholders": { "beta_ending_date": { "content": "$1", diff --git a/apps/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index 7a6c9c17439..95647694572 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1GB の暗号化されたファイルストレージ" }, - "premiumSignUpTwoStep": { - "message": "YubiKey、FIDO U2F、Duoなどの追加の2段階認証ログインオプション" + "premiumSignUpTwoStepOptions": { + "message": "YubiKey、Duo などのプロプライエタリな2段階認証オプション。" }, "premiumSignUpEmergency": { "message": "緊急アクセス" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "未払いのサブスクリプションについては、指定した支払い方法へ請求されます。" + }, "paymentChargedWithTrial": { "message": "ご利用のプランでは、7日間の無料トライアルが可能です。トライアル期間が終わるまでは課金されません。トライアル終了後、$INTERVAL$毎に請求されます。いつでもキャンセルできます。" }, diff --git a/apps/web/src/locales/ka/messages.json b/apps/web/src/locales/ka/messages.json index b0b9f6bd139..5f461f4b70b 100644 --- a/apps/web/src/locales/ka/messages.json +++ b/apps/web/src/locales/ka/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Emergency access" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "Your plan comes with a free 7 day trial. Your payment method will not be charged until the trial has ended. You may cancel at any time." }, diff --git a/apps/web/src/locales/km/messages.json b/apps/web/src/locales/km/messages.json index e416094e183..a71551e7710 100644 --- a/apps/web/src/locales/km/messages.json +++ b/apps/web/src/locales/km/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Emergency access" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "Your plan comes with a free 7 day trial. Your payment method will not be charged until the trial has ended. You may cancel at any time." }, diff --git a/apps/web/src/locales/kn/messages.json b/apps/web/src/locales/kn/messages.json index d22c69e97e2..94145e99c35 100644 --- a/apps/web/src/locales/kn/messages.json +++ b/apps/web/src/locales/kn/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "ಫೈಲ್ ಲಗತ್ತುಗಳಿಗಾಗಿ 1 ಜಿಬಿ ಎನ್‌ಕ್ರಿಪ್ಟ್ ಮಾಡಿದ ಸಂಗ್ರಹ." }, - "premiumSignUpTwoStep": { - "message": "ಹೆಚ್ಚುವರಿ ಎರಡು-ಹಂತದ ಲಾಗಿನ್ ಆಯ್ಕೆಗಳಾದ ಯೂಬಿಕೆ, ಎಫ್‌ಐಡಿಒ ಯು 2 ಎಫ್, ಮತ್ತು ಡ್ಯುವೋ." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "ತುರ್ತು ಪ್ರವೇಶ" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "ನಿಮ್ಮ ಯೋಜನೆ 7 ದಿನಗಳ ಉಚಿತ ಪ್ರಯೋಗದೊಂದಿಗೆ ಬರುತ್ತದೆ. ಪ್ರಯೋಗ ಮುಗಿಯುವವರೆಗೆ ನಿಮ್ಮ ಪಾವತಿ ವಿಧಾನಕ್ಕೆ ಶುಲ್ಕ ವಿಧಿಸಲಾಗುವುದಿಲ್ಲ. ಪ್ರತಿ $INTERVAL$ ಮರುಕಳಿಸುವ ಆಧಾರದ ಮೇಲೆ ಬಿಲ್ಲಿಂಗ್ ಸಂಭವಿಸುತ್ತದೆ. ನೀವು ಯಾವುದೇ ಸಮಯದಲ್ಲಿ ರದ್ದುಗೊಳಿಸಬಹುದು." }, diff --git a/apps/web/src/locales/ko/messages.json b/apps/web/src/locales/ko/messages.json index bd8ee6b5db8..9900cfa742e 100644 --- a/apps/web/src/locales/ko/messages.json +++ b/apps/web/src/locales/ko/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1GB의 암호화된 파일 저장소." }, - "premiumSignUpTwoStep": { - "message": "YubiKey나 FIDO U2F, Duo 등의 추가적인 2단계 인증 옵션." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "긴급 접근" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "귀하의 플랜은 7일 무료 평가판입니다. 평가 기간이 만료될 때까지 카드에서 대금이 지불되지 않습니다. 이후 정기적으로 매 $INTERVAL$ 청구됩니다. 언제든지 취소할 수 있습니다." }, diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index d2984de94bc..a1f9a5bc382 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB šifrētas krātuves datņu pielikumiem." }, - "premiumSignUpTwoStep": { - "message": "Tādas papildu divpakāpju pieteikšanās iespējas kā YubiKey, FIDO U2F un Duo." + "premiumSignUpTwoStepOptions": { + "message": "Tādas slēgtā pirmavota divpakāpju pieteikšanās iespējas kā YubiKey un Duo." }, "premiumSignUpEmergency": { "message": "Ārkārtas piekļuve" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Izvēlētais maksājumu veids tiks izmantots jebkuru neapmaksātu abonementu apmaksai." + }, "paymentChargedWithTrial": { "message": "Pašreizējā plānā ir iekļauts bezmaksas 7 dienu izmēģinājuma laiks. Izvēlētais apmaksas veids netiks izmantots līdz izmēģinājuma beigā. Norēķini notiks katru $INTERVAL$. To var atcelt jebkurā brīdī." }, diff --git a/apps/web/src/locales/ml/messages.json b/apps/web/src/locales/ml/messages.json index 483a78d6491..1623cf8c2f4 100644 --- a/apps/web/src/locales/ml/messages.json +++ b/apps/web/src/locales/ml/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "ഫയൽ അറ്റാച്ചുമെന്റുകൾക്കായി 1 GB എൻക്രിപ്റ്റുചെയ്‌ത സ്റ്റോറേജ്." }, - "premiumSignUpTwoStep": { - "message": "രണ്ട്-ഘട്ട പ്രവേശന ഓപ്ഷനുകളായ Yubikey, FIDO U2F, Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Emergency access" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "Your plan comes with a free 7 day trial. Your payment method will not be charged until the trial has ended. Billing will occur on a recurring basis each $INTERVAL$. You may cancel at any time." }, diff --git a/apps/web/src/locales/mr/messages.json b/apps/web/src/locales/mr/messages.json index e416094e183..a71551e7710 100644 --- a/apps/web/src/locales/mr/messages.json +++ b/apps/web/src/locales/mr/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Emergency access" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "Your plan comes with a free 7 day trial. Your payment method will not be charged until the trial has ended. You may cancel at any time." }, diff --git a/apps/web/src/locales/my/messages.json b/apps/web/src/locales/my/messages.json index e416094e183..a71551e7710 100644 --- a/apps/web/src/locales/my/messages.json +++ b/apps/web/src/locales/my/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Emergency access" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "Your plan comes with a free 7 day trial. Your payment method will not be charged until the trial has ended. You may cancel at any time." }, diff --git a/apps/web/src/locales/nb/messages.json b/apps/web/src/locales/nb/messages.json index 4f996710ba8..5d0b7f5eafd 100644 --- a/apps/web/src/locales/nb/messages.json +++ b/apps/web/src/locales/nb/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB med kryptert fillagring." }, - "premiumSignUpTwoStep": { - "message": "Ytterligere 2-trinnsinnloggingsmuligheter, slik som YubiKey, FIDO U2F, og Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Nødtilgang" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "Funksjonsplanen din kommer med en gratis 7-dagersprøveperiode. Din betalingsmetode vil ikke bli trekt før prøveperiode har utløpt. Regningstrekk vil skje på en gjentakende basis hver(t) $INTERVAL$. Du kan avbryte når som helst." }, diff --git a/apps/web/src/locales/ne/messages.json b/apps/web/src/locales/ne/messages.json index c28f306acb3..4a8c2e06c0c 100644 --- a/apps/web/src/locales/ne/messages.json +++ b/apps/web/src/locales/ne/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Emergency access" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "Your plan comes with a free 7 day trial. Your payment method will not be charged until the trial has ended. You may cancel at any time." }, diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index 6b12ef13058..dcead7cd8fd 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB versleutelde opslag voor bijlagen." }, - "premiumSignUpTwoStep": { - "message": "Extra tweestapsaanmeldingsopties zoals YubiKey, FIDO U2F en Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Noodtoegang" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "We brengen onbetaalde abonnementen in rekening bij je betalingsmethode." + }, "paymentChargedWithTrial": { "message": "Je lidmaatschap omvat een gratis proefperiode van 7 dagen. Kosten worden pas in rekening gebracht als de proefperiode voorbij is. De betaling vindt ieder(e) $INTERVAL$ op terugkerende basis plaats. Je kunt op ieder moment opzeggen." }, diff --git a/apps/web/src/locales/nn/messages.json b/apps/web/src/locales/nn/messages.json index 81ccc76c41c..22e4a7d30e1 100644 --- a/apps/web/src/locales/nn/messages.json +++ b/apps/web/src/locales/nn/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Emergency access" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "Your plan comes with a free 7 day trial. Your payment method will not be charged until the trial has ended. You may cancel at any time." }, diff --git a/apps/web/src/locales/or/messages.json b/apps/web/src/locales/or/messages.json index e416094e183..a71551e7710 100644 --- a/apps/web/src/locales/or/messages.json +++ b/apps/web/src/locales/or/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Emergency access" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "Your plan comes with a free 7 day trial. Your payment method will not be charged until the trial has ended. You may cancel at any time." }, diff --git a/apps/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index 757d09a97e5..80e7dd695d9 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB przestrzeni na zaszyfrowane załączniki." }, - "premiumSignUpTwoStep": { - "message": "Dodatkowe opcje logowania dwustopniowego, takie jak klucze YubiKey, FIDO U2F oraz Duo." + "premiumSignUpTwoStepOptions": { + "message": "Własnościowe opcje logowania dwuetapowego, takie jak YubiKey i Duo." }, "premiumSignUpEmergency": { "message": "Dostęp awaryjny" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Twoja metoda płatności zostanie obciążona opłatą za wszystkie nieopłacone subskrypcje." + }, "paymentChargedWithTrial": { "message": "Twój plan zawiera 7-dniowy okres próbny. W tym czasie nie poniesiesz żadnych kosztów. Możesz zrezygnować z niego w każdej chwili." }, diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index 2b9a1844de8..3d931f81d07 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB de armazenamento de arquivos encriptados." }, - "premiumSignUpTwoStep": { - "message": "Opções adicionais de login em duas etapas, como YubiKey, FIDO U2F e Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Acesso de Emergência" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Seu método de pagamento será cobrado por qualquer assinatura não paga." + }, "paymentChargedWithTrial": { "message": "Seu plano vem com um teste gratuito de 7 dias. Seu cartão não será cobrado até que o período de teste termine e de forma recorrente a cada $INTERVAL$. Você pode cancelar a qualquer momento." }, diff --git a/apps/web/src/locales/pt_PT/messages.json b/apps/web/src/locales/pt_PT/messages.json index fe115a0ff91..0bf7e684824 100644 --- a/apps/web/src/locales/pt_PT/messages.json +++ b/apps/web/src/locales/pt_PT/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB de armazenamento encriptado para anexos de ficheiros." }, - "premiumSignUpTwoStep": { - "message": "Opções adicionais de verificação de dois passos, como YubiKey, FIDO U2F e Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Acesso de emergência" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "O seu método de pagamento será cobrado por quaisquer subscrições não pagas." + }, "paymentChargedWithTrial": { "message": "O seu plano inclui um teste gratuito de 7 dias. O seu método de pagamento não será cobrado até ao fim do período de avaliação. Pode cancelar a qualquer momento." }, diff --git a/apps/web/src/locales/ro/messages.json b/apps/web/src/locales/ro/messages.json index 118c3570ecc..27e9d4c17b7 100644 --- a/apps/web/src/locales/ro/messages.json +++ b/apps/web/src/locales/ro/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB stocare criptată pentru fișiere atașate." }, - "premiumSignUpTwoStep": { - "message": "Opțiuni suplimentare de conectare în două etape, cum ar fi YubiKey, FIDO U2F și Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Acces de urgență" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "Planul dvs. vine cu o încercare gratuită de 7 zile. Metoda dvs. de plată nu va fi facturată până la sfârșitul perioadei de încercare. Puteți anula în orice moment." }, diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index 55b6724ad61..af9a088993f 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 ГБ зашифрованного хранилища для вложенных файлов." }, - "premiumSignUpTwoStep": { - "message": "Дополнительные варианты двухэтапной аутентификации, такие как YubiKey, FIDO U2F и Duo." + "premiumSignUpTwoStepOptions": { + "message": "Проприетарные варианты двухэтапной аутентификации, такие как YubiKey или Duo." }, "premiumSignUpEmergency": { "message": "Экстренный доступ" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "За любые неоплаченные подписки будет взиматься плата с вашего способа оплаты." + }, "paymentChargedWithTrial": { "message": "Ваш план включает семидневную бесплатную пробную версию. Ваш метод оплаты не будет использован до окончания пробной версии. Оплата будет выполняться каждый $INTERVAL$. Вы можете отказаться в любое время." }, diff --git a/apps/web/src/locales/si/messages.json b/apps/web/src/locales/si/messages.json index 855a48deda9..adff63eb2fc 100644 --- a/apps/web/src/locales/si/messages.json +++ b/apps/web/src/locales/si/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Emergency access" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "Your plan comes with a free 7 day trial. Your payment method will not be charged until the trial has ended. You may cancel at any time." }, diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index 0ab0af5a3e6..fd1d6d707bf 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB šifrovaného úložiska pre prílohy." }, - "premiumSignUpTwoStep": { - "message": "Ďalšie možnosti dvojstupňového prihlásenia ako YubiKey, FIDO U2F a Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Núdzový prístup" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Všetky nezaplatené predplatné budú účtované prostredníctvom vášho spôsobu platby." + }, "paymentChargedWithTrial": { "message": "Váš plán ponúka 7-dňovú skúšobnú dobu zadarmo. Z vašej platobnej metódy nebude stiahnutý poplatok, kým sa neskončí skúšobná doba. Plán môžete kedykoľvek zrušiť." }, diff --git a/apps/web/src/locales/sl/messages.json b/apps/web/src/locales/sl/messages.json index 1cb26cd5d8e..c3f3e479f1f 100644 --- a/apps/web/src/locales/sl/messages.json +++ b/apps/web/src/locales/sl/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Dostop v sili" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "Your plan comes with a free 7 day trial. Your payment method will not be charged until the trial has ended. You may cancel at any time." }, diff --git a/apps/web/src/locales/sr/messages.json b/apps/web/src/locales/sr/messages.json index 3683cf2b20c..f1ae3e829c1 100644 --- a/apps/web/src/locales/sr/messages.json +++ b/apps/web/src/locales/sr/messages.json @@ -955,7 +955,7 @@ "message": "Копирај верификациони код" }, "copyUuid": { - "message": "Copy UUID" + "message": "Копирај UUID" }, "warning": { "message": "Упозорење" @@ -1365,7 +1365,7 @@ "message": "Прикажи иконе сајтова" }, "faviconDesc": { - "message": "Прикажи препознатљиву слику поред сваке ставке за пријаву." + "message": "Прикажи препознатљиву иконицу поред сваке ставке за пријаву." }, "enableFullWidth": { "message": "Упали пуни ширину распореда", @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1ГБ шифровано складиште за прилоге." }, - "premiumSignUpTwoStep": { - "message": "Додатне опције пријаве у два корака као што су YubiKey, FIDO U2F, и Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Улаз у хитним случајевима" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "Ваш план долази са бесплатним 7-дневним пробним периодом. Начин плаћања неће бити наплаћен док се пробно време не заврши. Наплата ће се вршити периодично, сваки $INTERVAL$. Можете отказати било када." }, @@ -3537,7 +3540,7 @@ "message": "Изаберите када ће сеф истећи и да изврши одабрану радњу." }, "vaultTimeoutLogoutDesc": { - "message": "Choose when your vault will be logged out." + "message": "Одаберите када ће ваш сеф бити одјављен." }, "oneMinute": { "message": "1 минут" @@ -4936,7 +4939,7 @@ "message": "Онемогућите извоз личног сефа" }, "disablePersonalVaultExportDescription": { - "message": "Do not allow members to export data from their individual vault." + "message": "Не дозволи члановима да извозе податке из свог индивидуалног сефа." }, "vaultExportDisabled": { "message": "Извоз сефа онемогућен" @@ -5440,7 +5443,7 @@ "message": "Извоз сефа организације" }, "exportingIndividualVaultDescription": { - "message": "Only the individual vault items associated with $EMAIL$ will be exported. Organization vault items will not be included. Only vault item information will be exported and will not include associated attachments.", + "message": "Само појединачне ставке сефа повезане са $EMAIL$ ће бити извењене. Ставке организационог сефа неће бити укључене. Само информације о ставкама из сефа ће бити извезене и неће укључивати повезане прилоге.", "placeholders": { "email": { "content": "$1", @@ -6839,58 +6842,58 @@ "message": "Ажурирати KDF подешавања" }, "loginInitiated": { - "message": "Login initiated" + "message": "Пријава је покренута" }, "deviceApprovalRequired": { - "message": "Device approval required. Select an approval option below:" + "message": "Потребно је одобрење уређаја. Изаберите опцију одобрења испод:" }, "rememberThisDevice": { - "message": "Remember this device" + "message": "Запамти овај уређај" }, "uncheckIfPublicDevice": { - "message": "Uncheck if using a public device" + "message": "Искључите ако се користи јавни уређај" }, "approveFromYourOtherDevice": { - "message": "Approve from your other device" + "message": "Одобри са мојим другим уређајем" }, "requestAdminApproval": { - "message": "Request admin approval" + "message": "Затражити одобрење администратора" }, "approveWithMasterPassword": { - "message": "Approve with master password" + "message": "Одобрити са главном лозинком" }, "trustedDeviceEncryption": { - "message": "Trusted device encryption" + "message": "Шифровање поузданог уређаја" }, "trustedDevices": { "message": "Поуздани уређаји" }, "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", + "message": "Када се аутентификују, чланови ће дешифровати податке из сефљ користећи кључ сачуван на њиховом уређају", "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": "јединствена организација", "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": { - "message": "policy,", + "message": "полиса,", "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": "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": { - "message": "policy, and", + "message": "полиса, и", "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": "администрација опоравка налога", "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": "полисе са аутоматским уписом ће се укључити када се користи ова опција.", "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.'" }, "notFound": { @@ -6981,7 +6984,7 @@ "message": "Уклањање чланова који немају главну лозинку без постављања једне за њих може ограничити приступ њиховом пуном налогу." }, "approvedAuthRequest": { - "message": "Approved device for $ID$.", + "message": "Одобрен уређај за $ID$.", "placeholders": { "id": { "content": "$1", @@ -6990,7 +6993,7 @@ } }, "rejectedAuthRequest": { - "message": "Denied device for $ID$.", + "message": "Одбијен уређај за $ID$.", "placeholders": { "id": { "content": "$1", @@ -6999,7 +7002,7 @@ } }, "requestedDeviceApproval": { - "message": "Requested device approval." + "message": "Затражено је одобрење уређаја." }, "startYour7DayFreeTrialOfBitwardenFor": { "message": "Започните своју 7-дневну бесплатну пробну Bitwarden-а за $ORG$", @@ -7023,28 +7026,28 @@ "message": "Одабрана застава" }, "accountSuccessfullyCreated": { - "message": "Account successfully created!" + "message": "Налог је успешно креиран!" }, "adminApprovalRequested": { - "message": "Admin approval requested" + "message": "Захтевано је одобрење администратора" }, "adminApprovalRequestSentToAdmins": { - "message": "Your request has been sent to your admin." + "message": "Ваш захтев је послат вашем администратору." }, "youWillBeNotifiedOnceApproved": { - "message": "You will be notified once approved." + "message": "Бићете обавештени када буде одобрено." }, "troubleLoggingIn": { - "message": "Trouble logging in?" + "message": "Имате проблема са пријављивањем?" }, "loginApproved": { - "message": "Login approved" + "message": "Пријава је одобрена" }, "userEmailMissing": { - "message": "User email missing" + "message": "Недостаје имејл корисника" }, "deviceTrusted": { - "message": "Device trusted" + "message": "Уређај поуздан" }, "sendsNoItemsTitle": { "message": "Нема активних Sends", @@ -7070,13 +7073,13 @@ "message": "For engineering and DevOps teams to manage secrets throughout the software development lifecycle." }, "free2PersonOrganization": { - "message": "Free 2-person Organizations" + "message": "Бесплатна организација за 2 особе" }, "unlimitedSecrets": { - "message": "Unlimited secrets" + "message": "Неограничене тајне" }, "unlimitedProjects": { - "message": "Unlimited projects" + "message": "Неограничени пројекти" }, "projectsIncluded": { "message": "$COUNT$ projects included", @@ -7106,13 +7109,13 @@ } }, "addSecretsManager": { - "message": "Add Secrets Manager" + "message": "Додати Менаџер Тајни" }, "addSecretsManagerUpgradeDesc": { "message": "Add Secrets Manager to your upgraded plan to maintain access to any secrets created with your previous plan." }, "additionalServiceAccounts": { - "message": "Additional service accounts" + "message": "Додатни сервисни налози" }, "includedServiceAccounts": { "message": "Ваш план долази са $COUNT$ налога сервиса.", @@ -7133,16 +7136,16 @@ } }, "passwordManagerPlanPrice": { - "message": "Password Manager plan price" + "message": "Цена плана менаџера лозинки" }, "secretsManagerPlanPrice": { - "message": "Secrets Manager plan price" + "message": "Цена плана менаџера тајни" }, "passwordManager": { "message": "Менаџер лозинки" }, "freeOrganization": { - "message": "Free Organization" + "message": "Бесплатна организација" }, "limitServiceAccounts": { "message": "Limit service accounts (optional)" @@ -7157,7 +7160,7 @@ "message": "Max potential service account cost" }, "loggedInExclamation": { - "message": "Logged in!" + "message": "Пријављено!" }, "smBetaEndedDesc": { "message": "Бета менаџера тајни се завршио $BETA_ENDING_DATE$. Остало вам је $DAYS$ дана да додате Менаџер тајни у вашу претплату и да задржите приступ подацима Манагера тајни. Контактирајте подршку да додате Менаџер тајни у своју претплату.", @@ -7179,6 +7182,6 @@ "message": "Бета" }, "alreadyHaveAccount": { - "message": "Already have an account?" + "message": "Већ имате налог?" } } diff --git a/apps/web/src/locales/sr_CS/messages.json b/apps/web/src/locales/sr_CS/messages.json index 3daeae10af7..a50306355db 100644 --- a/apps/web/src/locales/sr_CS/messages.json +++ b/apps/web/src/locales/sr_CS/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Emergency access" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "Your plan comes with a free 7 day trial. Your payment method will not be charged until the trial has ended. You may cancel at any time." }, diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index df8d315d6d7..7d2e12239f7 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB krypterad lagring." }, - "premiumSignUpTwoStep": { - "message": "Ytterligare alternativ för tvåstegsverifiering såsom YubiKey, FIDO U2F och Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Nödåtkomst" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Din betalningsmetod kommer att debiteras för eventuella obetalda prenumerationer." + }, "paymentChargedWithTrial": { "message": "Din plan kommer med en kostnadsfri 7-dagars provperiod. Din betalningsmetod kommer inte att debiteras förrän provperioden har upphört. Du kan avbryta när som helst." }, diff --git a/apps/web/src/locales/te/messages.json b/apps/web/src/locales/te/messages.json index e416094e183..a71551e7710 100644 --- a/apps/web/src/locales/te/messages.json +++ b/apps/web/src/locales/te/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Emergency access" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "Your plan comes with a free 7 day trial. Your payment method will not be charged until the trial has ended. You may cancel at any time." }, diff --git a/apps/web/src/locales/th/messages.json b/apps/web/src/locales/th/messages.json index 7173b6d8d90..25206db2e37 100644 --- a/apps/web/src/locales/th/messages.json +++ b/apps/web/src/locales/th/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Emergency access" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "Your plan comes with a free 7 day trial. Your payment method will not be charged until the trial has ended. You may cancel at any time." }, diff --git a/apps/web/src/locales/tr/messages.json b/apps/web/src/locales/tr/messages.json index 9111d52fce2..55b2c51548e 100644 --- a/apps/web/src/locales/tr/messages.json +++ b/apps/web/src/locales/tr/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "Dosya ekleri için 1 GB şifrelenmiş depolama." }, - "premiumSignUpTwoStep": { - "message": "YubiKey, FIDO U2F ve Duo gibi iki aşamalı giriş seçenekleri." + "premiumSignUpTwoStepOptions": { + "message": "YubiKey ve Duo gibi marka bazlı iki aşamalı giriş seçenekleri." }, "premiumSignUpEmergency": { "message": "Acil durum erişimi" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "Paketiniz 7 günlük ücretsiz deneme süresiyle geliyor. Deneme süresi bitene kadar sizden ücret alınmayacak. İstediğiniz zaman aboneliğinizi iptal edebilirsiniz." }, diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index f3e5519cd76..4c336c0b30d 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 ГБ зашифрованого сховища для файлів." }, - "premiumSignUpTwoStep": { - "message": "Додаткові можливості двоетапної перевірки, наприклад, YubiKey, FIDO U2F та Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Екстрений доступ" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "Ваш тарифний план має 7 днів безплатного пробного періоду. З вас не буде стягнуто плату до завершення цього періоду. Ви можете скасувати це в будь-який час." }, diff --git a/apps/web/src/locales/vi/messages.json b/apps/web/src/locales/vi/messages.json index 2095ec36cb1..fb25525d61a 100644 --- a/apps/web/src/locales/vi/messages.json +++ b/apps/web/src/locales/vi/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, - "premiumSignUpTwoStep": { - "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "Emergency access" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "Gói của bạn đi kèm với 7 ngày dùng thử miễn phí. Phương thức thanh toán của bạn sẽ không bị tính phí cho đến khi hết thời gian dùng thử. Việc thanh toán sẽ thực hiện định kỳ mỗi $INTERVAL$. Bạn có thể hủy bỏ bất cứ lúc nào." }, diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index f38192eb899..be07ab7e208 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -606,7 +606,7 @@ "message": "登录或者创建一个账户来访问您的安全密码库。" }, "loginWithDevice": { - "message": "设备登录" + "message": "使用设备登录" }, "loginWithDeviceEnabledNote": { "message": "设备登录必须在 Bitwarden 应用程序的设置中设启用。需要其他选项吗?" @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "1 GB 文件附件加密存储。" }, - "premiumSignUpTwoStep": { - "message": "额外的两步登录选项,如 YubiKey、FIDO U2F 和 Duo。" + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "紧急访问" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "任何未付费订阅都将通过您的付款方式收取费用。" + }, "paymentChargedWithTrial": { "message": "您的计划包含了 7 天的免费试用期。在试用期结束前,不会从您的付款方式中扣款。您可以随时取消。" }, @@ -2448,7 +2451,7 @@ "message": "组织已创建" }, "organizationReadyToGo": { - "message": "你的组织准备好了!" + "message": "您的新组织已准备就绪!" }, "organizationUpgraded": { "message": "组织已升级" @@ -6936,7 +6939,7 @@ "message": "设备信息" }, "time": { - "message": "Time" + "message": "时间" }, "denyAllRequests": { "message": "拒绝所有请求" diff --git a/apps/web/src/locales/zh_TW/messages.json b/apps/web/src/locales/zh_TW/messages.json index c988b11a946..acdd1aa1a79 100644 --- a/apps/web/src/locales/zh_TW/messages.json +++ b/apps/web/src/locales/zh_TW/messages.json @@ -1924,8 +1924,8 @@ "premiumSignUpStorage": { "message": "用於檔案附件的 1 GB 的加密檔案儲存空間。" }, - "premiumSignUpTwoStep": { - "message": "YubiKey、FIDO U2F 和 Duo 等額外的兩步驟登入選項。" + "premiumSignUpTwoStepOptions": { + "message": "Proprietary two-step login options such as YubiKey and Duo." }, "premiumSignUpEmergency": { "message": "緊急存取" @@ -2040,6 +2040,9 @@ } } }, + "paymentChargedWithUnpaidSubscription": { + "message": "Your payment method will be charged for any unpaid subscriptions." + }, "paymentChargedWithTrial": { "message": "您的方案包含了 7 天的免費試用。在試用期結束之前,不會從您的付款方式中扣款。您可以隨時取消。" }, From bf7aa6473ee8e0a2d5e9055c3b4ec21571f306d0 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Fri, 1 Sep 2023 13:18:20 -0700 Subject: [PATCH 15/46] [PM-1509] Accessibility for elements (#5686) * change code color to meet accessibility requirements * updates to desktop and web * adjust colors for desktop, web, and browser * update color values * switch nord color to use same as Tailwind theme * align variable names --- apps/browser/src/popup/scss/variables.scss | 12 ++++++++---- apps/desktop/src/scss/variables.scss | 8 +++++--- apps/web/src/scss/variables.scss | 6 ++++-- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/apps/browser/src/popup/scss/variables.scss b/apps/browser/src/popup/scss/variables.scss index 2cdc49cd9ef..d8891cf620b 100644 --- a/apps/browser/src/popup/scss/variables.scss +++ b/apps/browser/src/popup/scss/variables.scss @@ -43,6 +43,10 @@ $button-color: lighten($text-color, 40%); $button-color-primary: darken($brand-primary, 8%); $button-color-danger: darken($brand-danger, 10%); +$code-color: #c01176; +$code-color-dark: #f08dc7; +$code-color-nord: #dbb1d5; + $solarizedDarkBase03: #002b36; $solarizedDarkBase02: #073642; $solarizedDarkBase01: #586e75; @@ -122,7 +126,7 @@ $themes: ( // light has no hover so use same color webkitCalendarPickerHoverFilter: invert(46%) sepia(69%) saturate(6397%) hue-rotate(211deg) brightness(85%) contrast(103%), - codeColor: #e83e8c, + codeColor: $code-color, ), dark: ( textColor: #ffffff, @@ -184,7 +188,7 @@ $themes: ( hue-rotate(184deg) brightness(87%) contrast(93%), webkitCalendarPickerHoverFilter: brightness(0) saturate(100%) invert(100%) sepia(0%) saturate(0%) hue-rotate(93deg) brightness(103%) contrast(103%), - codeColor: #e83e8c, + codeColor: $code-color-dark, ), nord: ( textColor: $nord5, @@ -246,7 +250,7 @@ $themes: ( // has no hover so use same color webkitCalendarPickerHoverFilter: brightness(0) saturate(100%) invert(94%) sepia(5%) saturate(454%) hue-rotate(185deg) brightness(93%) contrast(96%), - codeColor: #e83e8c, + codeColor: $code-color-nord, ), solarizedDark: ( textColor: $solarizedDarkBase2, @@ -307,7 +311,7 @@ $themes: ( hue-rotate(138deg) brightness(92%) contrast(90%), webkitCalendarPickerHoverFilter: brightness(0) saturate(100%) invert(94%) sepia(10%) saturate(462%) hue-rotate(345deg) brightness(103%) contrast(87%), - codeColor: #e83e8c, + codeColor: $code-color-dark, ), ); diff --git a/apps/desktop/src/scss/variables.scss b/apps/desktop/src/scss/variables.scss index 3ad4c0f0754..e4a2f124768 100644 --- a/apps/desktop/src/scss/variables.scss +++ b/apps/desktop/src/scss/variables.scss @@ -41,7 +41,9 @@ $button-color: lighten($text-color, 40%); $button-color-primary: darken($brand-primary, 8%); $button-color-danger: darken($brand-danger, 10%); -$code-color: #e83e8c; +$code-color: #c01176; +$code-color-dark: #f08dc7; +$code-color-nord: #dbb1d5; $themes: ( light: ( @@ -158,7 +160,7 @@ $themes: ( accountSwitcherTextColor: #ffffff, svgSuffix: "-dark.svg", hrColor: #bac0ce, - codeColor: $code-color, + codeColor: $code-color-dark, ), nord: ( textColor: $nord5, @@ -216,7 +218,7 @@ $themes: ( accountSwitcherTextColor: $nord5, svgSuffix: "-dark.svg", hrColor: $nord4, - codeColor: $code-color, + codeColor: $code-color-nord, ), ); diff --git a/apps/web/src/scss/variables.scss b/apps/web/src/scss/variables.scss index 719f403e385..af61daff512 100644 --- a/apps/web/src/scss/variables.scss +++ b/apps/web/src/scss/variables.scss @@ -88,6 +88,7 @@ $mfaTypes: 0, 2, 3, 4, 6; $lightDangerHover: #c43421; $lightInputColor: #465057; $lightInputPlaceholderColor: #b6b8b8; +$lightCodeColor: #c01176; // Dark @@ -107,6 +108,7 @@ $darkDarkBlue1: #2f343d; $darkDarkBlue2: #1f242e; $darkInputColor: $white; $darkInputPlaceholderColor: $darkGrey1; +$darkCodeColor: #f08dc7; $themes: ( light: ( @@ -167,7 +169,7 @@ $themes: ( calloutBackground: #fafafa, calloutColor: #212529, cdkDraggingBackground: $white, - codeColor: #e83e8c, + codeColor: $lightCodeColor, dropdownBackground: $white, dropdownHover: rgba(0, 0, 0, 0.06), dropdownTextColor: $body-color, @@ -276,7 +278,7 @@ $themes: ( calloutBackground: $darkBlue2, calloutColor: $white, cdkDraggingBackground: $darkDarkBlue1, - codeColor: #e83e8c, + codeColor: $darkCodeColor, dropdownBackground: $darkDarkBlue1, dropdownHover: rgba(255, 255, 255, 0.03), dropdownTextColor: $white, From a920d62dfeb3f339a366097376c8ef52d2360471 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pereira?= Date: Mon, 4 Sep 2023 21:01:16 +0100 Subject: [PATCH 16/46] [PM-3326] [CLI] Add minNumber, minSpecial and ambiguous password generation options (#5974) * feat(cli): add minNumber option and pass to generation service * feat(cli): add minSpecial option and pass to generation service * feat(cli): add ambiguous option and pass to generation service * refactor: extract utils to convert number and string options * feat(ts): add types to utils and options * feat: validate result of parsed value in convertNumberOption util --- apps/cli/src/program.ts | 3 +++ apps/cli/src/tools/generate.command.ts | 18 ++++++++++++++---- apps/cli/src/utils.ts | 16 ++++++++++++++++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/apps/cli/src/program.ts b/apps/cli/src/program.ts index 9eca236a3a0..8bca024b410 100644 --- a/apps/cli/src/program.ts +++ b/apps/cli/src/program.ts @@ -298,9 +298,12 @@ export class Program { .option("-p, --passphrase", "Generate a passphrase.") .option("--length ", "Length of the password.") .option("--words ", "Number of words.") + .option("--minNumber ", "Minimum number of numeric characters.") + .option("--minSpecial ", "Minimum number of special characters.") .option("--separator ", "Word separator.") .option("-c, --capitalize", "Title case passphrase.") .option("--includeNumber", "Passphrase includes number.") + .option("--ambiguous", "Avoid ambiguous characters.") .on("--help", () => { writeLn("\n Notes:"); writeLn(""); diff --git a/apps/cli/src/tools/generate.command.ts b/apps/cli/src/tools/generate.command.ts index bd9ad88a04f..30436e7db71 100644 --- a/apps/cli/src/tools/generate.command.ts +++ b/apps/cli/src/tools/generate.command.ts @@ -1,5 +1,6 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; +import { PasswordGeneratorOptions } from "@bitwarden/common/tools/generator/password/password-generator-options"; import { Response } from "../models/response"; import { StringResponse } from "../models/response/string.response"; @@ -13,7 +14,7 @@ export class GenerateCommand { async run(cmdOptions: Record): Promise { const normalizedOptions = new Options(cmdOptions); - const options = { + const options: PasswordGeneratorOptions = { uppercase: normalizedOptions.uppercase, lowercase: normalizedOptions.lowercase, number: normalizedOptions.number, @@ -24,6 +25,9 @@ export class GenerateCommand { numWords: normalizedOptions.words, capitalize: normalizedOptions.capitalize, includeNumber: normalizedOptions.includeNumber, + minNumber: normalizedOptions.minNumber, + minSpecial: normalizedOptions.minSpecial, + ambiguous: normalizedOptions.ambiguous, }; const enforcedOptions = (await this.stateService.getIsAuthenticated()) @@ -47,6 +51,9 @@ class Options { words: number; capitalize: boolean; includeNumber: boolean; + minNumber: number; + minSpecial: number; + ambiguous: boolean; constructor(passedOptions: Record) { this.uppercase = CliUtils.convertBooleanOption(passedOptions?.uppercase); @@ -55,10 +62,13 @@ class Options { this.special = CliUtils.convertBooleanOption(passedOptions?.special); this.capitalize = CliUtils.convertBooleanOption(passedOptions?.capitalize); this.includeNumber = CliUtils.convertBooleanOption(passedOptions?.includeNumber); - this.length = passedOptions?.length != null ? parseInt(passedOptions?.length, null) : 14; + this.ambiguous = CliUtils.convertBooleanOption(passedOptions?.ambiguous); + this.length = CliUtils.convertNumberOption(passedOptions?.length, 14); this.type = passedOptions?.passphrase ? "passphrase" : "password"; - this.separator = passedOptions?.separator == null ? "-" : passedOptions.separator + ""; - this.words = passedOptions?.words != null ? parseInt(passedOptions.words, null) : 3; + this.separator = CliUtils.convertStringOption(passedOptions?.separator, "-"); + this.words = CliUtils.convertNumberOption(passedOptions?.words, 3); + this.minNumber = CliUtils.convertNumberOption(passedOptions?.minNumber, 1); + this.minSpecial = CliUtils.convertNumberOption(passedOptions?.minSpecial, 1); if (!this.uppercase && !this.lowercase && !this.special && !this.number) { this.lowercase = true; diff --git a/apps/cli/src/utils.ts b/apps/cli/src/utils.ts index f8780dbec63..5d77f6d3730 100644 --- a/apps/cli/src/utils.ts +++ b/apps/cli/src/utils.ts @@ -253,4 +253,20 @@ export class CliUtils { static convertBooleanOption(optionValue: any) { return optionValue || optionValue === "" ? true : false; } + + static convertNumberOption(optionValue: any, defaultValue: number) { + try { + if (optionValue != null) { + const numVal = parseInt(optionValue); + return !Number.isNaN(numVal) ? numVal : defaultValue; + } + return defaultValue; + } catch { + return defaultValue; + } + } + + static convertStringOption(optionValue: any, defaultValue: string) { + return optionValue != null ? String(optionValue) : defaultValue; + } } From 182d5bf5ace604f93f17032b580efa399e3b862d Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Mon, 4 Sep 2023 22:07:14 -0400 Subject: [PATCH 17/46] [PM-3758] Handle user decryption options from pre-TDE server response (#6180) * Mapped pre-TDE server response to UserDecryptionOptions. * Updated logic on SsoLoginStrategy to match account. * Linting. * Adjusted tests. * Fixed tests. --- .../login-strategies/login.strategy.spec.ts | 11 +--- .../auth/login-strategies/login.strategy.ts | 4 +- .../sso-login.strategy.spec.ts | 56 ++++++++++++++++++- .../login-strategies/sso-login.strategy.ts | 24 +++++--- .../src/platform/models/domain/account.ts | 50 +++++++++++------ 5 files changed, 107 insertions(+), 38 deletions(-) diff --git a/libs/common/src/auth/login-strategies/login.strategy.spec.ts b/libs/common/src/auth/login-strategies/login.strategy.spec.ts index f7128b35dfb..735135f4061 100644 --- a/libs/common/src/auth/login-strategies/login.strategy.spec.ts +++ b/libs/common/src/auth/login-strategies/login.strategy.spec.ts @@ -41,10 +41,7 @@ import { IdentityCaptchaResponse } from "../models/response/identity-captcha.res import { IdentityTokenResponse } from "../models/response/identity-token.response"; import { IdentityTwoFactorResponse } from "../models/response/identity-two-factor.response"; import { MasterPasswordPolicyResponse } from "../models/response/master-password-policy.response"; -import { - IUserDecryptionOptionsServerResponse, - UserDecryptionOptionsResponse, -} from "../models/response/user-decryption-options/user-decryption-options.response"; +import { IUserDecryptionOptionsServerResponse } from "../models/response/user-decryption-options/user-decryption-options.response"; import { PasswordLogInStrategy } from "./password-login.strategy"; @@ -65,10 +62,6 @@ const name = "NAME"; const defaultUserDecryptionOptionsServerResponse: IUserDecryptionOptionsServerResponse = { HasMasterPassword: true, }; -const userDecryptionOptions = new UserDecryptionOptionsResponse( - defaultUserDecryptionOptionsServerResponse -); -const acctDecryptionOptions = AccountDecryptionOptions.fromResponse(userDecryptionOptions); const decodedToken = { sub: userId, @@ -197,7 +190,7 @@ describe("LogInStrategy", () => { }, }, keys: new AccountKeys(), - decryptionOptions: acctDecryptionOptions, + decryptionOptions: AccountDecryptionOptions.fromResponse(idTokenResponse), }) ); expect(messagingService.send).toHaveBeenCalledWith("loggedIn"); diff --git a/libs/common/src/auth/login-strategies/login.strategy.ts b/libs/common/src/auth/login-strategies/login.strategy.ts index 7bc83580164..6e51f215012 100644 --- a/libs/common/src/auth/login-strategies/login.strategy.ts +++ b/libs/common/src/auth/login-strategies/login.strategy.ts @@ -143,9 +143,7 @@ export abstract class LogInStrategy { }, }, keys: accountKeys, - decryptionOptions: AccountDecryptionOptions.fromResponse( - tokenResponse.userDecryptionOptions - ), + decryptionOptions: AccountDecryptionOptions.fromResponse(tokenResponse), adminAuthRequest: adminAuthRequest?.toJSON(), }) ); diff --git a/libs/common/src/auth/login-strategies/sso-login.strategy.spec.ts b/libs/common/src/auth/login-strategies/sso-login.strategy.spec.ts index 099a3a02a2b..f078a7b86b1 100644 --- a/libs/common/src/auth/login-strategies/sso-login.strategy.spec.ts +++ b/libs/common/src/auth/login-strategies/sso-login.strategy.spec.ts @@ -266,7 +266,61 @@ describe("SsoLogInStrategy", () => { describe("Key Connector", () => { let tokenResponse: IdentityTokenResponse; beforeEach(() => { - tokenResponse = identityTokenResponseFactory(null, { HasMasterPassword: false }); + tokenResponse = identityTokenResponseFactory(null, { + HasMasterPassword: false, + KeyConnectorOption: { KeyConnectorUrl: keyConnectorUrl }, + }); + tokenResponse.keyConnectorUrl = keyConnectorUrl; + }); + + it("gets and sets the master key if Key Connector is enabled and the user doesn't have a master password", async () => { + const masterKey = new SymmetricCryptoKey( + new Uint8Array(64).buffer as CsprngArray + ) as MasterKey; + + apiService.postIdentityToken.mockResolvedValue(tokenResponse); + cryptoService.getMasterKey.mockResolvedValue(masterKey); + + await ssoLogInStrategy.logIn(credentials); + + expect(keyConnectorService.setMasterKeyFromUrl).toHaveBeenCalledWith(keyConnectorUrl); + }); + + it("converts new SSO user with no master password to Key Connector on first login", async () => { + tokenResponse.key = null; + + apiService.postIdentityToken.mockResolvedValue(tokenResponse); + + await ssoLogInStrategy.logIn(credentials); + + expect(keyConnectorService.convertNewSsoUserToKeyConnector).toHaveBeenCalledWith( + tokenResponse, + ssoOrgId + ); + }); + + it("decrypts and sets the user key if Key Connector is enabled and the user doesn't have a master password", async () => { + const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey; + const masterKey = new SymmetricCryptoKey( + new Uint8Array(64).buffer as CsprngArray + ) as MasterKey; + + apiService.postIdentityToken.mockResolvedValue(tokenResponse); + cryptoService.getMasterKey.mockResolvedValue(masterKey); + cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); + + await ssoLogInStrategy.logIn(credentials); + + expect(cryptoService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(masterKey); + expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey); + }); + }); + + describe("Key Connector Pre-TDE", () => { + let tokenResponse: IdentityTokenResponse; + beforeEach(() => { + tokenResponse = identityTokenResponseFactory(); + tokenResponse.userDecryptionOptions = null; tokenResponse.keyConnectorUrl = keyConnectorUrl; }); diff --git a/libs/common/src/auth/login-strategies/sso-login.strategy.ts b/libs/common/src/auth/login-strategies/sso-login.strategy.ts index 3e9a7e33f33..09dbca72fea 100644 --- a/libs/common/src/auth/login-strategies/sso-login.strategy.ts +++ b/libs/common/src/auth/login-strategies/sso-login.strategy.ts @@ -101,16 +101,22 @@ export class SsoLogInStrategy extends LogInStrategy { private shouldSetMasterKeyFromKeyConnector(tokenResponse: IdentityTokenResponse): boolean { const userDecryptionOptions = tokenResponse?.userDecryptionOptions; - // If the user has a master password, this means that they need to migrate to Key Connector, so we won't set the key here. - // We default to false here because old server versions won't have hasMasterPassword and in that case we want to rely solely on the keyConnectorUrl. - // TODO: remove null default after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3537) - const userHasMasterPassword = userDecryptionOptions?.hasMasterPassword ?? false; + if (userDecryptionOptions != null) { + const userHasMasterPassword = userDecryptionOptions.hasMasterPassword; + const userHasKeyConnectorUrl = + userDecryptionOptions.keyConnectorOption?.keyConnectorUrl != null; - const keyConnectorUrl = this.getKeyConnectorUrl(tokenResponse); - - // In order for us to set the master key from Key Connector, we need to have a Key Connector URL - // and the user must not have a master password. - return keyConnectorUrl != null && !userHasMasterPassword; + // In order for us to set the master key from Key Connector, we need to have a Key Connector URL + // and the user must not have a master password. + return userHasKeyConnectorUrl && !userHasMasterPassword; + } else { + // In pre-TDE versions of the server, the userDecryptionOptions will not be present. + // In this case, we can determine if the user has a master password and has a Key Connector URL by + // just checking the keyConnectorUrl property. This is because the server short-circuits on the response + // and will not pass back the URL in the response if the user has a master password. + // TODO: remove compatibility check after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3537) + return tokenResponse.keyConnectorUrl != null; + } } private getKeyConnectorUrl(tokenResponse: IdentityTokenResponse): string { diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index 95a5a899129..09dc6971dcf 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -11,7 +11,7 @@ import { EnvironmentUrls } from "../../../auth/models/domain/environment-urls"; import { ForceResetPasswordReason } from "../../../auth/models/domain/force-reset-password-reason"; import { KeyConnectorUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/key-connector-user-decryption-option"; import { TrustedDeviceUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/trusted-device-user-decryption-option"; -import { UserDecryptionOptionsResponse } from "../../../auth/models/response/user-decryption-options/user-decryption-options.response"; +import { IdentityTokenResponse } from "../../../auth/models/response/identity-token.response"; import { KdfType, UriMatchType } from "../../../enums"; import { EventData } from "../../../models/data/event.data"; import { GeneratedPasswordHistory } from "../../../tools/generator/password"; @@ -311,28 +311,46 @@ export class AccountDecryptionOptions { // return this.keyConnectorOption !== null && this.keyConnectorOption !== undefined; // } - static fromResponse(response: UserDecryptionOptionsResponse): AccountDecryptionOptions { + static fromResponse(response: IdentityTokenResponse): AccountDecryptionOptions { if (response == null) { return null; } const accountDecryptionOptions = new AccountDecryptionOptions(); - accountDecryptionOptions.hasMasterPassword = response.hasMasterPassword; - if (response.trustedDeviceOption) { - accountDecryptionOptions.trustedDeviceOption = new TrustedDeviceUserDecryptionOption( - response.trustedDeviceOption.hasAdminApproval, - response.trustedDeviceOption.hasLoginApprovingDevice, - response.trustedDeviceOption.hasManageResetPasswordPermission - ); + if (response.userDecryptionOptions) { + // If the response has userDecryptionOptions, this means it's on a post-TDE server version and can interrogate + // the new decryption options. + const responseOptions = response.userDecryptionOptions; + accountDecryptionOptions.hasMasterPassword = responseOptions.hasMasterPassword; + + if (responseOptions.trustedDeviceOption) { + accountDecryptionOptions.trustedDeviceOption = new TrustedDeviceUserDecryptionOption( + responseOptions.trustedDeviceOption.hasAdminApproval, + responseOptions.trustedDeviceOption.hasLoginApprovingDevice, + responseOptions.trustedDeviceOption.hasManageResetPasswordPermission + ); + } + + if (responseOptions.keyConnectorOption) { + accountDecryptionOptions.keyConnectorOption = new KeyConnectorUserDecryptionOption( + responseOptions.keyConnectorOption.keyConnectorUrl + ); + } + } else { + // If the response does not have userDecryptionOptions, this means it's on a pre-TDE server version and so + // we must base our decryption options on the presence of the keyConnectorUrl. + // Note that the presence of keyConnectorUrl implies that the user does not have a master password, as in pre-TDE + // server versions, a master password short-circuited the addition of the keyConnectorUrl to the response. + // TODO: remove this check after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3537) + const usingKeyConnector = response.keyConnectorUrl != null; + accountDecryptionOptions.hasMasterPassword = !usingKeyConnector; + if (usingKeyConnector) { + accountDecryptionOptions.keyConnectorOption = new KeyConnectorUserDecryptionOption( + response.keyConnectorUrl + ); + } } - - if (response.keyConnectorOption) { - accountDecryptionOptions.keyConnectorOption = new KeyConnectorUserDecryptionOption( - response.keyConnectorOption.keyConnectorUrl - ); - } - return accountDecryptionOptions; } From b78d17aa62a07341c3c21bc6ca541eb7510115b8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 5 Sep 2023 10:55:37 -0600 Subject: [PATCH 18/46] Bump Desktop version to 2023.8.4 (#6192) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/package.json | 2 +- apps/desktop/src/package-lock.json | 4 ++-- apps/desktop/src/package.json | 2 +- package-lock.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 82c77156c12..6855485510a 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": "2023.8.3", + "version": "2023.8.4", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index 54af2baea1e..c838b242b50 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2023.8.3", + "version": "2023.8.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2023.8.3", + "version": "2023.8.4", "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 b171046bcef..bbef6bf36f5 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": "2023.8.3", + "version": "2023.8.4", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/package-lock.json b/package-lock.json index 0b364d58ed7..3ce3baf3174 100644 --- a/package-lock.json +++ b/package-lock.json @@ -231,7 +231,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2023.8.3", + "version": "2023.8.4", "hasInstallScript": true, "license": "GPL-3.0" }, From 255a7381b3cd7686d914abde248ea66e1f618e4f Mon Sep 17 00:00:00 2001 From: Daniel James Smith Date: Tue, 5 Sep 2023 21:48:34 +0200 Subject: [PATCH 19/46] [PM-3609] [Tech-Debt] Add types to password and username generator (#6090) * Create and use GeneratorOptions Selection between `password`and `username` * Use PasswordGeneratorOptions * Declare and use UsernameGeneratorOptions --- .../components/generator.component.ts | 17 +++++++++----- .../platform/abstractions/state.service.ts | 22 +++++++++++++------ .../src/platform/models/domain/account.ts | 13 +++++++---- .../src/platform/services/state.service.ts | 22 +++++++++++++------ .../src/tools/generator/generator-options.ts | 3 +++ .../src/tools/generator/password/index.ts | 1 + .../src/tools/generator/username/index.ts | 1 + .../username/username-generation-options.ts | 19 ++++++++++++++++ ...username-generation.service.abstraction.ts | 16 ++++++++------ .../username/username-generation.service.ts | 17 +++++++------- 10 files changed, 93 insertions(+), 38 deletions(-) create mode 100644 libs/common/src/tools/generator/generator-options.ts create mode 100644 libs/common/src/tools/generator/username/username-generation-options.ts diff --git a/libs/angular/src/tools/generator/components/generator.component.ts b/libs/angular/src/tools/generator/components/generator.component.ts index 9ef73186583..37904473ad9 100644 --- a/libs/angular/src/tools/generator/components/generator.component.ts +++ b/libs/angular/src/tools/generator/components/generator.component.ts @@ -8,8 +8,15 @@ 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 { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; -import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username"; +import { GeneratorOptions } from "@bitwarden/common/tools/generator/generator-options"; +import { + PasswordGenerationServiceAbstraction, + PasswordGeneratorOptions, +} from "@bitwarden/common/tools/generator/password"; +import { + UsernameGenerationServiceAbstraction, + UsernameGeneratorOptions, +} from "@bitwarden/common/tools/generator/username"; @Directive() export class GeneratorComponent implements OnInit { @@ -24,8 +31,8 @@ export class GeneratorComponent implements OnInit { subaddressOptions: any[]; catchallOptions: any[]; forwardOptions: EmailForwarderOptions[]; - usernameOptions: any = {}; - passwordOptions: any = {}; + usernameOptions: UsernameGeneratorOptions = {}; + passwordOptions: PasswordGeneratorOptions = {}; username = "-"; password = "-"; showOptions = false; @@ -118,7 +125,7 @@ export class GeneratorComponent implements OnInit { } async typeChanged() { - await this.stateService.setGeneratorOptions({ type: this.type }); + await this.stateService.setGeneratorOptions({ type: this.type } as GeneratorOptions); if (this.regenerateWithoutButtonPress()) { await this.regenerate(); } diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index 82813718de3..571dad6478e 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -13,7 +13,9 @@ import { BiometricKey } from "../../auth/types/biometric-key"; import { KdfType, ThemeType, UriMatchType } from "../../enums"; import { EventData } from "../../models/data/event.data"; import { WindowState } from "../../models/domain/window-state"; -import { GeneratedPasswordHistory } from "../../tools/generator/password"; +import { GeneratorOptions } from "../../tools/generator/generator-options"; +import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password"; +import { UsernameGeneratorOptions } from "../../tools/generator/username"; import { SendData } from "../../tools/send/models/data/send.data"; import { SendView } from "../../tools/send/models/view/send.view"; import { CipherData } from "../../vault/models/data/cipher.data"; @@ -439,12 +441,18 @@ export abstract class StateService { value: { [id: string]: OrganizationData }, options?: StorageOptions ) => Promise; - getPasswordGenerationOptions: (options?: StorageOptions) => Promise; - setPasswordGenerationOptions: (value: any, options?: StorageOptions) => Promise; - getUsernameGenerationOptions: (options?: StorageOptions) => Promise; - setUsernameGenerationOptions: (value: any, options?: StorageOptions) => Promise; - getGeneratorOptions: (options?: StorageOptions) => Promise; - setGeneratorOptions: (value: any, options?: StorageOptions) => Promise; + getPasswordGenerationOptions: (options?: StorageOptions) => Promise; + setPasswordGenerationOptions: ( + value: PasswordGeneratorOptions, + options?: StorageOptions + ) => Promise; + getUsernameGenerationOptions: (options?: StorageOptions) => Promise; + setUsernameGenerationOptions: ( + value: UsernameGeneratorOptions, + options?: StorageOptions + ) => Promise; + getGeneratorOptions: (options?: StorageOptions) => Promise; + setGeneratorOptions: (value: GeneratorOptions, options?: StorageOptions) => Promise; /** * Gets the user's Pin, encrypted by the user key */ diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index 09dc6971dcf..6d85d6501fe 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -14,7 +14,12 @@ import { TrustedDeviceUserDecryptionOption } from "../../../auth/models/domain/u import { IdentityTokenResponse } from "../../../auth/models/response/identity-token.response"; import { KdfType, UriMatchType } from "../../../enums"; import { EventData } from "../../../models/data/event.data"; -import { GeneratedPasswordHistory } from "../../../tools/generator/password"; +import { GeneratorOptions } from "../../../tools/generator/generator-options"; +import { + GeneratedPasswordHistory, + PasswordGeneratorOptions, +} from "../../../tools/generator/password"; +import { UsernameGeneratorOptions } from "../../../tools/generator/username/username-generation-options"; import { SendData } from "../../../tools/send/models/data/send.data"; import { SendView } from "../../../tools/send/models/view/send.view"; import { DeepJsonify } from "../../../types/deep-jsonify"; @@ -235,9 +240,9 @@ export class AccountSettings { equivalentDomains?: any; minimizeOnCopyToClipboard?: boolean; neverDomains?: { [id: string]: any }; - passwordGenerationOptions?: any; - usernameGenerationOptions?: any; - generatorOptions?: any; + passwordGenerationOptions?: PasswordGeneratorOptions; + usernameGenerationOptions?: UsernameGeneratorOptions; + generatorOptions?: GeneratorOptions; pinKeyEncryptedUserKey?: EncryptedString; pinKeyEncryptedUserKeyEphemeral?: EncryptedString; protectedPin?: string; diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 5fdf40e8458..d0983448d62 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -22,7 +22,9 @@ import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; import { EventData } from "../../models/data/event.data"; import { WindowState } from "../../models/domain/window-state"; import { migrate } from "../../state-migrations"; -import { GeneratedPasswordHistory } from "../../tools/generator/password"; +import { GeneratorOptions } from "../../tools/generator/generator-options"; +import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password"; +import { UsernameGeneratorOptions } from "../../tools/generator/username"; import { SendData } from "../../tools/send/models/data/send.data"; import { SendView } from "../../tools/send/models/view/send.view"; import { CipherData } from "../../vault/models/data/cipher.data"; @@ -2367,13 +2369,16 @@ export class StateService< ); } - async getPasswordGenerationOptions(options?: StorageOptions): Promise { + async getPasswordGenerationOptions(options?: StorageOptions): Promise { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) )?.settings?.passwordGenerationOptions; } - async setPasswordGenerationOptions(value: any, options?: StorageOptions): Promise { + async setPasswordGenerationOptions( + value: PasswordGeneratorOptions, + options?: StorageOptions + ): Promise { const account = await this.getAccount( this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()) ); @@ -2384,13 +2389,16 @@ export class StateService< ); } - async getUsernameGenerationOptions(options?: StorageOptions): Promise { + async getUsernameGenerationOptions(options?: StorageOptions): Promise { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) )?.settings?.usernameGenerationOptions; } - async setUsernameGenerationOptions(value: any, options?: StorageOptions): Promise { + async setUsernameGenerationOptions( + value: UsernameGeneratorOptions, + options?: StorageOptions + ): Promise { const account = await this.getAccount( this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()) ); @@ -2401,13 +2409,13 @@ export class StateService< ); } - async getGeneratorOptions(options?: StorageOptions): Promise { + async getGeneratorOptions(options?: StorageOptions): Promise { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) )?.settings?.generatorOptions; } - async setGeneratorOptions(value: any, options?: StorageOptions): Promise { + async setGeneratorOptions(value: GeneratorOptions, options?: StorageOptions): Promise { const account = await this.getAccount( this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()) ); diff --git a/libs/common/src/tools/generator/generator-options.ts b/libs/common/src/tools/generator/generator-options.ts new file mode 100644 index 00000000000..4f8eb293ab5 --- /dev/null +++ b/libs/common/src/tools/generator/generator-options.ts @@ -0,0 +1,3 @@ +export type GeneratorOptions = { + type?: "password" | "username"; +}; diff --git a/libs/common/src/tools/generator/password/index.ts b/libs/common/src/tools/generator/password/index.ts index 4dafe20d3aa..bacc2c0c70d 100644 --- a/libs/common/src/tools/generator/password/index.ts +++ b/libs/common/src/tools/generator/password/index.ts @@ -1,3 +1,4 @@ +export { PasswordGeneratorOptions } from "./password-generator-options"; export { PasswordGenerationServiceAbstraction } from "./password-generation.service.abstraction"; export { PasswordGenerationService } from "./password-generation.service"; export { GeneratedPasswordHistory } from "./generated-password-history"; diff --git a/libs/common/src/tools/generator/username/index.ts b/libs/common/src/tools/generator/username/index.ts index b4f73a29edb..c4197b4344f 100644 --- a/libs/common/src/tools/generator/username/index.ts +++ b/libs/common/src/tools/generator/username/index.ts @@ -1,2 +1,3 @@ +export { UsernameGeneratorOptions } from "./username-generation-options"; export { UsernameGenerationServiceAbstraction } from "./username-generation.service.abstraction"; export { UsernameGenerationService } from "./username-generation.service"; diff --git a/libs/common/src/tools/generator/username/username-generation-options.ts b/libs/common/src/tools/generator/username/username-generation-options.ts new file mode 100644 index 00000000000..96b6e2ef1be --- /dev/null +++ b/libs/common/src/tools/generator/username/username-generation-options.ts @@ -0,0 +1,19 @@ +export type UsernameGeneratorOptions = { + type?: "word" | "subaddress" | "catchall" | "forwarded"; + wordCapitalize?: boolean; + wordIncludeNumber?: boolean; + subaddressType?: "random" | "website-name"; + subaddressEmail?: string; + catchallType?: "random" | "website-name"; + catchallDomain?: string; + website?: string; + forwardedService?: string; + forwardedAnonAddyApiToken?: string; + forwardedAnonAddyDomain?: string; + forwardedDuckDuckGoToken?: string; + forwardedFirefoxApiToken?: string; + forwardedFastmailApiToken?: string; + forwardedForwardEmailApiToken?: string; + forwardedForwardEmailDomain?: string; + forwardedSimpleLoginApiKey?: string; +}; diff --git a/libs/common/src/tools/generator/username/username-generation.service.abstraction.ts b/libs/common/src/tools/generator/username/username-generation.service.abstraction.ts index 52accf7d8ca..05affef0e2f 100644 --- a/libs/common/src/tools/generator/username/username-generation.service.abstraction.ts +++ b/libs/common/src/tools/generator/username/username-generation.service.abstraction.ts @@ -1,9 +1,11 @@ +import { UsernameGeneratorOptions } from "./username-generation-options"; + export abstract class UsernameGenerationServiceAbstraction { - generateUsername: (options: any) => Promise; - generateWord: (options: any) => Promise; - generateSubaddress: (options: any) => Promise; - generateCatchall: (options: any) => Promise; - generateForwarded: (options: any) => Promise; - getOptions: () => Promise; - saveOptions: (options: any) => Promise; + generateUsername: (options: UsernameGeneratorOptions) => Promise; + generateWord: (options: UsernameGeneratorOptions) => Promise; + generateSubaddress: (options: UsernameGeneratorOptions) => Promise; + generateCatchall: (options: UsernameGeneratorOptions) => Promise; + generateForwarded: (options: UsernameGeneratorOptions) => Promise; + getOptions: () => Promise; + saveOptions: (options: UsernameGeneratorOptions) => Promise; } diff --git a/libs/common/src/tools/generator/username/username-generation.service.ts b/libs/common/src/tools/generator/username/username-generation.service.ts index 3ff9884331d..b1fed147db0 100644 --- a/libs/common/src/tools/generator/username/username-generation.service.ts +++ b/libs/common/src/tools/generator/username/username-generation.service.ts @@ -13,9 +13,10 @@ import { ForwarderOptions, SimpleLoginForwarder, } from "./email-forwarders"; +import { UsernameGeneratorOptions } from "./username-generation-options"; import { UsernameGenerationServiceAbstraction } from "./username-generation.service.abstraction"; -const DefaultOptions = { +const DefaultOptions: UsernameGeneratorOptions = { type: "word", wordCapitalize: true, wordIncludeNumber: true, @@ -33,7 +34,7 @@ export class UsernameGenerationService implements UsernameGenerationServiceAbstr private apiService: ApiService ) {} - generateUsername(options: any): Promise { + generateUsername(options: UsernameGeneratorOptions): Promise { if (options.type === "catchall") { return this.generateCatchall(options); } else if (options.type === "subaddress") { @@ -45,7 +46,7 @@ export class UsernameGenerationService implements UsernameGenerationServiceAbstr } } - async generateWord(options: any): Promise { + async generateWord(options: UsernameGeneratorOptions): Promise { const o = Object.assign({}, DefaultOptions, options); if (o.wordCapitalize == null) { @@ -67,7 +68,7 @@ export class UsernameGenerationService implements UsernameGenerationServiceAbstr return word; } - async generateSubaddress(options: any): Promise { + async generateSubaddress(options: UsernameGeneratorOptions): Promise { const o = Object.assign({}, DefaultOptions, options); const subaddressEmail = o.subaddressEmail; @@ -94,7 +95,7 @@ export class UsernameGenerationService implements UsernameGenerationServiceAbstr return emailBeginning + "+" + subaddressString + "@" + emailEnding; } - async generateCatchall(options: any): Promise { + async generateCatchall(options: UsernameGeneratorOptions): Promise { const o = Object.assign({}, DefaultOptions, options); if (o.catchallDomain == null || o.catchallDomain === "") { @@ -113,7 +114,7 @@ export class UsernameGenerationService implements UsernameGenerationServiceAbstr return startString + "@" + o.catchallDomain; } - async generateForwarded(options: any): Promise { + async generateForwarded(options: UsernameGeneratorOptions): Promise { const o = Object.assign({}, DefaultOptions, options); if (o.forwardedService == null) { @@ -152,7 +153,7 @@ export class UsernameGenerationService implements UsernameGenerationServiceAbstr return forwarder.generate(this.apiService, forwarderOptions); } - async getOptions(): Promise { + async getOptions(): Promise { let options = await this.stateService.getUsernameGenerationOptions(); if (options == null) { options = Object.assign({}, DefaultOptions); @@ -163,7 +164,7 @@ export class UsernameGenerationService implements UsernameGenerationServiceAbstr return options; } - async saveOptions(options: any) { + async saveOptions(options: UsernameGeneratorOptions) { await this.stateService.setUsernameGenerationOptions(options); } From d6aa85af6613b93a9823ae432b698f8dfcfdc528 Mon Sep 17 00:00:00 2001 From: Vince Grassia <593223+vgrassia@users.noreply.github.com> Date: Tue, 5 Sep 2023 16:52:03 -0400 Subject: [PATCH 20/46] Update Version Bump workflow inputs (#6143) --- .github/workflows/version-auto-bump.yml | 3 +- .github/workflows/version-bump.yml | 113 ++++++++++++++---------- 2 files changed, 68 insertions(+), 48 deletions(-) diff --git a/.github/workflows/version-auto-bump.yml b/.github/workflows/version-auto-bump.yml index 857099db511..7b1a787d946 100644 --- a/.github/workflows/version-auto-bump.yml +++ b/.github/workflows/version-auto-bump.yml @@ -44,4 +44,5 @@ jobs: uses: ./.github/workflows/version-bump.yml with: version_number: ${{ needs.setup.outputs.version_number }} - client: "Desktop" + bump_desktop: true + secrets: inherit diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml index 420ef456ec0..563facdb40c 100644 --- a/.github/workflows/version-bump.yml +++ b/.github/workflows/version-bump.yml @@ -4,16 +4,22 @@ name: Version Bump on: workflow_dispatch: inputs: - client: - description: "Client Project" - required: true - type: choice - options: - - Browser - - CLI - - Desktop - - Web - - All + bump_browser: + description: "Browser Project Version Bump" + type: boolean + default: false + bump_cli: + description: "CLI Project Version Bump" + type: boolean + default: false + bump_desktop: + description: "Desktop Project Version Bump" + type: boolean + default: false + bump_web: + description: "Web Project Version Bump" + type: boolean + default: false version_number: description: "New Version" required: true @@ -23,9 +29,10 @@ on: version_number: required: true type: string - client: - required: true - type: string + bump_desktop: + description: "Desktop Project Version Bump" + type: boolean + default: false defaults: run: @@ -33,8 +40,8 @@ defaults: jobs: bump_version: - name: "Bump ${{ github.event.inputs.client }} Version" - runs-on: ubuntu-20.04 + name: "Bump Version" + runs-on: ubuntu-22.04 steps: - name: Checkout Branch uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 @@ -42,7 +49,7 @@ jobs: - name: Login to Azure - Prod Subscription uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7 with: - creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - name: Retrieve secrets id: retrieve-secrets @@ -62,13 +69,27 @@ jobs: - name: Create Version Branch id: branch env: - CLIENT_NAME: ${{ github.event.inputs.client }} - VERSION: ${{ github.event.inputs.version_number }} + VERSION: ${{ inputs.version_number }} run: | - CLIENT=$(python -c "print('$CLIENT_NAME'.lower())") - echo "client=$CLIENT" >> $GITHUB_OUTPUT + CLIENTS=() + if [[ ${{ inputs.bump_browser }} == true ]]; then + CLIENTS+=("browser") + fi + if [[ ${{ inputs.bump_cli }} == true ]]; then + CLIENTS+=("cli") + fi + if [[ ${{ inputs.bump_desktop }} == true ]]; then + CLIENTS+=("desktop") + fi + if [[ ${{ inputs.bump_web }} == true ]]; then + CLIENTS+=("web") + fi + printf -v joined '%s,' "${CLIENTS[@]}" + echo "client=${joined%,}" >> $GITHUB_OUTPUT - git switch -c ${CLIENT}_version_bump_${VERSION} + BRANCH=version_bump_${VERSION}_${GITHUB_SHA:0:7} + echo "branch=$BRANCH" >> $GITHUB_OUTPUT + git switch -c ${BRANCH} ######################## # VERSION BUMP SECTION # @@ -76,27 +97,27 @@ jobs: ### Browser - name: Bump Browser Version - if: ${{ github.event.inputs.client == 'Browser' || github.event.inputs.client == 'All' }} + if: ${{ inputs.bump_browser == true }} env: - VERSION: ${{ github.event.inputs.version_number }} + VERSION: ${{ inputs.version_number }} run: npm version --workspace=@bitwarden/browser ${VERSION} - name: Bump Browser Version - Manifest - if: ${{ github.event.inputs.client == 'Browser' || github.event.inputs.client == 'All' }} + if: ${{ inputs.bump_browser == true }} uses: bitwarden/gh-actions/version-bump@67ab95d7a466bcefdedf3f93cbc10bcff436edfe with: - version: ${{ github.event.inputs.version_number }} + version: ${{ inputs.version_number }} file_path: "apps/browser/src/manifest.json" - name: Bump Browser Version - Manifest v3 - if: ${{ github.event.inputs.client == 'Browser' || github.event.inputs.client == 'All' }} + if: ${{ inputs.bump_browser == true }} uses: bitwarden/gh-actions/version-bump@67ab95d7a466bcefdedf3f93cbc10bcff436edfe with: - version: ${{ github.event.inputs.version_number }} + version: ${{ inputs.version_number }} file_path: "apps/browser/src/manifest.v3.json" - name: Run Prettier after Browser Version Bump - if: ${{ github.event.inputs.client == 'Browser' || github.event.inputs.client == 'All' }} + if: ${{ inputs.bump_browser == true }} run: | npm install -g prettier prettier --write apps/browser/src/manifest.json @@ -104,30 +125,30 @@ jobs: ### CLI - name: Bump CLI Version - if: ${{ github.event.inputs.client == 'CLI' || github.event.inputs.client == 'All' }} + if: ${{ inputs.bump_cli == true }} env: - VERSION: ${{ github.event.inputs.version_number }} + VERSION: ${{ inputs.version_number }} run: npm version --workspace=@bitwarden/cli ${VERSION} ### Desktop - name: Bump Desktop Version - Root - if: ${{ github.event.inputs.client == 'Desktop' || github.event.inputs.client == 'All' }} + if: ${{ inputs.bump_desktop == true }} env: - VERSION: ${{ github.event.inputs.version_number }} + VERSION: ${{ inputs.version_number }} run: npm version --workspace=@bitwarden/desktop ${VERSION} - name: Bump Desktop Version - App - if: ${{ github.event.inputs.client == 'Desktop' || github.event.inputs.client == 'All' }} + if: ${{ inputs.bump_desktop == true }} env: - VERSION: ${{ github.event.inputs.version_number }} + VERSION: ${{ inputs.version_number }} run: npm version ${VERSION} working-directory: "apps/desktop/src" ### Web - name: Bump Web Version - if: ${{ github.event.inputs.client == 'Web' || github.event.inputs.client == 'All' }} + if: ${{ inputs.bump_web == true }} env: - VERSION: ${{ github.event.inputs.version_number }} + VERSION: ${{ inputs.version_number }} run: npm version --workspace=@bitwarden/web-vault ${VERSION} ######################## @@ -151,27 +172,26 @@ jobs: if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} env: CLIENT: ${{ steps.branch.outputs.client }} - VERSION: ${{ github.event.inputs.version_number }} + VERSION: ${{ inputs.version_number }} run: git commit -m "Bumped ${CLIENT} version to ${VERSION}" -a - name: Push changes if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} env: - CLIENT: ${{ steps.branch.outputs.client }} - VERSION: ${{ github.event.inputs.version_number }} - run: git push -u origin ${CLIENT}_version_bump_${VERSION} + BRANCH: ${{ steps.branch.outputs.branch }} + run: git push -u origin ${BRANCH} - name: Create Bump Version PR if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} env: - PR_BRANCH: "${{ steps.branch.outputs.client }}_version_bump_${{ github.event.inputs.version_number }}" - GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" BASE_BRANCH: master - TITLE: "Bump ${{ github.event.inputs.client }} version to ${{ github.event.inputs.version_number }}" + BRANCH: ${{ steps.branch.outputs.branch }} + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + TITLE: "Bump ${{ steps.branch.outputs.client }} version to ${{ inputs.version_number }}" run: | gh pr create --title "$TITLE" \ - --base "$BASE" \ - --head "$PR_BRANCH" \ + --base "$BASE_BRANCH" \ + --head "$BRANCH" \ --label "version update" \ --label "automated pr" \ --body " @@ -183,5 +203,4 @@ jobs: - [X] Other ## Objective - Automated ${{ github.event.inputs.client }} version bump to ${{ github.event.inputs.version_number }}" - + Automated ${{ steps.branch.outputs.client }} version bump to ${{ inputs.version_number }}" From 1bd1127b61b5c1b784bc3c955cef6b9b889f97b6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 5 Sep 2023 18:44:46 -0400 Subject: [PATCH 21/46] Bumped browser,web version to 2023.8.3 (#6197) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/package.json | 2 +- apps/browser/src/manifest.json | 2 +- apps/browser/src/manifest.v3.json | 2 +- apps/web/package.json | 2 +- package-lock.json | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/browser/package.json b/apps/browser/package.json index 2e866653cd3..980eb3258a2 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2023.8.2", + "version": "2023.8.3", "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 7e471c39201..1e7b3a3139f 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": "2023.8.2", + "version": "2023.8.3", "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 321fdb0beb4..f10ac7f3824 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": "2023.8.2", + "version": "2023.8.3", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/web/package.json b/apps/web/package.json index 1624e0d491d..d3c4d286f08 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/web-vault", - "version": "2023.8.2", + "version": "2023.8.3", "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 3ce3baf3174..f1bb3b7d84a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -191,7 +191,7 @@ }, "apps/browser": { "name": "@bitwarden/browser", - "version": "2023.8.2" + "version": "2023.8.3" }, "apps/cli": { "name": "@bitwarden/cli", @@ -261,7 +261,7 @@ }, "apps/web": { "name": "@bitwarden/web-vault", - "version": "2023.8.2" + "version": "2023.8.3" }, "libs/angular": { "name": "@bitwarden/angular", From 864818c2d30165c6ad5f4cf1e19ca56ace1c5b22 Mon Sep 17 00:00:00 2001 From: Vince Grassia <593223+vgrassia@users.noreply.github.com> Date: Wed, 6 Sep 2023 09:51:40 -0400 Subject: [PATCH 22/46] Browser Build/Release Workflows - Change runners to linux (#6193) --- .github/workflows/build-browser.yml | 44 +++++++++++++++------------ .github/workflows/release-browser.yml | 8 ++--- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index 412f166629e..05b89d66c33 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -38,7 +38,7 @@ defaults: jobs: cloc: name: CLOC - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Checkout repo uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 @@ -54,7 +54,7 @@ jobs: setup: name: Setup - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 outputs: repo_url: ${{ steps.gen_vars.outputs.repo_url }} adj_build_number: ${{ steps.gen_vars.outputs.adj_build_number }} @@ -71,7 +71,7 @@ jobs: locales-test: name: Locales Test - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 needs: - setup defaults: @@ -108,7 +108,7 @@ jobs: build: name: Build - runs-on: windows-2019 + runs-on: ubuntu-22.04 needs: - setup - locales-test @@ -137,6 +137,7 @@ jobs: run: | node --version npm --version + node-gyp --version - name: NPM setup run: npm ci @@ -152,24 +153,27 @@ jobs: run: gulp ci - name: Build sources for reviewers - shell: cmd run: | - REM Remove ".git" directory - rmdir /S /Q ".git" + # Include hidden files in glob copy + shopt -s dotglob - REM Copy root level files to source directory + # Remove ".git" directory + rm -r .git + + # Copy root level files to source directory mkdir browser-source - copy * browser-source + FILES=$(find . -maxdepth 1 -type f) + for FILE in $FILES; do cp "$FILE" browser-source/; done - REM Copy apps\browser to Browser source directory - mkdir browser-source\apps\browser - xcopy apps\browser\* browser-source\apps\browser /E + # Copy apps/browser to Browser source directory + mkdir -p browser-source/apps/browser + cp -r apps/browser/* browser-source/apps/browser - REM Copy libs to Browser source directory - mkdir browser-source\libs - xcopy libs\* browser-source\libs /E + # Copy libs to Browser source directory + mkdir browser-source/libs + cp -r libs/* browser-source/libs - call 7z a browser-source.zip "browser-source\*" + zip -r browser-source.zip browser-source working-directory: ./ - name: Upload Opera artifact @@ -339,7 +343,7 @@ jobs: crowdin-push: name: Crowdin Push if: github.ref == 'refs/heads/master' - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 needs: - build - build-safari @@ -354,7 +358,7 @@ jobs: - name: Retrieve secrets id: retrieve-secrets - uses: bitwarden/gh-actions/get-keyvault-secrets@67ab95d7a466bcefdedf3f93cbc10bcff436edfe + uses: bitwarden/gh-actions/get-keyvault-secrets@37ffa14164a7308bc273829edfe75c97cd562375 with: keyvault: "bitwarden-ci" secrets: "crowdin-api-token" @@ -374,7 +378,7 @@ jobs: check-failures: name: Check for failures if: always() - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 needs: - cloc - setup @@ -416,7 +420,7 @@ jobs: - name: Retrieve secrets id: retrieve-secrets if: failure() - uses: bitwarden/gh-actions/get-keyvault-secrets@67ab95d7a466bcefdedf3f93cbc10bcff436edfe + uses: bitwarden/gh-actions/get-keyvault-secrets@37ffa14164a7308bc273829edfe75c97cd562375 with: keyvault: "bitwarden-ci" secrets: "devops-alerts-slack-webhook-url" diff --git a/.github/workflows/release-browser.yml b/.github/workflows/release-browser.yml index 20f3f7efac5..407f81deb60 100644 --- a/.github/workflows/release-browser.yml +++ b/.github/workflows/release-browser.yml @@ -22,7 +22,7 @@ defaults: jobs: setup: name: Setup - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 outputs: release-version: ${{ steps.version.outputs.version }} steps: @@ -41,7 +41,7 @@ jobs: - name: Check Release Version id: version - uses: bitwarden/gh-actions/release-version-check@67ab95d7a466bcefdedf3f93cbc10bcff436edfe + uses: bitwarden/gh-actions/release-version-check@58a2fdfbd3f1fc7e6727bc5dc51d159f4df07072 with: release-type: ${{ github.event.inputs.release_type }} project-type: ts @@ -52,7 +52,7 @@ jobs: locales-test: name: Locales Test - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 needs: setup steps: - name: Checkout repo @@ -86,7 +86,7 @@ jobs: release: name: Create GitHub Release - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 needs: - setup - locales-test From e8a5c5b337da1e70cc094011a651d88552182276 Mon Sep 17 00:00:00 2001 From: Daniel James Smith Date: Wed, 6 Sep 2023 16:12:14 +0200 Subject: [PATCH 23/46] [PM-3586] Fix short MP not showing minLength (#6086) * Fix short MP not showing minLength Added path to include the mininum password length defined as const in our Utils * Introduce previousMinimumPasswordLength and use a minLength for MP * Rename previousMinimumPasswordLength to originalMinimumPasswordLength --- libs/angular/src/auth/components/login.component.ts | 7 ++++++- libs/common/src/platform/misc/utils.ts | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/libs/angular/src/auth/components/login.component.ts b/libs/angular/src/auth/components/login.component.ts index 33b3dc2364e..63e8dcc3721 100644 --- a/libs/angular/src/auth/components/login.component.ts +++ b/libs/angular/src/auth/components/login.component.ts @@ -42,7 +42,10 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit formGroup = this.formBuilder.group({ email: ["", [Validators.required, Validators.email]], - masterPassword: ["", [Validators.required, Validators.minLength(8)]], + masterPassword: [ + "", + [Validators.required, Validators.minLength(Utils.originalMinimumPasswordLength)], + ], rememberEmail: [false], }); @@ -278,6 +281,8 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit switch (error.errorName) { case "email": return this.i18nService.t("invalidEmail"); + case "minlength": + return this.i18nService.t("masterPasswordMinlength", Utils.originalMinimumPasswordLength); default: return this.i18nService.t(this.errorTag(error)); } diff --git a/libs/common/src/platform/misc/utils.ts b/libs/common/src/platform/misc/utils.ts index cd1b5fe33aa..6711e78c3b7 100644 --- a/libs/common/src/platform/misc/utils.ts +++ b/libs/common/src/platform/misc/utils.ts @@ -32,6 +32,7 @@ export class Utils { static regexpEmojiPresentation = /(?:[\u231A\u231B\u23E9-\u23EC\u23F0\u23F3\u25FD\u25FE\u2614\u2615\u2648-\u2653\u267F\u2693\u26A1\u26AA\u26AB\u26BD\u26BE\u26C4\u26C5\u26CE\u26D4\u26EA\u26F2\u26F3\u26F5\u26FA\u26FD\u2705\u270A\u270B\u2728\u274C\u274E\u2753-\u2755\u2757\u2795-\u2797\u27B0\u27BF\u2B1B\u2B1C\u2B50\u2B55]|\uD83C[\uDC04\uDCCF\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF7C\uDF7E-\uDF93\uDFA0-\uDFCA\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF4\uDFF8-\uDFFF]|\uD83D[\uDC00-\uDC3E\uDC40\uDC42-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDD7A\uDD95\uDD96\uDDA4\uDDFB-\uDE4F\uDE80-\uDEC5\uDECC\uDED0-\uDED2\uDED5-\uDED7\uDEEB\uDEEC\uDEF4-\uDEFC\uDFE0-\uDFEB]|\uD83E[\uDD0C-\uDD3A\uDD3C-\uDD45\uDD47-\uDD78\uDD7A-\uDDCB\uDDCD-\uDDFF\uDE70-\uDE74\uDE78-\uDE7A\uDE80-\uDE86\uDE90-\uDEA8\uDEB0-\uDEB6\uDEC0-\uDEC2\uDED0-\uDED6])/g; static readonly validHosts: string[] = ["localhost"]; + static readonly originalMinimumPasswordLength = 8; static readonly minimumPasswordLength = 12; static readonly DomainMatchBlacklist = new Map>([ ["google.com", new Set(["script.google.com"])], From 6eb57ff312bac54aa827d6aec0f384a3e781f19f Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Wed, 6 Sep 2023 12:05:24 -0700 Subject: [PATCH 24/46] add route and params to link (#6103) --- .../environment-selector.component.html | 8 ++++++-- .../environment-selector.component.ts | 7 ++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/components/environment-selector/environment-selector.component.html b/apps/web/src/app/components/environment-selector/environment-selector.component.html index 2984d6a6a3b..5a5bada82df 100644 --- a/apps/web/src/app/components/environment-selector/environment-selector.component.html +++ b/apps/web/src/app/components/environment-selector/environment-selector.component.html @@ -2,7 +2,9 @@ diff --git a/apps/web/src/app/components/environment-selector/environment-selector.component.ts b/apps/web/src/app/components/environment-selector/environment-selector.component.ts index f9c06bbdd3d..2530a7b3642 100644 --- a/apps/web/src/app/components/environment-selector/environment-selector.component.ts +++ b/apps/web/src/app/components/environment-selector/environment-selector.component.ts @@ -1,4 +1,5 @@ import { Component, Input, OnInit } from "@angular/core"; +import { Router } from "@angular/router"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; @@ -13,14 +14,17 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; export class EnvironmentSelectorComponent implements OnInit { constructor( private configService: ConfigServiceAbstraction, - private platformUtilsService: PlatformUtilsService + private platformUtilsService: PlatformUtilsService, + private router: Router ) {} + @Input() hasFlags: boolean; isEuServer: boolean; isUsServer: boolean; showRegionSelector = false; euServerFlagEnabled: boolean; selectedRegionImageName: string; + routeAndParams: string; async ngOnInit() { this.euServerFlagEnabled = await this.configService.getFeatureFlagBool( @@ -31,6 +35,7 @@ export class EnvironmentSelectorComponent implements OnInit { this.isUsServer = domain.includes(RegionDomain.US) || domain.includes(RegionDomain.USQA); this.selectedRegionImageName = this.getRegionImage(); this.showRegionSelector = !this.platformUtilsService.isSelfHost(); + this.routeAndParams = `/#${this.router.url}`; } getRegionImage(): string { From 6f82a9914ba9e8935f7d55865269b44f18afee9c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 6 Sep 2023 15:56:35 -0400 Subject: [PATCH 25/46] Bumped web version to 2023.8.4 (#6206) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/web/package.json | 2 +- package-lock.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index d3c4d286f08..08698b75a39 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/web-vault", - "version": "2023.8.3", + "version": "2023.8.4", "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 f1bb3b7d84a..525fbb0a183 100644 --- a/package-lock.json +++ b/package-lock.json @@ -261,7 +261,7 @@ }, "apps/web": { "name": "@bitwarden/web-vault", - "version": "2023.8.3" + "version": "2023.8.4" }, "libs/angular": { "name": "@bitwarden/angular", From 86bdfaa7bac2d62e2d61ef7a4fc06edc1ae0fae5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Thu, 7 Sep 2023 10:41:59 +0100 Subject: [PATCH 26/46] [AC-1612] Disabled access to the Organization Vault tab if the user only has access to assigned collections (#6140) * [AC-1612] Disabled access to the Organization Vault tab if the user only has access to assigned collections * [AC-1612] Fixed issue that prevented Manager users to access the Organizations tab --- libs/common/src/admin-console/models/domain/organization.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/common/src/admin-console/models/domain/organization.ts b/libs/common/src/admin-console/models/domain/organization.ts index e1e5e8a6e2c..bd3c9036367 100644 --- a/libs/common/src/admin-console/models/domain/organization.ts +++ b/libs/common/src/admin-console/models/domain/organization.ts @@ -174,7 +174,7 @@ export class Organization { } get canViewAllCollections() { - return this.canCreateNewCollections || this.canEditAnyCollection || this.canDeleteAnyCollection; + return this.canEditAnyCollection || this.canDeleteAnyCollection; } get canEditAssignedCollections() { From 5f78aeaef2bf5ebe70b5d9a33863c1a4b6e87fe9 Mon Sep 17 00:00:00 2001 From: aj-rosado <109146700+aj-rosado@users.noreply.github.com> Date: Thu, 7 Sep 2023 13:49:13 +0100 Subject: [PATCH 27/46] [PM-2805] Migrate add edit send to Component Library (#6004) * Converted add-edit send component dialog into a bit-dialog * Updated Send AddEdit text fields to Component Library * Migrated Share and Options fields to ComponentLibrary on SendAddEdit * Migrated footer buttons to ComponentLibrary on SendAddEdit * Updated web's SendAddEdit component file fields * Replaced file upload with component library * Changed SendAddEdit to use Reactive Forms on web * Changed browser SendAddEdit to use ReactiveForms * Update SendAddEdit on desktop to use ReactiveForms * Added AppA11yTitle to button on web SendAddEdit * Initial efflux-dates web change to ComponentLibrary * Corrected delete button to check if it is in EditMode on SendAddEdit * Using BitLink on options button * Corrected typo on send add edit desktop * Replaced efflux-dates with datetime-local input on SendAddEdit web, browser and desktop * Removed efflux dates * Added firefox custom date popout message on DeletionDate to SendAddEdit browser component * moved desktop's new send data reload from send to SendAddEdit component * removing unnecessary attributes and spans from Send AddEdit web * removed redundant try catch from add edit and unnecessary parameter from close * Added type for date select options * Removed unnecessary classes and swapped bootstrap classes by corresponding tailwind classes * Removed unnecessary code * Added file as required field Submit only closes popup on success * Added pre validations at start of submit * PM-3668 removed expiration date from required * PM-3671 not defaulting maximum access count to 0 * PM-3669 Copying the link from link method * Removed required tag from html and added to formgroup * PM-3679 Checking if is not EditMode before validating if FormGroup file value is set * PM-3691 Moved error validation to web component as browser and desktop need to show popup error * PM-3696 - Disabling hide email when it is unset and has policy to not allow hiding * PM-3694 - Properly setting default value for dates on Desktop when changing from an existing send * Disabling hidden required fields * [PM-3800] Clearing password on new send --- apps/browser/src/popup/app.module.ts | 2 - .../popup/send/efflux-dates.component.html | 217 ------- .../popup/send/efflux-dates.component.ts | 25 - .../popup/send/send-add-edit.component.html | 210 +++++-- .../popup/send/send-add-edit.component.ts | 7 +- apps/desktop/src/app/app.module.ts | 2 - .../app/tools/send/add-edit.component.html | 158 ++--- .../src/app/tools/send/add-edit.component.ts | 15 +- .../tools/send/efflux-dates.component.html | 62 -- .../app/tools/send/efflux-dates.component.ts | 38 -- .../src/app/tools/send/send.component.ts | 6 +- .../src/app/shared/loose-components.module.ts | 3 - .../app/tools/send/add-edit.component.html | 557 +++++++++--------- .../src/app/tools/send/add-edit.component.ts | 40 +- .../tools/send/efflux-dates.component.html | 188 ------ .../app/tools/send/efflux-dates.component.ts | 22 - apps/web/src/app/tools/send/send.component.ts | 29 +- .../src/tools/send/add-edit.component.ts | 224 ++++++- .../src/tools/send/efflux-dates.component.ts | 356 ----------- .../src/form-field/form-field-control.ts | 4 +- 20 files changed, 777 insertions(+), 1388 deletions(-) delete mode 100644 apps/browser/src/tools/popup/send/efflux-dates.component.html delete mode 100644 apps/browser/src/tools/popup/send/efflux-dates.component.ts delete mode 100644 apps/desktop/src/app/tools/send/efflux-dates.component.html delete mode 100644 apps/desktop/src/app/tools/send/efflux-dates.component.ts delete mode 100644 apps/web/src/app/tools/send/efflux-dates.component.html delete mode 100644 apps/web/src/app/tools/send/efflux-dates.component.ts delete mode 100644 libs/angular/src/tools/send/efflux-dates.component.ts diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index f7539d6fa6e..0115768a40f 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -33,7 +33,6 @@ import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password. import { GeneratorComponent } from "../tools/popup/generator/generator.component"; import { PasswordGeneratorHistoryComponent } from "../tools/popup/generator/password-generator-history.component"; import { SendListComponent } from "../tools/popup/send/components/send-list.component"; -import { EffluxDatesComponent as SendEffluxDatesComponent } from "../tools/popup/send/efflux-dates.component"; import { SendAddEditComponent } from "../tools/popup/send/send-add-edit.component"; import { SendGroupingsComponent } from "../tools/popup/send/send-groupings.component"; import { SendTypeComponent } from "../tools/popup/send/send-type.component"; @@ -133,7 +132,6 @@ import "../platform/popup/locales"; PrivateModeWarningComponent, RegisterComponent, SendAddEditComponent, - SendEffluxDatesComponent, SendGroupingsComponent, SendListComponent, SendTypeComponent, diff --git a/apps/browser/src/tools/popup/send/efflux-dates.component.html b/apps/browser/src/tools/popup/send/efflux-dates.component.html deleted file mode 100644 index 737fdae4aab..00000000000 --- a/apps/browser/src/tools/popup/send/efflux-dates.component.html +++ /dev/null @@ -1,217 +0,0 @@ - - -
    -
    - -
    - - -
    -
    - -
    -
    -
    -
    - - -
    - -
    -
    - -
    - - - -
    - - -
    -
    - -
    - - -
    -
    - - - -
    -
    - - - -
    - - -
    -
    - -
    - - -
    -
    - - - -
    -
    - diff --git a/apps/browser/src/tools/popup/send/efflux-dates.component.ts b/apps/browser/src/tools/popup/send/efflux-dates.component.ts deleted file mode 100644 index 3d575b41fa7..00000000000 --- a/apps/browser/src/tools/popup/send/efflux-dates.component.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { DatePipe } from "@angular/common"; -import { Component, EventEmitter, Input, Output } from "@angular/core"; -import { ControlContainer, NgForm } from "@angular/forms"; - -import { EffluxDatesComponent as BaseEffluxDatesComponent } from "@bitwarden/angular/tools/send/efflux-dates.component"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; - -@Component({ - selector: "app-send-efflux-dates", - templateUrl: "efflux-dates.component.html", - viewProviders: [{ provide: ControlContainer, useExisting: NgForm }], -}) -export class EffluxDatesComponent extends BaseEffluxDatesComponent { - @Input() readonly inPopout: boolean; - @Output() popOutWindow = new EventEmitter(); - - constructor( - protected i18nService: I18nService, - protected platformUtilsService: PlatformUtilsService, - protected datePipe: DatePipe - ) { - super(i18nService, platformUtilsService, datePipe); - } -} diff --git a/apps/browser/src/tools/popup/send/send-add-edit.component.html b/apps/browser/src/tools/popup/send/send-add-edit.component.html index db31ad808a5..707adaa7a55 100644 --- a/apps/browser/src/tools/popup/send/send-add-edit.component.html +++ b/apps/browser/src/tools/popup/send/send-add-edit.component.html @@ -1,4 +1,4 @@ -
    +
    @@ -7,7 +7,7 @@ {{ title }}
    - @@ -42,9 +42,8 @@
    @@ -66,12 +65,9 @@ >
    -
    +
    @@ -93,9 +89,8 @@
    @@ -105,17 +100,15 @@
    -
    +
    @@ -125,13 +118,7 @@
    - +
    @@ -144,13 +131,7 @@
    - +
    @@ -170,15 +151,140 @@
    - - + +
    +
    + +
    + + +
    +
    + +
    +
    +
    + + +
    +
    + +
    + + +
    +
    + +
    + + +
    +
    + +
    +
    +
    +
    + + +
    + +
    +
    + +
    @@ -190,8 +296,7 @@ type="number" name="MaximumAccessCount" aria-describedby="maximumAccessCountHelp" - [(ngModel)]="send.maxAccessCount" - [readonly]="disableSend" + formControlName="maxAccessCount" />
    @@ -206,10 +311,9 @@
    @@ -227,9 +331,8 @@ name="Password" aria-describedby="passwordHelp" class="monospaced" - [(ngModel)]="password" + formControlName="password" appInputVerbatim - [readonly]="disableSend" />
    @@ -264,8 +367,7 @@ name="Notes" aria-describedby="notesHelp" rows="6" - [(ngModel)]="send.notes" - [readonly]="disableSend" + formControlName="notes" >
    @@ -278,13 +380,7 @@
    - +
    @@ -293,13 +389,7 @@
    - +
    diff --git a/apps/browser/src/tools/popup/send/send-add-edit.component.ts b/apps/browser/src/tools/popup/send/send-add-edit.component.ts index 1efd950c508..2d90957de7f 100644 --- a/apps/browser/src/tools/popup/send/send-add-edit.component.ts +++ b/apps/browser/src/tools/popup/send/send-add-edit.component.ts @@ -1,5 +1,6 @@ import { DatePipe, Location } from "@angular/common"; import { Component } from "@angular/core"; +import { FormBuilder } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { first } from "rxjs/operators"; @@ -47,7 +48,8 @@ export class SendAddEditComponent extends BaseAddEditComponent { private popupUtilsService: PopupUtilsService, logService: LogService, sendApiService: SendApiService, - dialogService: DialogService + dialogService: DialogService, + formBuilder: FormBuilder ) { super( i18nService, @@ -60,7 +62,8 @@ export class SendAddEditComponent extends BaseAddEditComponent { logService, stateService, sendApiService, - dialogService + dialogService, + formBuilder ); } diff --git a/apps/desktop/src/app/app.module.ts b/apps/desktop/src/app/app.module.ts index 3ac2ca29756..d14c40854cc 100644 --- a/apps/desktop/src/app/app.module.ts +++ b/apps/desktop/src/app/app.module.ts @@ -53,7 +53,6 @@ import { ExportComponent } from "./tools/export/export.component"; import { GeneratorComponent } from "./tools/generator.component"; import { PasswordGeneratorHistoryComponent } from "./tools/password-generator-history.component"; import { AddEditComponent as SendAddEditComponent } from "./tools/send/add-edit.component"; -import { EffluxDatesComponent as SendEffluxDatesComponent } from "./tools/send/efflux-dates.component"; import { SendComponent } from "./tools/send/send.component"; @NgModule({ @@ -87,7 +86,6 @@ import { SendComponent } from "./tools/send/send.component"; SearchComponent, SendAddEditComponent, SendComponent, - SendEffluxDatesComponent, SetPasswordComponent, SetPinComponent, SettingsComponent, diff --git a/apps/desktop/src/app/tools/send/add-edit.component.html b/apps/desktop/src/app/tools/send/add-edit.component.html index e9e1b319adb..d9f280be598 100644 --- a/apps/desktop/src/app/tools/send/add-edit.component.html +++ b/apps/desktop/src/app/tools/send/add-edit.component.html @@ -1,4 +1,4 @@ - +
    @@ -16,14 +16,7 @@
    - +
    @@ -31,20 +24,16 @@
    -
    +
    -
    +
    {{ send.file.fileName }} ({{ send.file.sizeName }})
    -
    +
    @@ -83,13 +70,7 @@
    - +
    @@ -112,14 +93,82 @@
    - - +
    +
    +
    + + + {{ + "deletionDateDesc" | i18n + }} +
    +
    + + + {{ + "deletionDateDesc" | i18n + }} +
    +
    + + + {{ + "expirationDateDesc" | i18n + }} +
    +
    + + + {{ + "expirationDateDesc" | i18n + }} +
    +
    +
    @@ -129,8 +178,7 @@ type="number" name="maxAccessCount" aria-describedby="maxAccessCountHelp" - [(ngModel)]="send.maxAccessCount" - [readOnly]="disableSend" + formControlName="maxAccessCount" />
    @@ -154,8 +202,7 @@ name="password" aria-describedby="passwordHelp" type="{{ showPassword ? 'text' : 'password' }}" - [(ngModel)]="password" - [readOnly]="disableSend" + formControlName="password" appInputVerbatim />
    @@ -167,7 +214,6 @@ appA11yTitle="{{ 'toggleVisibility' | i18n }}" [attr.aria-pressed]="showPassword" (click)="togglePasswordVisible()" - [disabled]="disableSend" >
    @@ -206,13 +251,7 @@
    - +
    @@ -220,13 +259,7 @@
    - +
    @@ -238,17 +271,11 @@
    - +
    - +
    @@ -259,13 +286,12 @@ type="submit" class="primary btn-submit" appA11yTitle="{{ 'save' | i18n }}" - [disabled]="form.loading" *ngIf="!disableSend" > -
    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 de5d2a601ab..98764866a54 100644 --- a/apps/desktop/src/app/tools/send/add-edit.component.ts +++ b/apps/desktop/src/app/tools/send/add-edit.component.ts @@ -1,5 +1,6 @@ import { DatePipe } from "@angular/common"; import { Component } from "@angular/core"; +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"; @@ -29,7 +30,8 @@ export class AddEditComponent extends BaseAddEditComponent { policyService: PolicyService, logService: LogService, sendApiService: SendApiService, - dialogService: DialogService + dialogService: DialogService, + formBuilder: FormBuilder ) { super( i18nService, @@ -42,7 +44,8 @@ export class AddEditComponent extends BaseAddEditComponent { logService, stateService, sendApiService, - dialogService + dialogService, + formBuilder ); } @@ -50,6 +53,7 @@ export class AddEditComponent extends BaseAddEditComponent { this.password = null; const send = await this.loadSend(); this.send = await send.decrypt(); + this.updateFormValues(); this.hasPassword = this.send.password != null && this.send.password.trim() !== ""; } @@ -65,4 +69,11 @@ export class AddEditComponent extends BaseAddEditComponent { this.i18nService.t("valueCopied", this.i18nService.t("sendLink")) ); } + + async resetAndLoad() { + this.sendId = null; + this.send = null; + await this.load(); + this.updateFormValues(); + } } diff --git a/apps/desktop/src/app/tools/send/efflux-dates.component.html b/apps/desktop/src/app/tools/send/efflux-dates.component.html deleted file mode 100644 index 156dfae9ddd..00000000000 --- a/apps/desktop/src/app/tools/send/efflux-dates.component.html +++ /dev/null @@ -1,62 +0,0 @@ - -
    -
    -
    - - - {{ "deletionDateDesc" | i18n }} -
    -
    - - - {{ - "deletionDateDesc" | i18n - }} -
    -
    - - - {{ "expirationDateDesc" | i18n }} -
    -
    - - - {{ - "expirationDateDesc" | i18n - }} -
    -
    -
    -
    diff --git a/apps/desktop/src/app/tools/send/efflux-dates.component.ts b/apps/desktop/src/app/tools/send/efflux-dates.component.ts deleted file mode 100644 index 40215348d55..00000000000 --- a/apps/desktop/src/app/tools/send/efflux-dates.component.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { DatePipe } from "@angular/common"; -import { Component, OnChanges } from "@angular/core"; -import { ControlContainer, NgForm } from "@angular/forms"; - -import { EffluxDatesComponent as BaseEffluxDatesComponent } from "@bitwarden/angular/tools/send/efflux-dates.component"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; - -@Component({ - selector: "app-send-efflux-dates", - templateUrl: "efflux-dates.component.html", - viewProviders: [{ provide: ControlContainer, useExisting: NgForm }], -}) -export class EffluxDatesComponent extends BaseEffluxDatesComponent implements OnChanges { - constructor( - protected i18nService: I18nService, - protected platformUtilsService: PlatformUtilsService, - protected datePipe: DatePipe - ) { - super(i18nService, platformUtilsService, datePipe); - } - - // We reuse the same form on desktop and just swap content, so need to watch these to maintin proper values. - ngOnChanges() { - this.selectedExpirationDatePreset.setValue(0); - this.selectedDeletionDatePreset.setValue(0); - this.defaultDeletionDateTime.setValue( - this.datePipe.transform(new Date(this.initialDeletionDate), "yyyy-MM-ddTHH:mm") - ); - if (this.initialExpirationDate) { - this.defaultExpirationDateTime.setValue( - this.datePipe.transform(new Date(this.initialExpirationDate), "yyyy-MM-ddTHH:mm") - ); - } else { - this.defaultExpirationDateTime.setValue(null); - } - } -} diff --git a/apps/desktop/src/app/tools/send/send.component.ts b/apps/desktop/src/app/tools/send/send.component.ts index 7ad98ceda1e..21b759e49bf 100644 --- a/apps/desktop/src/app/tools/send/send.component.ts +++ b/apps/desktop/src/app/tools/send/send.component.ts @@ -91,12 +91,10 @@ export class SendComponent extends BaseSendComponent implements OnInit, OnDestro this.searchBarService.setEnabled(false); } - addSend() { + async addSend() { this.action = Action.Add; if (this.addEditComponent != null) { - this.addEditComponent.sendId = null; - this.addEditComponent.send = null; - this.addEditComponent.load(); + await this.addEditComponent.resetAndLoad(); } } diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index 62702349f95..4552162cc8d 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -93,7 +93,6 @@ import { GeneratorComponent } from "../tools/generator.component"; import { PasswordGeneratorHistoryComponent } from "../tools/password-generator-history.component"; import { AccessComponent } from "../tools/send/access.component"; import { AddEditComponent as SendAddEditComponent } from "../tools/send/add-edit.component"; -import { EffluxDatesComponent as SendEffluxDatesComponent } from "../tools/send/efflux-dates.component"; import { ToolsComponent } from "../tools/tools.component"; import { PasswordRepromptComponent } from "../vault/components/password-reprompt.component"; import { PremiumBadgeComponent } from "../vault/components/premium-badge.component"; @@ -198,7 +197,6 @@ import { SharedModule } from "./shared.module"; SecurityKeysComponent, SelectableAvatarComponent, SendAddEditComponent, - SendEffluxDatesComponent, SetPasswordComponent, SettingsComponent, ShareComponent, @@ -302,7 +300,6 @@ import { SharedModule } from "./shared.module"; SecurityKeysComponent, SelectableAvatarComponent, SendAddEditComponent, - SendEffluxDatesComponent, SetPasswordComponent, SettingsComponent, ShareComponent, diff --git a/apps/web/src/app/tools/send/add-edit.component.html b/apps/web/src/app/tools/send/add-edit.component.html index 9cd90840b63..319d988f210 100644 --- a/apps/web/src/app/tools/send/add-edit.component.html +++ b/apps/web/src/app/tools/send/add-edit.component.html @@ -1,303 +1,276 @@ -
    -
    +
    + +
    - +
    @@ -112,7 +112,9 @@ selectedProviderType === providerType.OrganizationDuo " > -
    +
    + +
    @@ -123,7 +125,7 @@
    - +

    {{ "noTwoStepProviders" | i18n }}

    diff --git a/apps/desktop/src/auth/login/login.component.html b/apps/desktop/src/auth/login/login.component.html index 978f8df562f..3e02b49af2a 100644 --- a/apps/desktop/src/auth/login/login.component.html +++ b/apps/desktop/src/auth/login/login.component.html @@ -89,7 +89,11 @@
    - +
    - +
    diff --git a/apps/web/src/app/auth/register-form/register-form.component.html b/apps/web/src/app/auth/register-form/register-form.component.html index 9a5220c57d4..e53c963c938 100644 --- a/apps/web/src/app/auth/register-form/register-form.component.html +++ b/apps/web/src/app/auth/register-form/register-form.component.html @@ -89,7 +89,7 @@
    - +
    - +
    - +

    - +
    From 30e8a906ab7980b5fdf0867a92223fe2efb900cb Mon Sep 17 00:00:00 2001 From: Will Browning <20662079+willbrowningme@users.noreply.github.com> Date: Thu, 7 Sep 2023 19:23:56 +0200 Subject: [PATCH 37/46] [PM-3442] Change AnonAddy to addy.io (#6027) * Update anon-addy-forwarder.ts * Update generator.component.ts --- .../tools/generator/components/generator.component.ts | 2 +- .../username/email-forwarders/anon-addy-forwarder.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/libs/angular/src/tools/generator/components/generator.component.ts b/libs/angular/src/tools/generator/components/generator.component.ts index 37904473ad9..b0a5d13915f 100644 --- a/libs/angular/src/tools/generator/components/generator.component.ts +++ b/libs/angular/src/tools/generator/components/generator.component.ts @@ -244,7 +244,7 @@ export class GeneratorComponent implements OnInit { private async initForwardOptions() { this.forwardOptions = [ - { name: "AnonAddy", value: "anonaddy", validForSelfHosted: true }, + { name: "addy.io", value: "anonaddy", validForSelfHosted: true }, { name: "DuckDuckGo", value: "duckduckgo", validForSelfHosted: false }, { name: "Fastmail", value: "fastmail", validForSelfHosted: true }, { name: "Firefox Relay", value: "firefoxrelay", validForSelfHosted: false }, diff --git a/libs/common/src/tools/generator/username/email-forwarders/anon-addy-forwarder.ts b/libs/common/src/tools/generator/username/email-forwarders/anon-addy-forwarder.ts index b20f22ecf82..2b7563b2c39 100644 --- a/libs/common/src/tools/generator/username/email-forwarders/anon-addy-forwarder.ts +++ b/libs/common/src/tools/generator/username/email-forwarders/anon-addy-forwarder.ts @@ -6,10 +6,10 @@ import { ForwarderOptions } from "./forwarder-options"; export class AnonAddyForwarder implements Forwarder { async generate(apiService: ApiService, options: ForwarderOptions): Promise { if (options.apiKey == null || options.apiKey === "") { - throw "Invalid AnonAddy API token."; + throw "Invalid addy.io API token."; } if (options.anonaddy?.domain == null || options.anonaddy.domain === "") { - throw "Invalid AnonAddy domain."; + throw "Invalid addy.io domain."; } const requestInit: RequestInit = { redirect: "manual", @@ -21,7 +21,7 @@ export class AnonAddyForwarder implements Forwarder { "X-Requested-With": "XMLHttpRequest", }), }; - const url = "https://app.anonaddy.com/api/v1/aliases"; + const url = "https://app.addy.io/api/v1/aliases"; requestInit.body = JSON.stringify({ domain: options.anonaddy.domain, description: @@ -35,8 +35,8 @@ export class AnonAddyForwarder implements Forwarder { return json?.data?.email; } if (response.status === 401) { - throw "Invalid AnonAddy API token."; + throw "Invalid addy.io API token."; } - throw "Unknown AnonAddy error occurred."; + throw "Unknown addy.io error occurred."; } } From 8de65ea7915ded00297037c6a85103390176b5ca Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez Date: Thu, 7 Sep 2023 15:33:04 -0500 Subject: [PATCH 38/46] [PM-3285] Autofill v2 Feature Branch (#5939) * [PM-3285] Autofill v2 Feature Branch * [PM-2130] - Audit, Modularize, and Refactor Core autofill.js File (#5453) * split up autofill.ts, first pass * remove modification tracking comments * lessen and localize eslint disables * additional typing and formatting * update autofill v2 with PR #5364 changes (update/i18n confirm dialogs) * update autofill v2 with PR #4155 changes (add autofill support for textarea) Co-Authored-By: Manuel * move commonly used string values to constants * ts cleanup * [PM-2130] Starting work to re-architect autofillv2.ts * [PM-2130] Starting work to re-architect autofillv2.ts * [PM-2130] Working through autofill collect method * [PM-2130] Marking Removal of documentUUID as dead code * [PM-2130] Refining the implementation of collect and moving broken out utils back into class implementation * [PM-2130] Applying small refactors to AutofillCollect * [PM-2130] Refining the implementation of getAutofillFieldLabelTag to help with readability of the method * [PM-2130] Implementing jest tests for AutofillCollect methods * [PM-2130] Refining implementation for AutofillCollect * [PM-2200] Unit tests for autofill content script utilities with slight refactors (#5544) * add unit tests for urlNotSecure * add test coverage command * add unit tests for canSeeElementToStyle * canSeeElementToStyle should not return true if `animateTheFilling` or `currentEl` is false * add tests for selectAllFromDoc and getElementByOpId * clean up getElementByOpId * address some typing issues * add tests for setValueForElementByEvent, setValueForElement, and doSimpleSetByQuery * clean up setValueForElement and setValueForElementByEvent * more typescript cleanup * add tests for doClickByOpId and touchAllPasswordFields * add tests for doFocusByOpId and doClickByQuery * misc fill cleanup * move functions between collect and fill utils and replace getElementForOPID for duplicate getElementByOpId * add tests for isKnownTag and isElementVisible * rename addProp and remove redundant focusElement in favor of doFocusElement * cleanup * fix checkNodeType * add tests for shiftForLeftLabel * clean up and rename checkNodeType, isKnownTag, and shiftForLeftLabel * add tests for getFormElements * clean up getFormElements * add tests for getElementAttrValue, getElementValue, getSelectElementOptions, getLabelTop, and queryDoc * clean up and rename queryDoc to queryDocument * misc cleanup and rename getElementAttrValue to getPropertyOrAttribute * rebase cleanup * prettier formatting * [PM-2130] Fixing linting issues * [PM-2130] Fixing linting issues * [PM-2130] Migrating implementation for collect methods and tests for those methods into AutofillCollect context * [PM-2130] Migrating getPropertyOrAttribute method from utils to AutofillCollect * [PM-2130] Continuing migration of methods from collect utils into AutofillCollect * [PM-2130] Rework of isViewable method to better handle behavior for how we identify if an element is currently within the viewport * [PM-2130] Filling out implementation of autofill-insert * [PM-2130] Refining AutofillInsert * [PM-2130] Implementing jest tests for AutofillCollect methods and breaking out visibility related logic to a separate service * [PM-2130] Fixing jest tests for AutofillCollect * [PM-2130] Fixing jest tests for AutofillInit * [PM-2130] Adjusting how the AutofillFieldVisibilityService class is used in AutofillCollect * [PM-2130] Working through AutofillInsert implementation * [PM-2130] Migrating methods from fill.ts to AutofillInsert * [PM-2130] Migrating methods from fill.ts to AutofillInsert * [PM-2130] Applying fix for IntersectionObserver when triggering behavior in Safari and fixing issue with how we trigger an input event shortly after filling in a field * [PM-2130] Refactoring AutofillCollect to service CollectAutofillContentService * [PM-2130] Refactoring AutofillInsert to service InsertAutofillContentService * [PM-2130] Further organization of implementation * [PM-2130] Filling out missing jest test for AutofillInit.fillForm method * [PM-2130] Migrating the last of the collect jest tests to InsertAutofillContentService * [PM-2130] Further refactoring of elements including typing information * [PM-2130] Implementing jest tests for InsertAutofillContentService * [PM-2130] Implementing jest tests for InsertAutofillContentService * [PM-2130] Organization and refactoring of methods within InsertAutofillContent * [PM-2130] Implementation of jest tests for InsertAutofillContentService * [PM-2130] Implementation of Jest Test for IntertAutofillContentService * [PM-2130] Finalizing migration of methods and jest tests from util files into Autofill serivces * [PM-2130] Cleaning up dead code comments * [PM-2130] Removing unnecessary constants * [PM-2130] Finalizing jest tests for InsertAutofillContentService * [PM-2130] Refactoring FieldVisibiltyService to DomElementVisibilityService to allow service to act in a more general manner * [PM-2130] Implementing jest tests for DomElementVisibilityService * [PM-2130] Implementing jest tests for DomElementVisibilityService * [PM-2130] Implementing jest tests for DomElementVisibilityService * [PM-2130] Implementing jest tests for DomElementVisibilityService * [PM-2130] Breaking out the callback method used to resolve the IntersectionObserver promise * [PM-2130] Adding a comment explaining a fix for Safari * [PM-2130] Adding a comment explaining a fix for Safari * [PM-2130] Applying changes required for PM-2762 to implementation, and ensuring jest tests exist to validate the behavior * [PM-2130] Removing usage of IntersectionObserver when identifying element visibility due to broken interactions with React Components * [PM-2130] Fixing issue found when attempting to capture the elementAtCenterPoint in determining file visibility * [PM-2100] Create Unit Test Suite for autofill.service.ts (#5371) * [PM-2100] Create Unit Test Suite for Autofill.service.ts * [PM-2100] Finishing out tests for the getFormsWithPasswordFields method * [PM-2100] Implementing tests for the doAutofill method within the autofill service * [PM-2100] Working through implementation of doAutofill method * [PM-2100] Working through implementation of doAutofill method * [PM-2100] Finishing implementatino of isUntrustedIframe method within autofill service * [PM-2100] Finishing implementation of doAutoFill method within autofill service * [PM-2100] Finishing implementation of doAutoFillOnTab method within autofill service * [PM-2100] Working through tests for generateFillScript * [PM-2100] Finalizing generateFillScript method testing * [PM-2100] Starting implementation of generateLoginFillScript * [PM-2100] Working through tests for generateLoginFillScript * [PM-2100] Finalizing generateLoginFillScript method testing * [PM-2100] Removing unnecessary jest config file * [PM-2100] Fixing jest tests based on changes implemented within PM-2130 * [PM-2100] Fixing autofill mocks * [PM-2100] Fixing AutofillService jest tests * [PM-2100] Handling missing tests within coverage of AutofillService * [PM-2100] Handling missing tests within coverage of AutofillService.generateLoginFillScript * [PM-2100] Writing tests for AutofillService.generateCardFillScript * [PM-2100] Finalizing tests for AutofillService.generateCardFillScript * [PM-2100] Adding additional tests to cover changes introduced by TOTOP autofill PR * [PM-2100] Adding jest tests for Autofill.generateIdentityFillScript * [PM-2100] Finalizing tests for AutofillService.generateIdentityFillScript * [PM-2100] Implementing tests for AutofillService * [PM-2100] Implementing tests for AutofillService.loadPasswordFields * [PM-2100] Implementing tests for AutofillService.findUsernameField * [PM-2100] Implementing tests for AutofillService.findTotpField * [PM-2100] Implementing tests for AutofillService.fieldPropertyIsPrefixMatch * [PM-2100] Finalizing tests for AutofillService * [PM-2100] Modyfing placement of autofill-mocks * [PM-2100] Modyfing placement of autofill-mocks * [PM-2100] Removal of jest transform declaration * [PM-2130] Fixing issue with autofill service unit tests * [PM-2130] Fixing issue with autofill service unit tests * [PM-2130] Fixing test test for when we need to handle a password reprompt --------- Co-authored-by: Manuel Co-authored-by: Cesar Gonzalez Co-authored-by: Cesar Gonzalez * [PM-3285] Migrating Changes from PM-1407 into autofill v2 refactor implementation * [PM-2747] Add Support for Feature Flag of Autofill Version (#5695) * [PM-2100] Create Unit Test Suite for Autofill.service.ts * [PM-2100] Finishing out tests for the getFormsWithPasswordFields method * [PM-2100] Implementing tests for the doAutofill method within the autofill service * [PM-2100] Working through implementation of doAutofill method * [PM-2100] Working through implementation of doAutofill method * [PM-2100] Finishing implementatino of isUntrustedIframe method within autofill service * [PM-2100] Finishing implementation of doAutoFill method within autofill service * [PM-2100] Finishing implementation of doAutoFillOnTab method within autofill service * [PM-2100] Working through tests for generateFillScript * split up autofill.ts, first pass * remove modification tracking comments * lessen and localize eslint disables * additional typing and formatting * update autofill v2 with PR #5364 changes (update/i18n confirm dialogs) * update autofill v2 with PR #4155 changes (add autofill support for textarea) Co-Authored-By: Manuel * move commonly used string values to constants * ts cleanup * [PM-2100] Finalizing generateFillScript method testing * [PM-2100] Starting implementation of generateLoginFillScript * [PM-2100] Working through tests for generateLoginFillScript * [PM-2100] Finalizing generateLoginFillScript method testing * [PM-2130] Starting work to re-architect autofillv2.ts * [PM-2130] Starting work to re-architect autofillv2.ts * [PM-2130] Working through autofill collect method * [PM-2130] Marking Removal of documentUUID as dead code * [PM-2130] Refining the implementation of collect and moving broken out utils back into class implementation * [PM-2130] Applying small refactors to AutofillCollect * [PM-2130] Refining the implementation of getAutofillFieldLabelTag to help with readability of the method * [PM-2130] Implementing jest tests for AutofillCollect methods * [PM-2130] Refining implementation for AutofillCollect * [PM-2200] Unit tests for autofill content script utilities with slight refactors (#5544) * add unit tests for urlNotSecure * add test coverage command * add unit tests for canSeeElementToStyle * canSeeElementToStyle should not return true if `animateTheFilling` or `currentEl` is false * add tests for selectAllFromDoc and getElementByOpId * clean up getElementByOpId * address some typing issues * add tests for setValueForElementByEvent, setValueForElement, and doSimpleSetByQuery * clean up setValueForElement and setValueForElementByEvent * more typescript cleanup * add tests for doClickByOpId and touchAllPasswordFields * add tests for doFocusByOpId and doClickByQuery * misc fill cleanup * move functions between collect and fill utils and replace getElementForOPID for duplicate getElementByOpId * add tests for isKnownTag and isElementVisible * rename addProp and remove redundant focusElement in favor of doFocusElement * cleanup * fix checkNodeType * add tests for shiftForLeftLabel * clean up and rename checkNodeType, isKnownTag, and shiftForLeftLabel * add tests for getFormElements * clean up getFormElements * add tests for getElementAttrValue, getElementValue, getSelectElementOptions, getLabelTop, and queryDoc * clean up and rename queryDoc to queryDocument * misc cleanup and rename getElementAttrValue to getPropertyOrAttribute * rebase cleanup * prettier formatting * [PM-2130] Fixing linting issues * [PM-2130] Fixing linting issues * [PM-2130] Migrating implementation for collect methods and tests for those methods into AutofillCollect context * [PM-2130] Migrating getPropertyOrAttribute method from utils to AutofillCollect * [PM-2130] Continuing migration of methods from collect utils into AutofillCollect * [PM-2130] Rework of isViewable method to better handle behavior for how we identify if an element is currently within the viewport * [PM-2130] Filling out implementation of autofill-insert * [PM-2130] Refining AutofillInsert * [PM-2130] Implementing jest tests for AutofillCollect methods and breaking out visibility related logic to a separate service * [PM-2130] Fixing jest tests for AutofillCollect * [PM-2130] Fixing jest tests for AutofillInit * [PM-2130] Adjusting how the AutofillFieldVisibilityService class is used in AutofillCollect * [PM-2130] Working through AutofillInsert implementation * [PM-2130] Migrating methods from fill.ts to AutofillInsert * [PM-2130] Migrating methods from fill.ts to AutofillInsert * [PM-2130] Applying fix for IntersectionObserver when triggering behavior in Safari and fixing issue with how we trigger an input event shortly after filling in a field * [PM-2130] Refactoring AutofillCollect to service CollectAutofillContentService * [PM-2130] Refactoring AutofillInsert to service InsertAutofillContentService * [PM-2130] Further organization of implementation * [PM-2130] Filling out missing jest test for AutofillInit.fillForm method * [PM-2130] Migrating the last of the collect jest tests to InsertAutofillContentService * [PM-2130] Further refactoring of elements including typing information * [PM-2130] Implementing jest tests for InsertAutofillContentService * [PM-2130] Implementing jest tests for InsertAutofillContentService * [PM-2130] Organization and refactoring of methods within InsertAutofillContent * [PM-2130] Implementation of jest tests for InsertAutofillContentService * [PM-2130] Implementation of Jest Test for IntertAutofillContentService * [PM-2130] Finalizing migration of methods and jest tests from util files into Autofill serivces * [PM-2130] Cleaning up dead code comments * [PM-2130] Removing unnecessary constants * [PM-2130] Finalizing jest tests for InsertAutofillContentService * [PM-2130] Refactoring FieldVisibiltyService to DomElementVisibilityService to allow service to act in a more general manner * [PM-2130] Implementing jest tests for DomElementVisibilityService * [PM-2130] Implementing jest tests for DomElementVisibilityService * [PM-2130] Implementing jest tests for DomElementVisibilityService * [PM-2130] Implementing jest tests for DomElementVisibilityService * [PM-2130] Breaking out the callback method used to resolve the IntersectionObserver promise * [PM-2100] Removing unnecessary jest config file * [PM-2100] Fixing jest tests based on changes implemented within PM-2130 * [PM-2100] Fixing autofill mocks * [PM-2100] Fixing AutofillService jest tests * [PM-2100] Handling missing tests within coverage of AutofillService * [PM-2100] Handling missing tests within coverage of AutofillService.generateLoginFillScript * [PM-2100] Writing tests for AutofillService.generateCardFillScript * [PM-2100] Finalizing tests for AutofillService.generateCardFillScript * [PM-2100] Adding additional tests to cover changes introduced by TOTOP autofill PR * [PM-2100] Adding jest tests for Autofill.generateIdentityFillScript * [PM-2100] Finalizing tests for AutofillService.generateIdentityFillScript * [PM-2100] Implementing tests for AutofillService * [PM-2130] Adding a comment explaining a fix for Safari * [PM-2130] Adding a comment explaining a fix for Safari * [PM-2100] Implementing tests for AutofillService.loadPasswordFields * [PM-2100] Implementing tests for AutofillService.findUsernameField * [PM-2100] Implementing tests for AutofillService.findTotpField * [PM-2100] Implementing tests for AutofillService.fieldPropertyIsPrefixMatch * [PM-2100] Finalizing tests for AutofillService * [PM-2747] Add Support for Feature Flag of Autofill Version * [PM-2747] Adding Support for Manifest v3 within the implementation * [PM-2747] Modifying how the feature flag for autofill is named * [PM-2747] Modifying main.background.ts to load the ConfigApiService correctly * [PM-2747] Refactoring trigger of autofill scripts to be a simple immediately invoked function * [PM-2100] Modyfing placement of autofill-mocks * [PM-2100] Modyfing placement of autofill-mocks * [PM-2100] Removal of jest transform declaration * [PM-2130] Applying changes required for PM-2762 to implementation, and ensuring jest tests exist to validate the behavior * [PM-2747] Modifying how we inject the autofill scripts to ensure we are injecting into all frames within a page * [PM-2130] Removing usage of IntersectionObserver when identifying element visibility due to broken interactions with React Components * [PM-2130] Fixing issue found when attempting to capture the elementAtCenterPoint in determining file visibility * [PM-2100] Create Unit Test Suite for autofill.service.ts (#5371) * [PM-2100] Create Unit Test Suite for Autofill.service.ts * [PM-2100] Finishing out tests for the getFormsWithPasswordFields method * [PM-2100] Implementing tests for the doAutofill method within the autofill service * [PM-2100] Working through implementation of doAutofill method * [PM-2100] Working through implementation of doAutofill method * [PM-2100] Finishing implementatino of isUntrustedIframe method within autofill service * [PM-2100] Finishing implementation of doAutoFill method within autofill service * [PM-2100] Finishing implementation of doAutoFillOnTab method within autofill service * [PM-2100] Working through tests for generateFillScript * [PM-2100] Finalizing generateFillScript method testing * [PM-2100] Starting implementation of generateLoginFillScript * [PM-2100] Working through tests for generateLoginFillScript * [PM-2100] Finalizing generateLoginFillScript method testing * [PM-2100] Removing unnecessary jest config file * [PM-2100] Fixing jest tests based on changes implemented within PM-2130 * [PM-2100] Fixing autofill mocks * [PM-2100] Fixing AutofillService jest tests * [PM-2100] Handling missing tests within coverage of AutofillService * [PM-2100] Handling missing tests within coverage of AutofillService.generateLoginFillScript * [PM-2100] Writing tests for AutofillService.generateCardFillScript * [PM-2100] Finalizing tests for AutofillService.generateCardFillScript * [PM-2100] Adding additional tests to cover changes introduced by TOTOP autofill PR * [PM-2100] Adding jest tests for Autofill.generateIdentityFillScript * [PM-2100] Finalizing tests for AutofillService.generateIdentityFillScript * [PM-2100] Implementing tests for AutofillService * [PM-2100] Implementing tests for AutofillService.loadPasswordFields * [PM-2100] Implementing tests for AutofillService.findUsernameField * [PM-2100] Implementing tests for AutofillService.findTotpField * [PM-2100] Implementing tests for AutofillService.fieldPropertyIsPrefixMatch * [PM-2100] Finalizing tests for AutofillService * [PM-2100] Modyfing placement of autofill-mocks * [PM-2100] Modyfing placement of autofill-mocks * [PM-2100] Removal of jest transform declaration * [PM-2747] Applying a fix for a race condition that can occur when loading the notification bar and autofiller script login * [PM-2747] Reverting removal of autofill npm action. Now this will force usage of autofill-v2 regardless of whether a feature flag is set or not * [PM-2747] Fixing logic error incorporated when merging in master * [PM-2130] Fixing issue with autofill service unit tests * [PM-2130] Fixing issue with autofill service unit tests * [PM-2747] Fixing issue present with notification bar merge * [PM-2130] Fixing test test for when we need to handle a password reprompt * [PM-2747] Fixing wording for webpack script * [PM-2747] Addressing stylistic changes requested from code review * [PM-2747] Addressing stylistic changes requested from code review --------- Co-authored-by: Jonathan Prusik Co-authored-by: Manuel Co-authored-by: Jonathan Prusik * [PM-3285] Applying stylistic changes suggested by code review for the feature flag implementation * [PM-3285] Adding temporary console log to validate which version is being used * [PM-3285] Removing temporary console log indicating which version of autofill the user is currently loading --------- Co-authored-by: Jonathan Prusik Co-authored-by: Manuel Co-authored-by: Jonathan Prusik --- apps/browser/package.json | 2 +- apps/browser/src/autofill/constants.ts | 13 + .../content/abstractions/autofill-init.ts | 21 + .../autofill/content/autofill-init.spec.ts | 175 + .../src/autofill/content/autofill-init.ts | 130 + .../src/autofill/content/autofiller.ts | 10 +- .../src/autofill/content/autofillv2.ts | 1399 ------ .../src/autofill/content/notification-bar.ts | 121 +- .../trigger-autofill-script-injection.spec.ts | 16 + .../trigger-autofill-script-injection.ts | 3 + apps/browser/src/autofill/globals.d.ts | 7 + .../src/autofill/jest/autofill-mocks.ts | 131 + .../src/autofill/jest/testing-utils.ts | 5 + .../src/autofill/models/autofill-field.ts | 150 +- .../autofill/models/autofill-page-details.ts | 4 - .../src/autofill/models/autofill-script.ts | 21 +- .../services/abstractions/autofill.service.ts | 22 +- .../collect-autofill-content.service.ts | 8 + .../dom-element-visibility.service.ts | 6 + .../insert-autofill-content.service.ts | 7 + .../services/autofill.service.spec.ts | 4226 +++++++++++++++++ .../src/autofill/services/autofill.service.ts | 303 +- .../collect-autofill-content.service.spec.ts | 1588 +++++++ .../collect-autofill-content.service.ts | 578 +++ .../dom-element-visibility.service.spec.ts | 409 ++ .../dom-element-visibility.service.ts | 199 + .../insert-autofill-content.service.spec.ts | 1047 ++++ .../insert-autofill-content.service.ts | 349 ++ apps/browser/src/autofill/types/index.ts | 19 + .../browser/src/background/main.background.ts | 2 + .../src/background/runtime.background.ts | 7 + apps/browser/src/manifest.json | 7 +- .../src/platform/browser/browser-api.spec.ts | 56 + .../src/platform/browser/browser-api.ts | 27 + apps/browser/test.setup.ts | 16 + apps/browser/tsconfig.spec.json | 5 +- apps/browser/webpack.config.js | 15 +- libs/common/src/enums/feature-flag.enum.ts | 1 + 38 files changed, 9490 insertions(+), 1615 deletions(-) create mode 100644 apps/browser/src/autofill/constants.ts create mode 100644 apps/browser/src/autofill/content/abstractions/autofill-init.ts create mode 100644 apps/browser/src/autofill/content/autofill-init.spec.ts create mode 100644 apps/browser/src/autofill/content/autofill-init.ts delete mode 100644 apps/browser/src/autofill/content/autofillv2.ts create mode 100644 apps/browser/src/autofill/content/trigger-autofill-script-injection.spec.ts create mode 100644 apps/browser/src/autofill/content/trigger-autofill-script-injection.ts create mode 100644 apps/browser/src/autofill/globals.d.ts create mode 100644 apps/browser/src/autofill/jest/autofill-mocks.ts create mode 100644 apps/browser/src/autofill/jest/testing-utils.ts create mode 100644 apps/browser/src/autofill/services/abstractions/collect-autofill-content.service.ts create mode 100644 apps/browser/src/autofill/services/abstractions/dom-element-visibility.service.ts create mode 100644 apps/browser/src/autofill/services/abstractions/insert-autofill-content.service.ts create mode 100644 apps/browser/src/autofill/services/autofill.service.spec.ts create mode 100644 apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts create mode 100644 apps/browser/src/autofill/services/collect-autofill-content.service.ts create mode 100644 apps/browser/src/autofill/services/dom-element-visibility.service.spec.ts create mode 100644 apps/browser/src/autofill/services/dom-element-visibility.service.ts create mode 100644 apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts create mode 100644 apps/browser/src/autofill/services/insert-autofill-content.service.ts create mode 100644 apps/browser/src/platform/browser/browser-api.spec.ts diff --git a/apps/browser/package.json b/apps/browser/package.json index 980eb3258a2..cec3a9caab1 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -6,7 +6,6 @@ "build:mv3": "cross-env MANIFEST_VERSION=3 webpack", "build:watch": "webpack --watch", "build:watch:mv3": "cross-env MANIFEST_VERSION=3 webpack --watch", - "build:watch:autofill": "cross-env AUTOFILL_VERSION=2 webpack --watch", "build:prod": "cross-env NODE_ENV=production webpack", "build:prod:watch": "cross-env NODE_ENV=production webpack --watch", "dist": "npm run build:prod && gulp dist", @@ -19,6 +18,7 @@ "dist:safari:masdev": "npm run build:prod && gulp dist:safari:masdev", "dist:safari:dmg": "npm run build:prod && gulp dist:safari:dmg", "test": "jest", + "test:coverage": "jest --coverage --coverageDirectory=coverage", "test:watch": "jest --watch", "test:watch:all": "jest --watchAll" } diff --git a/apps/browser/src/autofill/constants.ts b/apps/browser/src/autofill/constants.ts new file mode 100644 index 00000000000..7f3637180b0 --- /dev/null +++ b/apps/browser/src/autofill/constants.ts @@ -0,0 +1,13 @@ +export const TYPE_CHECK = { + FUNCTION: "function", + NUMBER: "number", + STRING: "string", +} as const; + +export const EVENTS = { + CHANGE: "change", + INPUT: "input", + KEYDOWN: "keydown", + KEYPRESS: "keypress", + KEYUP: "keyup", +} as const; diff --git a/apps/browser/src/autofill/content/abstractions/autofill-init.ts b/apps/browser/src/autofill/content/abstractions/autofill-init.ts new file mode 100644 index 00000000000..706c6da4ee1 --- /dev/null +++ b/apps/browser/src/autofill/content/abstractions/autofill-init.ts @@ -0,0 +1,21 @@ +import AutofillScript from "../../models/autofill-script"; + +type AutofillExtensionMessage = { + command: string; + tab?: chrome.tabs.Tab; + sender?: string; + fillScript?: AutofillScript; +}; + +type AutofillExtensionMessageHandlers = { + [key: string]: CallableFunction; + collectPageDetails: (message: { message: AutofillExtensionMessage }) => void; + collectPageDetailsImmediately: (message: { message: AutofillExtensionMessage }) => void; + fillForm: (message: { message: AutofillExtensionMessage }) => void; +}; + +interface AutofillInit { + init(): void; +} + +export { AutofillExtensionMessage, AutofillExtensionMessageHandlers, AutofillInit }; diff --git a/apps/browser/src/autofill/content/autofill-init.spec.ts b/apps/browser/src/autofill/content/autofill-init.spec.ts new file mode 100644 index 00000000000..447fe31a8a3 --- /dev/null +++ b/apps/browser/src/autofill/content/autofill-init.spec.ts @@ -0,0 +1,175 @@ +import { mock } from "jest-mock-extended"; + +import AutofillPageDetails from "../models/autofill-page-details"; +import AutofillScript from "../models/autofill-script"; + +import { AutofillExtensionMessage } from "./abstractions/autofill-init"; + +describe("AutofillInit", () => { + let bitwardenAutofillInit: any; + + beforeEach(() => { + require("../content/autofill-init"); + bitwardenAutofillInit = window.bitwardenAutofillInit; + }); + + afterEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + }); + + describe("init", () => { + it("sets up the extension message listeners", () => { + jest.spyOn(bitwardenAutofillInit, "setupExtensionMessageListeners"); + + bitwardenAutofillInit.init(); + + expect(bitwardenAutofillInit.setupExtensionMessageListeners).toHaveBeenCalled(); + }); + }); + + describe("collectPageDetails", () => { + let extensionMessage: AutofillExtensionMessage; + let pageDetails: AutofillPageDetails; + + beforeEach(() => { + extensionMessage = { + command: "collectPageDetails", + tab: mock(), + sender: "sender", + }; + pageDetails = { + title: "title", + url: "http://example.com", + documentUrl: "documentUrl", + forms: {}, + fields: [], + collectedTimestamp: 0, + }; + jest + .spyOn(bitwardenAutofillInit.collectAutofillContentService, "getPageDetails") + .mockReturnValue(pageDetails); + }); + + it("returns collected page details for autofill if set to send the details in the response", async () => { + const response = await bitwardenAutofillInit["collectPageDetails"](extensionMessage, true); + + expect(bitwardenAutofillInit.collectAutofillContentService.getPageDetails).toHaveBeenCalled(); + expect(response).toEqual(pageDetails); + }); + + it("sends the collected page details for autofill using a background script message", async () => { + jest.spyOn(chrome.runtime, "sendMessage"); + + await bitwardenAutofillInit["collectPageDetails"](extensionMessage); + + expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({ + command: "collectPageDetailsResponse", + tab: extensionMessage.tab, + details: pageDetails, + sender: extensionMessage.sender, + }); + }); + }); + + describe("fillForm", () => { + it("will call the InsertAutofillContentService to fill the form", () => { + const fillScript = mock(); + jest + .spyOn(bitwardenAutofillInit.insertAutofillContentService, "fillForm") + .mockImplementation(); + + bitwardenAutofillInit.fillForm(fillScript); + + expect(bitwardenAutofillInit.insertAutofillContentService.fillForm).toHaveBeenCalledWith( + fillScript + ); + }); + }); + + describe("setupExtensionMessageListeners", () => { + it("sets up a chrome runtime on message listener", () => { + jest.spyOn(chrome.runtime.onMessage, "addListener"); + + bitwardenAutofillInit["setupExtensionMessageListeners"](); + + expect(chrome.runtime.onMessage.addListener).toHaveBeenCalledWith( + bitwardenAutofillInit["handleExtensionMessage"] + ); + }); + }); + + describe("handleExtensionMessage", () => { + let message: AutofillExtensionMessage; + let sender: chrome.runtime.MessageSender; + const sendResponse = jest.fn(); + + beforeEach(() => { + message = { + command: "collectPageDetails", + tab: mock(), + sender: "sender", + }; + sender = mock(); + }); + + it("returns a false value if a extension message handler is not found with the given message command", () => { + message.command = "unknownCommand"; + + const response = bitwardenAutofillInit["handleExtensionMessage"]( + message, + sender, + sendResponse + ); + + expect(response).toBe(false); + }); + + it("returns a false value if the message handler does not return a response", async () => { + const response1 = await bitwardenAutofillInit["handleExtensionMessage"]( + message, + sender, + sendResponse + ); + await Promise.resolve(response1); + + expect(response1).not.toBe(false); + + message.command = "fillForm"; + message.fillScript = mock(); + + const response2 = await bitwardenAutofillInit["handleExtensionMessage"]( + message, + sender, + sendResponse + ); + + expect(response2).toBe(false); + }); + + it("returns a true value and calls sendResponse if the message handler returns a response", async () => { + message.command = "collectPageDetailsImmediately"; + const pageDetails: AutofillPageDetails = { + title: "title", + url: "http://example.com", + documentUrl: "documentUrl", + forms: {}, + fields: [], + collectedTimestamp: 0, + }; + jest + .spyOn(bitwardenAutofillInit.collectAutofillContentService, "getPageDetails") + .mockReturnValue(pageDetails); + + const response = await bitwardenAutofillInit["handleExtensionMessage"]( + message, + sender, + sendResponse + ); + await Promise.resolve(response); + + expect(response).toBe(true); + expect(sendResponse).toHaveBeenCalledWith(pageDetails); + }); + }); +}); diff --git a/apps/browser/src/autofill/content/autofill-init.ts b/apps/browser/src/autofill/content/autofill-init.ts new file mode 100644 index 00000000000..8b441ae0e20 --- /dev/null +++ b/apps/browser/src/autofill/content/autofill-init.ts @@ -0,0 +1,130 @@ +import AutofillPageDetails from "../models/autofill-page-details"; +import AutofillScript from "../models/autofill-script"; +import CollectAutofillContentService from "../services/collect-autofill-content.service"; +import DomElementVisibilityService from "../services/dom-element-visibility.service"; +import InsertAutofillContentService from "../services/insert-autofill-content.service"; + +import { + AutofillExtensionMessage, + AutofillExtensionMessageHandlers, + AutofillInit as AutofillInitInterface, +} from "./abstractions/autofill-init"; + +class AutofillInit implements AutofillInitInterface { + private readonly domElementVisibilityService: DomElementVisibilityService; + private readonly collectAutofillContentService: CollectAutofillContentService; + private readonly insertAutofillContentService: InsertAutofillContentService; + private readonly extensionMessageHandlers: AutofillExtensionMessageHandlers = { + collectPageDetails: ({ message }) => this.collectPageDetails(message), + collectPageDetailsImmediately: ({ message }) => this.collectPageDetails(message, true), + fillForm: ({ message }) => this.fillForm(message.fillScript), + }; + + /** + * AutofillInit constructor. Initializes the DomElementVisibilityService, + * CollectAutofillContentService and InsertAutofillContentService classes. + */ + constructor() { + this.domElementVisibilityService = new DomElementVisibilityService(); + this.collectAutofillContentService = new CollectAutofillContentService( + this.domElementVisibilityService + ); + this.insertAutofillContentService = new InsertAutofillContentService( + this.domElementVisibilityService, + this.collectAutofillContentService + ); + } + + /** + * Initializes the autofill content script, setting up + * the extension message listeners. This method should + * be called once when the content script is loaded. + * @public + */ + init() { + this.setupExtensionMessageListeners(); + } + + /** + * Collects the page details and sends them to the + * extension background script. If the `sendDetailsInResponse` + * parameter is set to true, the page details will be + * returned to facilitate sending the details in the + * response to the extension message. + * @param {AutofillExtensionMessage} message + * @param {boolean} sendDetailsInResponse + * @returns {AutofillPageDetails | void} + * @private + */ + private async collectPageDetails( + message: AutofillExtensionMessage, + sendDetailsInResponse = false + ): Promise { + const pageDetails: AutofillPageDetails = + await this.collectAutofillContentService.getPageDetails(); + if (sendDetailsInResponse) { + return pageDetails; + } + + chrome.runtime.sendMessage({ + command: "collectPageDetailsResponse", + tab: message.tab, + details: pageDetails, + sender: message.sender, + }); + } + + /** + * Fills the form with the given fill script. + * @param {AutofillScript} fillScript + * @private + */ + private fillForm(fillScript: AutofillScript) { + this.insertAutofillContentService.fillForm(fillScript); + } + + /** + * Sets up the extension message listeners + * for the content script. + * @private + */ + private setupExtensionMessageListeners() { + chrome.runtime.onMessage.addListener(this.handleExtensionMessage); + } + + /** + * Handles the extension messages + * sent to the content script. + * @param {AutofillExtensionMessage} message + * @param {chrome.runtime.MessageSender} sender + * @param {(response?: any) => void} sendResponse + * @returns {boolean} + * @private + */ + private handleExtensionMessage = ( + message: AutofillExtensionMessage, + sender: chrome.runtime.MessageSender, + sendResponse: (response?: any) => void + ): boolean => { + const command: string = message.command; + const handler: CallableFunction | undefined = this.extensionMessageHandlers[command]; + if (!handler) { + return false; + } + + const messageResponse = handler({ message, sender }); + if (!messageResponse) { + return false; + } + + Promise.resolve(messageResponse).then((response) => sendResponse(response)); + return true; + }; +} + +(function () { + if (!window.bitwardenAutofillInit) { + window.bitwardenAutofillInit = new AutofillInit(); + window.bitwardenAutofillInit.init(); + } +})(); diff --git a/apps/browser/src/autofill/content/autofiller.ts b/apps/browser/src/autofill/content/autofiller.ts index 7fe9e5514a8..7f58e72c7d3 100644 --- a/apps/browser/src/autofill/content/autofiller.ts +++ b/apps/browser/src/autofill/content/autofiller.ts @@ -1,4 +1,10 @@ -document.addEventListener("DOMContentLoaded", (event) => { +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", loadAutofiller); +} else { + loadAutofiller(); +} + +function loadAutofiller() { let pageHref: string = null; let filledThisHref = false; let delayFillTimeout: number; @@ -49,4 +55,4 @@ document.addEventListener("DOMContentLoaded", (event) => { chrome.runtime.sendMessage(msg); } } -}); +} diff --git a/apps/browser/src/autofill/content/autofillv2.ts b/apps/browser/src/autofill/content/autofillv2.ts deleted file mode 100644 index 65813b3afe6..00000000000 --- a/apps/browser/src/autofill/content/autofillv2.ts +++ /dev/null @@ -1,1399 +0,0 @@ -/* eslint-disable no-var, no-console, no-prototype-builtins */ -// These eslint rules are disabled because the original JS was not written with them in mind and we don't want to fix -// them all now - -/* - 1Password Extension - - Lovingly handcrafted by Dave Teare, Michael Fey, Rad Azzouz, and Roustem Karimov. - Copyright (c) 2014 AgileBits. All rights reserved. - - ================================================================================ - - Copyright (c) 2014 AgileBits Inc. - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -/* - MODIFICATIONS FROM ORIGINAL - - 1. Populate isFirefox - 2. Remove isChrome and isSafari since they are not used. - 3. Unminify and format to meet Mozilla review requirements. - 4. Remove unnecessary input types from getFormElements query selector and limit number of elements returned. - 5. Remove fakeTested prop. - 6. Rename com.agilebits.* stuff to com.bitwarden.* - 7. Remove "some useful globals" on window - 8. Add ability to autofill span[data-bwautofill] elements - 9. Add new handler, for new command that responds with page details in response callback - 10. Handle sandbox iframe and sandbox rule in CSP - 11. Work on array of saved urls instead of just one to determine if we should autofill non-https sites - 12. Remove setting of attribute com.browser.browser.userEdited on user-inputs - 13. Handle null value URLs in urlNotSecure - 14. Convert to Typescript, add typings and remove dead code (not marked with START/END MODIFICATION) - */ -import AutofillForm from "../models/autofill-form"; -import AutofillPageDetails from "../models/autofill-page-details"; -import AutofillScript, { - AutofillScriptOptions, - FillScript, - FillScriptOp, -} from "../models/autofill-script"; - -/** - * The Document with additional custom properties added by this script - */ -type AutofillDocument = Document & { - elementsByOPID: Record; - elementForOPID: (opId: string) => Element; -}; - -/** - * A HTMLElement (usually a form element) with additional custom properties added by this script - */ -type ElementWithOpId = T & { - opid: string; -}; - -/** - * This script's definition of a Form Element (only a subset of HTML form elements) - * This is defined by getFormElements - */ -type FormElement = HTMLInputElement | HTMLSelectElement | HTMLSpanElement; - -/** - * A Form Element that we can set a value on (fill) - */ -type FillableControl = HTMLInputElement | HTMLSelectElement; - -function collect(document: Document) { - // START MODIFICATION - var isFirefox = - navigator.userAgent.indexOf("Firefox") !== -1 || navigator.userAgent.indexOf("Gecko/") !== -1; - // END MODIFICATION - - (document as AutofillDocument).elementsByOPID = {}; - - function getPageDetails(theDoc: Document, oneShotId: string) { - // start helpers - - /** - * For a given element `el`, returns the value of the attribute `attrName`. - * @param {HTMLElement} el - * @param {string} attrName - * @returns {string} The value of the attribute - */ - function getElementAttrValue(el: any, attrName: string) { - var attrVal = el[attrName]; - if ("string" == typeof attrVal) { - return attrVal; - } - attrVal = el.getAttribute(attrName); - return "string" == typeof attrVal ? attrVal : null; - } - - /** - * Returns the value of the given element. - * @param {HTMLElement} el - * @returns {any} Value of the element - */ - function getElementValue(el: any) { - switch (toLowerString(el.type)) { - case "checkbox": - return el.checked ? "✓" : ""; - - case "hidden": - el = el.value; - if (!el || "number" != typeof el.length) { - return ""; - } - 254 < el.length && (el = el.substr(0, 254) + "...SNIPPED"); - return el; - - default: - // START MODIFICATION - if (!el.type && el.tagName.toLowerCase() === "span") { - return el.innerText; - } - // END MODIFICATION - return el.value; - } - } - - /** - * If `el` is a `` elements, an array of the element's option `text` values - */ - selectInfo: any; - /** - * The `maxLength` attribute for the field - */ - maxLength: number; + htmlClass: string | null; + + tabindex: string | null; + + title: string | null; /** * The `tagName` for the field */ - tagName: string; - [key: string]: any; + tagName?: string | null; + /** + * The concatenated `innerText` or `textContent` of all the elements that are to the "left" of the field in the DOM + */ + "label-left"?: string; + /** + * The concatenated `innerText` or `textContent` of all the elements that are to the "right" of the field in the DOM + */ + "label-right"?: string; + /** + * For fields in a data table, the contents of the table row immediately above the field + */ + "label-top"?: string; + /** + * The concatenated `innerText` or `textContent` of all elements that are HTML labels for the field + */ + "label-tag"?: string; + /** + * The `aria-label` attribute for the field + */ + "label-aria"?: string | null; + + "label-data"?: string | null; + + "aria-hidden"?: boolean; + + "aria-disabled"?: boolean; + + "aria-haspopup"?: boolean; + + "data-stripe"?: string | null; + /** + * The HTML `placeholder` attribute for the field + */ + placeholder?: string | null; + /** + * The HTML `type` attribute for the field + */ + type?: string; + /** + * The HTML `value` for the field + */ + value?: string; + /** + * The `disabled` status of the field + */ + disabled?: boolean; + /** + * The `readonly` status of the field + */ + readonly?: boolean; + /** + * The `opid` attribute value of the form that contains the field + */ + form?: string; + /** + * The `x-autocompletetype`, `autocompletetype`, or `autocomplete` attribute for the field + */ + autoCompleteType?: string | null; + /** + * For ` + + +
    +`; + +describe("CollectAutofillContentService", () => { + const domElementVisibilityService = new DomElementVisibilityService(); + let collectAutofillContentService: CollectAutofillContentService; + + beforeEach(() => { + document.body.innerHTML = mockLoginForm; + collectAutofillContentService = new CollectAutofillContentService(domElementVisibilityService); + }); + + afterEach(() => { + jest.clearAllMocks(); + document.body.innerHTML = ""; + }); + + describe("getPageDetails", () => { + it("returns an object containing information about the curren page as well as autofill data for the forms and fields of the page", async () => { + const documentTitle = "Test Page"; + const formId = "validFormId"; + const formAction = "https://example.com/"; + const formMethod = "post"; + const formName = "validFormName"; + const usernameFieldId = "usernameField"; + const usernameFieldName = "username"; + const usernameFieldLabel = "User Name"; + const passwordFieldId = "passwordField"; + const passwordFieldName = "password"; + const passwordFieldLabel = "Password"; + document.title = documentTitle; + document.body.innerHTML = ` +
    + + + + +
    + `; + jest.spyOn(collectAutofillContentService as any, "buildAutofillFormsData"); + jest.spyOn(collectAutofillContentService as any, "buildAutofillFieldsData"); + jest + .spyOn(collectAutofillContentService["domElementVisibilityService"], "isFormFieldViewable") + .mockResolvedValue(true); + + const pageDetails = await collectAutofillContentService.getPageDetails(); + + expect(collectAutofillContentService["buildAutofillFormsData"]).toHaveBeenCalled(); + expect(collectAutofillContentService["buildAutofillFieldsData"]).toHaveBeenCalled(); + expect(pageDetails).toStrictEqual({ + title: documentTitle, + url: window.location.href, + documentUrl: document.location.href, + forms: { + __form__0: { + opid: "__form__0", + htmlAction: formAction, + htmlName: formName, + htmlID: formId, + htmlMethod: formMethod, + }, + }, + fields: [ + { + opid: "__0", + elementNumber: 0, + maxLength: 999, + viewable: true, + htmlID: usernameFieldId, + htmlName: usernameFieldName, + htmlClass: null, + tabindex: null, + title: "", + tagName: "input", + "label-tag": usernameFieldLabel, + "label-data": null, + "label-aria": null, + "label-top": null, + "label-right": passwordFieldLabel, + "label-left": usernameFieldLabel, + placeholder: "", + rel: null, + type: "text", + value: "", + checked: false, + autoCompleteType: "", + disabled: false, + readonly: false, + selectInfo: null, + form: "__form__0", + "aria-hidden": false, + "aria-disabled": false, + "aria-haspopup": false, + "data-stripe": null, + }, + { + opid: "__1", + elementNumber: 1, + maxLength: 999, + viewable: true, + htmlID: passwordFieldId, + htmlName: passwordFieldName, + htmlClass: null, + tabindex: null, + title: "", + tagName: "input", + "label-tag": passwordFieldLabel, + "label-data": null, + "label-aria": null, + "label-top": null, + "label-right": "", + "label-left": passwordFieldLabel, + placeholder: "", + rel: null, + type: "password", + value: "", + checked: false, + autoCompleteType: "", + disabled: false, + readonly: false, + selectInfo: null, + form: "__form__0", + "aria-hidden": false, + "aria-disabled": false, + "aria-haspopup": false, + "data-stripe": null, + }, + ], + collectedTimestamp: expect.any(Number), + }); + }); + }); + + describe("getAutofillFieldElementByOpid", () => { + it("returns the element with the opid property value matching the passed value", () => { + const textInput = document.querySelector('input[type="text"]') as FormElementWithAttribute; + const passwordInput = document.querySelector( + 'input[type="password"]' + ) as FormElementWithAttribute; + textInput.opid = "__0"; + passwordInput.opid = "__1"; + + const textInputWithOpid = collectAutofillContentService.getAutofillFieldElementByOpid("__0"); + const passwordInputWithOpid = + collectAutofillContentService.getAutofillFieldElementByOpid("__1"); + + expect(textInputWithOpid).toEqual(textInput); + expect(textInputWithOpid).not.toEqual(passwordInput); + expect(passwordInputWithOpid).toEqual(passwordInput); + }); + + it("returns the first of the element with an `opid` value matching the passed value and emits a console warning if multiple fields contain the same `opid`", () => { + const textInput = document.querySelector('input[type="text"]') as FormElementWithAttribute; + const passwordInput = document.querySelector( + 'input[type="password"]' + ) as FormElementWithAttribute; + jest.spyOn(console, "warn").mockImplementationOnce(jest.fn()); + textInput.opid = "__1"; + passwordInput.opid = "__1"; + + const elementWithOpid0 = collectAutofillContentService.getAutofillFieldElementByOpid("__0"); + const elementWithOpid1 = collectAutofillContentService.getAutofillFieldElementByOpid("__1"); + + expect(elementWithOpid0).toEqual(textInput); + expect(elementWithOpid1).toEqual(textInput); + expect(elementWithOpid1).not.toEqual(passwordInput); + // eslint-disable-next-line no-console + expect(console.warn).toHaveBeenCalledWith("More than one element found with opid __1"); + }); + + it("returns the element at the index position (parsed from passed opid) of all AutofillField elements when the passed opid value cannot be found", () => { + const textInput = document.querySelector('input[type="text"]') as FormElementWithAttribute; + const passwordInput = document.querySelector( + 'input[type="password"]' + ) as FormElementWithAttribute; + textInput.opid = undefined; + passwordInput.opid = "__1"; + + const elementWithOpid0 = collectAutofillContentService.getAutofillFieldElementByOpid("__0"); + const elementWithOpid2 = collectAutofillContentService.getAutofillFieldElementByOpid("__2"); + + expect(textInput.opid).toBeUndefined(); + expect(elementWithOpid0).toEqual(textInput); + expect(elementWithOpid0).not.toEqual(passwordInput); + expect(elementWithOpid2).toBeNull(); + }); + + it("returns null if no element can be found", () => { + const textInput = document.querySelector('input[type="text"]') as FormElementWithAttribute; + textInput.opid = "__0"; + + const foundElementWithOpid = + collectAutofillContentService.getAutofillFieldElementByOpid("__999"); + + expect(foundElementWithOpid).toBeNull(); + }); + }); + + describe("buildAutofillFormsData", () => { + it("returns an object of AutofillForm objects with the form id as a key", () => { + const documentTitle = "Test Page"; + const formId1 = "validFormId"; + const formAction1 = "https://example.com/"; + const formMethod1 = "post"; + const formName1 = "validFormName"; + const formId2 = "validFormId2"; + const formAction2 = "https://example2.com/"; + const formMethod2 = "get"; + const formName2 = "validFormName2"; + document.title = documentTitle; + document.body.innerHTML = ` +
    + + + + +
    +
    + + +
    + `; + + const autofillFormsData = collectAutofillContentService["buildAutofillFormsData"](); + + expect(autofillFormsData).toStrictEqual({ + __form__0: { + opid: "__form__0", + htmlAction: formAction1, + htmlName: formName1, + htmlID: formId1, + htmlMethod: formMethod1, + }, + __form__1: { + opid: "__form__1", + htmlAction: formAction2, + htmlName: formName2, + htmlID: formId2, + htmlMethod: formMethod2, + }, + }); + }); + }); + + describe("buildAutofillFieldsData", () => { + it("returns a promise containing an array of AutofillField objects", async () => { + jest.spyOn(collectAutofillContentService as any, "getAutofillFieldElements"); + jest.spyOn(collectAutofillContentService as any, "buildAutofillFieldItem"); + jest + .spyOn(collectAutofillContentService["domElementVisibilityService"], "isFormFieldViewable") + .mockResolvedValue(true); + + const autofillFieldsPromise = collectAutofillContentService["buildAutofillFieldsData"](); + const autofillFieldsData = await Promise.resolve(autofillFieldsPromise); + + expect(collectAutofillContentService["getAutofillFieldElements"]).toHaveBeenCalledWith(50); + expect(collectAutofillContentService["buildAutofillFieldItem"]).toHaveBeenCalledTimes(2); + expect(autofillFieldsPromise).toBeInstanceOf(Promise); + expect(autofillFieldsData).toStrictEqual([ + { + "aria-disabled": false, + "aria-haspopup": false, + "aria-hidden": false, + autoCompleteType: "", + checked: false, + "data-stripe": null, + disabled: false, + elementNumber: 0, + form: null, + htmlClass: null, + htmlID: "username", + htmlName: "", + "label-aria": null, + "label-data": null, + "label-left": "", + "label-right": "", + "label-tag": "", + "label-top": null, + maxLength: 999, + opid: "__0", + placeholder: "", + readonly: false, + rel: null, + selectInfo: null, + tabindex: null, + tagName: "input", + title: "", + type: "text", + value: "", + viewable: true, + }, + { + "aria-disabled": false, + "aria-haspopup": false, + "aria-hidden": false, + autoCompleteType: "", + checked: false, + "data-stripe": null, + disabled: false, + elementNumber: 1, + form: null, + htmlClass: null, + htmlID: "", + htmlName: "", + "label-aria": null, + "label-data": null, + "label-left": "", + "label-right": "", + "label-tag": "", + "label-top": null, + maxLength: 999, + opid: "__1", + placeholder: "", + readonly: false, + rel: null, + selectInfo: null, + tabindex: null, + tagName: "input", + title: "", + type: "password", + value: "", + viewable: true, + }, + ]); + }); + }); + + describe("getAutofillFieldElements", () => { + it("returns all form elements from the targeted document if no limit is set", () => { + document.body.innerHTML = ` +
    +
    + + + + + + + + + Span Element +
    +
    + `; + const usernameInput = document.getElementById("username"); + const passwordInput = document.querySelector('input[type="password"]'); + const commentsTextarea = document.getElementById("comments"); + const selectElement = document.getElementById("select"); + const spanElement = document.querySelector('span[data-bwautofill="true"]'); + jest.spyOn(document, "querySelectorAll"); + jest.spyOn(collectAutofillContentService as any, "getPropertyOrAttribute"); + + const formElements: FormFieldElement[] = + collectAutofillContentService["getAutofillFieldElements"](); + + expect(document.querySelectorAll).toHaveBeenCalledWith( + 'input:not([type="hidden"]):not([type="submit"]):not([type="reset"]):not([type="button"]):not([type="image"]):not([type="file"]):not([data-bwignore]), textarea:not([data-bwignore]), select:not([data-bwignore]), span[data-bwautofill]' + ); + expect(collectAutofillContentService["getPropertyOrAttribute"]).not.toHaveBeenCalled(); + expect(formElements).toEqual([ + usernameInput, + passwordInput, + commentsTextarea, + selectElement, + spanElement, + ]); + }); + + it("returns up to 2 (passed as `limit`) form elements from the targeted document with more than 2 form elements", () => { + document.body.innerHTML = ` +
    + included span + + ignored span + + + + + another included span +
    + `; + const spanElement = document.querySelector("span[data-bwautofill='true']"); + const textAreaInput = document.querySelector("textarea"); + jest.spyOn(collectAutofillContentService as any, "getPropertyOrAttribute"); + + const formElements: FormFieldElement[] = + collectAutofillContentService["getAutofillFieldElements"](2); + + expect(collectAutofillContentService["getPropertyOrAttribute"]).toHaveBeenNthCalledWith( + 1, + spanElement, + "type" + ); + expect(collectAutofillContentService["getPropertyOrAttribute"]).toHaveBeenNthCalledWith( + 2, + textAreaInput, + "type" + ); + expect(formElements).toEqual([spanElement, textAreaInput]); + }); + + it("returns form elements from the targeted document, ignoring input types `hidden`, `submit`, `reset`, `button`, `image`, `file`, and inputs tagged with `data-bwignore`, while giving lower order priority to `checkbox` and `radio` inputs if the returned list is truncated by `limit", () => { + document.body.innerHTML = ` +
    +
    + Select an option: +
    + + +
    +
    + + +
    +
    + + +
    +
    + included span + + ignored span + + + + + + + + + + + + + + another included span +
    + `; + const inputRadioA = document.querySelector('input[type="radio"][value="option-a"]'); + const inputRadioB = document.querySelector('input[type="radio"][value="option-b"]'); + const inputRadioC = document.querySelector('input[type="radio"][value="option-c"]'); + const firstSpan = document.getElementById("first-span"); + const textAreaInput = document.querySelector("textarea"); + const checkboxInput = document.querySelector('input[type="checkbox"]'); + const selectElement = document.querySelector("select"); + const usernameInput = document.getElementById("username"); + const passwordInput = document.querySelector('input[type="password"]'); + const secondSpan = document.getElementById("second-span"); + + const formElements: FormFieldElement[] = + collectAutofillContentService["getAutofillFieldElements"](); + + expect(formElements).toEqual([ + inputRadioA, + inputRadioB, + inputRadioC, + firstSpan, + textAreaInput, + checkboxInput, + selectElement, + usernameInput, + passwordInput, + secondSpan, + ]); + }); + + it("returns form elements from the targeted document while giving lower order priority to `checkbox` and `radio` inputs if the returned list is truncated by `limit`", () => { + document.body.innerHTML = ` +
    + + + + ignored span +
    + Select an option: +
    + + +
    +
    + + +
    +
    + + +
    +
    + + + + + another included span +
    + `; + const textAreaInput = document.querySelector("textarea"); + const selectElement = document.querySelector("select"); + const usernameInput = document.getElementById("username"); + const passwordInput = document.querySelector('input[type="password"]'); + const includedSpan = document.querySelector('span[data-bwautofill="true"]'); + const checkboxInput = document.querySelector('input[type="checkbox"]'); + const inputRadioA = document.querySelector('input[type="radio"][value="option-a"]'); + const inputRadioB = document.querySelector('input[type="radio"][value="option-b"]'); + + const truncatedFormElements: FormFieldElement[] = + collectAutofillContentService["getAutofillFieldElements"](8); + + expect(truncatedFormElements).toEqual([ + textAreaInput, + selectElement, + usernameInput, + passwordInput, + includedSpan, + checkboxInput, + inputRadioA, + inputRadioB, + ]); + }); + }); + + describe("buildAutofillFieldItem", () => { + it("returns the AutofillField base data values without the field labels or input values if the passed element is a span element", async () => { + const index = 0; + const spanElementId = "span-element"; + const spanElementClasses = "span element classes"; + const spanElementTabIndex = 0; + const spanElementTitle = "Span Element Title"; + document.body.innerHTML = ` + Span Element + `; + const spanElement = document.getElementById( + spanElementId + ) as ElementWithOpId; + jest.spyOn(collectAutofillContentService as any, "getAutofillFieldMaxLength"); + jest + .spyOn(collectAutofillContentService["domElementVisibilityService"], "isFormFieldViewable") + .mockResolvedValue(true); + jest.spyOn(collectAutofillContentService as any, "getPropertyOrAttribute"); + jest.spyOn(collectAutofillContentService as any, "getElementValue"); + + const autofillFieldItem = await collectAutofillContentService["buildAutofillFieldItem"]( + spanElement, + index + ); + + expect(collectAutofillContentService["getAutofillFieldMaxLength"]).toHaveBeenCalledWith( + spanElement + ); + expect( + collectAutofillContentService["domElementVisibilityService"].isFormFieldViewable + ).toHaveBeenCalledWith(spanElement); + expect(collectAutofillContentService["getPropertyOrAttribute"]).toHaveBeenNthCalledWith( + 1, + spanElement, + "id" + ); + expect(collectAutofillContentService["getPropertyOrAttribute"]).toHaveBeenNthCalledWith( + 2, + spanElement, + "name" + ); + expect(collectAutofillContentService["getPropertyOrAttribute"]).toHaveBeenNthCalledWith( + 3, + spanElement, + "class" + ); + expect(collectAutofillContentService["getPropertyOrAttribute"]).toHaveBeenNthCalledWith( + 4, + spanElement, + "tabindex" + ); + expect(collectAutofillContentService["getPropertyOrAttribute"]).toHaveBeenNthCalledWith( + 5, + spanElement, + "title" + ); + expect(collectAutofillContentService["getPropertyOrAttribute"]).toHaveBeenNthCalledWith( + 6, + spanElement, + "tagName" + ); + expect(collectAutofillContentService["getElementValue"]).not.toHaveBeenCalled(); + expect(autofillFieldItem).toEqual({ + elementNumber: index, + htmlClass: spanElementClasses, + htmlID: spanElementId, + htmlName: null, + maxLength: null, + opid: `__${index}`, + tabindex: String(spanElementTabIndex), + tagName: spanElement.tagName.toLowerCase(), + title: spanElementTitle, + viewable: true, + }); + }); + + it("returns the AutofillField base data, label data, and input element data", async () => { + const index = 0; + const usernameField = { + labelText: "Username", + id: "username-id", + classes: "username input classes", + name: "username", + type: "text", + maxLength: 42, + tabIndex: 0, + title: "Username Input Title", + autocomplete: "username-autocomplete", + dataLabel: "username-data-label", + ariaLabel: "username-aria-label", + placeholder: "username-placeholder", + rel: "username-rel", + value: "username-value", + dataStripe: "data-stripe", + }; + document.body.innerHTML = ` +
    + + +
    + `; + const formElement = document.querySelector("form"); + formElement.opid = "form-opid"; + const usernameInput = document.getElementById( + usernameField.id + ) as ElementWithOpId; + jest.spyOn(collectAutofillContentService as any, "getAutofillFieldMaxLength"); + jest + .spyOn(collectAutofillContentService["domElementVisibilityService"], "isFormFieldViewable") + .mockResolvedValue(true); + jest.spyOn(collectAutofillContentService as any, "getPropertyOrAttribute"); + jest.spyOn(collectAutofillContentService as any, "getElementValue"); + + const autofillFieldItem = await collectAutofillContentService["buildAutofillFieldItem"]( + usernameInput, + index + ); + + expect(autofillFieldItem).toEqual({ + "aria-disabled": false, + "aria-haspopup": false, + "aria-hidden": false, + autoCompleteType: usernameField.autocomplete, + checked: false, + "data-stripe": usernameField.dataStripe, + disabled: false, + elementNumber: index, + form: formElement.opid, + htmlClass: usernameField.classes, + htmlID: usernameField.id, + htmlName: usernameField.name, + "label-aria": usernameField.ariaLabel, + "label-data": usernameField.dataLabel, + "label-left": usernameField.labelText, + "label-right": "", + "label-tag": usernameField.labelText, + "label-top": null, + maxLength: usernameField.maxLength, + opid: `__${index}`, + placeholder: usernameField.placeholder, + readonly: false, + rel: usernameField.rel, + selectInfo: null, + tabindex: String(usernameField.tabIndex), + tagName: usernameInput.tagName.toLowerCase(), + title: usernameField.title, + type: usernameField.type, + value: usernameField.value, + viewable: true, + }); + }); + + it("returns the AutofillField base data and input element data, but not the label data if the input element is of type `hidden`", async () => { + const index = 0; + const hiddenField = { + labelText: "Hidden Field", + id: "hidden-id", + classes: "hidden input classes", + name: "hidden", + type: "hidden", + maxLength: 42, + tabIndex: 0, + title: "Hidden Input Title", + autocomplete: "off", + rel: "hidden-rel", + value: "hidden-value", + dataStripe: "data-stripe", + }; + document.body.innerHTML = ` +
    + + +
    + `; + const formElement = document.querySelector("form"); + formElement.opid = "form-opid"; + const hiddenInput = document.getElementById( + hiddenField.id + ) as ElementWithOpId; + jest.spyOn(collectAutofillContentService as any, "getAutofillFieldMaxLength"); + jest + .spyOn(collectAutofillContentService["domElementVisibilityService"], "isFormFieldViewable") + .mockResolvedValue(true); + jest.spyOn(collectAutofillContentService as any, "getPropertyOrAttribute"); + jest.spyOn(collectAutofillContentService as any, "getElementValue"); + + const autofillFieldItem = await collectAutofillContentService["buildAutofillFieldItem"]( + hiddenInput, + index + ); + + expect(autofillFieldItem).toEqual({ + "aria-disabled": false, + "aria-haspopup": false, + "aria-hidden": false, + autoCompleteType: null, + checked: false, + "data-stripe": hiddenField.dataStripe, + disabled: false, + elementNumber: index, + form: formElement.opid, + htmlClass: hiddenField.classes, + htmlID: hiddenField.id, + htmlName: hiddenField.name, + maxLength: hiddenField.maxLength, + opid: `__${index}`, + readonly: false, + rel: hiddenField.rel, + selectInfo: null, + tabindex: String(hiddenField.tabIndex), + tagName: hiddenInput.tagName.toLowerCase(), + title: hiddenField.title, + type: hiddenField.type, + value: hiddenField.value, + viewable: true, + }); + }); + }); + + describe("createAutofillFieldLabelTag", () => { + beforeEach(() => { + jest.spyOn(collectAutofillContentService as any, "createLabelElementsTag"); + jest.spyOn(document, "querySelectorAll"); + }); + + it("returns the label tag early if the passed element contains any labels", () => { + document.body.innerHTML = ` + + + + `; + const element = document.querySelector("#username-id") as FillableFormFieldElement; + + const labelTag = collectAutofillContentService["createAutofillFieldLabelTag"](element); + + expect(collectAutofillContentService["createLabelElementsTag"]).toHaveBeenCalledWith( + new Set(element.labels) + ); + expect(document.querySelectorAll).not.toHaveBeenCalled(); + expect(labelTag).toEqual("Username"); + }); + + it("queries all labels associated with the element's id", () => { + document.body.innerHTML = ` + + + `; + const element = document.querySelector("#country-id") as FillableFormFieldElement; + const elementLabel = document.querySelector("label[for='country-id']"); + + const labelTag = collectAutofillContentService["createAutofillFieldLabelTag"](element); + + expect(document.querySelectorAll).toHaveBeenCalledWith(`label[for="${element.id}"]`); + expect(collectAutofillContentService["createLabelElementsTag"]).toHaveBeenCalledWith( + new Set([elementLabel]) + ); + expect(labelTag).toEqual("Country"); + }); + + it("queries all labels associated with the element's name", () => { + document.body.innerHTML = ` + + + `; + const element = document.querySelector("select") as FillableFormFieldElement; + const elementLabel = document.querySelector("label[for='country-name']"); + + const labelTag = collectAutofillContentService["createAutofillFieldLabelTag"](element); + + expect(document.querySelectorAll).not.toHaveBeenCalledWith(`label[for="${element.id}"]`); + expect(document.querySelectorAll).toHaveBeenCalledWith(`label[for="${element.name}"]`); + expect(collectAutofillContentService["createLabelElementsTag"]).toHaveBeenCalledWith( + new Set([elementLabel]) + ); + expect(labelTag).toEqual("Country"); + }); + + it("will not add duplicate labels that are found to the label tag", () => { + document.body.innerHTML = ` + +
    + `; + const element = document.querySelector("#country-name") as FillableFormFieldElement; + element.name = "country-name"; + const elementLabel = document.querySelector("label[for='country-name']"); + + const labelTag = collectAutofillContentService["createAutofillFieldLabelTag"](element); + + expect(document.querySelectorAll).toHaveBeenCalledWith( + `label[for="${element.id}"], label[for="${element.name}"]` + ); + expect(collectAutofillContentService["createLabelElementsTag"]).toHaveBeenCalledWith( + new Set([elementLabel]) + ); + expect(labelTag).toEqual("Country"); + }); + + it("will attempt to identify the label of an element from its parent element", () => { + document.body.innerHTML = ``; + const element = document.querySelector("#username-id") as FillableFormFieldElement; + const elementLabel = element.parentElement; + + const labelTag = collectAutofillContentService["createAutofillFieldLabelTag"](element); + + expect(collectAutofillContentService["createLabelElementsTag"]).toHaveBeenCalledWith( + new Set([elementLabel]) + ); + expect(labelTag).toEqual("Username"); + }); + + it("will attempt to identify the label of an element from a `dt` element associated with the element's parent", () => { + document.body.innerHTML = ` +
    +
    Username
    +
    + +
    +
    + `; + const element = document.querySelector("#username-id") as FillableFormFieldElement; + const elementLabel = document.querySelector("#label-element"); + + const labelTag = collectAutofillContentService["createAutofillFieldLabelTag"](element); + + expect(collectAutofillContentService["createLabelElementsTag"]).toHaveBeenCalledWith( + new Set([elementLabel]) + ); + expect(labelTag).toEqual("Username"); + }); + + it("will return an empty string value if no labels can be found for an element", () => { + document.body.innerHTML = ` + + `; + const element = document.querySelector("#username-id") as FillableFormFieldElement; + + const labelTag = collectAutofillContentService["createAutofillFieldLabelTag"](element); + + expect(labelTag).toEqual(""); + }); + }); + + describe("queryElementLabels", () => { + it("returns null if the passed element has no id or name", () => { + document.body.innerHTML = ` + + `; + const element = document.querySelector("input") as FillableFormFieldElement; + + const labels = collectAutofillContentService["queryElementLabels"](element); + + expect(labels).toBeNull(); + }); + + it("returns an empty NodeList if the passed element has no label", () => { + document.body.innerHTML = ` + + `; + const element = document.querySelector("input") as FillableFormFieldElement; + + const labels = collectAutofillContentService["queryElementLabels"](element); + + expect(labels).toEqual(document.querySelectorAll("label")); + }); + + it("returns the label of an element associated with its ID value", () => { + document.body.innerHTML = ` + + + `; + const element = document.querySelector("input") as FillableFormFieldElement; + + const labels = collectAutofillContentService["queryElementLabels"](element); + + expect(labels).toEqual(document.querySelectorAll("label[for='username-id']")); + }); + + it("returns the label of an element associated with its name value", () => { + document.body.innerHTML = ` + + + `; + const element = document.querySelector("input") as FillableFormFieldElement; + + const labels = collectAutofillContentService["queryElementLabels"](element); + + expect(labels).toEqual(document.querySelectorAll("label[for='username']")); + }); + }); + + describe("createLabelElementsTag", () => { + it("returns a string containing all the labels associated with a given input element", () => { + const firstLabelText = "Username by name"; + const secondLabelText = "Username by ID"; + document.body.innerHTML = ` + + + + `; + const labels = document.querySelectorAll("label"); + jest.spyOn(collectAutofillContentService as any, "trimAndRemoveNonPrintableText"); + + const labelTag = collectAutofillContentService["createLabelElementsTag"](new Set(labels)); + + expect( + collectAutofillContentService["trimAndRemoveNonPrintableText"] + ).toHaveBeenNthCalledWith(1, firstLabelText); + expect( + collectAutofillContentService["trimAndRemoveNonPrintableText"] + ).toHaveBeenNthCalledWith(2, secondLabelText); + expect(labelTag).toEqual(`${firstLabelText}${secondLabelText}`); + }); + }); + + describe("getAutofillFieldMaxLength", () => { + it("returns null if the passed FormFieldElement is not an element type that has a max length property", () => { + document.body.innerHTML = ` + + `; + const element = document.querySelector("select") as FillableFormFieldElement; + + const maxLength = collectAutofillContentService["getAutofillFieldMaxLength"](element); + + expect(maxLength).toBeNull(); + }); + + it("returns a value of 999 if the passed FormFieldElement has no set maxLength value", () => { + document.body.innerHTML = ` + + `; + const element = document.querySelector("input") as FillableFormFieldElement; + + const maxLength = collectAutofillContentService["getAutofillFieldMaxLength"](element); + + expect(maxLength).toEqual(999); + }); + + it("returns a value of 999 if the passed FormFieldElement has a maxLength value higher than 999", () => { + document.body.innerHTML = ` + + `; + const element = document.querySelector("input") as FillableFormFieldElement; + + const maxLength = collectAutofillContentService["getAutofillFieldMaxLength"](element); + + expect(maxLength).toEqual(999); + }); + + it("returns the maxLength property of a passed FormFieldElement", () => { + document.body.innerHTML = ` + + `; + const element = document.querySelector("input") as FillableFormFieldElement; + + const maxLength = collectAutofillContentService["getAutofillFieldMaxLength"](element); + + expect(maxLength).toEqual(10); + }); + }); + + describe("createAutofillFieldRightLabel", () => { + it("returns an empty string if no siblings are found", () => { + document.body.innerHTML = ` + + `; + const element = document.querySelector("input") as FillableFormFieldElement; + + const labelTag = collectAutofillContentService["createAutofillFieldRightLabel"](element); + + expect(labelTag).toEqual(""); + }); + + it("returns the text content of the element's next sibling element", () => { + document.body.innerHTML = ` + + + `; + const element = document.querySelector("input") as FillableFormFieldElement; + + const labelTag = collectAutofillContentService["createAutofillFieldRightLabel"](element); + + expect(labelTag).toEqual("Username"); + }); + + it("returns the text content of the element's next sibling textNode", () => { + document.body.innerHTML = ` + + Username + `; + const element = document.querySelector("input") as FillableFormFieldElement; + + const labelTag = collectAutofillContentService["createAutofillFieldRightLabel"](element); + + expect(labelTag).toEqual("Username"); + }); + }); + + describe("createAutofillFieldLeftLabel", () => { + it("returns a string value of the text content associated with the previous siblings of the passed element", () => { + document.body.innerHTML = ` +
    + Text Content + + +
    + `; + const element = document.querySelector("input") as FillableFormFieldElement; + + const labelTag = collectAutofillContentService["createAutofillFieldLeftLabel"](element); + + expect(labelTag).toEqual("Text ContentUsername"); + }); + }); + + describe("createAutofillFieldTopLabel", () => { + it("returns the table column header value for the passed table element", () => { + document.body.innerHTML = ` + + + + + + + + + + + + + +
    UsernamePasswordLogin code
    + `; + const targetTableCellInput = document.querySelector( + 'input[name="password"]' + ) as HTMLInputElement; + + const targetTableCellLabel = + collectAutofillContentService["createAutofillFieldTopLabel"](targetTableCellInput); + + expect(targetTableCellLabel).toEqual("Password"); + }); + + it("will attempt to return the value for the previous sibling row as the label if a `th` cell is not found", () => { + document.body.innerHTML = ` + + + + + + + + + + + + + +
    UsernamePasswordLogin code
    + `; + const targetTableCellInput = document.querySelector( + 'input[name="auth-code"]' + ) as HTMLInputElement; + + const targetTableCellLabel = + collectAutofillContentService["createAutofillFieldTopLabel"](targetTableCellInput); + + expect(targetTableCellLabel).toEqual("Login code"); + }); + + it("returns null for the passed table element it's parent row has no previous sibling row", () => { + document.body.innerHTML = ` + + + + + + + + +
    + `; + const targetTableCellInput = document.querySelector( + 'input[name="password"]' + ) as HTMLInputElement; + + const targetTableCellLabel = + collectAutofillContentService["createAutofillFieldTopLabel"](targetTableCellInput); + + expect(targetTableCellLabel).toEqual(null); + }); + + it("returns null if the input element is not structured within a `td` element", () => { + document.body.innerHTML = ` + + + + + + + + + +
    + + + +
    UsernamePasswordLogin code
    + `; + const targetTableCellInput = document.querySelector( + 'input[name="password"]' + ) as HTMLInputElement; + + const targetTableCellLabel = + collectAutofillContentService["createAutofillFieldTopLabel"](targetTableCellInput); + + expect(targetTableCellLabel).toEqual(null); + }); + + it("returns null if the index of the `td` element is larger than the length of cells in the sibling row", () => { + document.body.innerHTML = ` + + + + + + + + + + + + +
    UsernamePassword
    + `; + const targetTableCellInput = document.querySelector( + 'input[name="auth-code"]' + ) as HTMLInputElement; + + const targetTableCellLabel = + collectAutofillContentService["createAutofillFieldTopLabel"](targetTableCellInput); + + expect(targetTableCellLabel).toEqual(null); + }); + }); + + describe("isNewSectionElement", () => { + const validElementTags = [ + "html", + "body", + "button", + "form", + "head", + "iframe", + "input", + "option", + "script", + "select", + "table", + "textarea", + ]; + const invalidElementTags = ["div", "span"]; + + describe("given a transitional element", () => { + validElementTags.forEach((tag) => { + const element = document.createElement(tag); + + it(`returns true if the element tag is a ${tag}`, () => { + expect(collectAutofillContentService["isNewSectionElement"](element)).toEqual(true); + }); + }); + }); + + describe("given an non-transitional element", () => { + invalidElementTags.forEach((tag) => { + const element = document.createElement(tag); + + it(`returns false if the element tag is a ${tag}`, () => { + expect(collectAutofillContentService["isNewSectionElement"](element)).toEqual(false); + }); + }); + }); + + it(`returns true if the provided element is falsy`, () => { + expect(collectAutofillContentService["isNewSectionElement"](undefined)).toEqual(true); + }); + }); + + describe("getTextContentFromElement", () => { + it("returns the node value for a text node", () => { + document.body.innerHTML = ` +
    + +
    + `; + const element = document.querySelector("#username-id"); + const textNode = element.previousSibling; + const parsedTextContent = collectAutofillContentService["trimAndRemoveNonPrintableText"]( + textNode.nodeValue + ); + jest.spyOn(collectAutofillContentService as any, "trimAndRemoveNonPrintableText"); + + const textContent = collectAutofillContentService["getTextContentFromElement"](textNode); + + expect(textNode.nodeType).toEqual(Node.TEXT_NODE); + expect(collectAutofillContentService["trimAndRemoveNonPrintableText"]).toHaveBeenCalledWith( + textNode.nodeValue + ); + expect(textContent).toEqual(parsedTextContent); + }); + + it("returns the text content for an element node", () => { + document.body.innerHTML = ` +
    + + +
    + `; + const element = document.querySelector('label[for="username-id"]'); + jest.spyOn(collectAutofillContentService as any, "trimAndRemoveNonPrintableText"); + + const textContent = collectAutofillContentService["getTextContentFromElement"](element); + + expect(element.nodeType).toEqual(Node.ELEMENT_NODE); + expect(collectAutofillContentService["trimAndRemoveNonPrintableText"]).toHaveBeenCalledWith( + element.textContent + ); + expect(textContent).toEqual(element.textContent); + }); + }); + + describe("trimAndRemoveNonPrintableText", () => { + it("returns an empty string if no text content is passed", () => { + const textContent = collectAutofillContentService["trimAndRemoveNonPrintableText"](undefined); + + expect(textContent).toEqual(""); + }); + + it("returns a trimmed string with all non-printable text removed", () => { + const nonParsedText = `Hello!\nThis is a \t + test string.\x0B\x08`; + + const parsedText = + collectAutofillContentService["trimAndRemoveNonPrintableText"](nonParsedText); + + expect(parsedText).toEqual("Hello! This is a test string."); + }); + }); + + describe("recursivelyGetTextFromPreviousSiblings", () => { + it("should find text adjacent to the target element likely to be a label", () => { + document.body.innerHTML = ` +
    + Text about things +
    some things
    +
    +

    Stuff Section Header

    + Other things which are also stuff +
    Not visible text
    + + +
    +
    + `; + const textInput = document.querySelector("#input-tag") as FormElementWithAttribute; + + const elementList: string[] = + collectAutofillContentService["recursivelyGetTextFromPreviousSiblings"](textInput); + + expect(elementList).toEqual([ + "something else", + "Not visible text", + "Other things which are also stuff", + "Stuff Section Header", + ]); + }); + + it("should stop looking at siblings for label values when a 'new section' element is seen", () => { + document.body.innerHTML = ` +
    + Text about things +
    some things
    +
    +

    Stuff Section Header

    + Other things which are also stuff +
    Not a label
    + + + +
    +
    + `; + + const textInput = document.querySelector("#input-tag") as FormElementWithAttribute; + const elementList: string[] = + collectAutofillContentService["recursivelyGetTextFromPreviousSiblings"](textInput); + + expect(elementList).toEqual(["something else"]); + }); + + it("should keep looking for labels in parents when there are no siblings of the target element", () => { + document.body.innerHTML = ` +
    + Text about things + +
    some things
    +
    + +
    +
    + `; + + const textInput = document.querySelector("#input-tag") as FormElementWithAttribute; + const elementList: string[] = + collectAutofillContentService["recursivelyGetTextFromPreviousSiblings"](textInput); + + expect(elementList).toEqual(["some things"]); + }); + + it("should find label in parent sibling last child if no other label candidates have been encountered and there are no text nodes along the way", () => { + document.body.innerHTML = ` +
    +
    +
    not the most relevant things
    +
    some nested things
    +
    + +
    +
    +
    + `; + + const textInput = document.querySelector("#input-tag") as FormElementWithAttribute; + const elementList: string[] = + collectAutofillContentService["recursivelyGetTextFromPreviousSiblings"](textInput); + + expect(elementList).toEqual(["some nested things"]); + }); + + it("should exit early if the target element has no parent element/node", () => { + const textInput = document.querySelector("html") as HTMLHtmlElement; + + const elementList: string[] = + collectAutofillContentService["recursivelyGetTextFromPreviousSiblings"](textInput); + + expect(elementList).toEqual([]); + }); + }); + + describe("getPropertyOrAttribute", () => { + it("returns the value of the named property of the target element if the property exists within the element", () => { + document.body.innerHTML += ''; + const textInput = document.querySelector("#username") as HTMLInputElement; + textInput.setAttribute("value", "jsmith"); + const checkboxInput = document.querySelector('input[type="checkbox"]') as HTMLInputElement; + jest.spyOn(textInput, "getAttribute"); + jest.spyOn(checkboxInput, "getAttribute"); + + const textInputValue = collectAutofillContentService["getPropertyOrAttribute"]( + textInput, + "value" + ); + const textInputId = collectAutofillContentService["getPropertyOrAttribute"](textInput, "id"); + const textInputBaseURI = collectAutofillContentService["getPropertyOrAttribute"]( + textInput, + "baseURI" + ); + const textInputAutofocus = collectAutofillContentService["getPropertyOrAttribute"]( + textInput, + "autofocus" + ); + const checkboxInputChecked = collectAutofillContentService["getPropertyOrAttribute"]( + checkboxInput, + "checked" + ); + + expect(textInput.getAttribute).not.toHaveBeenCalled(); + expect(checkboxInput.getAttribute).not.toHaveBeenCalled(); + expect(textInputValue).toEqual("jsmith"); + expect(textInputId).toEqual("username"); + expect(textInputBaseURI).toEqual("http://localhost/"); + expect(textInputAutofocus).toEqual(false); + expect(checkboxInputChecked).toEqual(true); + }); + + it("returns the value of the named attribute of the element if it does not exist as a property within the element", () => { + const textInput = document.querySelector("#username") as HTMLInputElement; + textInput.setAttribute("data-unique-attribute", "unique-value"); + jest.spyOn(textInput, "getAttribute"); + + const textInputUniqueAttribute = collectAutofillContentService["getPropertyOrAttribute"]( + textInput, + "data-unique-attribute" + ); + + expect(textInputUniqueAttribute).toEqual("unique-value"); + expect(textInput.getAttribute).toHaveBeenCalledWith("data-unique-attribute"); + }); + + it("returns a null value if the element does not contain the passed attribute name as either a property or attribute value", () => { + const textInput = document.querySelector("#username") as HTMLInputElement; + jest.spyOn(textInput, "getAttribute"); + + const textInputNonExistentAttribute = collectAutofillContentService["getPropertyOrAttribute"]( + textInput, + "non-existent-attribute" + ); + + expect(textInputNonExistentAttribute).toEqual(null); + expect(textInput.getAttribute).toHaveBeenCalledWith("non-existent-attribute"); + }); + }); + + describe("getElementValue", () => { + it("returns an empty string of passed input elements whose value is not set", () => { + document.body.innerHTML += ` + + + + `; + const textInput = document.querySelector("#username") as HTMLInputElement; + const checkboxInput = document.querySelector('input[type="checkbox"]') as HTMLInputElement; + const hiddenInput = document.querySelector("#hidden-input") as HTMLInputElement; + const spanInput = document.querySelector("#span-input") as HTMLInputElement; + + const textInputValue = collectAutofillContentService["getElementValue"](textInput); + const checkboxInputValue = collectAutofillContentService["getElementValue"](checkboxInput); + const hiddenInputValue = collectAutofillContentService["getElementValue"](hiddenInput); + const spanInputValue = collectAutofillContentService["getElementValue"](spanInput); + + expect(textInputValue).toEqual(""); + expect(checkboxInputValue).toEqual(""); + expect(hiddenInputValue).toEqual(""); + expect(spanInputValue).toEqual(""); + }); + + it("returns the value of the passed input element", () => { + document.body.innerHTML += ` + + + A span input value + `; + const textInput = document.querySelector("#username") as HTMLInputElement; + textInput.value = "jsmith"; + const checkboxInput = document.querySelector('input[type="checkbox"]') as HTMLInputElement; + checkboxInput.checked = true; + const hiddenInput = document.querySelector("#hidden-input") as HTMLInputElement; + hiddenInput.value = "aHiddenInputValue"; + const spanInput = document.querySelector("#span-input") as HTMLInputElement; + + const textInputValue = collectAutofillContentService["getElementValue"](textInput); + const checkboxInputValue = collectAutofillContentService["getElementValue"](checkboxInput); + const hiddenInputValue = collectAutofillContentService["getElementValue"](hiddenInput); + const spanInputValue = collectAutofillContentService["getElementValue"](spanInput); + + expect(textInputValue).toEqual("jsmith"); + expect(checkboxInputValue).toEqual("✓"); + expect(hiddenInputValue).toEqual("aHiddenInputValue"); + expect(spanInputValue).toEqual("A span input value"); + }); + + it("return the truncated value of the passed hidden input type if the value length exceeds 256 characters", () => { + document.body.innerHTML += ` + + `; + const longValueHiddenInput = document.querySelector( + "#long-value-hidden-input" + ) as HTMLInputElement; + + const longHiddenValue = + collectAutofillContentService["getElementValue"](longValueHiddenInput); + + expect(longHiddenValue).toEqual( + "’Twas brillig, and the slithy toves | Did gyre and gimble in the wabe: | All mimsy were the borogoves, | And the mome raths outgrabe. | “Beware the Jabberwock, my son! | The jaws that bite, the claws that catch! | Beware the Jubjub bird, and shun | The f...SNIPPED" + ); + }); + }); + + describe("getSelectElementOptions", () => { + it("returns the inner text and values of each `option` within the passed `select`", () => { + document.body.innerHTML = ` + + + `; + const selectWithOptions = document.querySelector("#select-with-options") as HTMLSelectElement; + const selectWithoutOptions = document.querySelector( + "#select-without-options" + ) as HTMLSelectElement; + + const selectWithOptionsOptions = + collectAutofillContentService["getSelectElementOptions"](selectWithOptions); + const selectWithoutOptionsOptions = + collectAutofillContentService["getSelectElementOptions"](selectWithoutOptions); + + expect(selectWithOptionsOptions).toEqual({ + options: [ + ["option1", "1"], + ["optionb", "b"], + ["optioniii", "iii"], + [null, "four"], + ], + }); + expect(selectWithoutOptionsOptions).toEqual({ options: [] }); + }); + }); +}); diff --git a/apps/browser/src/autofill/services/collect-autofill-content.service.ts b/apps/browser/src/autofill/services/collect-autofill-content.service.ts new file mode 100644 index 00000000000..ec7658c9863 --- /dev/null +++ b/apps/browser/src/autofill/services/collect-autofill-content.service.ts @@ -0,0 +1,578 @@ +import AutofillField from "../models/autofill-field"; +import AutofillForm from "../models/autofill-form"; +import AutofillPageDetails from "../models/autofill-page-details"; +import { + ElementWithOpId, + FillableFormFieldElement, + FormFieldElement, + FormElementWithAttribute, +} from "../types"; + +import { CollectAutofillContentService as CollectAutofillContentServiceInterface } from "./abstractions/collect-autofill-content.service"; +import DomElementVisibilityService from "./dom-element-visibility.service"; + +class CollectAutofillContentService implements CollectAutofillContentServiceInterface { + private readonly domElementVisibilityService: DomElementVisibilityService; + + constructor(domElementVisibilityService: DomElementVisibilityService) { + this.domElementVisibilityService = domElementVisibilityService; + } + + /** + * Builds the data for all the forms and fields + * that are found within the page DOM. + * @returns {Promise} + * @public + */ + async getPageDetails(): Promise { + const autofillFormsData: Record = this.buildAutofillFormsData(); + const autofillFieldsData: AutofillField[] = await this.buildAutofillFieldsData(); + + return { + title: document.title, + url: (document.defaultView || window).location.href, + documentUrl: document.location.href, + forms: autofillFormsData, + fields: autofillFieldsData, + collectedTimestamp: Date.now(), + }; + } + + /** + * Find an AutofillField element by its opid, will only return the first + * element if there are multiple elements with the same opid. If no + * element is found, null will be returned. + * @param {string} opid + * @returns {FormFieldElement | null} + */ + getAutofillFieldElementByOpid(opid: string): FormFieldElement | null { + const fieldElements = this.getAutofillFieldElements(); + const fieldElementsWithOpid = fieldElements.filter( + (fieldElement) => (fieldElement as ElementWithOpId).opid === opid + ) as ElementWithOpId[]; + + if (!fieldElementsWithOpid.length) { + const elementIndex = parseInt(opid.split("__")[1], 10); + + return fieldElements[elementIndex] || null; + } + + if (fieldElementsWithOpid.length > 1) { + // eslint-disable-next-line no-console + console.warn(`More than one element found with opid ${opid}`); + } + + return fieldElementsWithOpid[0]; + } + + /** + * Queries the DOM for all the forms elements and + * returns a collection of AutofillForm objects. + * @returns {Record} + * @private + */ + private buildAutofillFormsData(): Record { + const autofillForms: Record = {}; + const documentFormElements = document.querySelectorAll("form"); + + documentFormElements.forEach((formElement: HTMLFormElement, index: number) => { + formElement.opid = `__form__${index}`; + + autofillForms[formElement.opid] = { + opid: formElement.opid, + htmlAction: new URL( + this.getPropertyOrAttribute(formElement, "action"), + window.location.href + ).href, + htmlName: this.getPropertyOrAttribute(formElement, "name"), + htmlID: this.getPropertyOrAttribute(formElement, "id"), + htmlMethod: this.getPropertyOrAttribute(formElement, "method"), + }; + }); + + return autofillForms; + } + + /** + * Queries the DOM for all the field elements and + * returns a list of AutofillField objects. + * @returns {Promise} + * @private + */ + private async buildAutofillFieldsData(): Promise { + const autofillFieldElements = this.getAutofillFieldElements(50); + const autofillFieldDataPromises = autofillFieldElements.map(this.buildAutofillFieldItem); + + return Promise.all(autofillFieldDataPromises); + } + + /** + * Queries the DOM for all the field elements that can be autofilled, + * and returns a list limited to the given `fieldsLimit` number that + * is ordered by priority. + * @param {number} fieldsLimit - The maximum number of fields to return + * @returns {FormFieldElement[]} + * @private + */ + private getAutofillFieldElements(fieldsLimit?: number): FormFieldElement[] { + const formFieldElements: FormFieldElement[] = [ + ...(document.querySelectorAll( + 'input:not([type="hidden"]):not([type="submit"]):not([type="reset"]):not([type="button"]):not([type="image"]):not([type="file"]):not([data-bwignore]), ' + + "textarea:not([data-bwignore]), " + + "select:not([data-bwignore]), " + + "span[data-bwautofill]" + ) as NodeListOf), + ]; + + if (!fieldsLimit || formFieldElements.length <= fieldsLimit) { + return formFieldElements; + } + + const priorityFormFields: FormFieldElement[] = []; + const unimportantFormFields: FormFieldElement[] = []; + const unimportantFieldTypesSet = new Set(["checkbox", "radio"]); + for (const element of formFieldElements) { + if (priorityFormFields.length >= fieldsLimit) { + return priorityFormFields; + } + + const fieldType = this.getPropertyOrAttribute(element, "type")?.toLowerCase(); + if (unimportantFieldTypesSet.has(fieldType)) { + unimportantFormFields.push(element); + continue; + } + + priorityFormFields.push(element); + } + + const numberUnimportantFieldsToInclude = fieldsLimit - priorityFormFields.length; + for (let index = 0; index < numberUnimportantFieldsToInclude; index++) { + priorityFormFields.push(unimportantFormFields[index]); + } + + return priorityFormFields; + } + + /** + * Builds an AutofillField object from the given form element. Will only return + * shared field values if the element is a span element. Will not return any label + * values if the element is a hidden input element. + * @param {ElementWithOpId} element + * @param {number} index + * @returns {Promise} + * @private + */ + private buildAutofillFieldItem = async ( + element: ElementWithOpId, + index: number + ): Promise => { + element.opid = `__${index}`; + + const autofillFieldBase = { + opid: element.opid, + elementNumber: index, + maxLength: this.getAutofillFieldMaxLength(element), + viewable: await this.domElementVisibilityService.isFormFieldViewable(element), + htmlID: this.getPropertyOrAttribute(element, "id"), + htmlName: this.getPropertyOrAttribute(element, "name"), + htmlClass: this.getPropertyOrAttribute(element, "class"), + tabindex: this.getPropertyOrAttribute(element, "tabindex"), + title: this.getPropertyOrAttribute(element, "title"), + tagName: this.getPropertyOrAttribute(element, "tagName")?.toLowerCase(), + }; + + if (element instanceof HTMLSpanElement) { + return autofillFieldBase; + } + + let autofillFieldLabels = {}; + const autoCompleteType = + this.getPropertyOrAttribute(element, "x-autocompletetype") || + this.getPropertyOrAttribute(element, "autocompletetype") || + this.getPropertyOrAttribute(element, "autocomplete"); + const elementType = this.getPropertyOrAttribute(element, "type")?.toLowerCase(); + if (elementType !== "hidden") { + autofillFieldLabels = { + "label-tag": this.createAutofillFieldLabelTag(element), + "label-data": this.getPropertyOrAttribute(element, "data-label"), + "label-aria": this.getPropertyOrAttribute(element, "aria-label"), + "label-top": this.createAutofillFieldTopLabel(element), + "label-right": this.createAutofillFieldRightLabel(element), + "label-left": this.createAutofillFieldLeftLabel(element), + placeholder: this.getPropertyOrAttribute(element, "placeholder"), + }; + } + + return { + ...autofillFieldBase, + ...autofillFieldLabels, + rel: this.getPropertyOrAttribute(element, "rel"), + type: elementType, + value: this.getElementValue(element), + checked: Boolean(this.getPropertyOrAttribute(element, "checked")), + autoCompleteType: autoCompleteType !== "off" ? autoCompleteType : null, + disabled: Boolean(this.getPropertyOrAttribute(element, "disabled")), + readonly: Boolean(this.getPropertyOrAttribute(element, "readOnly")), + selectInfo: + element instanceof HTMLSelectElement ? this.getSelectElementOptions(element) : null, + form: element.form ? this.getPropertyOrAttribute(element.form, "opid") : null, + "aria-hidden": this.getPropertyOrAttribute(element, "aria-hidden") === "true", + "aria-disabled": this.getPropertyOrAttribute(element, "aria-disabled") === "true", + "aria-haspopup": this.getPropertyOrAttribute(element, "aria-haspopup") === "true", + "data-stripe": this.getPropertyOrAttribute(element, "data-stripe"), + }; + }; + + /** + * Creates a label tag used to autofill the element pulled from a label + * associated with the element's id, name, parent element or from an + * associated description term element if no other labels can be found. + * Returns a string containing all the `textContent` or `innerText` + * values of the label elements. + * @param {FillableFormFieldElement} element + * @returns {string} + * @private + */ + private createAutofillFieldLabelTag(element: FillableFormFieldElement): string { + const labelElementsSet: Set = new Set(element.labels); + + if (labelElementsSet.size) { + return this.createLabelElementsTag(labelElementsSet); + } + + const labelElements: NodeListOf | null = this.queryElementLabels(element); + labelElements?.forEach((labelElement) => labelElementsSet.add(labelElement)); + + let currentElement: HTMLElement | null = element; + while (currentElement && currentElement !== document.documentElement) { + if (currentElement instanceof HTMLLabelElement) { + labelElementsSet.add(currentElement); + } + + currentElement = currentElement.parentElement.closest("label"); + } + + if ( + !labelElementsSet.size && + element.parentElement?.tagName.toLowerCase() === "dd" && + element.parentElement.previousElementSibling?.tagName.toLowerCase() === "dt" + ) { + labelElementsSet.add(element.parentElement.previousElementSibling as HTMLElement); + } + + return this.createLabelElementsTag(labelElementsSet); + } + + /** + * Queries the DOM for label elements associated with the given element + * by id or name. Returns a NodeList of label elements or null if none + * are found. + * @param {FillableFormFieldElement} element + * @returns {NodeListOf | null} + * @private + */ + private queryElementLabels( + element: FillableFormFieldElement + ): NodeListOf | null { + let labelQuerySelectors = element.id ? `label[for="${element.id}"]` : ""; + if (element.name) { + const forElementNameSelector = `label[for="${element.name}"]`; + labelQuerySelectors = labelQuerySelectors + ? `${labelQuerySelectors}, ${forElementNameSelector}` + : forElementNameSelector; + } + + if (!labelQuerySelectors) { + return null; + } + + return document.querySelectorAll(labelQuerySelectors); + } + + /** + * Map over all the label elements and creates a + * string of the text content of each label element. + * @param {Set} labelElementsSet + * @returns {string} + * @private + */ + private createLabelElementsTag = (labelElementsSet: Set): string => { + return [...labelElementsSet] + .map((labelElement) => { + const textContent: string | null = labelElement + ? labelElement.textContent || labelElement.innerText + : null; + + return this.trimAndRemoveNonPrintableText(textContent || ""); + }) + .join(""); + }; + + /** + * Gets the maxLength property of the passed FormFieldElement and + * returns the value or null if the element does not have a + * maxLength property. If the element has a maxLength property + * greater than 999, it will return 999. + * @param {FormFieldElement} element + * @returns {number | null} + * @private + */ + private getAutofillFieldMaxLength(element: FormFieldElement): number | null { + const elementHasMaxLengthProperty = + element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement; + const elementMaxLength = + elementHasMaxLengthProperty && element.maxLength > -1 ? element.maxLength : 999; + + return elementHasMaxLengthProperty ? Math.min(elementMaxLength, 999) : null; + } + + /** + * Iterates over the next siblings of the passed element and + * returns a string of the text content of each element. Will + * stop iterating if it encounters a new section element. + * @param {FormFieldElement} element + * @returns {string} + * @private + */ + private createAutofillFieldRightLabel(element: FormFieldElement): string { + const labelTextContent: string[] = []; + let currentElement: ChildNode = element; + + while (currentElement && currentElement.nextSibling) { + currentElement = currentElement.nextSibling; + if (this.isNewSectionElement(currentElement)) { + break; + } + + const textContent = this.getTextContentFromElement(currentElement); + if (textContent) { + labelTextContent.push(textContent); + } + } + + return labelTextContent.join(""); + } + + /** + * Recursively gets the text content from an element's previous siblings + * and returns a string of the text content of each element. + * @param {FormFieldElement} element + * @returns {string} + * @private + */ + private createAutofillFieldLeftLabel(element: FormFieldElement): string { + const labelTextContent: string[] = this.recursivelyGetTextFromPreviousSiblings(element); + + return labelTextContent.reverse().join(""); + } + + /** + * Assumes that the input elements that are to be autofilled are within a + * table structure. Queries the previous sibling of the parent row that + * the input element is in and returns the text content of the cell that + * is in the same column as the input element. + * @param {FormFieldElement} element + * @returns {string | null} + * @private + */ + private createAutofillFieldTopLabel(element: FormFieldElement): string | null { + const tableDataElement = element.closest("td"); + if (!tableDataElement) { + return null; + } + + const tableDataElementIndex = tableDataElement.cellIndex; + const parentSiblingTableRowElement = tableDataElement.closest("tr") + ?.previousElementSibling as HTMLTableRowElement; + + return parentSiblingTableRowElement?.cells?.length > tableDataElementIndex + ? this.getTextContentFromElement(parentSiblingTableRowElement.cells[tableDataElementIndex]) + : null; + } + + /** + * Check if the element's tag indicates that a transition to a new section of the + * page is occurring. If so, we should not use the element or its children in order + * to get autofill context for the previous element. + * @param {HTMLElement} currentElement + * @returns {boolean} + * @private + */ + private isNewSectionElement(currentElement: HTMLElement | Node): boolean { + if (!currentElement) { + return true; + } + + const transitionalElementTagsSet = new Set([ + "html", + "body", + "button", + "form", + "head", + "iframe", + "input", + "option", + "script", + "select", + "table", + "textarea", + ]); + return ( + "tagName" in currentElement && + transitionalElementTagsSet.has(currentElement.tagName.toLowerCase()) + ); + } + + /** + * Gets the text content from a passed element, regardless of whether it is a + * text node, an element node or an HTMLElement. + * @param {Node | HTMLElement} element + * @returns {string} + * @private + */ + private getTextContentFromElement(element: Node | HTMLElement): string { + if (element.nodeType === Node.TEXT_NODE) { + return this.trimAndRemoveNonPrintableText(element.nodeValue); + } + + return this.trimAndRemoveNonPrintableText( + element.textContent || (element as HTMLElement).innerText + ); + } + + /** + * Removes non-printable characters from the passed text + * content and trims leading and trailing whitespace. + * @param {string} textContent + * @returns {string} + * @private + */ + private trimAndRemoveNonPrintableText(textContent: string): string { + return (textContent || "") + .replace(/[^\x20-\x7E]+|\s+/g, " ") // Strip out non-primitive characters and replace multiple spaces with a single space + .trim(); // Trim leading and trailing whitespace + } + + /** + * Get the text content from the previous siblings of the element. If + * no text content is found, recursively get the text content from the + * previous siblings of the parent element. + * @param {FormFieldElement} element + * @returns {string[]} + * @private + */ + private recursivelyGetTextFromPreviousSiblings(element: Node | HTMLElement): string[] { + const textContentItems: string[] = []; + let currentElement = element; + while (currentElement && currentElement.previousSibling) { + // Ensure we are capturing text content from nodes and elements. + currentElement = currentElement.previousSibling; + + if (this.isNewSectionElement(currentElement)) { + return textContentItems; + } + + const textContent = this.getTextContentFromElement(currentElement); + if (textContent) { + textContentItems.push(textContent); + } + } + + if (!currentElement || textContentItems.length) { + return textContentItems; + } + + // Prioritize capturing text content from elements rather than nodes. + currentElement = currentElement.parentElement || currentElement.parentNode; + + let siblingElement = + currentElement instanceof HTMLElement + ? currentElement.previousElementSibling + : currentElement.previousSibling; + while (siblingElement?.lastChild && !this.isNewSectionElement(siblingElement)) { + siblingElement = siblingElement.lastChild; + } + + if (this.isNewSectionElement(siblingElement)) { + return textContentItems; + } + + const textContent = this.getTextContentFromElement(siblingElement); + if (textContent) { + textContentItems.push(textContent); + return textContentItems; + } + + return this.recursivelyGetTextFromPreviousSiblings(siblingElement); + } + + /** + * Get the value of a property or attribute from a FormFieldElement. + * @param {HTMLElement} element + * @param {string} attributeName + * @returns {string | null} + * @private + */ + private getPropertyOrAttribute(element: HTMLElement, attributeName: string): string | null { + if (attributeName in element) { + return (element as FormElementWithAttribute)[attributeName]; + } + + return element.getAttribute(attributeName); + } + + /** + * Gets the value of the element. If the element is a checkbox, returns a checkmark if the + * checkbox is checked, or an empty string if it is not checked. If the element is a hidden + * input, returns the value of the input if it is less than 254 characters, or a truncated + * value if it is longer than 254 characters. + * @param {FormFieldElement} element + * @returns {string} + * @private + */ + private getElementValue(element: FormFieldElement): string { + if (element instanceof HTMLSpanElement) { + const spanTextContent = element.textContent || element.innerText; + return spanTextContent || ""; + } + + const elementValue = element.value || ""; + const elementType = String(element.type).toLowerCase(); + if ("checked" in element && elementType === "checkbox") { + return element.checked ? "✓" : ""; + } + + if (elementType === "hidden") { + const inputValueMaxLength = 254; + + return elementValue.length > inputValueMaxLength + ? `${elementValue.substring(0, inputValueMaxLength)}...SNIPPED` + : elementValue; + } + + return elementValue; + } + + /** + * Get the options from a select element and return them as an array + * of arrays indicating the select element option text and value. + * @param {HTMLSelectElement} element + * @returns {{options: (string | null)[][]}} + * @private + */ + private getSelectElementOptions(element: HTMLSelectElement): { options: (string | null)[][] } { + const options = [...element.options].map((option) => { + const optionText = option.text + ? String(option.text) + .toLowerCase() + .replace(/[\s~`!@$%^&#*()\-_+=:;'"[\]|\\,<.>?]/gm, "") // Remove whitespace and punctuation + : null; + + return [optionText, option.value]; + }); + + return { options }; + } +} + +export default CollectAutofillContentService; diff --git a/apps/browser/src/autofill/services/dom-element-visibility.service.spec.ts b/apps/browser/src/autofill/services/dom-element-visibility.service.spec.ts new file mode 100644 index 00000000000..e17783b7a65 --- /dev/null +++ b/apps/browser/src/autofill/services/dom-element-visibility.service.spec.ts @@ -0,0 +1,409 @@ +import { FormFieldElement } from "../types"; + +import DomElementVisibilityService from "./dom-element-visibility.service"; + +function createBoundingClientRectMock(customProperties: Partial = {}): DOMRectReadOnly { + return { + top: 0, + bottom: 0, + left: 0, + right: 0, + width: 500, + height: 500, + x: 0, + y: 0, + toJSON: jest.fn(), + ...customProperties, + }; +} + +describe("DomElementVisibilityService", () => { + let domElementVisibilityService: DomElementVisibilityService; + + beforeEach(() => { + document.body.innerHTML = ` +
    + + + + +
    + `; + domElementVisibilityService = new DomElementVisibilityService(); + }); + + afterEach(() => { + jest.clearAllMocks(); + document.body.innerHTML = ""; + }); + + describe("isFormFieldViewable", () => { + it("returns false if the element is outside viewport bounds", async () => { + const usernameElement = document.querySelector("input[name='username']") as FormFieldElement; + jest.spyOn(usernameElement, "getBoundingClientRect"); + jest + .spyOn(domElementVisibilityService as any, "isElementOutsideViewportBounds") + .mockResolvedValueOnce(true); + jest.spyOn(domElementVisibilityService, "isElementHiddenByCss"); + jest.spyOn(domElementVisibilityService as any, "formFieldIsNotHiddenBehindAnotherElement"); + + const isFormFieldViewable = await domElementVisibilityService.isFormFieldViewable( + usernameElement + ); + + expect(isFormFieldViewable).toEqual(false); + expect(usernameElement.getBoundingClientRect).toHaveBeenCalled(); + expect(domElementVisibilityService["isElementOutsideViewportBounds"]).toHaveBeenCalledWith( + usernameElement, + usernameElement.getBoundingClientRect() + ); + expect(domElementVisibilityService["isElementHiddenByCss"]).not.toHaveBeenCalled(); + expect( + domElementVisibilityService["formFieldIsNotHiddenBehindAnotherElement"] + ).not.toHaveBeenCalled(); + }); + + it("returns false if the element is hidden by CSS", async () => { + const usernameElement = document.querySelector("input[name='username']") as FormFieldElement; + jest.spyOn(usernameElement, "getBoundingClientRect"); + jest + .spyOn(domElementVisibilityService as any, "isElementOutsideViewportBounds") + .mockReturnValueOnce(false); + jest.spyOn(domElementVisibilityService, "isElementHiddenByCss").mockReturnValueOnce(true); + jest.spyOn(domElementVisibilityService as any, "formFieldIsNotHiddenBehindAnotherElement"); + + const isFormFieldViewable = await domElementVisibilityService.isFormFieldViewable( + usernameElement + ); + + expect(isFormFieldViewable).toEqual(false); + expect(usernameElement.getBoundingClientRect).toHaveBeenCalled(); + expect(domElementVisibilityService["isElementOutsideViewportBounds"]).toHaveBeenCalledWith( + usernameElement, + usernameElement.getBoundingClientRect() + ); + expect(domElementVisibilityService["isElementHiddenByCss"]).toHaveBeenCalledWith( + usernameElement + ); + expect( + domElementVisibilityService["formFieldIsNotHiddenBehindAnotherElement"] + ).not.toHaveBeenCalled(); + }); + + it("returns false if the element is hidden behind another element", async () => { + const usernameElement = document.querySelector("input[name='username']") as FormFieldElement; + jest.spyOn(usernameElement, "getBoundingClientRect"); + jest + .spyOn(domElementVisibilityService as any, "isElementOutsideViewportBounds") + .mockReturnValueOnce(false); + jest.spyOn(domElementVisibilityService, "isElementHiddenByCss").mockReturnValueOnce(false); + jest + .spyOn(domElementVisibilityService as any, "formFieldIsNotHiddenBehindAnotherElement") + .mockReturnValueOnce(false); + + const isFormFieldViewable = await domElementVisibilityService.isFormFieldViewable( + usernameElement + ); + + expect(isFormFieldViewable).toEqual(false); + expect(usernameElement.getBoundingClientRect).toHaveBeenCalled(); + expect(domElementVisibilityService["isElementOutsideViewportBounds"]).toHaveBeenCalledWith( + usernameElement, + usernameElement.getBoundingClientRect() + ); + expect(domElementVisibilityService["isElementHiddenByCss"]).toHaveBeenCalledWith( + usernameElement + ); + expect( + domElementVisibilityService["formFieldIsNotHiddenBehindAnotherElement"] + ).toHaveBeenCalledWith(usernameElement, usernameElement.getBoundingClientRect()); + }); + + it("returns true if the form field is viewable", async () => { + const usernameElement = document.querySelector("input[name='username']") as FormFieldElement; + jest.spyOn(usernameElement, "getBoundingClientRect"); + jest + .spyOn(domElementVisibilityService as any, "isElementOutsideViewportBounds") + .mockReturnValueOnce(false); + jest.spyOn(domElementVisibilityService, "isElementHiddenByCss").mockReturnValueOnce(false); + jest + .spyOn(domElementVisibilityService as any, "formFieldIsNotHiddenBehindAnotherElement") + .mockReturnValueOnce(true); + + const isFormFieldViewable = await domElementVisibilityService.isFormFieldViewable( + usernameElement + ); + + expect(isFormFieldViewable).toEqual(true); + expect(usernameElement.getBoundingClientRect).toHaveBeenCalled(); + expect(domElementVisibilityService["isElementOutsideViewportBounds"]).toHaveBeenCalledWith( + usernameElement, + usernameElement.getBoundingClientRect() + ); + expect(domElementVisibilityService["isElementHiddenByCss"]).toHaveBeenCalledWith( + usernameElement + ); + expect( + domElementVisibilityService["formFieldIsNotHiddenBehindAnotherElement"] + ).toHaveBeenCalledWith(usernameElement, usernameElement.getBoundingClientRect()); + }); + }); + + describe("isElementHiddenByCss", () => { + it("returns true when a non-hidden element is passed", () => { + document.body.innerHTML = ` + + `; + const usernameElement = document.getElementById("username"); + + const isElementHidden = domElementVisibilityService["isElementHiddenByCss"](usernameElement); + + expect(isElementHidden).toEqual(false); + }); + + it("returns true when the element has a `visibility: hidden;` CSS rule applied to it either inline or in a computed style", () => { + document.body.innerHTML = ` + + + + `; + const usernameElement = document.getElementById("username"); + const passwordElement = document.getElementById("password"); + jest.spyOn(usernameElement.style, "getPropertyValue"); + jest.spyOn(usernameElement.ownerDocument.defaultView, "getComputedStyle"); + jest.spyOn(passwordElement.style, "getPropertyValue"); + jest.spyOn(passwordElement.ownerDocument.defaultView, "getComputedStyle"); + + const isUsernameElementHidden = + domElementVisibilityService["isElementHiddenByCss"](usernameElement); + const isPasswordElementHidden = + domElementVisibilityService["isElementHiddenByCss"](passwordElement); + + expect(isUsernameElementHidden).toEqual(true); + expect(usernameElement.style.getPropertyValue).toHaveBeenCalled(); + expect(usernameElement.ownerDocument.defaultView.getComputedStyle).toHaveBeenCalledWith( + usernameElement + ); + expect(isPasswordElementHidden).toEqual(true); + expect(passwordElement.style.getPropertyValue).toHaveBeenCalled(); + expect(passwordElement.ownerDocument.defaultView.getComputedStyle).toHaveBeenCalledWith( + passwordElement + ); + }); + + it("returns true when the element has a `display: none;` CSS rule applied to it either inline or in a computed style", () => { + document.body.innerHTML = ` + + + + `; + const usernameElement = document.getElementById("username"); + const passwordElement = document.getElementById("password"); + + const isUsernameElementHidden = + domElementVisibilityService["isElementHiddenByCss"](usernameElement); + const isPasswordElementHidden = + domElementVisibilityService["isElementHiddenByCss"](passwordElement); + + expect(isUsernameElementHidden).toEqual(true); + expect(isPasswordElementHidden).toEqual(true); + }); + + it("returns true when the element has a `opacity: 0;` CSS rule applied to it either inline or in a computed style", () => { + document.body.innerHTML = ` + + + + `; + const usernameElement = document.getElementById("username"); + const passwordElement = document.getElementById("password"); + + const isUsernameElementHidden = + domElementVisibilityService["isElementHiddenByCss"](usernameElement); + const isPasswordElementHidden = + domElementVisibilityService["isElementHiddenByCss"](passwordElement); + + expect(isUsernameElementHidden).toEqual(true); + expect(isPasswordElementHidden).toEqual(true); + }); + + it("returns true when the element has a `clip-path` CSS rule applied to it that hides the element either inline or in a computed style", () => { + document.body.innerHTML = ` + + + + + `; + }); + }); + + describe("isElementOutsideViewportBounds", () => { + const mockViewportWidth = 1920; + const mockViewportHeight = 1080; + + beforeEach(() => { + Object.defineProperty(document.documentElement, "scrollWidth", { + writable: true, + value: mockViewportWidth, + }); + Object.defineProperty(document.documentElement, "scrollHeight", { + writable: true, + value: mockViewportHeight, + }); + }); + + it("returns true if the passed element's size is not sufficient for visibility", () => { + const usernameElement = document.querySelector("input[name='username']") as FormFieldElement; + const elementBoundingClientRect = createBoundingClientRectMock({ + width: 9, + height: 9, + }); + + const isElementOutsideViewportBounds = domElementVisibilityService[ + "isElementOutsideViewportBounds" + ](usernameElement, elementBoundingClientRect); + + expect(isElementOutsideViewportBounds).toEqual(true); + }); + + it("returns true if the passed element is overflowing the left viewport", () => { + const usernameElement = document.querySelector("input[name='username']") as FormFieldElement; + const elementBoundingClientRect = createBoundingClientRectMock({ + left: -1, + }); + + const isElementOutsideViewportBounds = domElementVisibilityService[ + "isElementOutsideViewportBounds" + ](usernameElement, elementBoundingClientRect); + + expect(isElementOutsideViewportBounds).toEqual(true); + }); + + it("returns true if the passed element is overflowing the right viewport", () => { + const usernameElement = document.querySelector("input[name='username']") as FormFieldElement; + const elementBoundingClientRect = createBoundingClientRectMock({ + left: mockViewportWidth + 1, + }); + + const isElementOutsideViewportBounds = domElementVisibilityService[ + "isElementOutsideViewportBounds" + ](usernameElement, elementBoundingClientRect); + + expect(isElementOutsideViewportBounds).toEqual(true); + }); + + it("returns true if the passed element is overflowing the top viewport", () => { + const usernameElement = document.querySelector("input[name='username']") as FormFieldElement; + const elementBoundingClientRect = createBoundingClientRectMock({ + top: -1, + }); + + const isElementOutsideViewportBounds = domElementVisibilityService[ + "isElementOutsideViewportBounds" + ](usernameElement, elementBoundingClientRect); + + expect(isElementOutsideViewportBounds).toEqual(true); + }); + + it("returns true if the passed element is overflowing the bottom viewport", () => { + const usernameElement = document.querySelector("input[name='username']") as FormFieldElement; + const elementBoundingClientRect = createBoundingClientRectMock({ + top: mockViewportHeight + 1, + }); + + const isElementOutsideViewportBounds = domElementVisibilityService[ + "isElementOutsideViewportBounds" + ](usernameElement, elementBoundingClientRect); + + expect(isElementOutsideViewportBounds).toEqual(true); + }); + + it("returns false if the passed element is not outside of the viewport bounds", () => { + const usernameElement = document.querySelector("input[name='username']") as FormFieldElement; + const elementBoundingClientRect = createBoundingClientRectMock({}); + + const isElementOutsideViewportBounds = domElementVisibilityService[ + "isElementOutsideViewportBounds" + ](usernameElement, elementBoundingClientRect); + + expect(isElementOutsideViewportBounds).toEqual(false); + }); + }); + + describe("formFieldIsNotHiddenBehindAnotherElement", () => { + it("returns true if the element found at the center point of the passed targetElement is the targetElement itself", () => { + const usernameElement = document.querySelector("input[name='username']") as FormFieldElement; + jest.spyOn(usernameElement, "getBoundingClientRect"); + document.elementFromPoint = jest.fn(() => usernameElement); + + const formFieldIsNotHiddenBehindAnotherElement = + domElementVisibilityService["formFieldIsNotHiddenBehindAnotherElement"](usernameElement); + + expect(formFieldIsNotHiddenBehindAnotherElement).toEqual(true); + expect(document.elementFromPoint).toHaveBeenCalled(); + expect(usernameElement.getBoundingClientRect).toHaveBeenCalled(); + }); + + it("returns true if the element found at the center point of the passed targetElement is an implicit label of the element", () => { + document.body.innerHTML = ` + + `; + const usernameElement = document.querySelector("input[name='username']") as FormFieldElement; + const labelTextElement = document.querySelector("span"); + document.elementFromPoint = jest.fn(() => labelTextElement); + + const formFieldIsNotHiddenBehindAnotherElement = + domElementVisibilityService["formFieldIsNotHiddenBehindAnotherElement"](usernameElement); + + expect(formFieldIsNotHiddenBehindAnotherElement).toEqual(true); + }); + + it("returns true if the element found at the center point of the passed targetElement is a label of the targetElement", () => { + const usernameElement = document.querySelector("input[name='username']") as FormFieldElement; + const labelElement = document.querySelector("label[for='username']") as FormFieldElement; + const mockBoundingRect = createBoundingClientRectMock({}); + jest.spyOn(usernameElement, "getBoundingClientRect"); + document.elementFromPoint = jest.fn(() => labelElement); + + const formFieldIsNotHiddenBehindAnotherElement = domElementVisibilityService[ + "formFieldIsNotHiddenBehindAnotherElement" + ](usernameElement, mockBoundingRect); + + expect(formFieldIsNotHiddenBehindAnotherElement).toEqual(true); + expect(document.elementFromPoint).toHaveBeenCalledWith( + mockBoundingRect.left + mockBoundingRect.width / 2, + mockBoundingRect.top + mockBoundingRect.height / 2 + ); + expect(usernameElement.getBoundingClientRect).not.toHaveBeenCalled(); + }); + + it("returns false if the element found at the center point is not the passed targetElement or a label of that element", () => { + const usernameElement = document.querySelector("input[name='username']") as FormFieldElement; + document.elementFromPoint = jest.fn(() => document.createElement("div")); + + const formFieldIsNotHiddenBehindAnotherElement = + domElementVisibilityService["formFieldIsNotHiddenBehindAnotherElement"](usernameElement); + + expect(formFieldIsNotHiddenBehindAnotherElement).toEqual(false); + }); + }); +}); diff --git a/apps/browser/src/autofill/services/dom-element-visibility.service.ts b/apps/browser/src/autofill/services/dom-element-visibility.service.ts new file mode 100644 index 00000000000..4be59d7f276 --- /dev/null +++ b/apps/browser/src/autofill/services/dom-element-visibility.service.ts @@ -0,0 +1,199 @@ +import { FillableFormFieldElement, FormFieldElement } from "../types"; + +import { DomElementVisibilityService as domElementVisibilityServiceInterface } from "./abstractions/dom-element-visibility.service"; + +class DomElementVisibilityService implements domElementVisibilityServiceInterface { + private cachedComputedStyle: CSSStyleDeclaration | null = null; + + /** + * Checks if a form field is viewable. This is done by checking if the element is within the + * viewport bounds, not hidden by CSS, and not hidden behind another element. + * @param {FormFieldElement} element + * @returns {Promise} + */ + async isFormFieldViewable(element: FormFieldElement): Promise { + const elementBoundingClientRect = element.getBoundingClientRect(); + + if ( + this.isElementOutsideViewportBounds(element, elementBoundingClientRect) || + this.isElementHiddenByCss(element) + ) { + return false; + } + + return this.formFieldIsNotHiddenBehindAnotherElement(element, elementBoundingClientRect); + } + + /** + * Check if the target element is hidden using CSS. This is done by checking the opacity, display, + * visibility, and clip-path CSS properties of the element. We also check the opacity of all + * parent elements to ensure that the target element is not hidden by a parent element. + * @param {HTMLElement} element + * @returns {boolean} + * @public + */ + isElementHiddenByCss(element: HTMLElement): boolean { + this.cachedComputedStyle = null; + + if ( + this.isElementInvisible(element) || + this.isElementNotDisplayed(element) || + this.isElementNotVisible(element) || + this.isElementClipped(element) + ) { + return true; + } + + let parentElement = element.parentElement; + while (parentElement && parentElement !== element.ownerDocument.documentElement) { + this.cachedComputedStyle = null; + if (this.isElementInvisible(parentElement)) { + return true; + } + + parentElement = parentElement.parentElement; + } + + return false; + } + + /** + * Gets the computed style of a given element, will only calculate the computed + * style if the element's style has not been previously cached. + * @param {HTMLElement} element + * @param {string} styleProperty + * @returns {string} + * @private + */ + private getElementStyle(element: HTMLElement, styleProperty: string): string { + if (!this.cachedComputedStyle) { + this.cachedComputedStyle = (element.ownerDocument.defaultView || window).getComputedStyle( + element + ); + } + + return this.cachedComputedStyle.getPropertyValue(styleProperty); + } + + /** + * Checks if the opacity of the target element is less than 0.1. + * @param {HTMLElement} element + * @returns {boolean} + * @private + */ + private isElementInvisible(element: HTMLElement): boolean { + return parseFloat(this.getElementStyle(element, "opacity")) < 0.1; + } + + /** + * Checks if the target element has a display property of none. + * @param {HTMLElement} element + * @returns {boolean} + * @private + */ + private isElementNotDisplayed(element: HTMLElement): boolean { + return this.getElementStyle(element, "display") === "none"; + } + + /** + * Checks if the target element has a visibility property of hidden or collapse. + * @param {HTMLElement} element + * @returns {boolean} + * @private + */ + private isElementNotVisible(element: HTMLElement): boolean { + return new Set(["hidden", "collapse"]).has(this.getElementStyle(element, "visibility")); + } + + /** + * Checks if the target element has a clip-path property that hides the element. + * @param {HTMLElement} element + * @returns {boolean} + * @private + */ + private isElementClipped(element: HTMLElement): boolean { + return new Set([ + "inset(50%)", + "inset(100%)", + "circle(0)", + "circle(0px)", + "circle(0px at 50% 50%)", + "polygon(0 0, 0 0, 0 0, 0 0)", + "polygon(0px 0px, 0px 0px, 0px 0px, 0px 0px)", + ]).has(this.getElementStyle(element, "clipPath")); + } + + /** + * Checks if the target element is outside the viewport bounds. This is done by checking if the + * element is too small or is overflowing the viewport bounds. + * @param {HTMLElement} targetElement + * @param {DOMRectReadOnly | null} targetElementBoundingClientRect + * @returns {boolean} + * @private + */ + private isElementOutsideViewportBounds( + targetElement: HTMLElement, + targetElementBoundingClientRect: DOMRectReadOnly | null = null + ): boolean { + const documentElement = targetElement.ownerDocument.documentElement; + const documentElementWidth = documentElement.scrollWidth; + const documentElementHeight = documentElement.scrollHeight; + const elementBoundingClientRect = + targetElementBoundingClientRect || targetElement.getBoundingClientRect(); + const elementTopOffset = elementBoundingClientRect.top - documentElement.clientTop; + const elementLeftOffset = elementBoundingClientRect.left - documentElement.clientLeft; + + const isElementSizeInsufficient = + elementBoundingClientRect.width < 10 || elementBoundingClientRect.height < 10; + const isElementOverflowingLeftViewport = elementLeftOffset < 0; + const isElementOverflowingRightViewport = + elementLeftOffset + elementBoundingClientRect.width > documentElementWidth; + const isElementOverflowingTopViewport = elementTopOffset < 0; + const isElementOverflowingBottomViewport = + elementTopOffset + elementBoundingClientRect.height > documentElementHeight; + + return ( + isElementSizeInsufficient || + isElementOverflowingLeftViewport || + isElementOverflowingRightViewport || + isElementOverflowingTopViewport || + isElementOverflowingBottomViewport + ); + } + + /** + * Checks if a passed FormField is not hidden behind another element. This is done by + * checking if the element at the center point of the FormField is the FormField itself + * or one of its labels. + * @param {FormFieldElement} targetElement + * @param {DOMRectReadOnly | null} targetElementBoundingClientRect + * @returns {boolean} + * @private + */ + private formFieldIsNotHiddenBehindAnotherElement( + targetElement: FormFieldElement, + targetElementBoundingClientRect: DOMRectReadOnly | null = null + ): boolean { + const elementBoundingClientRect = + targetElementBoundingClientRect || targetElement.getBoundingClientRect(); + const elementAtCenterPoint = targetElement.ownerDocument.elementFromPoint( + elementBoundingClientRect.left + elementBoundingClientRect.width / 2, + elementBoundingClientRect.top + elementBoundingClientRect.height / 2 + ); + + if (elementAtCenterPoint === targetElement) { + return true; + } + + const targetElementLabelsSet = new Set((targetElement as FillableFormFieldElement).labels); + if (targetElementLabelsSet.has(elementAtCenterPoint as HTMLLabelElement)) { + return true; + } + + const closestParentLabel = elementAtCenterPoint?.parentElement?.closest("label"); + + return targetElementLabelsSet.has(closestParentLabel); + } +} + +export default DomElementVisibilityService; diff --git a/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts b/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts new file mode 100644 index 00000000000..828d768ca25 --- /dev/null +++ b/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts @@ -0,0 +1,1047 @@ +import { EVENTS } from "../constants"; +import AutofillScript, { FillScript, FillScriptActions } from "../models/autofill-script"; +import { FillableFormFieldElement, FormElementWithAttribute, FormFieldElement } from "../types"; + +import CollectAutofillContentService from "./collect-autofill-content.service"; +import DomElementVisibilityService from "./dom-element-visibility.service"; +import InsertAutofillContentService from "./insert-autofill-content.service"; + +const mockLoginForm = ` +
    +
    + + +
    +
    +`; + +const eventsToTest = [ + EVENTS.CHANGE, + EVENTS.INPUT, + EVENTS.KEYDOWN, + EVENTS.KEYPRESS, + EVENTS.KEYUP, + "blur", + "click", + "focus", + "focusin", + "focusout", + "mousedown", + "paste", + "select", + "selectionchange", + "touchend", + "touchstart", +]; + +const initEventCount = Object.freeze( + eventsToTest.reduce( + (eventCounts, eventName) => ({ + ...eventCounts, + [eventName]: 0, + }), + {} + ) +); + +let confirmSpy: jest.SpyInstance; +let windowSpy: jest.SpyInstance; +let savedURLs: string[] | null = ["https://bitwarden.com"]; +function setMockWindowLocation({ + protocol, + hostname, +}: { + protocol: "http:" | "https:"; + hostname: string; +}) { + windowSpy.mockImplementation(() => ({ + location: { + protocol, + hostname, + }, + })); +} + +describe("InsertAutofillContentService", () => { + const domElementVisibilityService = new DomElementVisibilityService(); + const collectAutofillContentService = new CollectAutofillContentService( + domElementVisibilityService + ); + let insertAutofillContentService: InsertAutofillContentService; + let fillScript: AutofillScript; + + beforeEach(() => { + document.body.innerHTML = mockLoginForm; + confirmSpy = jest.spyOn(window, "confirm"); + windowSpy = jest.spyOn(window, "window", "get"); + insertAutofillContentService = new InsertAutofillContentService( + domElementVisibilityService, + collectAutofillContentService + ); + fillScript = { + script: [ + ["click_on_opid", "username"], + ["focus_by_opid", "username"], + ["fill_by_opid", "username", "test"], + ], + properties: { + delay_between_operations: 20, + }, + metadata: {}, + autosubmit: null, + savedUrls: ["https://bitwarden.com"], + untrustedIframe: false, + itemType: "login", + }; + }); + + afterEach(() => { + jest.resetAllMocks(); + windowSpy.mockRestore(); + confirmSpy.mockRestore(); + document.body.innerHTML = ""; + }); + + describe("fillForm", () => { + it("returns early if the passed fill script does not have a script property", () => { + fillScript.script = []; + jest.spyOn(insertAutofillContentService as any, "fillingWithinSandboxedIframe"); + jest.spyOn(insertAutofillContentService as any, "userCancelledInsecureUrlAutofill"); + jest.spyOn(insertAutofillContentService as any, "userCancelledUntrustedIframeAutofill"); + jest.spyOn(insertAutofillContentService as any, "runFillScriptAction"); + + insertAutofillContentService.fillForm(fillScript); + + expect(insertAutofillContentService["fillingWithinSandboxedIframe"]).not.toHaveBeenCalled(); + expect( + insertAutofillContentService["userCancelledInsecureUrlAutofill"] + ).not.toHaveBeenCalled(); + expect( + insertAutofillContentService["userCancelledUntrustedIframeAutofill"] + ).not.toHaveBeenCalled(); + expect(insertAutofillContentService["runFillScriptAction"]).not.toHaveBeenCalled(); + }); + + it("returns early if the script is filling within a sand boxed iframe", () => { + jest + .spyOn(insertAutofillContentService as any, "fillingWithinSandboxedIframe") + .mockReturnValue(true); + jest.spyOn(insertAutofillContentService as any, "userCancelledInsecureUrlAutofill"); + jest.spyOn(insertAutofillContentService as any, "userCancelledUntrustedIframeAutofill"); + jest.spyOn(insertAutofillContentService as any, "runFillScriptAction"); + + insertAutofillContentService.fillForm(fillScript); + + expect(insertAutofillContentService["fillingWithinSandboxedIframe"]).toHaveBeenCalled(); + expect( + insertAutofillContentService["userCancelledInsecureUrlAutofill"] + ).not.toHaveBeenCalled(); + expect( + insertAutofillContentService["userCancelledUntrustedIframeAutofill"] + ).not.toHaveBeenCalled(); + expect(insertAutofillContentService["runFillScriptAction"]).not.toHaveBeenCalled(); + }); + + it("returns early if the autofill is occurring on an insecure url and the user cancels the autofill", () => { + jest + .spyOn(insertAutofillContentService as any, "fillingWithinSandboxedIframe") + .mockReturnValue(false); + jest + .spyOn(insertAutofillContentService as any, "userCancelledInsecureUrlAutofill") + .mockReturnValue(true); + jest.spyOn(insertAutofillContentService as any, "userCancelledUntrustedIframeAutofill"); + jest.spyOn(insertAutofillContentService as any, "runFillScriptAction"); + + insertAutofillContentService.fillForm(fillScript); + + expect(insertAutofillContentService["fillingWithinSandboxedIframe"]).toHaveBeenCalled(); + expect(insertAutofillContentService["userCancelledInsecureUrlAutofill"]).toHaveBeenCalled(); + expect( + insertAutofillContentService["userCancelledUntrustedIframeAutofill"] + ).not.toHaveBeenCalled(); + expect(insertAutofillContentService["runFillScriptAction"]).not.toHaveBeenCalled(); + }); + + it("returns early if the iframe is untrusted and the user cancelled the autofill", () => { + jest + .spyOn(insertAutofillContentService as any, "fillingWithinSandboxedIframe") + .mockReturnValue(false); + jest + .spyOn(insertAutofillContentService as any, "userCancelledInsecureUrlAutofill") + .mockReturnValue(false); + jest + .spyOn(insertAutofillContentService as any, "userCancelledUntrustedIframeAutofill") + .mockReturnValue(true); + jest.spyOn(insertAutofillContentService as any, "runFillScriptAction"); + + insertAutofillContentService.fillForm(fillScript); + + expect(insertAutofillContentService["fillingWithinSandboxedIframe"]).toHaveBeenCalled(); + expect(insertAutofillContentService["userCancelledInsecureUrlAutofill"]).toHaveBeenCalled(); + expect( + insertAutofillContentService["userCancelledUntrustedIframeAutofill"] + ).toHaveBeenCalled(); + expect(insertAutofillContentService["runFillScriptAction"]).not.toHaveBeenCalled(); + }); + + it("runs the fill script action for all scripts found within the fill script", () => { + jest + .spyOn(insertAutofillContentService as any, "fillingWithinSandboxedIframe") + .mockReturnValue(false); + jest + .spyOn(insertAutofillContentService as any, "userCancelledInsecureUrlAutofill") + .mockReturnValue(false); + jest + .spyOn(insertAutofillContentService as any, "userCancelledUntrustedIframeAutofill") + .mockReturnValue(false); + jest.spyOn(insertAutofillContentService as any, "runFillScriptAction"); + + insertAutofillContentService.fillForm(fillScript); + + expect(insertAutofillContentService["fillingWithinSandboxedIframe"]).toHaveBeenCalled(); + expect(insertAutofillContentService["userCancelledInsecureUrlAutofill"]).toHaveBeenCalled(); + expect( + insertAutofillContentService["userCancelledUntrustedIframeAutofill"] + ).toHaveBeenCalled(); + expect(insertAutofillContentService["runFillScriptAction"]).toHaveBeenCalledTimes(3); + expect(insertAutofillContentService["runFillScriptAction"]).toHaveBeenNthCalledWith( + 1, + fillScript.script[0], + 0, + fillScript.script + ); + expect(insertAutofillContentService["runFillScriptAction"]).toHaveBeenNthCalledWith( + 2, + fillScript.script[1], + 1, + fillScript.script + ); + expect(insertAutofillContentService["runFillScriptAction"]).toHaveBeenNthCalledWith( + 3, + fillScript.script[2], + 2, + fillScript.script + ); + }); + }); + + describe("fillingWithinSandboxedIframe", () => { + afterEach(() => { + Object.defineProperty(globalThis, "window", { + value: { frameElement: null }, + writable: true, + }); + }); + + it("returns false if the `self.origin` value is not null", () => { + const result = insertAutofillContentService["fillingWithinSandboxedIframe"](); + + expect(result).toBe(false); + expect(self.origin).not.toBeNull(); + }); + + it("returns true if the frameElement has a sandbox attribute", () => { + Object.defineProperty(globalThis, "window", { + value: { frameElement: { hasAttribute: jest.fn(() => true) } }, + writable: true, + }); + + const result = insertAutofillContentService["fillingWithinSandboxedIframe"](); + + expect(result).toBe(true); + }); + + it("returns true if the window location hostname is empty", () => { + setMockWindowLocation({ protocol: "http:", hostname: "" }); + + const result = insertAutofillContentService["fillingWithinSandboxedIframe"](); + + expect(result).toBe(true); + }); + }); + + describe("userCancelledInsecureUrlAutofill", () => { + const currentHostname = "bitwarden.com"; + + beforeEach(() => { + savedURLs = [`https://${currentHostname}`]; + }); + + describe("returns false if Autofill occurring...", () => { + it("when there are no saved URLs", () => { + savedURLs = []; + setMockWindowLocation({ protocol: "http:", hostname: currentHostname }); + + const userCancelledInsecureUrlAutofill = + insertAutofillContentService["userCancelledInsecureUrlAutofill"](savedURLs); + + expect(userCancelledInsecureUrlAutofill).toBe(false); + + savedURLs = null; + + const userCancelledInsecureUrlAutofill2 = + insertAutofillContentService["userCancelledInsecureUrlAutofill"](savedURLs); + + expect(confirmSpy).not.toHaveBeenCalled(); + expect(userCancelledInsecureUrlAutofill2).toBe(false); + }); + + it("on http page and saved URLs contain no https values", () => { + savedURLs = ["http://bitwarden.com"]; + setMockWindowLocation({ protocol: "http:", hostname: currentHostname }); + + const userCancelledInsecureUrlAutofill = + insertAutofillContentService["userCancelledInsecureUrlAutofill"](savedURLs); + + expect(confirmSpy).not.toHaveBeenCalled(); + expect(userCancelledInsecureUrlAutofill).toBe(false); + }); + + it("on https page with saved https URL", () => { + setMockWindowLocation({ protocol: "https:", hostname: currentHostname }); + + const userCancelledInsecureUrlAutofill = + insertAutofillContentService["userCancelledInsecureUrlAutofill"](savedURLs); + + expect(confirmSpy).not.toHaveBeenCalled(); + expect(userCancelledInsecureUrlAutofill).toBe(false); + }); + + it("on page with no password field", () => { + setMockWindowLocation({ protocol: "https:", hostname: currentHostname }); + + document.body.innerHTML = ` +
    +
    + +
    +
    + `; + + const userCancelledInsecureUrlAutofill = + insertAutofillContentService["userCancelledInsecureUrlAutofill"](savedURLs); + + expect(confirmSpy).not.toHaveBeenCalled(); + expect(userCancelledInsecureUrlAutofill).toBe(false); + }); + + it("on http page with saved https URL and user approval", () => { + setMockWindowLocation({ protocol: "http:", hostname: currentHostname }); + confirmSpy.mockImplementation(jest.fn(() => true)); + + const userCancelledInsecureUrlAutofill = + insertAutofillContentService["userCancelledInsecureUrlAutofill"](savedURLs); + + expect(confirmSpy).toHaveBeenCalled(); + expect(userCancelledInsecureUrlAutofill).toBe(false); + }); + }); + + it("returns true if Autofill occurring on http page with saved https URL and user disapproval", () => { + setMockWindowLocation({ protocol: "http:", hostname: currentHostname }); + confirmSpy.mockImplementation(jest.fn(() => false)); + + const userCancelledInsecureUrlAutofill = + insertAutofillContentService["userCancelledInsecureUrlAutofill"](savedURLs); + + expect(confirmSpy).toHaveBeenCalled(); + expect(userCancelledInsecureUrlAutofill).toBe(true); + }); + + it("returns false if the vault item contains uris with both secure and insecure uris, but a insecure uri is being used on a insecure web page", () => { + setMockWindowLocation({ protocol: "http:", hostname: currentHostname }); + savedURLs = ["http://bitwarden.com", "https://some-other-uri.com"]; + + const userCancelledInsecureUrlAutofill = + insertAutofillContentService["userCancelledInsecureUrlAutofill"](savedURLs); + + expect(confirmSpy).not.toHaveBeenCalled(); + expect(userCancelledInsecureUrlAutofill).toBe(false); + }); + }); + + describe("userCancelledUntrustedIframeAutofill", () => { + it("returns false if Autofill occurring within a trusted iframe", () => { + fillScript.untrustedIframe = false; + + const result = + insertAutofillContentService["userCancelledUntrustedIframeAutofill"](fillScript); + + expect(result).toBe(false); + expect(confirmSpy).not.toHaveBeenCalled(); + }); + + it("returns false if Autofill occurring within an untrusted iframe and the user approves", () => { + fillScript.untrustedIframe = true; + confirmSpy.mockImplementation(jest.fn(() => true)); + + const result = + insertAutofillContentService["userCancelledUntrustedIframeAutofill"](fillScript); + + expect(result).toBe(false); + expect(confirmSpy).toHaveBeenCalled(); + }); + + it("returns true if Autofill occurring within an untrusted iframe and the user disapproves", () => { + fillScript.untrustedIframe = true; + confirmSpy.mockImplementation(jest.fn(() => false)); + + const result = + insertAutofillContentService["userCancelledUntrustedIframeAutofill"](fillScript); + + expect(result).toBe(true); + expect(confirmSpy).toHaveBeenCalled(); + }); + }); + + describe("runFillScriptAction", () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + it("returns early if no opid is provided", () => { + const action = "fill_by_opid"; + const opid = ""; + const value = "value"; + const scriptAction: FillScript = [action, opid, value]; + jest.spyOn(insertAutofillContentService["autofillInsertActions"], action); + + insertAutofillContentService["runFillScriptAction"](scriptAction, 0); + jest.advanceTimersByTime(20); + + expect(insertAutofillContentService["autofillInsertActions"][action]).not.toHaveBeenCalled(); + }); + + describe("given a valid fill script action and opid", () => { + const fillScriptActions: FillScriptActions[] = [ + "fill_by_opid", + "click_on_opid", + "focus_by_opid", + ]; + fillScriptActions.forEach((action) => { + it(`triggers a ${action} action`, () => { + const opid = "opid"; + const value = "value"; + const scriptAction: FillScript = [action, opid, value]; + jest.spyOn(insertAutofillContentService["autofillInsertActions"], action); + + insertAutofillContentService["runFillScriptAction"](scriptAction, 0); + jest.advanceTimersByTime(20); + + expect( + insertAutofillContentService["autofillInsertActions"][action] + ).toHaveBeenCalledWith({ + opid, + value, + }); + }); + }); + }); + }); + + describe("handleFillFieldByOpidAction", () => { + it("finds the field element by opid and inserts the value into the field", () => { + const opid = "__1"; + const value = "value"; + const textInput = document.querySelector('input[type="text"]') as FormElementWithAttribute; + textInput.opid = opid; + textInput.value = value; + jest.spyOn( + insertAutofillContentService["collectAutofillContentService"], + "getAutofillFieldElementByOpid" + ); + jest.spyOn(insertAutofillContentService as any, "insertValueIntoField"); + + insertAutofillContentService["handleFillFieldByOpidAction"](opid, value); + + expect( + insertAutofillContentService["collectAutofillContentService"].getAutofillFieldElementByOpid + ).toHaveBeenCalledWith(opid); + expect(insertAutofillContentService["insertValueIntoField"]).toHaveBeenCalledWith( + textInput, + value + ); + }); + }); + + describe("handleClickOnFieldByOpidAction", () => { + it("clicks on the elements targeted by the passed opid", () => { + const textInput = document.querySelector('input[type="text"]') as FormElementWithAttribute; + textInput.opid = "__1"; + let clickEventCount = 0; + const expectedClickEventCount = 1; + const clickEventHandler: (handledEvent: Event) => void = (handledEvent) => { + const eventTarget = handledEvent.target as HTMLInputElement; + + if (eventTarget.id === "username") { + clickEventCount++; + } + }; + textInput.addEventListener("click", clickEventHandler); + jest.spyOn( + insertAutofillContentService["collectAutofillContentService"], + "getAutofillFieldElementByOpid" + ); + jest.spyOn(insertAutofillContentService as any, "triggerClickOnElement"); + + insertAutofillContentService["handleClickOnFieldByOpidAction"]("__1"); + + expect( + insertAutofillContentService["collectAutofillContentService"].getAutofillFieldElementByOpid + ).toBeCalledWith("__1"); + expect((insertAutofillContentService as any)["triggerClickOnElement"]).toHaveBeenCalledWith( + textInput + ); + expect(clickEventCount).toBe(expectedClickEventCount); + + textInput.removeEventListener("click", clickEventHandler); + }); + + it("should not trigger click when no suitable elements can be found", () => { + const textInput = document.querySelector('input[type="text"]') as FormElementWithAttribute; + let clickEventCount = 0; + const expectedClickEventCount = 0; + const clickEventHandler: (handledEvent: Event) => void = (handledEvent) => { + const eventTarget = handledEvent.target as HTMLInputElement; + + if (eventTarget.id === "username") { + clickEventCount++; + } + }; + textInput.addEventListener("click", clickEventHandler); + + insertAutofillContentService["handleClickOnFieldByOpidAction"]("__2"); + + expect(clickEventCount).toEqual(expectedClickEventCount); + + textInput.removeEventListener("click", clickEventHandler); + }); + }); + + describe("handleFocusOnFieldByOpidAction", () => { + it("simulates click and focus events on the element targeted by the passed opid", () => { + const targetInput = document.querySelector('input[type="text"]') as FormElementWithAttribute; + targetInput.opid = "__0"; + const elementEventCount: { [key: string]: number } = { + ...initEventCount, + }; + // Testing all the relevant events to ensure downstream side-effects are firing correctly + const expectedElementEventCount: { [key: string]: number } = { + ...initEventCount, + click: 1, + focus: 1, + focusin: 1, + }; + const eventHandlers: { [key: string]: EventListener } = {}; + eventsToTest.forEach((eventType) => { + eventHandlers[eventType] = (handledEvent) => { + elementEventCount[handledEvent.type]++; + }; + targetInput.addEventListener(eventType, eventHandlers[eventType]); + }); + jest.spyOn( + insertAutofillContentService["collectAutofillContentService"], + "getAutofillFieldElementByOpid" + ); + jest.spyOn( + insertAutofillContentService as any, + "simulateUserMouseClickAndFocusEventInteractions" + ); + + insertAutofillContentService["handleFocusOnFieldByOpidAction"]("__0"); + + expect( + insertAutofillContentService["collectAutofillContentService"].getAutofillFieldElementByOpid + ).toBeCalledWith("__0"); + expect( + insertAutofillContentService["simulateUserMouseClickAndFocusEventInteractions"] + ).toHaveBeenCalledWith(targetInput, true); + expect(elementEventCount).toEqual(expectedElementEventCount); + }); + }); + + describe("insertValueIntoField", () => { + it("returns early if an element is not provided", () => { + const value = "test"; + const element: FormFieldElement | null = null; + jest.spyOn(insertAutofillContentService as any, "handleInsertValueAndTriggerSimulatedEvents"); + + insertAutofillContentService["insertValueIntoField"](element, value); + + expect( + insertAutofillContentService["handleInsertValueAndTriggerSimulatedEvents"] + ).not.toHaveBeenCalled(); + }); + + it("returns early if a value is not provided", () => { + const value = ""; + const element: FormFieldElement | null = document.querySelector('input[type="text"]'); + jest.spyOn(insertAutofillContentService as any, "handleInsertValueAndTriggerSimulatedEvents"); + + insertAutofillContentService["insertValueIntoField"](element, value); + + expect( + insertAutofillContentService["handleInsertValueAndTriggerSimulatedEvents"] + ).not.toHaveBeenCalled(); + }); + + it("will set the inner text of the element if a span element is passed", () => { + document.body.innerHTML = ``; + const value = "test"; + const element = document.getElementById("username") as FormFieldElement; + jest.spyOn(insertAutofillContentService as any, "handleInsertValueAndTriggerSimulatedEvents"); + + insertAutofillContentService["insertValueIntoField"](element, value); + + expect(element.innerText).toBe(value); + expect( + insertAutofillContentService["handleInsertValueAndTriggerSimulatedEvents"] + ).toHaveBeenCalledWith(element, expect.any(Function)); + }); + + it("will set the `checked` attribute of any passed checkbox or radio elements", () => { + document.body.innerHTML = ``; + const checkboxElement = document.getElementById("checkbox") as HTMLInputElement; + const radioElement = document.getElementById("radio") as HTMLInputElement; + jest.spyOn(insertAutofillContentService as any, "handleInsertValueAndTriggerSimulatedEvents"); + + const possibleValues = ["true", "y", "1", "yes", "✓"]; + possibleValues.forEach((value) => { + insertAutofillContentService["insertValueIntoField"](checkboxElement, value); + insertAutofillContentService["insertValueIntoField"](radioElement, value); + + expect(checkboxElement.checked).toBe(true); + expect(radioElement.checked).toBe(true); + expect( + insertAutofillContentService["handleInsertValueAndTriggerSimulatedEvents"] + ).toHaveBeenCalledWith(checkboxElement, expect.any(Function)); + expect( + insertAutofillContentService["handleInsertValueAndTriggerSimulatedEvents"] + ).toHaveBeenCalledWith(radioElement, expect.any(Function)); + + checkboxElement.checked = false; + radioElement.checked = false; + }); + }); + + it("will set the `value` attribute of any passed input or textarea elements", () => { + document.body.innerHTML = ``; + const value1 = "test"; + const value2 = "test2"; + const textInputElement = document.getElementById("username") as HTMLInputElement; + textInputElement.value = value1; + const textareaElement = document.getElementById("bio") as HTMLTextAreaElement; + textareaElement.value = value2; + jest.spyOn(insertAutofillContentService as any, "handleInsertValueAndTriggerSimulatedEvents"); + + insertAutofillContentService["insertValueIntoField"](textInputElement, value1); + + expect(textInputElement.value).toBe(value1); + expect( + insertAutofillContentService["handleInsertValueAndTriggerSimulatedEvents"] + ).toHaveBeenCalledWith(textInputElement, expect.any(Function)); + + insertAutofillContentService["insertValueIntoField"](textareaElement, value2); + + expect(textareaElement.value).toBe(value2); + expect( + insertAutofillContentService["handleInsertValueAndTriggerSimulatedEvents"] + ).toHaveBeenCalledWith(textareaElement, expect.any(Function)); + }); + }); + + describe("handleInsertValueAndTriggerSimulatedEvents", () => { + it("triggers pre- and post-insert events on the element while filling the value into the element", () => { + const value = "test"; + const element = document.querySelector('input[type="text"]') as FormFieldElement; + jest.spyOn(insertAutofillContentService as any, "triggerPreInsertEventsOnElement"); + jest.spyOn(insertAutofillContentService as any, "triggerPostInsertEventsOnElement"); + jest.spyOn(insertAutofillContentService as any, "triggerFillAnimationOnElement"); + const valueChangeCallback = jest.fn( + () => ((element as FillableFormFieldElement).value = value) + ); + + insertAutofillContentService["handleInsertValueAndTriggerSimulatedEvents"]( + element, + valueChangeCallback + ); + + expect(insertAutofillContentService["triggerPreInsertEventsOnElement"]).toHaveBeenCalledWith( + element + ); + expect(valueChangeCallback).toHaveBeenCalled(); + expect(insertAutofillContentService["triggerPostInsertEventsOnElement"]).toHaveBeenCalledWith( + element + ); + expect(insertAutofillContentService["triggerFillAnimationOnElement"]).toHaveBeenCalledWith( + element + ); + expect((element as FillableFormFieldElement).value).toBe(value); + }); + }); + + describe("triggerPreInsertEventsOnElement", () => { + it("triggers a simulated click and keyboard event on the element", () => { + const initialElementValue = "test"; + document.body.innerHTML = ``; + const element = document.getElementById("username") as FillableFormFieldElement; + jest.spyOn( + insertAutofillContentService as any, + "simulateUserMouseClickAndFocusEventInteractions" + ); + jest.spyOn(insertAutofillContentService as any, "simulateUserKeyboardEventInteractions"); + + insertAutofillContentService["triggerPreInsertEventsOnElement"](element); + + expect( + insertAutofillContentService["simulateUserMouseClickAndFocusEventInteractions"] + ).toHaveBeenCalledWith(element); + expect( + insertAutofillContentService["simulateUserKeyboardEventInteractions"] + ).toHaveBeenCalledWith(element); + expect(element.value).toBe(initialElementValue); + }); + }); + + describe("triggerPostInsertEventsOnElement", () => { + it("triggers simulated event interactions and blurs the element after", () => { + const elementValue = "test"; + document.body.innerHTML = ``; + const element = document.getElementById("username") as FillableFormFieldElement; + jest.spyOn(element, "blur"); + jest.spyOn(insertAutofillContentService as any, "simulateUserKeyboardEventInteractions"); + jest.spyOn(insertAutofillContentService as any, "simulateInputElementChangedEvent"); + + insertAutofillContentService["triggerPostInsertEventsOnElement"](element); + + expect( + insertAutofillContentService["simulateUserKeyboardEventInteractions"] + ).toHaveBeenCalledWith(element); + expect(insertAutofillContentService["simulateInputElementChangedEvent"]).toHaveBeenCalledWith( + element + ); + expect(element.blur).toHaveBeenCalled(); + expect(element.value).toBe(elementValue); + }); + }); + + describe("triggerFillAnimationOnElement", () => { + beforeEach(() => { + jest.useFakeTimers(); + jest.clearAllTimers(); + }); + + describe("will not trigger the animation when...", () => { + it("the element is a non-hidden hidden input type", async () => { + document.body.innerHTML = mockLoginForm + ''; + const testElement = document.querySelector( + 'input[type="hidden"]' + ) as FillableFormFieldElement; + jest.spyOn(testElement.classList, "add"); + jest.spyOn(testElement.classList, "remove"); + + insertAutofillContentService["triggerFillAnimationOnElement"](testElement); + await jest.advanceTimersByTime(200); + + expect(testElement.classList.add).not.toHaveBeenCalled(); + expect(testElement.classList.remove).not.toHaveBeenCalled(); + }); + + it("the element is a non-hidden textarea", () => { + document.body.innerHTML = mockLoginForm + ""; + const testElement = document.querySelector("textarea") as FillableFormFieldElement; + jest.spyOn(testElement.classList, "add"); + jest.spyOn(testElement.classList, "remove"); + + insertAutofillContentService["triggerFillAnimationOnElement"](testElement); + jest.advanceTimersByTime(200); + + expect(testElement.classList.add).not.toHaveBeenCalled(); + expect(testElement.classList.remove).not.toHaveBeenCalled(); + }); + + it("the element is a unsupported tag", () => { + document.body.innerHTML = mockLoginForm + '
    '; + const testElement = document.querySelector("#input-tag") as FillableFormFieldElement; + jest.spyOn(testElement.classList, "add"); + jest.spyOn(testElement.classList, "remove"); + + insertAutofillContentService["triggerFillAnimationOnElement"](testElement); + jest.advanceTimersByTime(200); + + expect(testElement.classList.add).not.toHaveBeenCalled(); + expect(testElement.classList.remove).not.toHaveBeenCalled(); + }); + + it("the element has a `visibility: hidden;` CSS rule applied to it", () => { + const testElement = document.querySelector( + 'input[type="password"]' + ) as FillableFormFieldElement; + testElement.style.visibility = "hidden"; + jest.spyOn(testElement.classList, "add"); + jest.spyOn(testElement.classList, "remove"); + + insertAutofillContentService["triggerFillAnimationOnElement"](testElement); + jest.advanceTimersByTime(200); + + expect(testElement.classList.add).not.toHaveBeenCalled(); + expect(testElement.classList.remove).not.toHaveBeenCalled(); + }); + + it("the element has a `display: none;` CSS rule applied to it", () => { + const testElement = document.querySelector( + 'input[type="password"]' + ) as FillableFormFieldElement; + testElement.style.display = "none"; + jest.spyOn(testElement.classList, "add"); + jest.spyOn(testElement.classList, "remove"); + + insertAutofillContentService["triggerFillAnimationOnElement"](testElement); + jest.advanceTimersByTime(200); + + expect(testElement.classList.add).not.toHaveBeenCalled(); + expect(testElement.classList.remove).not.toHaveBeenCalled(); + }); + + it("a parent of the element has an `opacity: 0;` CSS rule applied to it", () => { + document.body.innerHTML = + mockLoginForm + '
    '; + const testElement = document.querySelector( + 'input[type="email"]' + ) as FillableFormFieldElement; + jest.spyOn(testElement.classList, "add"); + jest.spyOn(testElement.classList, "remove"); + + insertAutofillContentService["triggerFillAnimationOnElement"](testElement); + jest.advanceTimersByTime(200); + + expect(testElement.classList.add).not.toHaveBeenCalled(); + expect(testElement.classList.remove).not.toHaveBeenCalled(); + }); + }); + + describe("will trigger the animation when...", () => { + it("the element is a non-hidden password field", () => { + const testElement = document.querySelector( + 'input[type="password"]' + ) as FillableFormFieldElement; + jest.spyOn( + insertAutofillContentService["domElementVisibilityService"], + "isElementHiddenByCss" + ); + jest.spyOn(testElement.classList, "add"); + jest.spyOn(testElement.classList, "remove"); + + insertAutofillContentService["triggerFillAnimationOnElement"](testElement); + jest.advanceTimersByTime(200); + + expect( + insertAutofillContentService["domElementVisibilityService"].isElementHiddenByCss + ).toHaveBeenCalledWith(testElement); + expect(testElement.classList.add).toHaveBeenCalledWith( + "com-bitwarden-browser-animated-fill" + ); + expect(testElement.classList.remove).toHaveBeenCalledWith( + "com-bitwarden-browser-animated-fill" + ); + }); + + it("the element is a non-hidden email input", () => { + document.body.innerHTML = mockLoginForm + ''; + const testElement = document.querySelector( + 'input[type="email"]' + ) as FillableFormFieldElement; + jest.spyOn(testElement.classList, "add"); + jest.spyOn(testElement.classList, "remove"); + + insertAutofillContentService["triggerFillAnimationOnElement"](testElement); + jest.advanceTimersByTime(200); + + expect(testElement.classList.add).toHaveBeenCalledWith( + "com-bitwarden-browser-animated-fill" + ); + expect(testElement.classList.remove).toHaveBeenCalledWith( + "com-bitwarden-browser-animated-fill" + ); + }); + + it("the element is a non-hidden text input", () => { + document.body.innerHTML = mockLoginForm + ''; + const testElement = document.querySelector( + 'input[type="text"]' + ) as FillableFormFieldElement; + jest.spyOn(testElement.classList, "add"); + jest.spyOn(testElement.classList, "remove"); + + insertAutofillContentService["triggerFillAnimationOnElement"](testElement); + jest.advanceTimersByTime(200); + + expect(testElement.classList.add).toHaveBeenCalledWith( + "com-bitwarden-browser-animated-fill" + ); + expect(testElement.classList.remove).toHaveBeenCalledWith( + "com-bitwarden-browser-animated-fill" + ); + }); + + it("the element is a non-hidden number input", () => { + document.body.innerHTML = mockLoginForm + ''; + const testElement = document.querySelector( + 'input[type="number"]' + ) as FillableFormFieldElement; + jest.spyOn(testElement.classList, "add"); + jest.spyOn(testElement.classList, "remove"); + + insertAutofillContentService["triggerFillAnimationOnElement"](testElement); + jest.advanceTimersByTime(200); + + expect(testElement.classList.add).toHaveBeenCalledWith( + "com-bitwarden-browser-animated-fill" + ); + expect(testElement.classList.remove).toHaveBeenCalledWith( + "com-bitwarden-browser-animated-fill" + ); + }); + + it("the element is a non-hidden tel input", () => { + document.body.innerHTML = mockLoginForm + ''; + const testElement = document.querySelector('input[type="tel"]') as FillableFormFieldElement; + jest.spyOn(testElement.classList, "add"); + jest.spyOn(testElement.classList, "remove"); + + insertAutofillContentService["triggerFillAnimationOnElement"](testElement); + jest.advanceTimersByTime(200); + + expect(testElement.classList.add).toHaveBeenCalledWith( + "com-bitwarden-browser-animated-fill" + ); + expect(testElement.classList.remove).toHaveBeenCalledWith( + "com-bitwarden-browser-animated-fill" + ); + }); + + it("the element is a non-hidden url input", () => { + document.body.innerHTML = mockLoginForm + ''; + const testElement = document.querySelector('input[type="url"]') as FillableFormFieldElement; + jest.spyOn(testElement.classList, "add"); + jest.spyOn(testElement.classList, "remove"); + + insertAutofillContentService["triggerFillAnimationOnElement"](testElement); + jest.advanceTimersByTime(200); + + expect(testElement.classList.add).toHaveBeenCalledWith( + "com-bitwarden-browser-animated-fill" + ); + expect(testElement.classList.remove).toHaveBeenCalledWith( + "com-bitwarden-browser-animated-fill" + ); + }); + + it("the element is a non-hidden span", () => { + document.body.innerHTML = mockLoginForm + ''; + const testElement = document.querySelector("#input-tag") as FillableFormFieldElement; + jest.spyOn(testElement.classList, "add"); + jest.spyOn(testElement.classList, "remove"); + + insertAutofillContentService["triggerFillAnimationOnElement"](testElement); + jest.advanceTimersByTime(200); + + expect(testElement.classList.add).toHaveBeenCalledWith( + "com-bitwarden-browser-animated-fill" + ); + expect(testElement.classList.remove).toHaveBeenCalledWith( + "com-bitwarden-browser-animated-fill" + ); + }); + }); + }); + + describe("triggerClickOnElement", () => { + it("will trigger a click event on the passed element", () => { + const inputElement = document.querySelector('input[type="text"]') as HTMLElement; + jest.spyOn(inputElement, "click"); + + insertAutofillContentService["triggerClickOnElement"](inputElement); + + expect(inputElement.click).toHaveBeenCalled(); + }); + }); + + describe("triggerFocusOnElement", () => { + it("will trigger a focus event on the passed element and attempt to reset the value", () => { + const value = "test"; + const inputElement = document.querySelector('input[type="text"]') as HTMLInputElement; + inputElement.value = "test"; + jest.spyOn(inputElement, "focus"); + jest.spyOn(window, "String"); + + insertAutofillContentService["triggerFocusOnElement"](inputElement, true); + + expect(window.String).toHaveBeenCalledWith(value); + expect(inputElement.focus).toHaveBeenCalled(); + expect(inputElement.value).toEqual(value); + }); + + it("will not attempt to reset the value but will still focus the element", () => { + const value = "test"; + const inputElement = document.querySelector('input[type="text"]') as HTMLInputElement; + inputElement.value = "test"; + jest.spyOn(inputElement, "focus"); + jest.spyOn(window, "String"); + + insertAutofillContentService["triggerFocusOnElement"](inputElement, false); + + expect(window.String).not.toHaveBeenCalledWith(); + expect(inputElement.focus).toHaveBeenCalled(); + expect(inputElement.value).toEqual(value); + }); + }); + + describe("simulateUserMouseClickAndFocusEventInteractions", () => { + it("will trigger click and focus events on the passed element", () => { + const inputElement = document.querySelector('input[type="text"]') as HTMLInputElement; + jest.spyOn(insertAutofillContentService as any, "triggerClickOnElement"); + jest.spyOn(insertAutofillContentService as any, "triggerFocusOnElement"); + + insertAutofillContentService["simulateUserMouseClickAndFocusEventInteractions"](inputElement); + + expect(insertAutofillContentService["triggerClickOnElement"]).toHaveBeenCalledWith( + inputElement + ); + expect(insertAutofillContentService["triggerFocusOnElement"]).toHaveBeenCalledWith( + inputElement, + false + ); + }); + }); + + describe("simulateUserKeyboardEventInteractions", () => { + it("will trigger `keydown`, `keypress`, and `keyup` events on the passed element", () => { + const inputElement = document.querySelector('input[type="text"]') as HTMLInputElement; + jest.spyOn(inputElement, "dispatchEvent"); + + insertAutofillContentService["simulateUserKeyboardEventInteractions"](inputElement); + + [EVENTS.KEYDOWN, EVENTS.KEYPRESS, EVENTS.KEYUP].forEach((eventName) => { + expect(inputElement.dispatchEvent).toHaveBeenCalledWith( + new KeyboardEvent(eventName, { bubbles: true }) + ); + }); + }); + }); + + describe("simulateInputElementChangedEvent", () => { + it("will trigger `input` and `change` events on the passed element", () => { + const inputElement = document.querySelector('input[type="text"]') as HTMLInputElement; + jest.spyOn(inputElement, "dispatchEvent"); + + insertAutofillContentService["simulateInputElementChangedEvent"](inputElement); + + [EVENTS.INPUT, EVENTS.CHANGE].forEach((eventName) => { + expect(inputElement.dispatchEvent).toHaveBeenCalledWith( + new Event(eventName, { bubbles: true }) + ); + }); + }); + }); +}); diff --git a/apps/browser/src/autofill/services/insert-autofill-content.service.ts b/apps/browser/src/autofill/services/insert-autofill-content.service.ts new file mode 100644 index 00000000000..89f644ba6be --- /dev/null +++ b/apps/browser/src/autofill/services/insert-autofill-content.service.ts @@ -0,0 +1,349 @@ +import { EVENTS, TYPE_CHECK } from "../constants"; +import AutofillScript, { AutofillInsertActions, FillScript } from "../models/autofill-script"; +import { FormFieldElement } from "../types"; + +import { InsertAutofillContentService as InsertAutofillContentServiceInterface } from "./abstractions/insert-autofill-content.service"; +import CollectAutofillContentService from "./collect-autofill-content.service"; +import DomElementVisibilityService from "./dom-element-visibility.service"; + +class InsertAutofillContentService implements InsertAutofillContentServiceInterface { + private readonly domElementVisibilityService: DomElementVisibilityService; + private readonly collectAutofillContentService: CollectAutofillContentService; + private readonly autofillInsertActions: AutofillInsertActions = { + fill_by_opid: ({ opid, value }) => this.handleFillFieldByOpidAction(opid, value), + click_on_opid: ({ opid }) => this.handleClickOnFieldByOpidAction(opid), + focus_by_opid: ({ opid }) => this.handleFocusOnFieldByOpidAction(opid), + }; + + /** + * InsertAutofillContentService constructor. Instantiates the + * DomElementVisibilityService and CollectAutofillContentService classes. + */ + constructor( + domElementVisibilityService: DomElementVisibilityService, + collectAutofillContentService: CollectAutofillContentService + ) { + this.domElementVisibilityService = domElementVisibilityService; + this.collectAutofillContentService = collectAutofillContentService; + } + + /** + * Handles autofill of the forms on the current page based on the + * data within the passed fill script object. + * @param {AutofillScript} fillScript + * @public + */ + fillForm(fillScript: AutofillScript) { + if ( + !fillScript.script?.length || + this.fillingWithinSandboxedIframe() || + this.userCancelledInsecureUrlAutofill(fillScript.savedUrls) || + this.userCancelledUntrustedIframeAutofill(fillScript) + ) { + return; + } + + fillScript.script.forEach(this.runFillScriptAction); + } + + /** + * Identifies if the execution of this script is happening + * within a sandboxed iframe. + * @returns {boolean} + * @private + */ + private fillingWithinSandboxedIframe() { + return ( + String(self.origin).toLowerCase() === "null" || + window.frameElement?.hasAttribute("sandbox") || + window.location.hostname === "" + ); + } + + /** + * Checks if the autofill is occurring on a page that can be considered secure. If the page is not secure, + * the user is prompted to confirm that they want to autofill on the page. + * @param {string[] | null} savedUrls + * @returns {boolean} + * @private + */ + private userCancelledInsecureUrlAutofill(savedUrls?: string[] | null): boolean { + if ( + !savedUrls?.some((url) => url.startsWith(`https://${window.location.hostname}`)) || + window.location.protocol !== "http:" || + !document.querySelectorAll("input[type=password]")?.length + ) { + return false; + } + + const confirmationWarning = [ + chrome.i18n.getMessage("insecurePageWarning"), + chrome.i18n.getMessage("insecurePageWarningFillPrompt", [window.location.hostname]), + ].join("\n\n"); + + return !confirm(confirmationWarning); + } + + /** + * Checking if the autofill is occurring within an untrusted iframe. If the page is within an untrusted iframe, + * the user is prompted to confirm that they want to autofill on the page. If the user cancels the autofill, + * the script will not continue. + * + * Note: confirm() is blocked by sandboxed iframes, but we don't want to fill sandboxed iframes anyway. + * If this occurs, confirm() returns false without displaying the dialog box, and autofill will be aborted. + * The browser may print a message to the console, but this is not a standard error that we can handle. + * @param {AutofillScript} fillScript + * @returns {boolean} + * @private + */ + private userCancelledUntrustedIframeAutofill(fillScript: AutofillScript): boolean { + if (!fillScript.untrustedIframe) { + return false; + } + + const confirmationWarning = [ + chrome.i18n.getMessage("autofillIframeWarning"), + chrome.i18n.getMessage("autofillIframeWarningTip", [window.location.hostname]), + ].join("\n\n"); + + return !confirm(confirmationWarning); + } + + /** + * Runs the autofill action based on the action type and the opid. + * Each action is subsequently delayed by 20 milliseconds. + * @param {FillScriptActions} action + * @param {string} opid + * @param {string} value + * @param {number} actionIndex + */ + private runFillScriptAction = ([action, opid, value]: FillScript, actionIndex: number): void => { + if (!opid || !this.autofillInsertActions[action]) { + return; + } + + const delayActionsInMilliseconds = 20; + setTimeout( + () => this.autofillInsertActions[action]({ opid, value }), + delayActionsInMilliseconds * actionIndex + ); + }; + + /** + * Queries the DOM for an element by opid and inserts the passed value into the element. + * @param {string} opid + * @param {string} value + * @private + */ + private handleFillFieldByOpidAction(opid: string, value: string) { + const element = this.collectAutofillContentService.getAutofillFieldElementByOpid(opid); + this.insertValueIntoField(element, value); + } + + /** + * Handles finding an element by opid and triggering a click event on the element. + * @param {string} opid + * @private + */ + private handleClickOnFieldByOpidAction(opid: string) { + const element = this.collectAutofillContentService.getAutofillFieldElementByOpid(opid); + this.triggerClickOnElement(element); + } + + /** + * Handles finding an element by opid and triggering click and focus events on the element. + * @param {string} opid + * @private + */ + private handleFocusOnFieldByOpidAction(opid: string) { + const element = this.collectAutofillContentService.getAutofillFieldElementByOpid(opid); + this.simulateUserMouseClickAndFocusEventInteractions(element, true); + } + + /** + * Identifies the type of element passed and inserts the value into the element. + * Will trigger simulated events on the element to ensure that the element is + * properly updated. + * @param {FormFieldElement | null} element + * @param {string} value + * @private + */ + private insertValueIntoField(element: FormFieldElement | null, value: string) { + const elementCanBeReadonly = + element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement; + const elementCanBeFilled = elementCanBeReadonly || element instanceof HTMLSelectElement; + + if ( + !element || + !value || + (elementCanBeReadonly && element.readOnly) || + (elementCanBeFilled && element.disabled) + ) { + return; + } + + if (element instanceof HTMLSpanElement) { + this.handleInsertValueAndTriggerSimulatedEvents(element, () => (element.innerText = value)); + return; + } + + const isFillableCheckboxOrRadioElement = + element instanceof HTMLInputElement && + new Set(["checkbox", "radio"]).has(element.type) && + new Set(["true", "y", "1", "yes", "✓"]).has(String(value).toLowerCase()); + if (isFillableCheckboxOrRadioElement) { + this.handleInsertValueAndTriggerSimulatedEvents(element, () => (element.checked = true)); + return; + } + + this.handleInsertValueAndTriggerSimulatedEvents(element, () => (element.value = value)); + } + + /** + * Simulates pre- and post-insert events on the element meant to mimic user interactions + * while inserting the autofill value into the element. + * @param {FormFieldElement} element + * @param {Function} valueChangeCallback + * @private + */ + private handleInsertValueAndTriggerSimulatedEvents( + element: FormFieldElement, + valueChangeCallback: CallableFunction + ): void { + this.triggerPreInsertEventsOnElement(element); + valueChangeCallback(); + this.triggerPostInsertEventsOnElement(element); + this.triggerFillAnimationOnElement(element); + } + + /** + * Simulates a mouse click event on the element, including focusing the event, and + * the triggers a simulated keyboard event on the element. Will attempt to ensure + * that the initial element value is not arbitrarily changed by the simulated events. + * @param {FormFieldElement} element + * @private + */ + private triggerPreInsertEventsOnElement(element: FormFieldElement): void { + const initialElementValue = "value" in element ? element.value : ""; + + this.simulateUserMouseClickAndFocusEventInteractions(element); + this.simulateUserKeyboardEventInteractions(element); + + if ("value" in element && initialElementValue !== element.value) { + element.value = initialElementValue; + } + } + + /** + * Simulates a keyboard event on the element before assigning the autofilled value to the element, and then + * simulates an input change event on the element to trigger expected events after autofill occurs. + * @param {FormFieldElement} element + * @private + */ + private triggerPostInsertEventsOnElement(element: FormFieldElement): void { + const autofilledValue = "value" in element ? element.value : ""; + this.simulateUserKeyboardEventInteractions(element); + + if ("value" in element && autofilledValue !== element.value) { + element.value = autofilledValue; + } + + this.simulateInputElementChangedEvent(element); + element.blur(); + } + + /** + * Identifies if a passed element can be animated and sets a class on the element + * to trigger a CSS animation. The animation is removed after a short delay. + * @param {FormFieldElement} element + * @private + */ + private triggerFillAnimationOnElement(element: FormFieldElement): void { + const skipAnimatingElement = + !(element instanceof HTMLSpanElement) && + !new Set(["email", "text", "password", "number", "tel", "url"]).has(element?.type); + + if (this.domElementVisibilityService.isElementHiddenByCss(element) || skipAnimatingElement) { + return; + } + + element.classList.add("com-bitwarden-browser-animated-fill"); + setTimeout(() => element.classList.remove("com-bitwarden-browser-animated-fill"), 200); + } + + /** + * Simulates a click event on the element. + * @param {HTMLElement} element + * @private + */ + private triggerClickOnElement(element?: HTMLElement): void { + if (typeof element?.click !== TYPE_CHECK.FUNCTION) { + return; + } + + element.click(); + } + + /** + * Simulates a focus event on the element. Will optionally reset the value of the element + * if the element has a value property. + * @param {HTMLElement | undefined} element + * @param {boolean} shouldResetValue + * @private + */ + private triggerFocusOnElement(element: HTMLElement | undefined, shouldResetValue = false): void { + if (typeof element?.focus !== TYPE_CHECK.FUNCTION) { + return; + } + + let initialValue = ""; + if (shouldResetValue && "value" in element) { + initialValue = String(element.value); + } + + element.focus(); + + if (initialValue && "value" in element) { + element.value = initialValue; + } + } + + /** + * Simulates a mouse click and focus event on the element. + * @param {FormFieldElement} element + * @param {boolean} shouldResetValue + * @private + */ + private simulateUserMouseClickAndFocusEventInteractions( + element: FormFieldElement, + shouldResetValue = false + ): void { + this.triggerClickOnElement(element); + this.triggerFocusOnElement(element, shouldResetValue); + } + + /** + * Simulates several keyboard events on the element, mocking a user interaction with the element. + * @param {FormFieldElement} element + * @private + */ + private simulateUserKeyboardEventInteractions(element: FormFieldElement): void { + [EVENTS.KEYDOWN, EVENTS.KEYPRESS, EVENTS.KEYUP].forEach((eventType) => + element.dispatchEvent(new KeyboardEvent(eventType, { bubbles: true })) + ); + } + + /** + * Simulates an input change event on the element, mocking behavior that would occur if a user + * manually changed a value for the element. + * @param {FormFieldElement} element + * @private + */ + private simulateInputElementChangedEvent(element: FormFieldElement): void { + [EVENTS.INPUT, EVENTS.CHANGE].forEach((eventType) => + element.dispatchEvent(new Event(eventType, { bubbles: true })) + ); + } +} + +export default InsertAutofillContentService; diff --git a/apps/browser/src/autofill/types/index.ts b/apps/browser/src/autofill/types/index.ts index d6891325353..8bab87709d2 100644 --- a/apps/browser/src/autofill/types/index.ts +++ b/apps/browser/src/autofill/types/index.ts @@ -39,3 +39,22 @@ export type UserSettings = { vaultTimeout: number; vaultTimeoutAction: VaultTimeoutAction; }; + +/** + * A HTMLElement (usually a form element) with additional custom properties added by this script + */ +export type ElementWithOpId = T & { + opid: string; +}; + +/** + * A Form Element that we can set a value on (fill) + */ +export type FillableFormFieldElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement; + +/** + * The autofill script's definition of a Form Element (only a subset of HTML form elements) + */ +export type FormFieldElement = FillableFormFieldElement | HTMLSpanElement; + +export type FormElementWithAttribute = FormFieldElement & Record; diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 31e81c198f6..f9963bcf7da 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -52,6 +52,7 @@ import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/pla import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; 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"; import { ConfigService } from "@bitwarden/common/platform/services/config/config.service"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; @@ -532,6 +533,7 @@ export default class MainBackground { this.authService, this.messagingService ); + this.configApiService = new ConfigApiService(this.apiService, this.authService); this.configService = new ConfigService( this.stateService, this.configApiService, diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index ee15c0a3b9c..c1cfdf0420f 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -1,4 +1,5 @@ import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -135,6 +136,12 @@ export default class RuntimeBackground { BrowserApi.closeBitwardenExtensionTab(); }, msg.delay ?? 0); break; + case "triggerAutofillScriptInjection": + await this.autofillService.injectAutofillScripts( + sender, + await this.configService.getFeatureFlagBool(FeatureFlag.AutofillV2) + ); + break; case "bgCollectPageDetails": await this.main.collectPageDetailsForContentScript(sender.tab, msg.sender, sender.frameId); break; diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index 1e7b3a3139f..b6276b434e3 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -17,12 +17,7 @@ "content_scripts": [ { "all_frames": true, - "js": [ - "content/autofill.js", - "content/autofiller.js", - "content/notificationBar.js", - "content/contextMenuHandler.js" - ], + "js": ["content/trigger-autofill-script-injection.js"], "matches": ["http://*/*", "https://*/*", "file:///*"], "run_at": "document_start" }, diff --git a/apps/browser/src/platform/browser/browser-api.spec.ts b/apps/browser/src/platform/browser/browser-api.spec.ts new file mode 100644 index 00000000000..af9e633a7f1 --- /dev/null +++ b/apps/browser/src/platform/browser/browser-api.spec.ts @@ -0,0 +1,56 @@ +import { mock } from "jest-mock-extended"; + +import { BrowserApi } from "./browser-api"; + +describe("BrowserApi", () => { + const executeScriptResult = ["value"]; + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("executeScriptInTab", () => { + it("calls to the extension api to execute a script within the give tabId", async () => { + const tabId = 1; + const injectDetails = mock(); + jest.spyOn(BrowserApi, "manifestVersion", "get").mockReturnValue(2); + (chrome.tabs.executeScript as jest.Mock).mockImplementation( + (tabId, injectDetails, callback) => callback(executeScriptResult) + ); + + const result = await BrowserApi.executeScriptInTab(tabId, injectDetails); + + expect(chrome.tabs.executeScript).toHaveBeenCalledWith( + tabId, + injectDetails, + expect.any(Function) + ); + expect(result).toEqual(executeScriptResult); + }); + + it("calls the manifest v3 scripting API if the extension manifest is for v3", async () => { + const tabId = 1; + const injectDetails = mock({ + file: "file.js", + allFrames: true, + runAt: "document_start", + frameId: null, + }); + jest.spyOn(BrowserApi, "manifestVersion", "get").mockReturnValue(3); + (chrome.scripting.executeScript as jest.Mock).mockResolvedValue(executeScriptResult); + + const result = await BrowserApi.executeScriptInTab(tabId, injectDetails); + + expect(chrome.scripting.executeScript).toHaveBeenCalledWith({ + target: { + tabId: tabId, + allFrames: injectDetails.allFrames, + frameIds: null, + }, + files: [injectDetails.file], + injectImmediately: true, + }); + expect(result).toEqual(executeScriptResult); + }); + }); +}); diff --git a/apps/browser/src/platform/browser/browser-api.ts b/apps/browser/src/platform/browser/browser-api.ts index 675fd0b119e..5a5596a795a 100644 --- a/apps/browser/src/platform/browser/browser-api.ts +++ b/apps/browser/src/platform/browser/browser-api.ts @@ -308,4 +308,31 @@ export class BrowserApi { } return win.opr?.sidebarAction || browser.sidebarAction; } + + /** + * Extension API helper method used to execute a script in a tab. + * @see https://developer.chrome.com/docs/extensions/reference/tabs/#method-executeScript + * @param {number} tabId + * @param {chrome.tabs.InjectDetails} details + * @returns {Promise} + */ + static executeScriptInTab(tabId: number, details: chrome.tabs.InjectDetails) { + if (BrowserApi.manifestVersion === 3) { + return chrome.scripting.executeScript({ + target: { + tabId: tabId, + allFrames: details.allFrames, + frameIds: details.frameId ? [details.frameId] : null, + }, + files: details.file ? [details.file] : null, + injectImmediately: details.runAt === "document_start", + }); + } + + return new Promise((resolve) => { + chrome.tabs.executeScript(tabId, details, (result) => { + resolve(result); + }); + }); + } } diff --git a/apps/browser/test.setup.ts b/apps/browser/test.setup.ts index f87fa9c2c12..6feb163e0a6 100644 --- a/apps/browser/test.setup.ts +++ b/apps/browser/test.setup.ts @@ -30,9 +30,25 @@ const contextMenus = { removeAll: jest.fn(), }; +const i18n = { + getMessage: jest.fn(), +}; + +const tabs = { + executeScript: jest.fn(), + sendMessage: jest.fn(), +}; + +const scripting = { + executeScript: jest.fn(), +}; + // set chrome global.chrome = { + i18n, storage, runtime, contextMenus, + tabs, + scripting, } as any; diff --git a/apps/browser/tsconfig.spec.json b/apps/browser/tsconfig.spec.json index de184bd7608..79b5f5bc4b6 100644 --- a/apps/browser/tsconfig.spec.json +++ b/apps/browser/tsconfig.spec.json @@ -1,4 +1,7 @@ { "extends": "./tsconfig.json", - "files": ["./test.setup.ts"] + "files": ["./test.setup.ts"], + "compilerOptions": { + "esModuleInterop": true + } } diff --git a/apps/browser/webpack.config.js b/apps/browser/webpack.config.js index 231b9ab1561..23fba305b6e 100644 --- a/apps/browser/webpack.config.js +++ b/apps/browser/webpack.config.js @@ -14,10 +14,9 @@ if (process.env.NODE_ENV == null) { } const ENV = (process.env.ENV = process.env.NODE_ENV); const manifestVersion = process.env.MANIFEST_VERSION == 3 ? 3 : 2; -const autofillVersion = process.env.AUTOFILL_VERSION == 2 ? 2 : 1; console.log(`Building Manifest Version ${manifestVersion} app`); -console.log(`Using Autofill v${autofillVersion}`); + const envConfig = configurator.load(ENV); configurator.log(envConfig); @@ -153,6 +152,10 @@ const mainConfig = { entry: { "popup/polyfills": "./src/popup/polyfills.ts", "popup/main": "./src/popup/main.ts", + "content/trigger-autofill-script-injection": + "./src/autofill/content/trigger-autofill-script-injection.ts", + "content/autofill": "./src/autofill/content/autofill.js", + "content/autofill-init": "./src/autofill/content/autofill-init.ts", "content/autofiller": "./src/autofill/content/autofiller.ts", "content/notificationBar": "./src/autofill/content/notification-bar.ts", "content/contextMenuHandler": "./src/autofill/content/context-menu-handler.ts", @@ -312,12 +315,4 @@ if (manifestVersion == 2) { configs.push(backgroundConfig); } -if (autofillVersion == 2) { - // Typescript refactors (WIP) - mainConfig.entry["content/autofill"] = "./src/autofill/content/autofillv2.ts"; -} else { - // Javascript (used in production) - mainConfig.entry["content/autofill"] = "./src/autofill/content/autofill.js"; -} - module.exports = configs; diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index fb155f54e2d..7016849b3bc 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -2,5 +2,6 @@ export enum FeatureFlag { DisplayEuEnvironmentFlag = "display-eu-environment", DisplayLowKdfIterationWarningFlag = "display-kdf-iteration-warning", TrustedDeviceEncryption = "trusted-device-encryption", + AutofillV2 = "autofill-v2", SecretsManagerBilling = "sm-ga-billing", } From 5440e372f66f978b6ea830fa9e799ab8ca0f237c Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Thu, 7 Sep 2023 14:44:55 -0700 Subject: [PATCH 39/46] [PM-3804] Remove Server Flag Icons (#6207) * remove flags from web component * remove selectedRegionImageName from web component * remove input * delete image files and update browser translation * update translation and popup width for destkop/browser * remove translations * revert width on dialog --- apps/browser/src/_locales/en/messages.json | 4 +-- apps/browser/src/popup/images/flag-eu.svg | 28 ------------------ apps/browser/src/popup/images/flag-us.svg | 9 ------ apps/browser/src/popup/scss/environment.scss | 13 --------- .../src/popup/settings/about.component.html | 2 +- apps/desktop/src/images/flag-eu.svg | 28 ------------------ apps/desktop/src/images/flag-us.svg | 9 ------ apps/desktop/src/locales/en/messages.json | 4 +-- apps/desktop/src/scss/environment.scss | 13 --------- .../trial-initiation.component.html | 1 - .../environment-selector.component.html | 29 +------------------ .../environment-selector.component.ts | 13 +-------- .../layouts/frontend-layout.component.html | 2 +- apps/web/src/images/flag-eu.svg | 28 ------------------ apps/web/src/images/flag-us.svg | 9 ------ apps/web/src/locales/en/messages.json | 6 ---- .../environment-selector.component.html | 11 ++----- 17 files changed, 10 insertions(+), 199 deletions(-) delete mode 100644 apps/browser/src/popup/images/flag-eu.svg delete mode 100644 apps/browser/src/popup/images/flag-us.svg delete mode 100644 apps/desktop/src/images/flag-eu.svg delete mode 100644 apps/desktop/src/images/flag-us.svg delete mode 100644 apps/web/src/images/flag-eu.svg delete mode 100644 apps/web/src/images/flag-us.svg diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 6aea5876eac..43c3cc0b68e 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Server version" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Third-party" diff --git a/apps/browser/src/popup/images/flag-eu.svg b/apps/browser/src/popup/images/flag-eu.svg deleted file mode 100644 index bbfefd6b47a..00000000000 --- a/apps/browser/src/popup/images/flag-eu.svg +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/browser/src/popup/images/flag-us.svg b/apps/browser/src/popup/images/flag-us.svg deleted file mode 100644 index 615946d4b59..00000000000 --- a/apps/browser/src/popup/images/flag-us.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/apps/browser/src/popup/scss/environment.scss b/apps/browser/src/popup/scss/environment.scss index 19d39655f2a..1ac0f4240bd 100644 --- a/apps/browser/src/popup/scss/environment.scss +++ b/apps/browser/src/popup/scss/environment.scss @@ -90,19 +90,6 @@ html.browser_safari { background-color: themed("listItemBackgroundHoverColor") !important; } } - - img { - margin-bottom: -2px; - height: 14px; - } - - .img-us { - content: url("../images/flag-us.svg"); - } - - .img-eu { - content: url("../images/flag-eu.svg"); - } } .environment-selector-padding { diff --git a/apps/browser/src/popup/settings/about.component.html b/apps/browser/src/popup/settings/about.component.html index c8840833cf7..24fea4eb9da 100644 --- a/apps/browser/src/popup/settings/about.component.html +++ b/apps/browser/src/popup/settings/about.component.html @@ -33,7 +33,7 @@

    - {{ "serverVersion" | i18n }} ({{ "selfHosted" | i18n }}): + {{ "serverVersion" | i18n }} ({{ "selfHostedServer" | i18n }}): {{ this.serverConfig?.version }} ({{ "lastSeenOn" | i18n : (serverConfig.utcDate | date : "mediumDate") }}) diff --git a/apps/desktop/src/images/flag-eu.svg b/apps/desktop/src/images/flag-eu.svg deleted file mode 100644 index bbfefd6b47a..00000000000 --- a/apps/desktop/src/images/flag-eu.svg +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/desktop/src/images/flag-us.svg b/apps/desktop/src/images/flag-us.svg deleted file mode 100644 index 615946d4b59..00000000000 --- a/apps/desktop/src/images/flag-us.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index dbcb70d5e0c..b032c6d2de3 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." diff --git a/apps/desktop/src/scss/environment.scss b/apps/desktop/src/scss/environment.scss index b9e3eaa7b23..b18032f1a71 100644 --- a/apps/desktop/src/scss/environment.scss +++ b/apps/desktop/src/scss/environment.scss @@ -83,17 +83,4 @@ background-color: themed("listItemBackgroundHoverColor") !important; } } - - img { - margin-bottom: -2px; - height: 14px; - } - - .img-us { - content: url("../images/flag-us.svg"); - } - - .img-eu { - content: url("../images/flag-eu.svg"); - } } diff --git a/apps/web/src/app/auth/trial-initiation/trial-initiation.component.html b/apps/web/src/app/auth/trial-initiation/trial-initiation.component.html index b8127d00c1c..af8c255f632 100644 --- a/apps/web/src/app/auth/trial-initiation/trial-initiation.component.html +++ b/apps/web/src/app/auth/trial-initiation/trial-initiation.component.html @@ -63,7 +63,6 @@ {{ "startYour7DayFreeTrialOfBitwardenFor" | i18n : org }}

    diff --git a/apps/web/src/app/components/environment-selector/environment-selector.component.html b/apps/web/src/app/components/environment-selector/environment-selector.component.html index 5a5bada82df..d17a9c2b43c 100644 --- a/apps/web/src/app/components/environment-selector/environment-selector.component.html +++ b/apps/web/src/app/components/environment-selector/environment-selector.component.html @@ -12,11 +12,6 @@ aria-hidden="true" [style.visibility]="isUsServer ? 'visible' : 'hidden'" > - {{ 'usFlag' | i18n }} {{ "usDomain" | i18n }}
    - - - - {{ 'selectedRegionFlag' | i18n }} - - -
    +
    {{ "server" | i18n }}: {{ isEuServer ? ("euDomain" | i18n) : ("usDomain" | i18n) }}
    - + © {{ year }} Bitwarden Inc.
    {{ "versionNumber" | i18n : version }}
    diff --git a/apps/web/src/images/flag-eu.svg b/apps/web/src/images/flag-eu.svg deleted file mode 100644 index bbfefd6b47a..00000000000 --- a/apps/web/src/images/flag-eu.svg +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/web/src/images/flag-us.svg b/apps/web/src/images/flag-us.svg deleted file mode 100644 index 615946d4b59..00000000000 --- a/apps/web/src/images/flag-us.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index dee5121acd0..43ef1799ea2 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -7019,12 +7019,6 @@ "next": { "message": "Next" }, - "usFlag": { - "message": "US flag" - }, - "euFlag": { - "message": "EU flag" - }, "selectedRegionFlag": { "message": "Selected region flag" }, diff --git a/libs/angular/src/auth/components/environment-selector.component.html b/libs/angular/src/auth/components/environment-selector.component.html index c7307edce90..4e8d33bde68 100644 --- a/libs/angular/src/auth/components/environment-selector.component.html +++ b/libs/angular/src/auth/components/environment-selector.component.html @@ -15,7 +15,7 @@ "euDomain" | i18n }}
    @@ -44,7 +44,6 @@ selectedEnvironment === ServerEnvironmentType.US ? 'visible' : 'hidden' " > - {{ "usDomain" | i18n }}
    @@ -62,7 +61,6 @@ selectedEnvironment === ServerEnvironmentType.EU ? 'visible' : 'hidden' " >
    - {{ "euDomain" | i18n }}
    @@ -79,12 +77,7 @@ selectedEnvironment === ServerEnvironmentType.SelfHosted ? 'visible' : 'hidden' " > - - {{ "selfHosted" | i18n }} + {{ "selfHostedServer" | i18n }}
    From a21892103aa92008ad242861967a281a9741027a Mon Sep 17 00:00:00 2001 From: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Date: Thu, 7 Sep 2023 17:51:16 -0500 Subject: [PATCH 40/46] Display max project error on import (#6211) --- .../settings/porting/sm-import.component.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-import.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-import.component.ts index 3542ee7b8ee..3819b7f1dbe 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-import.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-import.component.ts @@ -8,6 +8,7 @@ import { FileDownloadService } from "@bitwarden/common/platform/abstractions/fil 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 { Utils } from "@bitwarden/common/platform/misc/utils"; import { DialogService } from "@bitwarden/components"; import { @@ -73,6 +74,13 @@ export class SecretsManagerImportComponent implements OnInit, OnDestroy { if (error?.lines?.length > 0) { this.openImportErrorDialog(error); return; + } else if (!Utils.isNullOrWhitespace(error?.message)) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + error.message + ); + return; } else if (error != null) { this.platformUtilsService.showToast( "error", From 1c0037993140964b9e50ab1d37facefe2a49f3c5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 8 Sep 2023 09:36:54 +0000 Subject: [PATCH 41/46] Autosync the updated translations (#6229) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/af/messages.json | 4 +- apps/desktop/src/locales/ar/messages.json | 4 +- apps/desktop/src/locales/az/messages.json | 6 +- apps/desktop/src/locales/be/messages.json | 82 +- apps/desktop/src/locales/bg/messages.json | 16 +- apps/desktop/src/locales/bn/messages.json | 4 +- apps/desktop/src/locales/bs/messages.json | 4 +- apps/desktop/src/locales/ca/messages.json | 84 +- apps/desktop/src/locales/cs/messages.json | 6 +- apps/desktop/src/locales/cy/messages.json | 4 +- apps/desktop/src/locales/da/messages.json | 4 +- apps/desktop/src/locales/de/messages.json | 4 +- apps/desktop/src/locales/el/messages.json | 4 +- apps/desktop/src/locales/en_GB/messages.json | 4 +- apps/desktop/src/locales/en_IN/messages.json | 4 +- apps/desktop/src/locales/eo/messages.json | 4 +- apps/desktop/src/locales/es/messages.json | 4 +- apps/desktop/src/locales/et/messages.json | 4 +- apps/desktop/src/locales/eu/messages.json | 4 +- apps/desktop/src/locales/fa/messages.json | 4 +- apps/desktop/src/locales/fi/messages.json | 12 +- apps/desktop/src/locales/fil/messages.json | 4 +- apps/desktop/src/locales/fr/messages.json | 78 +- apps/desktop/src/locales/gl/messages.json | 4 +- apps/desktop/src/locales/he/messages.json | 6 +- apps/desktop/src/locales/hi/messages.json | 4 +- apps/desktop/src/locales/hr/messages.json | 4 +- apps/desktop/src/locales/hu/messages.json | 6 +- apps/desktop/src/locales/id/messages.json | 4 +- apps/desktop/src/locales/it/messages.json | 6 +- apps/desktop/src/locales/ja/messages.json | 4 +- apps/desktop/src/locales/ka/messages.json | 4 +- apps/desktop/src/locales/km/messages.json | 4 +- apps/desktop/src/locales/kn/messages.json | 4 +- apps/desktop/src/locales/ko/messages.json | 4 +- apps/desktop/src/locales/lt/messages.json | 938 +++++++++---------- apps/desktop/src/locales/lv/messages.json | 6 +- apps/desktop/src/locales/me/messages.json | 4 +- apps/desktop/src/locales/ml/messages.json | 4 +- apps/desktop/src/locales/mr/messages.json | 4 +- apps/desktop/src/locales/my/messages.json | 4 +- apps/desktop/src/locales/nb/messages.json | 4 +- apps/desktop/src/locales/ne/messages.json | 20 +- apps/desktop/src/locales/nl/messages.json | 4 +- apps/desktop/src/locales/nn/messages.json | 4 +- apps/desktop/src/locales/or/messages.json | 4 +- apps/desktop/src/locales/pl/messages.json | 4 +- apps/desktop/src/locales/pt_BR/messages.json | 4 +- apps/desktop/src/locales/pt_PT/messages.json | 6 +- apps/desktop/src/locales/ro/messages.json | 4 +- apps/desktop/src/locales/ru/messages.json | 12 +- apps/desktop/src/locales/si/messages.json | 4 +- apps/desktop/src/locales/sk/messages.json | 6 +- apps/desktop/src/locales/sl/messages.json | 4 +- apps/desktop/src/locales/sr/messages.json | 6 +- apps/desktop/src/locales/sv/messages.json | 4 +- apps/desktop/src/locales/te/messages.json | 4 +- apps/desktop/src/locales/th/messages.json | 4 +- apps/desktop/src/locales/tr/messages.json | 4 +- apps/desktop/src/locales/uk/messages.json | 10 +- apps/desktop/src/locales/vi/messages.json | 4 +- apps/desktop/src/locales/zh_CN/messages.json | 10 +- apps/desktop/src/locales/zh_TW/messages.json | 4 +- 63 files changed, 746 insertions(+), 746 deletions(-) diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json index fa880624311..7ef63a8aab2 100644 --- a/apps/desktop/src/locales/af/messages.json +++ b/apps/desktop/src/locales/af/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Selghehuisves" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Toegang geweier. U het nie toestemming om hierdie blad te sien nie." diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json index 914d2026267..ceefd1ecc26 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "استضافة ذاتية" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "غير مسموح بالدخول. ليس لديك الصلاحية لعرض هذه الصفحة." diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index 3256e1198b2..8d0f0211a4c 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -1078,7 +1078,7 @@ "message": "Fayl qoşmaları üçün 1 GB şifrələnmiş saxlama sahəsi." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "YubiKey və Duo kimi mülkiyyətçi iki addımlı giriş seçimləri." }, "premiumSignUpReports": { "message": "Anbarınızın təhlükəsiyini təmin etmək üçün parol gigiyenası, hesab sağlamlığı və verilənlərin pozulması hesabatları." @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Öz-özünə sahiblik edən" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Müraciət rədd edildi. Bu səhifəyə baxmaq üçün icazəniz yoxdur." diff --git a/apps/desktop/src/locales/be/messages.json b/apps/desktop/src/locales/be/messages.json index fc45db18b3f..666dac70c24 100644 --- a/apps/desktop/src/locales/be/messages.json +++ b/apps/desktop/src/locales/be/messages.json @@ -1078,7 +1078,7 @@ "message": "1 ГБ зашыфраванага сховішча для далучаных файлаў." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Прапрыетарныя варыянты двухэтапнага ўваходу, такія як YubiKey, FIDO U2F і Duo." }, "premiumSignUpReports": { "message": "Гігіена пароляў, здароўе ўліковага запісу і справаздачы аб уцечках даных для забеспячэння бяспекі вашага сховішча." @@ -1493,7 +1493,7 @@ "message": "Для таго, каб аднавіць доступ да сховішча, патрабуецца паўторная аўтарызацыя." }, "unlockMethodNeededToChangeTimeoutActionDesc": { - "message": "Set up an unlock method to change your vault timeout action." + "message": "Наладзіць метад разблакіроўкі для змянення дзеяння часу чакання вашага сховішча." }, "lock": { "message": "Заблакіраваць", @@ -2110,7 +2110,7 @@ "message": "Увайсці з іншай прылады" }, "loginInitiated": { - "message": "Login initiated" + "message": "Ініцыяваны ўваход" }, "notificationSentDevice": { "message": "Апавяшчэнне было адпраўлена на вашу прыладу." @@ -2250,31 +2250,31 @@ "message": "Рэкамендаваныя налады абнаўлення" }, "deviceApprovalRequired": { - "message": "Device approval required. Select an approval option below:" + "message": "Патрабуецца ўхваленне прылады. Выберыце параметры ўхвалення ніжэй:" }, "rememberThisDevice": { - "message": "Remember this device" + "message": "Запомніць гэту прыладу" }, "uncheckIfPublicDevice": { - "message": "Uncheck if using a public device" + "message": "Здыміце пазнаку, калі выкарыстоўваеце агульнадаступную прыладу" }, "approveFromYourOtherDevice": { - "message": "Approve from your other device" + "message": "Ухваліць з іншай вашай прылады" }, "requestAdminApproval": { - "message": "Request admin approval" + "message": "Запытаць ухваленне адміністратара" }, "approveWithMasterPassword": { - "message": "Approve with master password" + "message": "Ухваліць з дапамогай асноўнага пароля" }, "region": { - "message": "Region" + "message": "Рэгіён" }, "ssoIdentifierRequired": { - "message": "Organization SSO identifier is required." + "message": "Неабходны ідэнтыфікатар SSO арганізацыі." }, "eu": { - "message": "EU", + "message": "ЕС", "description": "European Union" }, "loggingInOn": { @@ -2286,47 +2286,47 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Уласнае размяшчэнне" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Доступ забаронены. У вас не дастаткова правоў для прагляду гэтай старонкі." }, "accountSuccessfullyCreated": { - "message": "Account successfully created!" + "message": "Уліковы запіс паспяхова створаны!" }, "adminApprovalRequested": { - "message": "Admin approval requested" + "message": "Патрабуецца ўхваленне адміністратара" }, "adminApprovalRequestSentToAdmins": { - "message": "Your request has been sent to your admin." + "message": "Ваш запыт адпраўлены адміністратару." }, "youWillBeNotifiedOnceApproved": { - "message": "You will be notified once approved." + "message": "Вы атрымаеце апавяшчэння пасля яго ўхвалення." }, "troubleLoggingIn": { - "message": "Trouble logging in?" + "message": "Праблемы з уваходам?" }, "loginApproved": { - "message": "Login approved" + "message": "Уваход ухвалены" }, "userEmailMissing": { - "message": "User email missing" + "message": "Адсутнічае электронная пошта карыстальніка" }, "deviceTrusted": { - "message": "Device trusted" + "message": "Давераная прылада" }, "inputRequired": { - "message": "Input is required." + "message": "Неабходны ўвод даных." }, "required": { - "message": "required" + "message": "патрабуецца" }, "search": { - "message": "Search" + "message": "Пошук" }, "inputMinLength": { - "message": "Input must be at least $COUNT$ characters long.", + "message": "Даўжыня ўведзеных даных павінна складаць прынамсі $COUNT$ сімв.", "placeholders": { "count": { "content": "$1", @@ -2335,7 +2335,7 @@ } }, "inputMaxLength": { - "message": "Input must not exceed $COUNT$ characters in length.", + "message": "Даўжыня ўведзеных даных не можа перавышаць наступную колькасць сімвалаў: $COUNT$", "placeholders": { "count": { "content": "$1", @@ -2344,7 +2344,7 @@ } }, "inputForbiddenCharacters": { - "message": "The following characters are not allowed: $CHARACTERS$", + "message": "Наступныя сімвалы забаронены: $CHARACTERS$", "placeholders": { "characters": { "content": "$1", @@ -2353,7 +2353,7 @@ } }, "inputMinValue": { - "message": "Input value must be at least $MIN$.", + "message": "Мінімальная даўжыня сімвалаў значэння, якое будзе ўводзіцца: $MIN$.", "placeholders": { "min": { "content": "$1", @@ -2362,7 +2362,7 @@ } }, "inputMaxValue": { - "message": "Input value must not exceed $MAX$.", + "message": "Максімальная даўжыня сімвалаў значэння, якое будзе ўводзіцца: $MAX$.", "placeholders": { "max": { "content": "$1", @@ -2371,17 +2371,17 @@ } }, "multipleInputEmails": { - "message": "1 or more emails are invalid" + "message": "1 або некалькі адрасоў электроннай пошты з'яўляюцца памылковымі" }, "inputTrimValidator": { - "message": "Input must not contain only whitespace.", + "message": "Уведзенае значэнне не павінна змяшчаць толькі прабелы.", "description": "Notification to inform the user that a form's input can't contain only whitespace." }, "inputEmail": { - "message": "Input is not an email address." + "message": "Уведзеныя даныя не з'яўляюцца адрасам электроннай пошты." }, "fieldsNeedAttention": { - "message": "$COUNT$ field(s) above need your attention.", + "message": "Колькасць палёў, якія патрабуюць вашай увагі: $COUNT$", "placeholders": { "count": { "content": "$1", @@ -2390,22 +2390,22 @@ } }, "selectPlaceholder": { - "message": "-- Select --" + "message": "-- Выбраць --" }, "multiSelectPlaceholder": { - "message": "-- Type to filter --" + "message": "- Увядзіце для фільтрацыі -" }, "multiSelectLoading": { - "message": "Retrieving options..." + "message": "Атрыманне параметраў..." }, "multiSelectNotFound": { - "message": "No items found" + "message": "Элементаў не знойдзена" }, "multiSelectClearAll": { - "message": "Clear all" + "message": "Ачысціць усё" }, "plusNMore": { - "message": "+ $QUANTITY$ more", + "message": "+ яшчэ $QUANTITY$", "placeholders": { "quantity": { "content": "$1", @@ -2414,6 +2414,6 @@ } }, "submenu": { - "message": "Submenu" + "message": "Падменю" } } diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index 2283ccbde6e..bd6552824a5 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -542,7 +542,7 @@ "message": "Повторното въвеждане на главната парола е задължително." }, "masterPasswordMinlength": { - "message": "Master password must be at least $VALUE$ characters long.", + "message": "Главната парола трябва да е дълга поне $VALUE$ знака.", "description": "The Master Password must be at least a specific number of characters long.", "placeholders": { "value": { @@ -1078,7 +1078,7 @@ "message": "1 ГБ пространство за файлове, които се шифроват." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Частно двустепенно удостоверяване чрез YubiKey и Duo." }, "premiumSignUpReports": { "message": "Проверки в списъците с публикувани пароли, проверка на регистрациите и доклади за пробивите в сигурността, което спомага трезорът ви да е допълнително защитен." @@ -2214,16 +2214,16 @@ "message": "Направена е заявка за вписване" }, "exposedMasterPassword": { - "message": "Exposed Master Password" + "message": "Разобличена главна парола" }, "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": "Паролата е намерена в пробив на данни. Използвайте уникална парола, за да защитите вашия акаунт. Наистина ли искате да използвате слаба парола?" }, "weakAndExposedMasterPassword": { - "message": "Weak and Exposed Master Password" + "message": "Слаба и разобличена главна парола" }, "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": "Разпозната е слаба парола. Използвайте силна парола, за да защитете данните си. Наистина ли искате да използвате слаба парола?" }, "checkForBreaches": { "message": "Проверяване в известните случаи на изтекли данни за тази парола" @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Собствен хостинг" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Достъпът е отказан. Нямате право за преглед на тази страница." diff --git a/apps/desktop/src/locales/bn/messages.json b/apps/desktop/src/locales/bn/messages.json index e59dd9de8b9..0297dfc157c 100644 --- a/apps/desktop/src/locales/bn/messages.json +++ b/apps/desktop/src/locales/bn/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." diff --git a/apps/desktop/src/locales/bs/messages.json b/apps/desktop/src/locales/bs/messages.json index c88f7f392f1..0d5ed75caf9 100644 --- a/apps/desktop/src/locales/bs/messages.json +++ b/apps/desktop/src/locales/bs/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index 9850e4b82a9..ab40caa8431 100644 --- a/apps/desktop/src/locales/ca/messages.json +++ b/apps/desktop/src/locales/ca/messages.json @@ -1078,7 +1078,7 @@ "message": "1 GB d'emmagatzematge xifrat per als fitxers adjunts." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Opcions propietàries de doble factor com ara YubiKey i Duo." }, "premiumSignUpReports": { "message": "Requisits d'higiene de la contrasenya, salut del compte i informe d'infraccions de dades per mantenir la seguretat de la vostra caixa forta." @@ -1493,7 +1493,7 @@ "message": "Una caixa forta desconnectada requereix que torneu a autentificar-vos per accedir-hi de nou." }, "unlockMethodNeededToChangeTimeoutActionDesc": { - "message": "Set up an unlock method to change your vault timeout action." + "message": "Configura un mètode de desbloqueig per canviar l'acció del temps d'espera de la caixa forta." }, "lock": { "message": "Bloqueja", @@ -2110,7 +2110,7 @@ "message": "Inicia sessió amb un altre dispositiu" }, "loginInitiated": { - "message": "Login initiated" + "message": "S'ha iniciat la sessió" }, "notificationSentDevice": { "message": "S'ha enviat una notificació al vostre dispositiu." @@ -2250,35 +2250,35 @@ "message": "Actualització de configuració recomanada" }, "deviceApprovalRequired": { - "message": "Device approval required. Select an approval option below:" + "message": "Cal l'aprovació del dispositiu. Seleccioneu una opció d'aprovació a continuació:" }, "rememberThisDevice": { - "message": "Remember this device" + "message": "Recorda aquest dispositiu" }, "uncheckIfPublicDevice": { - "message": "Uncheck if using a public device" + "message": "Desmarqueu si utilitzeu un dispositiu públic" }, "approveFromYourOtherDevice": { - "message": "Approve from your other device" + "message": "Aproveu des d'un altre dispositiu vostre" }, "requestAdminApproval": { - "message": "Request admin approval" + "message": "Sol·liciteu l'aprovació de l'administrador" }, "approveWithMasterPassword": { - "message": "Approve with master password" + "message": "Aprova amb contrasenya mestra" }, "region": { - "message": "Region" + "message": "Regió" }, "ssoIdentifierRequired": { - "message": "Organization SSO identifier is required." + "message": "Es requereix un identificador SSO de l'organització." }, "eu": { - "message": "EU", + "message": "UE", "description": "European Union" }, "loggingInOn": { - "message": "Logging in on" + "message": "Inici de sessió en" }, "usDomain": { "message": "bitwarden.com" @@ -2286,47 +2286,47 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Autoallotjat" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Accés denegat. No teniu permís per veure aquesta pàgina." }, "accountSuccessfullyCreated": { - "message": "Account successfully created!" + "message": "Compte creat correctament!" }, "adminApprovalRequested": { - "message": "Admin approval requested" + "message": "S'ha sol·licitat l'aprovació de l'administrador" }, "adminApprovalRequestSentToAdmins": { - "message": "Your request has been sent to your admin." + "message": "La vostra sol·licitud s'ha enviat a l'administrador." }, "youWillBeNotifiedOnceApproved": { - "message": "You will be notified once approved." + "message": "Se us notificarà una vegada aprovat." }, "troubleLoggingIn": { - "message": "Trouble logging in?" + "message": "Teniu problemes per iniciar la sessió?" }, "loginApproved": { - "message": "Login approved" + "message": "S'ha aprovat l'inici de sessió" }, "userEmailMissing": { - "message": "User email missing" + "message": "Falta el correu electrònic de l'usuari" }, "deviceTrusted": { - "message": "Device trusted" + "message": "Dispositiu de confiança" }, "inputRequired": { - "message": "Input is required." + "message": "L'entrada és obligatòria." }, "required": { - "message": "required" + "message": "obligatori" }, "search": { - "message": "Search" + "message": "Cerca" }, "inputMinLength": { - "message": "Input must be at least $COUNT$ characters long.", + "message": "L'entrada ha de tenir com a mínim $COUNT$ caràcters.", "placeholders": { "count": { "content": "$1", @@ -2335,7 +2335,7 @@ } }, "inputMaxLength": { - "message": "Input must not exceed $COUNT$ characters in length.", + "message": "L'entrada no ha de superar $COUNT$ caràcters de longitud.", "placeholders": { "count": { "content": "$1", @@ -2344,7 +2344,7 @@ } }, "inputForbiddenCharacters": { - "message": "The following characters are not allowed: $CHARACTERS$", + "message": "Els següents caràcters no estan permesos:\n$CHARACTERS$", "placeholders": { "characters": { "content": "$1", @@ -2353,7 +2353,7 @@ } }, "inputMinValue": { - "message": "Input value must be at least $MIN$.", + "message": "El valor d'entrada ha de ser com a mínim $MIN$.", "placeholders": { "min": { "content": "$1", @@ -2362,7 +2362,7 @@ } }, "inputMaxValue": { - "message": "Input value must not exceed $MAX$.", + "message": "El valor d'entrada no ha de ser superior a $MAX$.", "placeholders": { "max": { "content": "$1", @@ -2371,17 +2371,17 @@ } }, "multipleInputEmails": { - "message": "1 or more emails are invalid" + "message": "1 o més correus no són vàlids" }, "inputTrimValidator": { - "message": "Input must not contain only whitespace.", + "message": "L'entrada no ha de contenir només espais en blanc.", "description": "Notification to inform the user that a form's input can't contain only whitespace." }, "inputEmail": { - "message": "Input is not an email address." + "message": "L'entrada no és una adreça de correu electrònic." }, "fieldsNeedAttention": { - "message": "$COUNT$ field(s) above need your attention.", + "message": "$COUNT$ camp(s) de dalt necessiten la vostra atenció.", "placeholders": { "count": { "content": "$1", @@ -2390,22 +2390,22 @@ } }, "selectPlaceholder": { - "message": "-- Select --" + "message": "-- Selecciona --" }, "multiSelectPlaceholder": { - "message": "-- Type to filter --" + "message": "-- Escriviu per filtrar --" }, "multiSelectLoading": { - "message": "Retrieving options..." + "message": "Obtenint opcions..." }, "multiSelectNotFound": { - "message": "No items found" + "message": "No s'ha trobat cap element" }, "multiSelectClearAll": { - "message": "Clear all" + "message": "Esborra-ho tot" }, "plusNMore": { - "message": "+ $QUANTITY$ more", + "message": "+ $QUANTITY$ més", "placeholders": { "quantity": { "content": "$1", @@ -2414,6 +2414,6 @@ } }, "submenu": { - "message": "Submenu" + "message": "Submenú" } } diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index 4ce336733f6..3d90680ae0e 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -1078,7 +1078,7 @@ "message": "1 GB šifrovaného uložiště pro přílohy." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Volby proprietálních dvoufázových přihlášení jako je YubiKey a Duo." }, "premiumSignUpReports": { "message": "Reporty o hygieně Vašich hesel, zdraví účtu a narušeních bezpečnosti." @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Vlastní hosting" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Přístup byl odepřen. Nemáte oprávnění k zobrazení této stránky." diff --git a/apps/desktop/src/locales/cy/messages.json b/apps/desktop/src/locales/cy/messages.json index 38e81a83bfd..6d569a89554 100644 --- a/apps/desktop/src/locales/cy/messages.json +++ b/apps/desktop/src/locales/cy/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index 157e1313ff5..0f3a5603b7e 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Selv-hostet" + "selfHostedServer": { + "message": "selv-hostet" }, "accessDenied": { "message": "Adgang nægtet. Nødvendig tilladelse til at se siden mangler." diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index d16eb636284..889446eff62 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Selbst gehostet" + "selfHostedServer": { + "message": "selbst gehostet" }, "accessDenied": { "message": "Zugriff verweigert. Du hast keine Berechtigung, diese Seite anzuzeigen." diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index fd800e8d713..9e2c135b873 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index 6bc9772eb88..923a8e143b9 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json index 363648da567..19c80a97d3b 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." diff --git a/apps/desktop/src/locales/eo/messages.json b/apps/desktop/src/locales/eo/messages.json index 66195d90739..0da8a023615 100644 --- a/apps/desktop/src/locales/eo/messages.json +++ b/apps/desktop/src/locales/eo/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index b40c409d1b0..aaaa54a35a4 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Autoalojado" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Acceso denegado. No tiene permiso para ver esta página." diff --git a/apps/desktop/src/locales/et/messages.json b/apps/desktop/src/locales/et/messages.json index fb74084186c..603f695d5b3 100644 --- a/apps/desktop/src/locales/et/messages.json +++ b/apps/desktop/src/locales/et/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json index 4c3931490df..517b56a07f8 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." diff --git a/apps/desktop/src/locales/fa/messages.json b/apps/desktop/src/locales/fa/messages.json index 9e5f6f8468c..bf7f8b25329 100644 --- a/apps/desktop/src/locales/fa/messages.json +++ b/apps/desktop/src/locales/fa/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "خود میزبان" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "دسترسی رد شد. شما اجازه مشاهده این صفحه را ندارید." diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index 9025306a708..76bd8d322d4 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -1078,7 +1078,7 @@ "message": "1 Gt salattua tallennustilaa tiedostoliitteille." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Omisteiset kaksivaiheisen kirjautumisen vaihtoehdot, kuten YubiKey ja Duo." }, "premiumSignUpReports": { "message": "Salasanahygienian, tilin terveyden ja tietovuotojen raportointitoiminnot pitävät holvisi turvassa." @@ -2256,7 +2256,7 @@ "message": "Muista tämä laite" }, "uncheckIfPublicDevice": { - "message": "Poista käytöstä julkisilla laitteilla" + "message": "Poista valinta julkisilla laitteilla" }, "approveFromYourOtherDevice": { "message": "Hyväksy muilta laitteiltasi" @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Itse ylläpidetty" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Pääsy estetty. Sinulla ei ole oikeutta avata sivua." @@ -2299,7 +2299,7 @@ "message": "Hyväksyntää pyydetty ylläpidolta" }, "adminApprovalRequestSentToAdmins": { - "message": "Pyyntösi on välitetty ylläpidolle." + "message": "Pyyntösi on välitetty ylläpidollesi." }, "youWillBeNotifiedOnceApproved": { "message": "Saat ilmoituksen kun se on hyväksytty." @@ -2308,7 +2308,7 @@ "message": "Ongelmia kirjautumisessa?" }, "loginApproved": { - "message": "Kirjautuminen hyväksyttiin" + "message": "Kirjautuminen hyväksytty" }, "userEmailMissing": { "message": "Käyttäjän sähköpostiosoite puuttuu" diff --git a/apps/desktop/src/locales/fil/messages.json b/apps/desktop/src/locales/fil/messages.json index 87b9bde0c46..c89830c0d8f 100644 --- a/apps/desktop/src/locales/fil/messages.json +++ b/apps/desktop/src/locales/fil/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index 3f249000bd5..776e518fea8 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -1493,7 +1493,7 @@ "message": "Une nouvelle authentification est requise pour accéder à nouveau à votre coffre." }, "unlockMethodNeededToChangeTimeoutActionDesc": { - "message": "Set up an unlock method to change your vault timeout action." + "message": "Configurez une méthode de déverrouillage pour changer le délai d'attente de votre coffre." }, "lock": { "message": "Verrouiller", @@ -2110,7 +2110,7 @@ "message": "Connectez-vous avec un autre appareil" }, "loginInitiated": { - "message": "Login initiated" + "message": "Connexion initiée" }, "notificationSentDevice": { "message": "Une notification a été envoyée à votre appareil." @@ -2250,28 +2250,28 @@ "message": "Une mise à jour des paramètres est recommandée" }, "deviceApprovalRequired": { - "message": "Device approval required. Select an approval option below:" + "message": "L'approbation de l'appareil est requise. Sélectionnez une option d'approbation ci-dessous:" }, "rememberThisDevice": { - "message": "Remember this device" + "message": "Se souvenir de cet appareil" }, "uncheckIfPublicDevice": { - "message": "Uncheck if using a public device" + "message": "Décocher si vous utilisez un appareil public" }, "approveFromYourOtherDevice": { - "message": "Approve from your other device" + "message": "Approuver sur votre autre appareil" }, "requestAdminApproval": { - "message": "Request admin approval" + "message": "Demander l'approbation de l'administrateur" }, "approveWithMasterPassword": { - "message": "Approve with master password" + "message": "Approuver avec le mot de passe principal" }, "region": { - "message": "Region" + "message": "Région" }, "ssoIdentifierRequired": { - "message": "Organization SSO identifier is required." + "message": "Identifiant SSO de l'organisation requis." }, "eu": { "message": "EU", @@ -2286,47 +2286,47 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Auto-hébergé" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Accès refusé. Vous n'avez pas l'autorisation de voir cette page." }, "accountSuccessfullyCreated": { - "message": "Account successfully created!" + "message": "Compte créé avec succès !" }, "adminApprovalRequested": { - "message": "Admin approval requested" + "message": "Approbation d'un admin demandée" }, "adminApprovalRequestSentToAdmins": { - "message": "Your request has been sent to your admin." + "message": "Votre demande a été envoyée à votre administrateur." }, "youWillBeNotifiedOnceApproved": { - "message": "You will be notified once approved." + "message": "Vous serez notifié une fois approuvé." }, "troubleLoggingIn": { - "message": "Trouble logging in?" + "message": "Problème pour vous connecter ?" }, "loginApproved": { - "message": "Login approved" + "message": "Identifiant approuvée" }, "userEmailMissing": { - "message": "User email missing" + "message": "E-mail de l'utilisateur manquant" }, "deviceTrusted": { - "message": "Device trusted" + "message": "Appareil de confiance" }, "inputRequired": { - "message": "Input is required." + "message": "Saisie requise." }, "required": { - "message": "required" + "message": "requis" }, "search": { - "message": "Search" + "message": "Rechercher" }, "inputMinLength": { - "message": "Input must be at least $COUNT$ characters long.", + "message": "La saisie doit comporter au moins $COUNT$ caractères.", "placeholders": { "count": { "content": "$1", @@ -2335,7 +2335,7 @@ } }, "inputMaxLength": { - "message": "Input must not exceed $COUNT$ characters in length.", + "message": "La saisie ne doit pas dépasser $COUNT$ caractères de long.", "placeholders": { "count": { "content": "$1", @@ -2344,7 +2344,7 @@ } }, "inputForbiddenCharacters": { - "message": "The following characters are not allowed: $CHARACTERS$", + "message": "Les caractères suivants ne sont pas autorisés: $CHARACTERS$", "placeholders": { "characters": { "content": "$1", @@ -2353,7 +2353,7 @@ } }, "inputMinValue": { - "message": "Input value must be at least $MIN$.", + "message": "La valeur d'entrée doit être au moins de $MIN$.", "placeholders": { "min": { "content": "$1", @@ -2362,7 +2362,7 @@ } }, "inputMaxValue": { - "message": "Input value must not exceed $MAX$.", + "message": "La valeur d'entrée ne doit pas excéder $MAX$.", "placeholders": { "max": { "content": "$1", @@ -2371,17 +2371,17 @@ } }, "multipleInputEmails": { - "message": "1 or more emails are invalid" + "message": "Un ou plusieurs adresses e-mail ne sont pas valides" }, "inputTrimValidator": { - "message": "Input must not contain only whitespace.", + "message": "La saisie ne doit pas contenir que des espaces.", "description": "Notification to inform the user that a form's input can't contain only whitespace." }, "inputEmail": { - "message": "Input is not an email address." + "message": "La saisie n'est pas une adresse e-mail." }, "fieldsNeedAttention": { - "message": "$COUNT$ field(s) above need your attention.", + "message": "$COUNT$ champ(s) ci-dessus nécessitent votre attention.", "placeholders": { "count": { "content": "$1", @@ -2390,22 +2390,22 @@ } }, "selectPlaceholder": { - "message": "-- Select --" + "message": "-- Sélectionner--" }, "multiSelectPlaceholder": { - "message": "-- Type to filter --" + "message": "-- Tapez pour Filtrer --" }, "multiSelectLoading": { - "message": "Retrieving options..." + "message": "Récupération des options..." }, "multiSelectNotFound": { - "message": "No items found" + "message": "Aucun élément trouvé" }, "multiSelectClearAll": { - "message": "Clear all" + "message": "Effacer tout" }, "plusNMore": { - "message": "+ $QUANTITY$ more", + "message": "+ $QUANTITY$ de plus", "placeholders": { "quantity": { "content": "$1", @@ -2414,6 +2414,6 @@ } }, "submenu": { - "message": "Submenu" + "message": "Sous-menu" } } diff --git a/apps/desktop/src/locales/gl/messages.json b/apps/desktop/src/locales/gl/messages.json index 38e81a83bfd..6d569a89554 100644 --- a/apps/desktop/src/locales/gl/messages.json +++ b/apps/desktop/src/locales/gl/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json index ad625991fa1..6fd77738b06 100644 --- a/apps/desktop/src/locales/he/messages.json +++ b/apps/desktop/src/locales/he/messages.json @@ -1078,7 +1078,7 @@ "message": "1 ג'יגה של מקום אחסון מוצפן עבור קבצים מצורפים." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "אפשרויות כניסה דו שלבית קנייניות כמו YubiKey ו־Duo." }, "premiumSignUpReports": { "message": "היגיינת סיסמאות, מצב בריאות החשבון, ודיווחים מעודכנים על פרצות חדשות בכדי לשמור על הכספת שלך בטוחה." @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." diff --git a/apps/desktop/src/locales/hi/messages.json b/apps/desktop/src/locales/hi/messages.json index 156a431a2d2..b19b9abd72e 100644 --- a/apps/desktop/src/locales/hi/messages.json +++ b/apps/desktop/src/locales/hi/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json index 42966a51575..ab08f7d5de9 100644 --- a/apps/desktop/src/locales/hr/messages.json +++ b/apps/desktop/src/locales/hr/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index 796da6fdece..7df69e64b83 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -1078,7 +1078,7 @@ "message": "1 GB titkosított fájlmelléklet tárhely." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Saját kétlépcsős bejelentkezési lehetőségek mint a YubiKey és a Duo." }, "premiumSignUpReports": { "message": "Jelszó higiénia, felhasználói fiók biztonsága, és adatszivárgási jelentések a széf biztonsága érdekében." @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Saját kiszolgáló" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "A hozzáférés megtagadásra került. Nincs jogosultság az oldal megtekintésére." diff --git a/apps/desktop/src/locales/id/messages.json b/apps/desktop/src/locales/id/messages.json index b976d1b8fd9..5dca15ddee2 100644 --- a/apps/desktop/src/locales/id/messages.json +++ b/apps/desktop/src/locales/id/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index f294888cc38..3b4c20e0f00 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -1078,7 +1078,7 @@ "message": "1 GB di spazio di archiviazione criptato per gli allegati." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Opzioni di verifica in due passaggi proprietarie come YubiKey e Duo." }, "premiumSignUpReports": { "message": "Sicurezza delle password, integrità dell'account e rapporti sulle violazioni dei dati per mantenere la tua cassaforte sicura." @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Accesso negato. Non hai i permessi necessari per visualizzare questa pagina." diff --git a/apps/desktop/src/locales/ja/messages.json b/apps/desktop/src/locales/ja/messages.json index f8c11aca7e5..a4f830256fa 100644 --- a/apps/desktop/src/locales/ja/messages.json +++ b/apps/desktop/src/locales/ja/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "自己ホスト型" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "アクセスが拒否されました。このページを表示する権限がありません。" diff --git a/apps/desktop/src/locales/ka/messages.json b/apps/desktop/src/locales/ka/messages.json index 38e81a83bfd..6d569a89554 100644 --- a/apps/desktop/src/locales/ka/messages.json +++ b/apps/desktop/src/locales/ka/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." diff --git a/apps/desktop/src/locales/km/messages.json b/apps/desktop/src/locales/km/messages.json index 38e81a83bfd..6d569a89554 100644 --- a/apps/desktop/src/locales/km/messages.json +++ b/apps/desktop/src/locales/km/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." diff --git a/apps/desktop/src/locales/kn/messages.json b/apps/desktop/src/locales/kn/messages.json index bfc29e570a1..5ba16861ece 100644 --- a/apps/desktop/src/locales/kn/messages.json +++ b/apps/desktop/src/locales/kn/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json index 7bff705c8ec..afe34afb3f3 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json index 38e81a83bfd..c15d273916d 100644 --- a/apps/desktop/src/locales/lt/messages.json +++ b/apps/desktop/src/locales/lt/messages.json @@ -3,52 +3,52 @@ "message": "Bitwarden" }, "filters": { - "message": "Filters" + "message": "Filtrai" }, "allItems": { - "message": "All items" + "message": "Visi elementai" }, "favorites": { - "message": "Favorites" + "message": "Mėgstamiausi" }, "types": { - "message": "Types" + "message": "Tipai" }, "typeLogin": { - "message": "Login" + "message": "Prisijungti" }, "typeCard": { - "message": "Card" + "message": "Kortelė" }, "typeIdentity": { - "message": "Identity" + "message": "Tapatybė" }, "typeSecureNote": { - "message": "Secure note" + "message": "Saugus įrašas" }, "folders": { - "message": "Folders" + "message": "Aplankai" }, "collections": { - "message": "Collections" + "message": "Rinkiniai" }, "searchVault": { - "message": "Search vault" + "message": "Ieškoti saugykloje" }, "addItem": { - "message": "Add item" + "message": "Pridėti elementą" }, "shared": { - "message": "Shared" + "message": "Pasidalinta" }, "share": { - "message": "Share" + "message": "Bendrinti" }, "moveToOrganization": { - "message": "Move to organization" + "message": "Perkelti į organizaciją" }, "movedItemToOrg": { - "message": "$ITEMNAME$ moved to $ORGNAME$", + "message": "$ITEMNAME$ perkelta(s) į $ORGNAME$", "placeholders": { "itemname": { "content": "$1", @@ -61,16 +61,16 @@ } }, "moveToOrgDesc": { - "message": "Choose an organization that you wish to move this item to. Moving to an organization transfers ownership of the item to that organization. You will no longer be the direct owner of this item once it has been moved." + "message": "Pasirinkite organizaciją, į kurią norite priskirti šį elementą. Priskiriant elementą organizacijai, visos elemento valdymo teisės bus perleistos tai organizacijai. Kai elementas bus perkeltas, nebebūsite tiesioginis šio elemento savininkas." }, "attachments": { - "message": "Attachments" + "message": "Priedai" }, "viewItem": { - "message": "View item" + "message": "Peržiūrėti elementą" }, "name": { - "message": "Name" + "message": "Pavadinimas" }, "uri": { "message": "URI" @@ -86,463 +86,463 @@ } }, "newUri": { - "message": "New URI" + "message": "Naujas URI" }, "username": { - "message": "Username" + "message": "Vartotojo vardas" }, "password": { - "message": "Password" + "message": "Slaptažodis" }, "passphrase": { - "message": "Passphrase" + "message": "Slaptafrazė" }, "editItem": { - "message": "Edit item" + "message": "Redaguoti elementą" }, "emailAddress": { - "message": "Email address" + "message": "El. pašto adresas" }, "verificationCodeTotp": { - "message": "Verification code (TOTP)" + "message": "Patvirtinimo kodas (TOTP)" }, "website": { - "message": "Website" + "message": "Tinklapis" }, "notes": { - "message": "Notes" + "message": "Pastabos" }, "customFields": { - "message": "Custom fields" + "message": "Papildomi laukai" }, "launch": { - "message": "Launch" + "message": "Paleisti" }, "copyValue": { - "message": "Copy value", + "message": "Kopijuoti reikšmę", "description": "Copy value to clipboard" }, "minimizeOnCopyToClipboard": { - "message": "Minimize when copying to clipboard" + "message": "Sumažinti kopijuojant į iškarpinę" }, "minimizeOnCopyToClipboardDesc": { - "message": "Minimize application when copying an item's data to the clipboard." + "message": "Sumažinti aplikaciją kopijuojant elemento duomenis į iškarpinę." }, "toggleVisibility": { - "message": "Toggle visibility" + "message": "Perjungti matomumą" }, "toggleCollapse": { - "message": "Toggle collapse", + "message": "Perjungti sulankstymą", "description": "Toggling an expand/collapse state." }, "cardholderName": { - "message": "Cardholder name" + "message": "Kortelės savininko vardas" }, "number": { - "message": "Number" + "message": "Numeris" }, "brand": { - "message": "Brand" + "message": "Prekės ženklas" }, "expiration": { - "message": "Expiration" + "message": "Galiojimo pabaiga" }, "securityCode": { - "message": "Security code" + "message": "Apsaugos kodas" }, "identityName": { - "message": "Identity name" + "message": "Tapatybės vardas" }, "company": { - "message": "Company" + "message": "Įmonė" }, "ssn": { - "message": "Social Security number" + "message": "ID kortelės numeris" }, "passportNumber": { - "message": "Passport number" + "message": "Paso numeris" }, "licenseNumber": { - "message": "License number" + "message": "Licencijos numeris" }, "email": { - "message": "Email" + "message": "El. paštas" }, "phone": { - "message": "Phone" + "message": "Telefonas" }, "address": { - "message": "Address" + "message": "Adresas" }, "premiumRequired": { - "message": "Premium required" + "message": "Reikalinga Premium narystė" }, "premiumRequiredDesc": { - "message": "A Premium membership is required to use this feature." + "message": "Premium narystė reikalinga šiai funkcijai naudoti." }, "errorOccurred": { - "message": "An error has occurred." + "message": "Įvyko klaida." }, "error": { - "message": "Error" + "message": "Klaida" }, "january": { - "message": "January" + "message": "Sausis" }, "february": { - "message": "February" + "message": "Vasaris" }, "march": { - "message": "March" + "message": "Kovas" }, "april": { - "message": "April" + "message": "Balandis" }, "may": { - "message": "May" + "message": "Gegužė" }, "june": { - "message": "June" + "message": "Birželis" }, "july": { - "message": "July" + "message": "Liepa" }, "august": { - "message": "August" + "message": "Rugpjūtis" }, "september": { - "message": "September" + "message": "Rugsėjis" }, "october": { - "message": "October" + "message": "Spalis" }, "november": { - "message": "November" + "message": "Lapkritis" }, "december": { - "message": "December" + "message": "Gruodis" }, "ex": { - "message": "ex.", + "message": "pvz.", "description": "Short abbreviation for 'example'." }, "title": { - "message": "Title" + "message": "Pavadinimas" }, "mr": { - "message": "Mr" + "message": "Ponas" }, "mrs": { - "message": "Mrs" + "message": "Ponia" }, "ms": { - "message": "Ms" + "message": "Panelė" }, "mx": { - "message": "Mx" + "message": "Neutralus (-i)" }, "dr": { "message": "Dr" }, "expirationMonth": { - "message": "Expiration month" + "message": "Galiojimo pabaigos mėnesis" }, "expirationYear": { - "message": "Expiration year" + "message": "Galiojimo pabaigos metai" }, "select": { - "message": "Select" + "message": "Pasirinkti" }, "other": { - "message": "Other" + "message": "Kita" }, "generatePassword": { - "message": "Generate password" + "message": "Sugeneruoti slaptažodį" }, "type": { - "message": "Type" + "message": "Tipas" }, "firstName": { - "message": "First name" + "message": "Vardas" }, "middleName": { - "message": "Middle name" + "message": "Antras vardas" }, "lastName": { - "message": "Last name" + "message": "Pavardė" }, "fullName": { - "message": "Full name" + "message": "Pilnas vardas" }, "address1": { - "message": "Address 1" + "message": "Adresas 1" }, "address2": { - "message": "Address 2" + "message": "Adresas 2" }, "address3": { - "message": "Address 3" + "message": "Adresas 3" }, "cityTown": { - "message": "City / Town" + "message": "Miestas" }, "stateProvince": { - "message": "State / Province" + "message": "Rajonas/apskritis" }, "zipPostalCode": { - "message": "Zip / Postal code" + "message": "Pašto kodas" }, "country": { - "message": "Country" + "message": "Šalis" }, "save": { - "message": "Save" + "message": "Išsaugoti" }, "cancel": { - "message": "Cancel" + "message": "Atšaukti" }, "delete": { - "message": "Delete" + "message": "Ištrinti" }, "favorite": { - "message": "Favorite" + "message": "Mėgstamiausias" }, "edit": { - "message": "Edit" + "message": "Redaguoti" }, "authenticatorKeyTotp": { - "message": "Authenticator key (TOTP)" + "message": "Autentifikavimo raktas (TOTP)" }, "folder": { - "message": "Folder" + "message": "Aplankas" }, "newCustomField": { - "message": "New custom field" + "message": "Naujas pasirinktinis laukelis" }, "value": { - "message": "Value" + "message": "Reikšmė" }, "dragToSort": { - "message": "Drag to sort" + "message": "Tempti, kad surūšiuoti" }, "cfTypeText": { - "message": "Text" + "message": "Tekstas" }, "cfTypeHidden": { - "message": "Hidden" + "message": "Paslėpta" }, "cfTypeBoolean": { - "message": "Boolean" + "message": "Taip/Ne" }, "cfTypeLinked": { - "message": "Linked", + "message": "Susieta", "description": "This describes a field that is 'linked' (related) to another field." }, "linkedValue": { - "message": "Linked value", + "message": "Susieta reikšmė", "description": "This describes a value that is 'linked' (related) to another value." }, "remove": { - "message": "Remove" + "message": "Pašalinti" }, "nameRequired": { - "message": "Name is required." + "message": "Pavadinimas yra būtinas." }, "addedItem": { - "message": "Item added" + "message": "Elementas pridėtas" }, "editedItem": { - "message": "Item saved" + "message": "Elementas išsaugotas" }, "deleteItem": { - "message": "Delete item" + "message": "Šalinti elementą" }, "deleteFolder": { - "message": "Delete folder" + "message": "Šalinti aplanką" }, "deleteAttachment": { - "message": "Delete attachment" + "message": "Šalinti priedą" }, "deleteItemConfirmation": { - "message": "Do you really want to send to the trash?" + "message": "Ar tikrai norite perkelti į šiukšlinę?" }, "deletedItem": { - "message": "Item sent to trash" + "message": "Elementas perkeltas į šiukšlinę" }, "overwritePasswordConfirmation": { - "message": "Are you sure you want to overwrite the current password?" + "message": "Ar tikrai norite perrašyti dabartinį slaptažodį?" }, "overwriteUsername": { - "message": "Overwrite username" + "message": "Perrašyti vartotojo vardą" }, "overwriteUsernameConfirmation": { - "message": "Are you sure you want to overwrite the current username?" + "message": "Ar tikrai norite perrašyti dabartinį vartotojo vardą?" }, "noneFolder": { - "message": "No folder", + "message": "Nėra aplankų", "description": "This is the folder for uncategorized items" }, "addFolder": { - "message": "Add folder" + "message": "Pridėti aplanką" }, "editFolder": { - "message": "Edit folder" + "message": "Redaguoti aplanką" }, "regeneratePassword": { - "message": "Regenerate password" + "message": "Generuoti slaptažodį iš naujo" }, "copyPassword": { - "message": "Copy password" + "message": "Kopijuoti slaptažodį" }, "copyUri": { - "message": "Copy URI" + "message": "Kopijuoti nuorodą" }, "copyVerificationCodeTotp": { - "message": "Copy verification code (TOTP)" + "message": "Kopijuoti patvirtinimo kodą (TOTP)" }, "length": { - "message": "Length" + "message": "Ilgis" }, "uppercase": { - "message": "Uppercase (A-Z)" + "message": "Didžiosiomis (A-Z)" }, "lowercase": { - "message": "Lowercase (a-z)" + "message": "Mažosiomis (a-z)" }, "numbers": { - "message": "Numbers (0-9)" + "message": "Skaitmenys (0-9)" }, "specialCharacters": { - "message": "Special characters (!@#$%^&*)" + "message": "Specialieji simboliai (!@#$%^&*)" }, "numWords": { - "message": "Number of words" + "message": "Žodžių skaičius" }, "wordSeparator": { - "message": "Word separator" + "message": "Žodžių skirtukas" }, "capitalize": { - "message": "Capitalize", + "message": "Pradėti didžiosiomis", "description": "Make the first letter of a word uppercase." }, "includeNumber": { - "message": "Include number" + "message": "Įtraukti skaičius" }, "close": { - "message": "Close" + "message": "Uždaryti" }, "minNumbers": { - "message": "Minimum numbers" + "message": "Mažiausias skaičių kiekis" }, "minSpecial": { - "message": "Minimum special", + "message": "Mažiausias spec. simbolių kiekis", "description": "Minimum Special Characters" }, "ambiguous": { - "message": "Avoid ambiguous characters" + "message": "Vengti dviprasmiškų simbolių" }, "searchCollection": { - "message": "Search collection" + "message": "Ieškoti rinkinyje" }, "searchFolder": { - "message": "Search folder" + "message": "Ieškoti aplanke" }, "searchFavorites": { - "message": "Search favorites" + "message": "Ieškoti mėgstamiausiuose" }, "searchType": { - "message": "Search type", + "message": "Paieškos tipas", "description": "Search item type" }, "newAttachment": { - "message": "Add new attachment" + "message": "Pridėti naują priedą" }, "deletedAttachment": { - "message": "Attachment deleted" + "message": "Priedas ištrintas" }, "deleteAttachmentConfirmation": { - "message": "Are you sure you want to delete this attachment?" + "message": "Ar esate tikri, kad norite ištrinti šį priedą?" }, "attachmentSaved": { - "message": "Attachment saved" + "message": "Priedas išsaugotas" }, "file": { - "message": "File" + "message": "Failas" }, "selectFile": { - "message": "Select a file" + "message": "Pasirinkite failą" }, "maxFileSize": { - "message": "Maximum file size is 500 MB." + "message": "Didžiausias failo dydis – 500 MB." }, "updateKey": { - "message": "You cannot use this feature until you update your encryption key." + "message": "Negalite naudoti šios funkcijos, kol neatnaujinsite šifravimo rakto." }, "editedFolder": { - "message": "Folder saved" + "message": "Aplankas išsaugotas" }, "addedFolder": { - "message": "Folder added" + "message": "Aplankas pridėtas" }, "deleteFolderConfirmation": { - "message": "Are you sure you want to delete this folder?" + "message": "Ar tikrai norite ištrinti šį aplanką?" }, "deletedFolder": { - "message": "Folder deleted" + "message": "Aplankas ištrintas" }, "loginOrCreateNewAccount": { - "message": "Log in or create a new account to access your secure vault." + "message": "Prisijunkite arba sukurkite naują paskyrą, kad galėtumėte pasiekti saugyklą." }, "createAccount": { - "message": "Create account" + "message": "Sukurti paskyrą" }, "logIn": { - "message": "Log in" + "message": "Prisijungti" }, "submit": { - "message": "Submit" + "message": "Išsaugoti" }, "masterPass": { - "message": "Master password" + "message": "Pagrindinis slaptažodis" }, "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." + "message": "Pagrindinis slaptažodis yra slaptažodis, kurį naudojate norėdami pasiekti savo saugyklą. Labai svarbu nepamiršti pagrindinio slaptažodžio. Negalėsite atkurti slaptažodį, jei jį pamiršote." }, "masterPassHintDesc": { - "message": "A master password hint can help you remember your password if you forget it." + "message": "Pagrindinio slaptažodžio užuomina gali padėti prisiminti slaptažodį, jei jį pamiršite." }, "reTypeMasterPass": { - "message": "Re-type master password" + "message": "Pakartokite pagrindinį slaptažodį" }, "masterPassHint": { - "message": "Master password hint (optional)" + "message": "Pagrindinio slaptažodžio užuomina (neprivaloma)" }, "settings": { - "message": "Settings" + "message": "Nustatymai" }, "passwordHint": { - "message": "Password hint" + "message": "Slaptažodžio užuomina" }, "enterEmailToGetHint": { - "message": "Enter your account email address to receive your master password hint." + "message": "Įveskite savo paskyros el. pašto adresą, kad gautumėte pagrindinio slaptažodžio užuominą." }, "getMasterPasswordHint": { - "message": "Get master password hint" + "message": "Gauti pagrindinio slaptažodžio užuominą" }, "emailRequired": { - "message": "Email address is required." + "message": "Reikalingas el. pašto adresas." }, "invalidEmail": { - "message": "Invalid email address." + "message": "Netinkamas el. pašto adresas." }, "masterPasswordRequired": { - "message": "Master password is required." + "message": "Būtinas pagrindinis slaptažodis." }, "confirmMasterPasswordRequired": { - "message": "Master password retype is required." + "message": "Būtinas prisijungimo slaptažodžio patvirtinimas." }, "masterPasswordMinlength": { - "message": "Master password must be at least $VALUE$ characters long.", + "message": "Pagrindinis slaptažodis turi būti bent $VALUE$ simbolių ilgio.", "description": "The Master Password must be at least a specific number of characters long.", "placeholders": { "value": { @@ -552,52 +552,52 @@ } }, "masterPassDoesntMatch": { - "message": "Master password confirmation does not match." + "message": "Pagrindinio slaptažodžio patvirtinimas nesutampa." }, "newAccountCreated": { - "message": "Your new account has been created! You may now log in." + "message": "Jūsų paskyra sukurta! Galite prisijungti." }, "masterPassSent": { - "message": "We've sent you an email with your master password hint." + "message": "Išsiuntėme jums el. laišką su pagrindinio slaptažodžio užuomina." }, "unexpectedError": { - "message": "An unexpected error has occurred." + "message": "Įvyko netikėta klaida." }, "itemInformation": { - "message": "Item information" + "message": "Elemento informacija" }, "noItemsInList": { - "message": "There are no items to list." + "message": "Nėra rodytinų elementų." }, "sendVerificationCode": { - "message": "Send a verification code to your email" + "message": "Siųsti patvirtinimo kodą į el. paštą" }, "sendCode": { - "message": "Send code" + "message": "Siųsti kodą" }, "codeSent": { - "message": "Code sent" + "message": "Kodas išsiųstas" }, "verificationCode": { - "message": "Verification code" + "message": "Patvirtinimo kodas" }, "confirmIdentity": { - "message": "Confirm your identity to continue." + "message": "Norint tęsti, patvirtinkite tapatybę." }, "verificationCodeRequired": { - "message": "Verification code is required." + "message": "Būtinas patvirtinimo kodas." }, "invalidVerificationCode": { - "message": "Invalid verification code" + "message": "Neteisingas patvirtinimo kodas" }, "continue": { - "message": "Continue" + "message": "Tęsti" }, "enterVerificationCodeApp": { - "message": "Enter the 6 digit verification code from your authenticator app." + "message": "Įveskite 6 skaitmenų patvirtinimo kodą iš autentifikavimo programos." }, "enterVerificationCodeEmail": { - "message": "Enter the 6 digit verification code that was emailed to $EMAIL$.", + "message": "Įveskite 6 skaitmenų patvirtinimo kodą, kuris buvo išsiųstas $EMAIL$ el. paštu.", "placeholders": { "email": { "content": "$1", @@ -606,7 +606,7 @@ } }, "verificationCodeEmailSent": { - "message": "Verification email sent to $EMAIL$.", + "message": "Patvirtinimo laiškas išsiųstas į $EMAIL$.", "placeholders": { "email": { "content": "$1", @@ -615,216 +615,216 @@ } }, "rememberMe": { - "message": "Remember me" + "message": "Prisiminti mane" }, "sendVerificationCodeEmailAgain": { - "message": "Send verification code email again" + "message": "Pakartotinai atsiųsti patvirtinimo kodą el. paštu" }, "useAnotherTwoStepMethod": { - "message": "Use another two-step login method" + "message": "Naudoti kitą dviejų žingsnių prisijungimo metodą" }, "insertYubiKey": { - "message": "Insert your YubiKey into your computer's USB port, then touch its button." + "message": "Įkiškite YubiKey į savo kompiuterio USB prievadą, tada palieskite jo mygtuką." }, "insertU2f": { - "message": "Insert your security key into your computer's USB port. If it has a button, touch it." + "message": "Įkiškite savo saugos raktą į kompiuterio USB prievadą. Jei jame yra mygtukas, palieskite jį." }, "recoveryCodeDesc": { - "message": "Lost access to all of your two-factor providers? Use your recovery code to turn off all two-factor providers on your account." + "message": "Praradote prieigą prie visų savo dviejų faktorių tiekėjų? Naudokite atkūrimo kodą, kad iš savo paskyros išjungtumėte visus dviejų faktorių tiekėjus." }, "recoveryCodeTitle": { - "message": "Recovery code" + "message": "Atkūrimo kodas" }, "authenticatorAppTitle": { - "message": "Authenticator app" + "message": "Autentifikavimo programa" }, "authenticatorAppDesc": { - "message": "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes.", + "message": "Naudokite autentifikatoriaus programėlę (pvz. Authy arba Google Authenticator), kad sugeneruotumėte laiko patikrinimo kodus.", "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." }, "yubiKeyTitle": { - "message": "YubiKey OTP security key" + "message": "YubiKey OTP saugumo raktas" }, "yubiKeyDesc": { - "message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices." + "message": "Naudokite YubiKey, kad prisijungtumėte prie savo paskyros. Veikia su YubiKey 4, 4 Nano, 4C ir NEO įrenginiais." }, "duoDesc": { - "message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.", + "message": "Patvirtinkite su Duo Security naudodami Duo Mobile programą, SMS žinutę, telefono skambutį arba U2F saugumo raktą.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { - "message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.", + "message": "Patikrinkite su Duo Security savo organizacijai naudodami Duo Mobile programą, SMS žinutę, telefono skambutį arba U2F saugumo raktą.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, "webAuthnDesc": { - "message": "Use any WebAuthn compatible security key to access your account." + "message": "Naudokite bet kurį WebAuthn palaikantį saugumo raktą, kad galėtumėte naudotis savo paskyra." }, "emailTitle": { - "message": "Email" + "message": "El. paštas" }, "emailDesc": { - "message": "Verification codes will be emailed to you." + "message": "Patvirtinimo kodai bus atsiųsti el. paštu." }, "loginUnavailable": { - "message": "Login unavailable" + "message": "Prisijungimas nepasiekiamas" }, "noTwoStepProviders": { - "message": "This account has two-step login set up, however, none of the configured two-step providers are supported by this device." + "message": "Šioje paskyroje nustatytas dviejų žingsnių prisijungimas, tačiau nė vienas iš sukonfigūruotų dviejų žingsnių paslaugų teikėjų nėra palaikomi šiame įrenginyje." }, "noTwoStepProviders2": { - "message": "Please add additional providers that are better supported across devices (such as an authenticator app)." + "message": "Prašome pridėti papildomus tiekėjus, kurie labiau palaikomi tarp įrenginių (pvz. autentifikavimo programėlę)." }, "twoStepOptions": { - "message": "Two-step login options" + "message": "Dviejų žingsnių prisijungimo parinktys" }, "selfHostedEnvironment": { - "message": "Self-hosted environment" + "message": "Savarankiškai sukurta aplinka" }, "selfHostedEnvironmentFooter": { - "message": "Specify the base URL of your on-premises hosted Bitwarden installation." + "message": "Nurodykite pagrindinį URL adresą savo patalpose esančio Bitwarden įdiegimo." }, "customEnvironment": { - "message": "Custom environment" + "message": "Individualizuota aplinka" }, "customEnvironmentFooter": { - "message": "For advanced users. You can specify the base URL of each service independently." + "message": "Pažengusiems naudotojams. Galite nurodyti kiekvienos paslaugos pagrindinį URL adresą atskirai." }, "baseUrl": { - "message": "Server URL" + "message": "Serverio nuoroda" }, "apiUrl": { - "message": "API server URL" + "message": "API serverio nuoroda" }, "webVaultUrl": { - "message": "Web vault server URL" + "message": "Internetinės saugyklos serverio nuoroda" }, "identityUrl": { - "message": "Identity server URL" + "message": "Identifikavimo serverio nuoroda" }, "notificationsUrl": { - "message": "Notifications server URL" + "message": "Notifikacijų serverio nuoroda" }, "iconsUrl": { - "message": "Icons server URL" + "message": "Piktogramų serverio nuoroda" }, "environmentSaved": { - "message": "Environment URLs saved" + "message": "Aplinkos URL nuorodos išsaugotos" }, "ok": { - "message": "Ok" + "message": "Gerai" }, "yes": { - "message": "Yes" + "message": "Taip" }, "no": { - "message": "No" + "message": "Ne" }, "overwritePassword": { - "message": "Overwrite password" + "message": "Perrašyti slaptažodį" }, "learnMore": { - "message": "Learn more" + "message": "Sužinoti daugiau" }, "featureUnavailable": { - "message": "Feature unavailable" + "message": "Funkcija nepasiekiama" }, "loggedOut": { - "message": "Logged out" + "message": "Atsijungta" }, "loginExpired": { - "message": "Your login session has expired." + "message": "Sesijos laikas baigėsi." }, "logOutConfirmation": { - "message": "Are you sure you want to log out?" + "message": "Ar tikrai norite atsijungti?" }, "logOut": { - "message": "Log out" + "message": "Atsijungti" }, "addNewLogin": { - "message": "New login" + "message": "Naujas prisijungimas" }, "addNewItem": { - "message": "New item" + "message": "Naujas elementas" }, "addNewFolder": { - "message": "New folder" + "message": "Naujas aplankas" }, "view": { - "message": "View" + "message": "Peržiūrėti" }, "account": { - "message": "Account" + "message": "Paskyra" }, "loading": { - "message": "Loading..." + "message": "Įkeliama..." }, "lockVault": { - "message": "Lock vault" + "message": "Užrakinti saugyklą" }, "passwordGenerator": { - "message": "Password generator" + "message": "Slaptažodžių generatorius" }, "contactUs": { - "message": "Contact us" + "message": "Susisiekite" }, "helpAndFeedback": { - "message": "Help and feedback" + "message": "Pagalba ir atsiliepimai" }, "getHelp": { - "message": "Get help" + "message": "Gauti pagalbą" }, "fileBugReport": { - "message": "File a bug report" + "message": "Pateikite pranešimą apie klaidą" }, "blog": { - "message": "Blog" + "message": "Tinklaraštis" }, "followUs": { - "message": "Follow us" + "message": "Sekite mus" }, "syncVault": { - "message": "Sync vault" + "message": "Sinchronizuoti saugyklą" }, "changeMasterPass": { - "message": "Change master password" + "message": "Keisti pagrindinį slaptažodį" }, "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "message": "Pagrindinį slaptažodį galite pakeisti bitwarden.com žiniatinklio saugykloje. Ar norite dabar apsilankyti svetainėje?" }, "fingerprintPhrase": { - "message": "Fingerprint phrase", + "message": "Piršto antspaudo 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." }, "yourAccountsFingerprint": { - "message": "Your account's fingerprint phrase", + "message": "Jūsų paskyros piršto antspaudo 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." }, "goToWebVault": { - "message": "Go to web vault" + "message": "Eiti į mano saugyklą" }, "getMobileApp": { - "message": "Get mobile app" + "message": "Gauti mobiliąją aplikaciją" }, "getBrowserExtension": { - "message": "Get browser extension" + "message": "Gauti naršyklės plėtinį" }, "syncingComplete": { - "message": "Syncing complete" + "message": "Sinchronizacija baigta" }, "syncingFailed": { - "message": "Syncing failed" + "message": "Sinchronizuoti nepavyko" }, "yourVaultIsLocked": { - "message": "Your vault is locked. Verify your identity to continue." + "message": "Jūsų saugykla užrakinta. Norėdami tęsti, patvirtinkite savo tapatybę." }, "unlock": { - "message": "Unlock" + "message": "Atrakinti" }, "loggedInAsOn": { - "message": "Logged in as $EMAIL$ on $HOSTNAME$.", + "message": "Prisijungta prie $HOSTNAME$ kaip $EMAIL$.", "placeholders": { "email": { "content": "$1", @@ -837,16 +837,16 @@ } }, "invalidMasterPassword": { - "message": "Invalid master password" + "message": "Neteisingas pagrindinis slaptažodis" }, "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?" + "message": "Prisijungus dviem veiksmais, jūsų paskyra tampa saugesnė, reikalaujant patvirtinti prisijungimą naudojant kitą įrenginį, pvz., Saugos raktą, autentifikavimo programą, SMS, telefono skambutį ar el. Paštą. Dviejų žingsnių prisijungimą galima įjungti „bitwarden.com“ interneto saugykloje. Ar norite dabar apsilankyti svetainėje?" }, "twoStepLogin": { - "message": "Two-step login" + "message": "Dviejų žingsnių prisijungimas" }, "vaultTimeout": { - "message": "Vault timeout" + "message": "Atsijungta nuo saugyklos" }, "vaultTimeoutDesc": { "message": "Choose when your vault will take the vault timeout action." @@ -962,49 +962,49 @@ "message": "Start automatically on login" }, "openAtLoginDesc": { - "message": "Start the Bitwarden desktop application automatically on login." + "message": "Prisijungus pradėti Bitwarden darbalaukio aplikaciją automatiškai." }, "alwaysShowDock": { - "message": "Always show in the Dock" + "message": "Visada rodyti Dock" }, "alwaysShowDockDesc": { - "message": "Show the Bitwarden icon in the Dock even when minimized to the menu bar." + "message": "Rodyti Bitwarden ikoną, esančią Dock, net kai ji sumažinta į meniu juostą." }, "confirmTrayTitle": { - "message": "Confirm hiding tray" + "message": "Patvirtinti sumažinimą informacijos srityje" }, "confirmTrayDesc": { - "message": "Turning off this setting will also turn off all other tray related settings." + "message": "Išjungus šį nustatymą bus išjungti visi kiti su informacijos sritimi susiję nustatymai." }, "language": { - "message": "Language" + "message": "Kalba" }, "languageDesc": { - "message": "Change the language used by the application. Restart is required." + "message": "Pakeisti aplikacijos naudojamą kalbą. Privaloma paleisti iš naujo." }, "theme": { - "message": "Theme" + "message": "Tema" }, "themeDesc": { - "message": "Change the application's color theme." + "message": "Pakeisti programos spalvos temą." }, "dark": { - "message": "Dark", + "message": "Tamsi", "description": "Dark color" }, "light": { - "message": "Light", + "message": "Šviesi", "description": "Light color" }, "copy": { - "message": "Copy", + "message": "Kopijuoti", "description": "Copy to clipboard" }, "checkForUpdates": { - "message": "Check for updates…" + "message": "Tikrinti, ar yra atnaujinimų…" }, "version": { - "message": "Version $VERSION_NUM$", + "message": "Versija $VERSION_NUM$", "placeholders": { "version_num": { "content": "$1", @@ -1013,10 +1013,10 @@ } }, "restartToUpdate": { - "message": "Restart to update" + "message": "Norint atnaujinti, perkraukite" }, "restartToUpdateDesc": { - "message": "Version $VERSION_NUM$ is ready to install. You must restart the application to complete the installation. Do you want to restart and update now?", + "message": "Versija $VERSION_NUM$ yra paruošta diegimui. Privalote perkrauti aplikaciją norint užbaigti diegimą. Ar norite perkrauti ir atsinaujinti dabar?", "placeholders": { "version_num": { "content": "$1", @@ -1025,87 +1025,87 @@ } }, "updateAvailable": { - "message": "Update available" + "message": "Prieinamas atnaujinimas" }, "updateAvailableDesc": { - "message": "An update was found. Do you want to download it now?" + "message": "Rastas atnaujinimas. Ar norite jį parsisiųsti dabar?" }, "restart": { - "message": "Restart" + "message": "Paleisti iš naujo" }, "later": { - "message": "Later" + "message": "Vėliau" }, "noUpdatesAvailable": { - "message": "No updates are currently available. You are using the latest version." + "message": "Nerasta jokių atnaujinimų. Naudojate naujausią versiją." }, "updateError": { - "message": "Update error" + "message": "Atnaujinimo klaida" }, "unknown": { - "message": "Unknown" + "message": "Nežinoma" }, "copyUsername": { - "message": "Copy username" + "message": "Kopijuoti vartotojo vardą" }, "copyNumber": { - "message": "Copy number", + "message": "Kopijuoti numerį", "description": "Copy credit card number" }, "copySecurityCode": { - "message": "Copy security code", + "message": "Kopijuoti saugos kodą", "description": "Copy credit card security code (CVV)" }, "premiumMembership": { - "message": "Premium membership" + "message": "Premium narystė" }, "premiumManage": { - "message": "Manage membership" + "message": "Tvarkyti narystę" }, "premiumManageAlert": { - "message": "You can manage your membership on the bitwarden.com web vault. Do you want to visit the website now?" + "message": "Gali tvarkyti savo Premium narystę bitwarden.com interneto saugykloje. Ar norite aplankyti svetainę dabar?" }, "premiumRefresh": { - "message": "Refresh membership" + "message": "Atnaujinti narystę" }, "premiumNotCurrentMember": { - "message": "You are not currently a Premium member." + "message": "Neturite Premium narystės." }, "premiumSignUpAndGet": { - "message": "Sign up for a Premium membership and get:" + "message": "Prisijunkite prie Premium narystės ir gaukite:" }, "premiumSignUpStorage": { - "message": "1 GB encrypted storage for file attachments." + "message": "1 GB užšifruotos vietos diske failų prisegimams." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Patentuotos dviejų žingsnių prisijungimo parinktys, tokios kaip YubiKey ir Duo." }, "premiumSignUpReports": { - "message": "Password hygiene, account health, and data breach reports to keep your vault safe." + "message": "Slaptažodžio higiena, paskyros sveikata ir duomenų nutekinimo ataskaitos, kad jūsų saugykla būtų saugi." }, "premiumSignUpTotp": { - "message": "TOTP verification code (2FA) generator for logins in your vault." + "message": "TOTP patvirtinimo kodų (2FA) generatorius prisijungimams prie jūsų saugyklos." }, "premiumSignUpSupport": { - "message": "Priority customer support." + "message": "Prioritetinis klientų aptarnavimas." }, "premiumSignUpFuture": { - "message": "All future premium features. More coming soon!" + "message": "Visos būsimos Premium savybės. Daugiau jau greitai!" }, "premiumPurchase": { - "message": "Purchase Premium" + "message": "Įsigyti Premium" }, "premiumPurchaseAlert": { - "message": "You can purchase premium membership on the bitwarden.com web vault. Do you want to visit the website now?" + "message": "Galite įsigyti Premium narystę bitwarden.com interneto saugykloje. Ar norite aplankyti svetainę dabar?" }, "premiumCurrentMember": { - "message": "You are a premium member!" + "message": "Esate Premium narys!" }, "premiumCurrentMemberThanks": { - "message": "Thank you for supporting Bitwarden." + "message": "Dėkojame, kad remiate Bitwarden." }, "premiumPrice": { - "message": "All for just $PRICE$ /year!", + "message": "Visa tai tik už $PRICE$ / metus!", "placeholders": { "price": { "content": "$1", @@ -1114,84 +1114,84 @@ } }, "refreshComplete": { - "message": "Refresh complete" + "message": "Atnaujinimas įvykdytas" }, "passwordHistory": { - "message": "Password history" + "message": "Slaptažodžių istorija" }, "clear": { - "message": "Clear", + "message": "Išvalyti", "description": "To clear something out. example: To clear browser history." }, "noPasswordsInList": { - "message": "There are no passwords to list." + "message": "Nėra rodytinų slaptažodžių." }, "undo": { - "message": "Undo" + "message": "Anuliuoti" }, "redo": { - "message": "Redo" + "message": "Grąžinti" }, "cut": { - "message": "Cut", + "message": "Iškirpti", "description": "Cut to clipboard" }, "paste": { - "message": "Paste", + "message": "Įklijuoti", "description": "Paste from clipboard" }, "selectAll": { - "message": "Select all" + "message": "Pažymėti visus" }, "zoomIn": { - "message": "Zoom in" + "message": "Priartinti" }, "zoomOut": { - "message": "Zoom out" + "message": "Nutolinti" }, "resetZoom": { - "message": "Reset zoom" + "message": "Atstatyti mastelį" }, "toggleFullScreen": { - "message": "Toggle full screen" + "message": "Perjungti viso ekrano režimą" }, "reload": { - "message": "Reload" + "message": "Perkrauti" }, "toggleDevTools": { - "message": "Toggle developer tools" + "message": "Įjungti kūrėjo įrankius" }, "minimize": { - "message": "Minimize", + "message": "Sumažinti", "description": "Minimize window" }, "zoom": { - "message": "Zoom" + "message": "Mastelis" }, "bringAllToFront": { - "message": "Bring all to front", + "message": "Perkelti visus į priekį", "description": "Bring all windows to front (foreground)" }, "aboutBitwarden": { - "message": "About Bitwarden" + "message": "Apie Bitwarden" }, "services": { - "message": "Services" + "message": "Paslaugos" }, "hideBitwarden": { - "message": "Hide Bitwarden" + "message": "Paslėpti Bitwarden" }, "hideOthers": { - "message": "Hide others" + "message": "Slėpti kitus" }, "showAll": { - "message": "Show all" + "message": "Rodyti viską" }, "quitBitwarden": { - "message": "Quit Bitwarden" + "message": "Išeiti iš Bitwarden" }, "valueCopied": { - "message": "$VALUE$ copied", + "message": "$VALUE$ nukopijuota", "description": "Value has been copied to the clipboard.", "placeholders": { "value": { @@ -1201,16 +1201,16 @@ } }, "help": { - "message": "Help" + "message": "Pagalba" }, "window": { - "message": "Window" + "message": "Langas" }, "checkPassword": { - "message": "Check if password has been exposed." + "message": "Patikrinti ar slaptažodis buvo atskleistas." }, "passwordExposed": { - "message": "This password has been exposed $VALUE$ time(s) in data breaches. You should change it.", + "message": "Šis slaptažodis buvo atskleistas $VALUE$ kartą (-us) dėl duomenų pažeidimų. Turėtumėte jį pakeisti.", "placeholders": { "value": { "content": "$1", @@ -1219,156 +1219,156 @@ } }, "passwordSafe": { - "message": "This password was not found in any known data breaches. It should be safe to use." + "message": "Šis slaptažodis nebuvo rastas per jokius žinomus duomenų pažeidimus. Jis turėtų būti saugus naudoti." }, "baseDomain": { - "message": "Base domain", + "message": "Bazinis domenas", "description": "Domain name. Ex. website.com" }, "domainName": { - "message": "Domain name", + "message": "Domeno pavadinimas", "description": "Domain name. Ex. website.com" }, "host": { - "message": "Host", + "message": "Serveris", "description": "A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'." }, "exact": { - "message": "Exact" + "message": "Tikslus" }, "startsWith": { - "message": "Starts with" + "message": "Prasideda su" }, "regEx": { - "message": "Regular expression", + "message": "Reguliari išraiška", "description": "A programming term, also known as 'RegEx'." }, "matchDetection": { - "message": "Match detection", + "message": "Atitikmens aptikimas", "description": "URI match detection for auto-fill." }, "defaultMatchDetection": { - "message": "Default match detection", + "message": "Numatytasis atitikties aptikimas", "description": "Default URI match detection for auto-fill." }, "toggleOptions": { - "message": "Toggle options" + "message": "Perjungti nustatymus" }, "organization": { - "message": "Organization", + "message": "Organizacija", "description": "An entity of multiple related people (ex. a team or business organization)." }, "default": { - "message": "Default" + "message": "Numatytas" }, "exit": { - "message": "Exit" + "message": "Išeiti" }, "showHide": { - "message": "Show / Hide", + "message": "Rodyti / Slėpti", "description": "Text for a button that toggles the visibility of the window. Shows the window when it is hidden or hides the window if it is currently open." }, "hideToTray": { - "message": "Hide to tray" + "message": "Paslėpti" }, "alwaysOnTop": { - "message": "Always on top", + "message": "Visada viršuje", "description": "Application window should always stay on top of other windows" }, "dateUpdated": { - "message": "Updated", + "message": "Atnaujintas", "description": "ex. Date this item was updated" }, "dateCreated": { - "message": "Created", + "message": "Sukurtas", "description": "ex. Date this item was created" }, "datePasswordUpdated": { - "message": "Password updated", + "message": "Slaptažodis atnaujintas", "description": "ex. Date this password was updated" }, "exportVault": { - "message": "Export vault" + "message": "Eksportuoti saugyklą" }, "fileFormat": { - "message": "File format" + "message": "Failo formatas" }, "hCaptchaUrl": { - "message": "hCaptcha Url", + "message": "hCaptcha nuoroda", "description": "hCaptcha is the name of a website, should not be translated" }, "loadAccessibilityCookie": { - "message": "Load accessibility cookie" + "message": "Užkrauti prieinamumo slapuką" }, "registerAccessibilityUser": { - "message": "Register as an accessibility user at", + "message": "Registruotis kaip prieinamumo vartotojas", "description": "ex. Register as an accessibility user at hcaptcha.com" }, "copyPasteLink": { - "message": "Copy and paste the link sent to your email below" + "message": "Nukopijuokite ir įklijuokite adresą atsiųstą į jūsų paštą apačioje" }, "enterhCaptchaUrl": { - "message": "Enter URL to load accessibility cookie for hCaptcha", + "message": "Įveskite nuorodą reikalingą hCaptcha prieinamumo slapukui", "description": "hCaptcha is the name of a website, should not be translated" }, "hCaptchaUrlRequired": { - "message": "hCaptcha Url is required", + "message": "hCaptcha nuoroda yra privaloma", "description": "hCaptcha is the name of a website, should not be translated" }, "invalidUrl": { - "message": "Invalid Url" + "message": "Klaidingas URL" }, "done": { - "message": "Done" + "message": "Atlikta" }, "accessibilityCookieSaved": { - "message": "Accessibility cookie saved!" + "message": "Prieinamumo slapukai išsaugoti!" }, "noAccessibilityCookieSaved": { - "message": "No accessibility cookie saved" + "message": "Prieinamumo slapukas neišsaugotas" }, "warning": { - "message": "WARNING", + "message": "ĮSPĖJIMAS", "description": "WARNING (should stay in capitalized letters if the language permits)" }, "confirmVaultExport": { - "message": "Confirm vault export" + "message": "Patvirtinkite saugyklos eksportavimą" }, "exportWarningDesc": { - "message": "This export contains your vault data in an unencrypted format. You should not store or send the exported file over unsecure channels (such as email). Delete it immediately after you are done using it." + "message": "Šiame duomenų eksporte jūsų saugyklos duomenys yra neužšifruoti. Jūs neturėtumete laikyti ar siųsti išeksportuotos duomenų bylos nesaugiu komunikaciniu kanalu (tokiu kaip el. paštas). Ištrinkite jį kaip galima greičiau po to kai pasinaudojote." }, "encExportKeyWarningDesc": { - "message": "This export encrypts your data using your account's encryption key. If you ever rotate your account's encryption key you should export again since you will not be able to decrypt this export file." + "message": "Šis duomenų eksportavimas užšifruoja jūsų duomenis naudodamas jūsų paskyros šifravimo raktą. Jei jūs kada nuspręsite pakeisti paskyros šifravimo raktą, turėtumėte iš naujo eksportuoti duomenis, nes kitaip jūs negalėsite atšifruoti išeksportuotų duomenų." }, "encExportAccountWarningDesc": { - "message": "Account encryption keys are unique to each Bitwarden user account, so you can't import an encrypted export into a different account." + "message": "Paskyros šifravimo raktai yra unikalūs kiekvienai Bitwarden vartotojo paskyrai, taigi jums nepavyktų importuoti užkoduotų eksportuotų duomenų į kitą paskyrą." }, "noOrganizationsList": { - "message": "You do not belong to any organizations. Organizations allow you to securely share items with other users." + "message": "Jūs nepriklausote jokiai organizacijai. Organizacijos leidžia saugiai dalintis elementais su kitais vartotojais." }, "noCollectionsInList": { - "message": "There are no collections to list." + "message": "Nėra rodytinų rinkinių." }, "ownership": { - "message": "Ownership" + "message": "Nuosavybė" }, "whoOwnsThisItem": { - "message": "Who owns this item?" + "message": "Kam priklauso šis elementas?" }, "strong": { - "message": "Strong", + "message": "Stiprus", "description": "ex. A strong password. Scale: Weak -> Good -> Strong" }, "good": { - "message": "Good", + "message": "Geras", "description": "ex. A good password. Scale: Weak -> Good -> Strong" }, "weak": { - "message": "Weak", + "message": "Silpnas", "description": "ex. A weak password. Scale: Weak -> Good -> Strong" }, "weakMasterPassword": { - "message": "Weak master password" + "message": "Silpnas pagrindinis slaptažodis" }, "weakMasterPasswordDesc": { "message": "The master password you have chosen is weak. You should use a strong master password (or a passphrase) to properly protect your Bitwarden account. Are you sure you want to use this master password?" @@ -1411,145 +1411,145 @@ "message": "Ask for Windows Hello on app start" }, "autoPromptTouchId": { - "message": "Ask for Touch ID on app start" + "message": "Prašyti Touch ID paleidus programėlę" }, "requirePasswordOnStart": { - "message": "Require password or PIN on app start" + "message": "Reikalauti slaptažodžio arba PIN paleidus programėlę" }, "recommendedForSecurity": { - "message": "Recommended for security." + "message": "Rekomenduojama dėl saugumo." }, "lockWithMasterPassOnRestart": { - "message": "Lock with master password on restart" + "message": "Užrakinti su pagrindiniu slaptažodžiu perkrovus" }, "deleteAccount": { - "message": "Delete account" + "message": "Ištrinti paskyrą" }, "deleteAccountDesc": { - "message": "Proceed below to delete your account and all vault data." + "message": "Tęskite apačioje norėdami ištrinti savo paskyrą ir visus saugyklos duomenis." }, "deleteAccountWarning": { - "message": "Deleting your account is permanent. It cannot be undone." + "message": "Paskyros ištrinimas yra amžinas. Jos nebus galima atkurti." }, "accountDeleted": { - "message": "Account deleted" + "message": "Paskyra ištrinta" }, "accountDeletedDesc": { - "message": "Your account has been closed and all associated data has been deleted." + "message": "Jūsų paskyra buvo uždaryta ir visi su ja susiję duomenys buvo ištrinti." }, "preferences": { - "message": "Preferences" + "message": "Nuostatos" }, "enableMenuBar": { - "message": "Show menu bar icon" + "message": "Rodyti meniu juostos ikoną" }, "enableMenuBarDesc": { - "message": "Always show an icon in the menu bar." + "message": "Visada rodyti ikoną meniu juostoje." }, "hideToMenuBar": { - "message": "Hide to menu bar" + "message": "Paslėpti meniu juostoje" }, "selectOneCollection": { - "message": "You must select at least one collection." + "message": "Turite pasirinkti bent vieną rinkinį." }, "premiumUpdated": { - "message": "You've upgraded to Premium." + "message": "Jūs įsigijote Premium." }, "restore": { - "message": "Restore" + "message": "Atkurti" }, "premiumManageAlertAppStore": { - "message": "You can manage your subscription from the App Store. Do you want to visit the App Store now?" + "message": "Galite apžvelgti savo prenumeratą App Store. Ar norite aplankyti App Store dabar?" }, "legal": { - "message": "Legal", + "message": "Teisinės", "description": "Noun. As in 'legal documents', like our terms of service and privacy policy." }, "termsOfService": { - "message": "Terms of Service" + "message": "Paslaugų teikimo sąlygos" }, "privacyPolicy": { - "message": "Privacy Policy" + "message": "Privatumo politika" }, "unsavedChangesConfirmation": { - "message": "Are you sure you want to leave? If you leave now then your current information will not be saved." + "message": "Ar jūs tikrai norite išeiti? Jei išeisite dabar dabartinė informacija nebus išsaugota." }, "unsavedChangesTitle": { - "message": "Unsaved changes" + "message": "Neišsaugoti pakeitimai" }, "clone": { - "message": "Clone" + "message": "Klonuoti" }, "passwordGeneratorPolicyInEffect": { - "message": "One or more organization policies are affecting your generator settings." + "message": "Viena ar daugiau organizacijos politikų turi įtakos jūsų generatoriaus nustatymams." }, "vaultTimeoutAction": { - "message": "Vault timeout action" + "message": "Saugyklos skirtojo laiko veiksmas" }, "vaultTimeoutActionLockDesc": { - "message": "Master password or other unlock method is required to access your vault again." + "message": "Pagrindinis slaptažodis arba kitas atrakinimo būdas yra privalomas norint vėl prieiti prie jūsų saugyklos." }, "vaultTimeoutActionLogOutDesc": { - "message": "Re-authentication is required to access your vault again." + "message": "Privaloma autentifikuotis iš naujo norint vėl prieiti prie savo saugyklos." }, "unlockMethodNeededToChangeTimeoutActionDesc": { - "message": "Set up an unlock method to change your vault timeout action." + "message": "Nustatykite atrakinimo būdą, kad pakeistumėte saugyklos laiko limito veiksmą." }, "lock": { - "message": "Lock", + "message": "Užrakinti", "description": "Verb form: to make secure or inaccesible by" }, "trash": { - "message": "Trash", + "message": "Šiukšliadėžė", "description": "Noun: a special folder to hold deleted items" }, "searchTrash": { - "message": "Search trash" + "message": "Ieškoti šiukšliadėžėje" }, "permanentlyDeleteItem": { - "message": "Permanently delete item" + "message": "Ištrinti visam laikui" }, "permanentlyDeleteItemConfirmation": { - "message": "Are you sure you want to permanently delete this item?" + "message": "Ar tikrai norite visam laikui ištrinti šį elementą?" }, "permanentlyDeletedItem": { - "message": "Item permanently deleted" + "message": "Elementas ištrintas visam laikui" }, "restoredItem": { - "message": "Item restored" + "message": "Elementas atkurtas" }, "permanentlyDelete": { - "message": "Permanently delete" + "message": "Ištrinti visam laikui" }, "vaultTimeoutLogOutConfirmation": { "message": "Logging out will remove all access to your vault and requires online authentication after the timeout period. Are you sure you want to use this setting?" }, "vaultTimeoutLogOutConfirmationTitle": { - "message": "Timeout action confirmation" + "message": "Laiko limito atjungimo veiksmo patvirtinimas" }, "enterpriseSingleSignOn": { - "message": "Enterprise single sign-on" + "message": "Vienkartinis įmonės prisijungimas" }, "setMasterPassword": { - "message": "Set master password" + "message": "Nustatyti pagrindinį slaptažodį" }, "ssoCompleteRegistration": { - "message": "In order to complete logging in with SSO, please set a master password to access and protect your vault." + "message": "Kad užbaigtumėte prisijungimą naudodami SSO, nustatykite pagrindinį slaptažodį, kad galėtumėte pasiekti ir apsaugoti savo saugyklą." }, "currentMasterPass": { - "message": "Current master password" + "message": "Dabartinis pagrindinis slaptažodis" }, "newMasterPass": { - "message": "New master password" + "message": "Naujas pagrindinis slaptažodis" }, "confirmNewMasterPass": { - "message": "Confirm new master password" + "message": "Patvirtinti naują pagrindinį slaptažodį" }, "masterPasswordPolicyInEffect": { - "message": "One or more organization policies require your master password to meet the following requirements:" + "message": "Viena ar daugiau organizacijos politikos reikalauja, kad jūsų pagrindinis slaptažodis atitiktų šiuos reikalavimus:" }, "policyInEffectMinComplexity": { - "message": "Minimum complexity score of $SCORE$", + "message": "Minimalus sudėtingumo balas $SCORE$", "placeholders": { "score": { "content": "$1", @@ -1558,7 +1558,7 @@ } }, "policyInEffectMinLength": { - "message": "Minimum length of $LENGTH$", + "message": "Minimalus ilgis $LENGTH$", "placeholders": { "length": { "content": "$1", @@ -1567,16 +1567,16 @@ } }, "policyInEffectUppercase": { - "message": "Contain one or more uppercase characters" + "message": "Turi vieną ar daugiau didžiųjų raidžių" }, "policyInEffectLowercase": { - "message": "Contain one or more lowercase characters" + "message": "Turi vieną ar daugiau mažųjų raidžių" }, "policyInEffectNumbers": { - "message": "Contain one or more numbers" + "message": "Turi vieną ar daugiau skaičių" }, "policyInEffectSpecial": { - "message": "Contain one or more of the following special characters $CHARS$", + "message": "Turi vieną ar daugiau šių specialiųjų simbolių: $CHARS$", "placeholders": { "chars": { "content": "$1", @@ -1585,55 +1585,55 @@ } }, "masterPasswordPolicyRequirementsNotMet": { - "message": "Your new master password does not meet the policy requirements." + "message": "Jūsų naujasis pagrindinis slaptažodis neatitinka politikos reikalavimų." }, "acceptPolicies": { - "message": "By checking this box you agree to the following:" + "message": "Pažymėdami šį laukelį, sutinkate su šiais dalykais:" }, "acceptPoliciesRequired": { - "message": "Terms of Service and Privacy Policy have not been acknowledged." + "message": "Paslaugų teikimo sąlygos ir privatumo politika nebuvo pripažinti." }, "enableBrowserIntegration": { - "message": "Allow browser integration" + "message": "Leisti naršyklės integravimą" }, "enableBrowserIntegrationDesc": { - "message": "Used for biometrics in browser." + "message": "Naudojama biometrikai naršyklėje." }, "enableDuckDuckGoBrowserIntegration": { - "message": "Allow DuckDuckGo browser integration" + "message": "Leisti DuckDuckGo naršyklės integravimą" }, "enableDuckDuckGoBrowserIntegrationDesc": { - "message": "Use your Bitwarden vault when browsing with DuckDuckGo." + "message": "Naudokite savo Bitwarden saugyklą naršydami su DuckDuckGo." }, "browserIntegrationUnsupportedTitle": { - "message": "Browser integration not supported" + "message": "Naršyklės integravimas nepalaikomas" }, "browserIntegrationMasOnlyDesc": { - "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." + "message": "Deja, bet naršyklės integravimas kol kas palaikomas tik Mac App Store versijoje." }, "browserIntegrationWindowsStoreDesc": { - "message": "Unfortunately browser integration is currently not supported in the Microsoft Store version." + "message": "Deja, bet naršyklės integravimas nepalaikomas Microsoft Store versijoje." }, "browserIntegrationLinuxDesc": { - "message": "Unfortunately browser integration is currently not supported in the linux version." + "message": "Deja, bet naršyklės integravimas nepalaikomas Linux versijoje." }, "enableBrowserIntegrationFingerprint": { - "message": "Require verification for browser integration" + "message": "Reikalauti patvirtinimo naršyklės integravimui" }, "enableBrowserIntegrationFingerprintDesc": { - "message": "Add an additional layer of security by requiring fingerprint phrase confirmation when establishing a link between your desktop and browser. This requires user action and verification each time a connection is created." + "message": "Pridėkite papildomą apsaugos sluoksnį reikalaudami piršto antspaudo frazės patvirtinimo kuriant ryšį tarp savo darbalaukio ir naršyklės. Tam reikalingas vartotojo veiksmas ir patvirtinimas kiekvieną kartą prisijungus." }, "approve": { - "message": "Approve" + "message": "Patvirtinti" }, "verifyBrowserTitle": { - "message": "Verify browser connection" + "message": "Patikrinti naršyklės jungtį" }, "verifyBrowserDesc": { - "message": "Please ensure the shown fingerprint is identical to the fingerprint showed in the browser extension." + "message": "Prašome patikrinti, ar rodomas piršto antspaudas yra identiškas piršto antspaudui rodomam naršyklės plėtinyje." }, "verifyNativeMessagingConnectionTitle": { - "message": "$APPID$ wants to connect to Bitwarden", + "message": "$APPID$ nori prisijungti prie Bitwarden", "placeholders": { "appid": { "content": "$1", @@ -1642,66 +1642,66 @@ } }, "verifyNativeMessagingConnectionDesc": { - "message": "Would you like to approve this request?" + "message": "Ar norėtumėte patvirtinti šią užklausą?" }, "verifyNativeMessagingConnectionWarning": { - "message": "If you did not initiate this request, do not approve it." + "message": "Jei jūs nepradėjote šios užklausos, jos nepatvirtinkite." }, "biometricsNotEnabledTitle": { - "message": "Biometrics not set up" + "message": "Trūksta biometrinių duomenų nustatymų" }, "biometricsNotEnabledDesc": { - "message": "Browser biometrics requires desktop biometrics to be set up in the settings first." + "message": "Pirma reikia nustatymuose nustatyti darbalaukio biometrinius duomenys, prieš juos naudojant naršyklėje." }, "personalOwnershipSubmitError": { - "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." + "message": "Dėl įmonės politikos jums neleidžiama saugoti daiktų asmeninėje saugykloje. Pakeiskite nuosavybės parinktį į organizaciją ir pasirinkite iš galimų rinkinių." }, "hintEqualsPassword": { - "message": "Your password hint cannot be the same as your password." + "message": "Jūsų slaptažodžio užuomina negali būti lygi jūsų slaptažodžiui." }, "personalOwnershipPolicyInEffect": { - "message": "An organization policy is affecting your ownership options." + "message": "Organizacijos politika turi įtakos jūsų nuosavybės galimybėms." }, "allSends": { - "message": "All Sends", + "message": "Visi Siuntiniai", "description": "'Sends' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendTypeFile": { - "message": "File" + "message": "Failas" }, "sendTypeText": { - "message": "Text" + "message": "Tekstas" }, "searchSends": { - "message": "Search Sends", + "message": "Ieškoti Siuntinių", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "editSend": { - "message": "Edit Send", + "message": "Redaguoti Siuntinį", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "myVault": { - "message": "My vault" + "message": "Mano saugykla" }, "text": { - "message": "Text" + "message": "Tekstas" }, "deletionDate": { - "message": "Deletion date" + "message": "Ištrynimo data" }, "deletionDateDesc": { - "message": "The Send will be permanently deleted on the specified date and time.", + "message": "Nurodytos datos ir laiko metu Siuntinys bus visam laikui ištrintas.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "expirationDate": { - "message": "Expiration date" + "message": "Galiojimo data" }, "expirationDateDesc": { - "message": "If set, access to this Send will expire on the specified date and time.", + "message": "Jei nustatyta, prieiga prie šio Siuntinio nustos galioti pasiekus nurodytą datą ir laiką.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "maxAccessCount": { - "message": "Maximum access count", + "message": "Maksimalus prisijungimų skaičius", "description": "This text will be displayed after a Send has been accessed the maximum amount of times." }, "maxAccessCountDesc": { @@ -1907,25 +1907,25 @@ "message": "Your vault timeout exceeds the restrictions set by your organization." }, "resetPasswordPolicyAutoEnroll": { - "message": "Automatic enrollment" + "message": "Automatinis įtraukimas" }, "resetPasswordAutoEnrollInviteWarning": { - "message": "This organization has an enterprise policy that will automatically enroll you in password reset. Enrollment will allow organization administrators to change your master password." + "message": "Ši organizacija turi įmonės politiką, kuri automatiškai įtrauks jus į slaptažodžio nustatymą iš naujo. Organizacijos administratoriai galės pakeisti jūsų pagrindinį slaptažodį." }, "vaultExportDisabled": { - "message": "Vault export removed" + "message": "Saugyklos eksportas panaikintas" }, "personalVaultExportPolicyInEffect": { - "message": "One or more organization policies prevents you from exporting your personal vault." + "message": "Viena ar daugiau organizacijos politikų neleidžia eksportuoti jūsų asmeninės saugyklos." }, "addAccount": { - "message": "Add account" + "message": "Pridėti paskyrą" }, "removeMasterPassword": { - "message": "Remove master password" + "message": "Panaikinti pagrindinį slaptažodį" }, "removedMasterPassword": { - "message": "Master password removed" + "message": "Pagrindinis slaptažodis pašalintas" }, "convertOrganizationEncryptionDesc": { "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index 5d75ee2208d..b82788f8d30 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -1078,7 +1078,7 @@ "message": "1 GB šifrētas krātuves datņu pielikumiem." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Tādas slēgtā pirmavota divpakāpju pieteikšanās iespējas kā YubiKey un Duo." }, "premiumSignUpReports": { "message": "Paroļu higiēnas, konta veselības un datu noplūžu pārskati, lai uzturētu glabātavu drošu." @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Pašizvietots" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Piekļuve liegta. Nav nepieciešamo atļauju, lai skatītu šo lapu." diff --git a/apps/desktop/src/locales/me/messages.json b/apps/desktop/src/locales/me/messages.json index 20336258846..9a72e54c6bf 100644 --- a/apps/desktop/src/locales/me/messages.json +++ b/apps/desktop/src/locales/me/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." diff --git a/apps/desktop/src/locales/ml/messages.json b/apps/desktop/src/locales/ml/messages.json index 18b448b1d1e..b133b150885 100644 --- a/apps/desktop/src/locales/ml/messages.json +++ b/apps/desktop/src/locales/ml/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." diff --git a/apps/desktop/src/locales/mr/messages.json b/apps/desktop/src/locales/mr/messages.json index 38e81a83bfd..6d569a89554 100644 --- a/apps/desktop/src/locales/mr/messages.json +++ b/apps/desktop/src/locales/mr/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." diff --git a/apps/desktop/src/locales/my/messages.json b/apps/desktop/src/locales/my/messages.json index 29d7954de21..294124b241b 100644 --- a/apps/desktop/src/locales/my/messages.json +++ b/apps/desktop/src/locales/my/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." diff --git a/apps/desktop/src/locales/nb/messages.json b/apps/desktop/src/locales/nb/messages.json index f4f65b9f536..a84f7539b88 100644 --- a/apps/desktop/src/locales/nb/messages.json +++ b/apps/desktop/src/locales/nb/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." diff --git a/apps/desktop/src/locales/ne/messages.json b/apps/desktop/src/locales/ne/messages.json index 38e81a83bfd..369bdaef7b9 100644 --- a/apps/desktop/src/locales/ne/messages.json +++ b/apps/desktop/src/locales/ne/messages.json @@ -3,28 +3,28 @@ "message": "Bitwarden" }, "filters": { - "message": "Filters" + "message": "फिल्‍टरहरु" }, "allItems": { - "message": "All items" + "message": "सबै सामाग्रिहरु" }, "favorites": { - "message": "Favorites" + "message": "मनपर्नेहरू" }, "types": { - "message": "Types" + "message": "प्रकार" }, "typeLogin": { - "message": "Login" + "message": "लगइन" }, "typeCard": { - "message": "Card" + "message": "कार्ड" }, "typeIdentity": { - "message": "Identity" + "message": "पहिचान" }, "typeSecureNote": { - "message": "Secure note" + "message": "सुरक्षित नोट" }, "folders": { "message": "Folders" @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index a124cede5cf..13f6df5faf6 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Zelfgehost" + "selfHostedServer": { + "message": "zelfgehost" }, "accessDenied": { "message": "Toegang geweigerd. Je hebt geen toestemming om deze pagina te bekijken." diff --git a/apps/desktop/src/locales/nn/messages.json b/apps/desktop/src/locales/nn/messages.json index 46dee838abf..cd30ef54fdd 100644 --- a/apps/desktop/src/locales/nn/messages.json +++ b/apps/desktop/src/locales/nn/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." diff --git a/apps/desktop/src/locales/or/messages.json b/apps/desktop/src/locales/or/messages.json index d6cf45a696e..355595090e0 100644 --- a/apps/desktop/src/locales/or/messages.json +++ b/apps/desktop/src/locales/or/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index ce85ae771dd..6ddad947f21 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Samodzielnie hostowany" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Odmowa dostępu. Nie masz uprawnień do przeglądania tej strony." diff --git a/apps/desktop/src/locales/pt_BR/messages.json b/apps/desktop/src/locales/pt_BR/messages.json index 8e3aac885c3..8b4eb7dcc35 100644 --- a/apps/desktop/src/locales/pt_BR/messages.json +++ b/apps/desktop/src/locales/pt_BR/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index 52188383594..49257f09d83 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -1078,7 +1078,7 @@ "message": "1 GB de armazenamento encriptado para anexos de ficheiros." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Opções proprietárias de verificação de dois passos, como YubiKey e Duo." }, "premiumSignUpReports": { "message": "Higiene de palavras-passe, saúde da conta e relatórios de violação de dados para manter o seu cofre seguro." @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Auto-hospedado" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Acesso negado. Não tem permissão para visualizar esta página." diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json index ca46af9d5d5..139e0813cc8 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index 7785e84f1cf..b16eb66ac89 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -1078,7 +1078,7 @@ "message": "1 ГБ зашифрованного хранилища для вложенных файлов." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Проприетарные варианты двухэтапной аутентификации, такие как YubiKey или Duo." }, "premiumSignUpReports": { "message": "Гигиена паролей, здоровье аккаунта и отчеты об утечках данных для обеспечения безопасности вашего хранилища." @@ -1288,7 +1288,7 @@ "description": "ex. Date this password was updated" }, "exportVault": { - "message": "Экспортировать хранилище" + "message": "Экспорт хранилища" }, "fileFormat": { "message": "Формат файла" @@ -2116,7 +2116,7 @@ "message": "На ваше устройство отправлено уведомление." }, "fingerprintMatchInfo": { - "message": "Убедитесь, что ваше хранилище разблокировано и фраза отпечатка пальца совпадает на другом устройстве." + "message": "Убедитесь, что ваше хранилище разблокировано и фраза отпечатка совпадает на другом устройстве." }, "fingerprintPhraseHeader": { "message": "Фраза отпечатка" @@ -2226,7 +2226,7 @@ "message": "Обнаружен слабый пароль, найденный в утечке данных. Используйте надежный и уникальный пароль для защиты вашего аккаунта. Вы уверены, что хотите использовать этот пароль?" }, "checkForBreaches": { - "message": "Проверьте известные случаи утечки данных для этого пароля" + "message": "Проверять известные случаи утечки данных для этого пароля" }, "important": { "message": "Важно:" @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Собственный хостинг" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Доступ запрещен. У вас нет разрешения на просмотр этой страницы." diff --git a/apps/desktop/src/locales/si/messages.json b/apps/desktop/src/locales/si/messages.json index e2e5347aeb9..9d8a1428da0 100644 --- a/apps/desktop/src/locales/si/messages.json +++ b/apps/desktop/src/locales/si/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index 41411606590..7277dfca0b8 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -1078,7 +1078,7 @@ "message": "1 GB šifrovaného úložiska." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Proprietárne možnosti dvojstupňového prihlásenia ako napríklad YubiKey a Duo." }, "premiumSignUpReports": { "message": "Správy o sile hesla, zabezpečení účtov a únikoch dát ktoré vám pomôžu udržať vaše kontá v bezpečí." @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Vlastný hosting" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Prístup zamietnutý. Nemáte oprávnenie na zobrazenie tejto stránky." diff --git a/apps/desktop/src/locales/sl/messages.json b/apps/desktop/src/locales/sl/messages.json index 089da060600..f12e6f5e855 100644 --- a/apps/desktop/src/locales/sl/messages.json +++ b/apps/desktop/src/locales/sl/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index b5712de8625..428cabcb9ee 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -1078,7 +1078,7 @@ "message": "1ГБ шифровано складиште за прилоге." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Приоритарне опције пријаве у два корака као што су YubiKey и Duo." }, "premiumSignUpReports": { "message": "Извештаји о хигијени лозинки, здравственом стању налога и кршењу података да бисте заштитили сеф." @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Личан хостинг" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Одбијен приступ. Немате дозволу да видите ову страницу." diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index b224bb083c5..012566d49fc 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json index 38e81a83bfd..6d569a89554 100644 --- a/apps/desktop/src/locales/te/messages.json +++ b/apps/desktop/src/locales/te/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." diff --git a/apps/desktop/src/locales/th/messages.json b/apps/desktop/src/locales/th/messages.json index 32f10e27404..f35de19ff34 100644 --- a/apps/desktop/src/locales/th/messages.json +++ b/apps/desktop/src/locales/th/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index ff5185c7141..ae8cc13e1bd 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Barındırılan" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Erişim engellendi. Bu sayfayı görüntüleme iznine sahip değilsiniz." diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index 09a7ec512bc..cf119fc5b07 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -67,7 +67,7 @@ "message": "Вкладення" }, "viewItem": { - "message": "Перегляд запису" + "message": "Переглянути запис" }, "name": { "message": "Назва" @@ -753,7 +753,7 @@ "message": "Нова тека" }, "view": { - "message": "Перегляд" + "message": "Переглянути" }, "account": { "message": "Обліковий запис" @@ -1078,7 +1078,7 @@ "message": "1 ГБ зашифрованого сховища для файлів." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Додаткові можливості двоетапної авторизації, як-от YubiKey та Duo." }, "premiumSignUpReports": { "message": "Гігієна паролів, здоров'я облікового запису, а також звіти про вразливості даних, щоб зберігати ваше сховище в безпеці." @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Власне розміщення" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Доступ заборонено. У вас немає дозволу на перегляд цієї сторінки." diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json index dcbedbe2938..51577198cf9 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "Access denied. You do not have permission to view this page." diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index ff74b521eed..a8da710ca61 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -1078,7 +1078,7 @@ "message": "1 GB 文件附件加密存储。" }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "专有的两步登录选项,如 YubiKey 和 Duo。" }, "premiumSignUpReports": { "message": "密码健康、账户体检以及数据泄露报告,保障您的密码库安全。" @@ -2116,13 +2116,13 @@ "message": "通知已发送到您的设备。" }, "fingerprintMatchInfo": { - "message": "请确保您的密码库已解锁,并且指纹短语与其他设备匹配。" + "message": "请确保您的密码库已解锁,并且指纹短语与其他设备上的相匹配。" }, "fingerprintPhraseHeader": { "message": "指纹短语" }, "needAnotherOption": { - "message": "设备登录必须在 Bitwarden 应用程序的设置中设启用。需要其他选项吗?" + "message": "设备登录必须在 Bitwarden 应用程序的设置中启用。需要其他登录选项吗?" }, "viewAllLoginOptions": { "message": "查看所有登录选项" @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "自托管" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "访问被拒绝。您没有权限查看此页面。" diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index 1de910b1b44..27d9993f543 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -2286,8 +2286,8 @@ "euDomain": { "message": "bitwarden.eu" }, - "selfHosted": { - "message": "自建" + "selfHostedServer": { + "message": "self-hosted" }, "accessDenied": { "message": "拒絕存取。您沒有檢視此頁面的權限。" From 1d667c3b3f31c5760410d5070468d770ad199d25 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 8 Sep 2023 10:27:26 +0000 Subject: [PATCH 42/46] Autosync the updated translations (#6228) 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 | 6 +- 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 | 50 +++---- apps/browser/src/_locales/cs/messages.json | 4 +- apps/browser/src/_locales/cy/messages.json | 130 +++++++++--------- apps/browser/src/_locales/da/messages.json | 4 +- apps/browser/src/_locales/de/messages.json | 4 +- apps/browser/src/_locales/el/messages.json | 100 +++++++------- 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 | 10 +- 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 | 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 | 4 +- apps/browser/src/_locales/ml/messages.json | 4 +- apps/browser/src/_locales/mr/messages.json | 10 +- 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 | 6 +- apps/browser/src/_locales/ro/messages.json | 4 +- apps/browser/src/_locales/ru/messages.json | 10 +- 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 | 6 +- 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 | 10 +- apps/browser/src/_locales/vi/messages.json | 4 +- apps/browser/src/_locales/zh_CN/messages.json | 8 +- apps/browser/src/_locales/zh_TW/messages.json | 4 +- apps/browser/store/locales/el/copy.resx | 2 +- 61 files changed, 275 insertions(+), 275 deletions(-) diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index cf6bf851dd0..6bb0efb8fd3 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "إصدار الخادم" }, - "selfHosted": { - "message": "استضافة ذاتية" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Third-party" diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 283b03a17a9..4ee8ab8edaf 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -796,7 +796,7 @@ "message": "Fayl qoşmaları üçün 1 GB şifrələnmiş saxlama sahəsi" }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "YubiKey və Duo kimi mülkiyyətçi iki addımlı giriş seçimləri." }, "ppremiumSignUpReports": { "message": "Anbarınızın təhlükəsiyini təmin etmək üçün parol gigiyenası, hesab sağlamlığı və verilənlərin pozulması hesabatları." @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Server Versiyası" }, - "selfHosted": { - "message": "Öz-özünə sahiblik edən" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Üçüncü tərəf" diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 6aada06265d..e57ea2eff54 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Версія сервера" }, - "selfHosted": { - "message": "Уласнае размяшчэнне" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Іншы пастаўшчык" diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index cf9ec1b8ed9..9e4d696193e 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -796,7 +796,7 @@ "message": "1 GB пространство за файлове, които се шифрират." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Частно двустепенно удостоверяване чрез YubiKey и Duo." }, "ppremiumSignUpReports": { "message": "Проверки в списъците с публикувани пароли, проверка на регистрациите и доклади за пробивите в сигурността, което спомага трезорът ви да е допълнително защитен." @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Версия на сървъра" }, - "selfHosted": { - "message": "Собствен хостинг" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Third-party" diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index 7d7151b58f6..261ceea180d 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Server version" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Third-party" diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index 657ce243606..5b27e7186d4 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Server version" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Third-party" diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index fbe9e33c65d..7a7edc3d896 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -339,7 +339,7 @@ "message": "Altres" }, "unlockMethodNeededToChangeTimeoutActionDesc": { - "message": "Set up an unlock method to change your vault timeout action." + "message": "Configura un mètode de desbloqueig per canviar l'acció del temps d'espera de la caixa forta." }, "rateExtension": { "message": "Valora aquesta extensió" @@ -634,10 +634,10 @@ "message": "Actualitza" }, "notificationUnlockDesc": { - "message": "Unlock your Bitwarden vault to complete the auto-fill request." + "message": "Desbloquegeu la vostra caixa forta de Bitwarden per completar la sol·licitud d'emplenament automàtic." }, "notificationUnlock": { - "message": "Unlock" + "message": "Desbloqueja" }, "enableContextMenuItem": { "message": "Mostra les opcions del menú contextual" @@ -1606,10 +1606,10 @@ "message": "La biometria del navegador no és compatible amb aquest dispositiu." }, "biometricsFailedTitle": { - "message": "Biometrics failed" + "message": "La biometria ha fallat" }, "biometricsFailedDesc": { - "message": "Biometrics cannot be completed, consider using a master password or logging out. If this persists, please contact Bitwarden support." + "message": "La biometria no es pot completar, considereu utilitzar una contrasenya mestra o tancar la sessió. Si això continua, poseu-vos en contacte amb el servei d'assistència de Bitwarden." }, "nativeMessaginPermissionErrorTitle": { "message": "No s'ha proporcionat el permís" @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Versió del servidor" }, - "selfHosted": { - "message": "Autoallotjat" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Tercers" @@ -2153,7 +2153,7 @@ "message": "S'ha enviat una notificació al vostre dispositiu." }, "loginInitiated": { - "message": "Login initiated" + "message": "S'ha iniciat la sessió" }, "exposedMasterPassword": { "message": "Contrasenya mestra exposada" @@ -2234,34 +2234,34 @@ } }, "loggingInOn": { - "message": "Logging in on" + "message": "Inici de sessió en" }, "opensInANewWindow": { "message": "S'obri en una finestra nova" }, "deviceApprovalRequired": { - "message": "Device approval required. Select an approval option below:" + "message": "Cal l'aprovació del dispositiu. Seleccioneu una opció d'aprovació a continuació:" }, "rememberThisDevice": { - "message": "Remember this device" + "message": "Recorda aquest dispositiu" }, "uncheckIfPublicDevice": { - "message": "Uncheck if using a public device" + "message": "Desmarqueu si utilitzeu un dispositiu públic" }, "approveFromYourOtherDevice": { - "message": "Approve from your other device" + "message": "Aproveu des d'un altre dispositiu vostre" }, "requestAdminApproval": { - "message": "Request admin approval" + "message": "Sol·liciteu l'aprovació de l'administrador" }, "approveWithMasterPassword": { - "message": "Approve with master password" + "message": "Aprova amb contrasenya mestra" }, "ssoIdentifierRequired": { - "message": "Organization SSO identifier is required." + "message": "Es requereix un identificador SSO de l'organització." }, "eu": { - "message": "EU", + "message": "UE", "description": "European Union" }, "usDomain": { @@ -2280,28 +2280,28 @@ "message": "Mostra" }, "accountSuccessfullyCreated": { - "message": "Account successfully created!" + "message": "Compte creat correctament!" }, "adminApprovalRequested": { - "message": "Admin approval requested" + "message": "S'ha sol·licitat l'aprovació de l'administrador" }, "adminApprovalRequestSentToAdmins": { - "message": "Your request has been sent to your admin." + "message": "La vostra sol·licitud s'ha enviat a l'administrador." }, "youWillBeNotifiedOnceApproved": { - "message": "You will be notified once approved." + "message": "Se us notificarà una vegada aprovat." }, "troubleLoggingIn": { - "message": "Trouble logging in?" + "message": "Teniu problemes per iniciar la sessió?" }, "loginApproved": { - "message": "Login approved" + "message": "S'ha aprovat l'inici de sessió" }, "userEmailMissing": { - "message": "User email missing" + "message": "Falta el correu electrònic de l'usuari" }, "deviceTrusted": { - "message": "Device trusted" + "message": "Dispositiu de confiança" }, "inputRequired": { "message": "L'entrada és obligatòria." diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index a7bca0d78eb..253aab484b7 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Verze serveru" }, - "selfHosted": { - "message": "Vlastní hosting" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Tretí strana" diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 043a0fffead..7861a5e758b 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -7,7 +7,7 @@ "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": "Rheolydd cyfrineiriau diogel a rhad ac am ddim ar gyfer eich holl ddyfeisiau.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -29,7 +29,7 @@ "message": "Cau" }, "submit": { - "message": "Submit" + "message": "Cyflwyno" }, "emailAddress": { "message": "Cyfeiriad ebost" @@ -227,10 +227,10 @@ "message": "Cell we Bitwarden" }, "importItems": { - "message": "Import items" + "message": "Mewnforio eitemau" }, "select": { - "message": "Select" + "message": "Dewis" }, "generatePassword": { "message": "Cynhyrchu cyfrinair" @@ -345,7 +345,7 @@ "message": "Rate the extension" }, "rateExtensionDesc": { - "message": "Please consider helping us out with a good review!" + "message": "Ystyriwch ein helpu ni gydag adolygiad da!" }, "browserNotSupportClipboard": { "message": "Your web browser does not support easy clipboard copying. Copy it manually instead." @@ -360,7 +360,7 @@ "message": "Datgloi" }, "loggedInAsOn": { - "message": "Logged in as $EMAIL$ on $HOSTNAME$.", + "message": "Wedi mewngofnodi gyda $EMAIL$ ar $HOSTNAME$.", "placeholders": { "email": { "content": "$1", @@ -379,7 +379,7 @@ "message": "Cloi'r gell" }, "lockNow": { - "message": "Lock now" + "message": "Cloi nawr" }, "immediately": { "message": "ar unwaith" @@ -427,7 +427,7 @@ "message": "Diogelwch" }, "errorOccurred": { - "message": "An error has occurred" + "message": "Bu gwall" }, "emailRequired": { "message": "Mae angen cyfeiriad ebost." @@ -513,13 +513,13 @@ "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?" }, "editedFolder": { - "message": "Folder saved" + "message": "Ffolder wedi'i chadw" }, "deleteFolderConfirmation": { "message": "Are you sure you want to delete this folder?" }, "deletedFolder": { - "message": "Folder deleted" + "message": "Ffolder wedi'i dileu" }, "gettingStartedTutorial": { "message": "Getting started tutorial" @@ -534,7 +534,7 @@ "message": "Syncing failed" }, "passwordCopied": { - "message": "Password copied" + "message": "Cyfrinair wedi'i gopïo" }, "uri": { "message": "URI" @@ -553,10 +553,10 @@ "message": "URI newydd" }, "addedItem": { - "message": "Item added" + "message": "Eitem wedi'i hychwanegu" }, "editedItem": { - "message": "Item saved" + "message": "Eitem wedi'i chadw" }, "deleteItemConfirmation": { "message": "Ydych chi wir eisiau anfon i'r sbwriel?" @@ -565,13 +565,13 @@ "message": "Anfonwyd yr eitem i'r sbwriel" }, "overwritePassword": { - "message": "Overwrite password" + "message": "Trosysgrifo'r cyfrinair" }, "overwritePasswordConfirmation": { "message": "Are you sure you want to overwrite the current password?" }, "overwriteUsername": { - "message": "Overwrite username" + "message": "Trosysgrifo'r enw defnyddiwr" }, "overwriteUsernameConfirmation": { "message": "Are you sure you want to overwrite the current username?" @@ -608,7 +608,7 @@ "message": "List identity items on the Tab page for easy auto-fill." }, "clearClipboard": { - "message": "Clear clipboard", + "message": "Clirio'r clipfwrdd", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." }, "clearClipboardDesc": { @@ -637,7 +637,7 @@ "message": "Unlock your Bitwarden vault to complete the auto-fill request." }, "notificationUnlock": { - "message": "Unlock" + "message": "Datgloi" }, "enableContextMenuItem": { "message": "Show context menu options" @@ -711,7 +711,7 @@ "message": "Rhannu" }, "movedItemToOrg": { - "message": "$ITEMNAME$ moved to $ORGNAME$", + "message": "Symudwyd $ITEMNAME$ i $ORGNAME$", "placeholders": { "itemname": { "content": "$1", @@ -751,10 +751,10 @@ "message": "Attachment deleted" }, "newAttachment": { - "message": "Add new attachment" + "message": "Ychwanegu atodiad newydd" }, "noAttachments": { - "message": "No attachments." + "message": "Dim atodiadau." }, "attachmentSaved": { "message": "Attachment saved" @@ -763,7 +763,7 @@ "message": "Ffeil" }, "selectFile": { - "message": "Select a file" + "message": "Dewis ffeil" }, "maxFileSize": { "message": "Maximum file size is 500 MB." @@ -787,40 +787,40 @@ "message": "Adnewyddu'ch aelodaeth" }, "premiumNotCurrentMember": { - "message": "You are not currently a Premium member." + "message": "Does gennych chi ddim aeloaeth uwch ar hyn o bryd." }, "premiumSignUpAndGet": { - "message": "Sign up for a Premium membership and get:" + "message": "Cofrestrwch ar gyfer aelodaeth uwch i gael:" }, "ppremiumSignUpStorage": { - "message": "1 GB encrypted storage for file attachments." + "message": "Storfa 1GB wedi'i hamgryptio ar gyfer atodiadau ffeiliau." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Dewisiadau mewngofnodi dau gam perchenogol megis YubiKey a Duo." }, "ppremiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." }, "ppremiumSignUpTotp": { - "message": "TOTP verification code (2FA) generator for logins in your vault." + "message": "Cynhyrchydd codau dilysu TOTP (2FA) ar gyfer manylion mewngofnodi yn eich cell." }, "ppremiumSignUpSupport": { - "message": "Priority customer support." + "message": "Cymorth wedi'i flaenoriaethu." }, "ppremiumSignUpFuture": { "message": "All future Premium features. More coming soon!" }, "premiumPurchase": { - "message": "Purchase Premium" + "message": "Prynu aelodaeth uwch" }, "premiumPurchaseAlert": { "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" }, "premiumCurrentMember": { - "message": "You are a Premium member!" + "message": "Mae gennych aelodaeth uwch!" }, "premiumCurrentMemberThanks": { - "message": "Thank you for supporting Bitwarden." + "message": "Diolch am gefnogi Bitwarden." }, "premiumPrice": { "message": "Hyn oll am $PRICE$ y flwyddyn!", @@ -844,10 +844,10 @@ "message": "Ask for biometrics on launch" }, "premiumRequired": { - "message": "Premium required" + "message": "Mae angen aelodaeth uwch" }, "premiumRequiredDesc": { - "message": "A Premium membership is required to use this feature." + "message": "Mae angen aelodaeth uwch i ddefnyddio'r nodwedd hon." }, "enterVerificationCodeApp": { "message": "Enter the 6 digit verification code from your authenticator app." @@ -904,7 +904,7 @@ "message": "Please use a supported web browser (such as Chrome) and/or add additional providers that are better supported across web browsers (such as an authenticator app)." }, "twoStepOptions": { - "message": "Two-step login options" + "message": "Dewisiadau mewngofnodi dau gam" }, "recoveryCodeDesc": { "message": "Lost access to all of your two-factor providers? Use your recovery code to turn off all two-factor providers from your account." @@ -1033,7 +1033,7 @@ "message": "Copy value" }, "value": { - "message": "Value" + "message": "Gwerth" }, "newCustomField": { "message": "Maes addasedig newydd" @@ -1048,7 +1048,7 @@ "message": "Hidden" }, "cfTypeBoolean": { - "message": "Boolean" + "message": "Gwerth Boole" }, "cfTypeLinked": { "message": "Linked", @@ -1134,7 +1134,7 @@ "message": "Cod diogelwch" }, "ex": { - "message": "ex." + "message": "engh." }, "title": { "message": "Teitl" @@ -1297,7 +1297,7 @@ "message": "Starts with" }, "regEx": { - "message": "Regular expression", + "message": "Mynegiant rheolaidd", "description": "A programming term, also known as 'RegEx'." }, "matchDetection": { @@ -1427,7 +1427,7 @@ "message": "Vault timeout action" }, "lock": { - "message": "Lock", + "message": "Cloi", "description": "Verb form: to make secure or inaccesible by" }, "trash": { @@ -1438,13 +1438,13 @@ "message": "Chwilio drwy'r sbwriel" }, "permanentlyDeleteItem": { - "message": "Permanently delete item" + "message": "Dileu'r eitem yn barhaol" }, "permanentlyDeleteItemConfirmation": { "message": "Are you sure you want to permanently delete this item?" }, "permanentlyDeletedItem": { - "message": "Item permanently deleted" + "message": "Eitem wedi'i dileu'n barhaol" }, "restoreItem": { "message": "Adfer yr eitem" @@ -1546,7 +1546,7 @@ "message": "Terms of Service and Privacy Policy have not been acknowledged." }, "termsOfService": { - "message": "Terms of Service" + "message": "Telerau gwasanaeth" }, "privacyPolicy": { "message": "Polisi preifatrwydd" @@ -1671,7 +1671,7 @@ "description": "This text will be displayed after a Send has been accessed the maximum amount of times." }, "expired": { - "message": "Expired" + "message": "Wedi dod i ben" }, "pendingDeletion": { "message": "Pending deletion" @@ -1744,10 +1744,10 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "oneDay": { - "message": "1 day" + "message": "1 diwrnod" }, "days": { - "message": "$DAYS$ days", + "message": "$DAYS$ o ddyddiau", "placeholders": { "days": { "content": "$1", @@ -1854,7 +1854,7 @@ "message": "There was an error saving your deletion and expiration dates." }, "hideEmail": { - "message": "Hide my email address from recipients." + "message": "Cuddio fy nghyfeiriad ebost rhag derbynwyr." }, "sendOptionsPolicyInEffect": { "message": "One or more organization policies are affecting your Send options." @@ -2007,10 +2007,10 @@ "message": "Regenerate username" }, "generateUsername": { - "message": "Generate username" + "message": "Cynhyrchu enw defnyddiwr" }, "usernameType": { - "message": "Username type" + "message": "Math o enw defnyddiwr" }, "plusAddressedEmail": { "message": "Plus addressed email", @@ -2026,10 +2026,10 @@ "message": "Use your domain's configured catch-all inbox." }, "random": { - "message": "Random" + "message": "Hap" }, "randomWord": { - "message": "Random word" + "message": "Gair ar hap" }, "websiteName": { "message": "Website name" @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Server version" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Third-party" @@ -2129,7 +2129,7 @@ "message": "New around here?" }, "rememberEmail": { - "message": "Remember email" + "message": "Cofio'r ebost" }, "loginWithDevice": { "message": "Mewngofnodi â dyfais" @@ -2165,19 +2165,19 @@ "message": "Weak and Exposed Master Password" }, "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": "Cyfrinair gwan a gafodd ei ganfod mewn achos o ddatgelu data. Defnyddiwch gyfrinair cryf ac unigryw i ddiogelu eich cyfrif. Ydych chi wir eisiau defnyddio cyfrinair sydd wedi'i ddatgelu?" }, "checkForBreaches": { - "message": "Check known data breaches for this password" + "message": "Chwilio am achosion o ddatgelu data sy'n cynnwys y cyfrinair hwn" }, "important": { "message": "Pwysig:" }, "masterPasswordHint": { - "message": "Your master password cannot be recovered if you forget it!" + "message": "Allwch chi ddim adfer eich prif gyfrinair os caiff ei anghofio!" }, "characterMinimum": { - "message": "$LENGTH$ character minimum", + "message": "Isafswm o $LENGTH$ nod", "placeholders": { "length": { "content": "$1", @@ -2243,7 +2243,7 @@ "message": "Device approval required. Select an approval option below:" }, "rememberThisDevice": { - "message": "Remember this device" + "message": "Cofio'r ddyfais hon" }, "uncheckIfPublicDevice": { "message": "Uncheck if using a public device" @@ -2261,7 +2261,7 @@ "message": "Organization SSO identifier is required." }, "eu": { - "message": "EU", + "message": "UE", "description": "European Union" }, "usDomain": { @@ -2310,7 +2310,7 @@ "message": "required" }, "search": { - "message": "Search" + "message": "Chwilio" }, "inputMinLength": { "message": "Input must be at least $COUNT$ characters long.", @@ -2377,19 +2377,19 @@ } }, "selectPlaceholder": { - "message": "-- Select --" + "message": "-- Dewis --" }, "multiSelectPlaceholder": { - "message": "-- Type to filter --" + "message": "-- Teipiwch i hidlo --" }, "multiSelectLoading": { - "message": "Retrieving options..." + "message": "Yn nôl dewisiadau..." }, "multiSelectNotFound": { - "message": "No items found" + "message": "Heb ganfod eitemau" }, "multiSelectClearAll": { - "message": "Clear all" + "message": "Clirio'r cyfan" }, "plusNMore": { "message": "+ $QUANTITY$ more", @@ -2401,7 +2401,7 @@ } }, "submenu": { - "message": "Submenu" + "message": "Is-ddewislen" }, "toggleCollapse": { "message": "Toggle collapse", diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index d2ddc3381b5..df94e9aab71 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Server version" }, - "selfHosted": { - "message": "Selv-hostet" + "selfHostedServer": { + "message": "selv-hostet" }, "thirdParty": { "message": "Tredjepart" diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 500329ab9ca..7dcdbc3c51f 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Server-Version" }, - "selfHosted": { - "message": "Selbst gehostet" + "selfHostedServer": { + "message": "selbst gehostet" }, "thirdParty": { "message": "Drittanbieter" diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index e168fd93576..1e377c35d0d 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -339,7 +339,7 @@ "message": "Άλλες" }, "unlockMethodNeededToChangeTimeoutActionDesc": { - "message": "Set up an unlock method to change your vault timeout action." + "message": "Ρυθμίστε μια μέθοδο ξεκλειδώματος για να αλλάξετε την ενέργεια χρονικού ορίου θησαυ/κιου." }, "rateExtension": { "message": "Βαθμολογήστε την επέκταση" @@ -796,7 +796,7 @@ "message": "1 GB κρυπτογραφημένο αποθηκευτικό χώρο για συνημμένα αρχεία." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Πρόσθετες επιλογές σύνδεσης δύο βημάτων, όπως το YubiKey και το Duo." }, "ppremiumSignUpReports": { "message": "Ασφάλεια κωδικών, υγεία λογαριασμού και αναφορές παραβίασης δεδομένων για να διατηρήσετε ασφαλές το vault σας." @@ -1438,7 +1438,7 @@ "message": "Αναζήτηση Κάδου" }, "permanentlyDeleteItem": { - "message": "Μόνιμη Διαγραφή Αντικειμένου" + "message": "Οριστική διαγραφή αντικειμένου" }, "permanentlyDeleteItemConfirmation": { "message": "Είστε βέβαιοι ότι θέλετε να διαγράψετε μόνιμα αυτό το στοιχείο;" @@ -1471,13 +1471,13 @@ "message": "Προειδοποίηση: Αυτή είναι μια μη ασφαλή σελίδα HTTP και οποιαδήποτε πληροφορία υποβάλλετε μπορεί να γίνει ορατή και επεμβάσιμη από άλλους. Αυτή η σύνδεση αποθηκεύτηκε αρχικά σε μια ασφαλή (HTTPS) σελίδα." }, "insecurePageWarningFillPrompt": { - "message": "Do you still wish to fill this login?" + "message": "Θέλετε ακόμα να συμπληρώσετε αυτή τη σύνδεση;" }, "autofillIframeWarning": { "message": "Η φόρμα φιλοξενείται από διαφορετικό τομέα (domain) από το λινκ (uri) της αποθηκευμένης σύνδεσης σας (login). Επιλέξτε OK για αυτόματη συμπλήρωση, ή Ακύρωση για να σταματήσετε." }, "autofillIframeWarningTip": { - "message": "To prevent this warning in the future, save this URI, $HOSTNAME$, to your Bitwarden login item for this site.", + "message": "Για να αποτρέψετε αυτή την προειδοποίηση στο μέλλον, αποθηκεύστε αυτό το URI, $HOSTNAME$, στο στοιχείο σύνδεσης Bitwarden σας για αυτόν τον ιστότοπο.", "placeholders": { "hostname": { "content": "$1", @@ -1486,13 +1486,13 @@ } }, "setMasterPassword": { - "message": "Ορισμός Κύριου Κωδικού" + "message": "Καθορισμός κύριου κωδικού" }, "currentMasterPass": { "message": "Τρέχων Κύριος Κωδικός" }, "newMasterPass": { - "message": "Νέος Κύριος Κωδικός" + "message": "Νέος κύριος κωδικός" }, "confirmNewMasterPass": { "message": "Επιβεβαίωση Νέου Κύριου Κωδικού" @@ -1606,10 +1606,10 @@ "message": "Τα βιομετρικά στοιχεία του προγράμματος περιήγησης δεν υποστηρίζονται σε αυτήν τη συσκευή." }, "biometricsFailedTitle": { - "message": "Biometrics failed" + "message": "Ο βιομετρικός έλεγχος απέτυχε" }, "biometricsFailedDesc": { - "message": "Biometrics cannot be completed, consider using a master password or logging out. If this persists, please contact Bitwarden support." + "message": "Τα βιομετρικά δεν μπόρεσαν να ολοκληρωθούν, σκεφτείτε να χρησιμοποιήσετε έναν κύριο κωδικό πρόσβασης ή να αποσυνδεθείτε. Αν αυτό εξακολουθεί να συμβαίνει, παρακαλώ επικοινωνήστε με την υποστήριξη της Bitwarden." }, "nativeMessaginPermissionErrorTitle": { "message": "Δεν Έχει Χορηγηθεί Άδεια" @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Έκδοση διακομιστή" }, - "selfHosted": { - "message": "Αυτο-φιλοξενείται" + "selfHostedServer": { + "message": "αυτο-φιλοξενούμενο" }, "thirdParty": { "message": "Τρίτο μέρος" @@ -2153,7 +2153,7 @@ "message": "Μια ειδοποίηση έχει σταλεί στη συσκευή σας." }, "loginInitiated": { - "message": "Login initiated" + "message": "Η σύνδεση ξεκίνησε" }, "exposedMasterPassword": { "message": "Εκτεθειμένος Κύριος Κωδικός Πρόσβασης" @@ -2240,28 +2240,28 @@ "message": "Ανοίγει σε νέο παράθυρο" }, "deviceApprovalRequired": { - "message": "Device approval required. Select an approval option below:" + "message": "Απαιτείται έγκριση συσκευής. Επιλέξτε μια επιλογή έγκρισης παρακάτω:" }, "rememberThisDevice": { - "message": "Remember this device" + "message": "Απομνημόνευση αυτής της συσκευής" }, "uncheckIfPublicDevice": { - "message": "Uncheck if using a public device" + "message": "Αποεπιλέξτε αν γίνεται χρήση δημόσιας συσκευής" }, "approveFromYourOtherDevice": { - "message": "Approve from your other device" + "message": "Έγκριση από άλλη συσκευή σας" }, "requestAdminApproval": { - "message": "Request admin approval" + "message": "Αίτηση έγκρισης διαχειριστή" }, "approveWithMasterPassword": { - "message": "Approve with master password" + "message": "Έγκριση με τον κύριο κωδικό" }, "ssoIdentifierRequired": { - "message": "Organization SSO identifier is required." + "message": "Απαιτείται αναγνωριστικό οργανισμού SSO." }, "eu": { - "message": "EU", + "message": "ΕΕ", "description": "European Union" }, "usDomain": { @@ -2274,46 +2274,46 @@ "message": "Δεν επιτρέπεται η πρόσβαση. Δεν έχετε άδεια για να δείτε αυτή τη σελίδα." }, "general": { - "message": "General" + "message": "Γενικά" }, "display": { - "message": "Display" + "message": "Εμφάνιση" }, "accountSuccessfullyCreated": { - "message": "Account successfully created!" + "message": "Επιτυχής δημιουργία λογαριασμού!" }, "adminApprovalRequested": { - "message": "Admin approval requested" + "message": "Ζητήθηκε έγκριση διαχειριστή" }, "adminApprovalRequestSentToAdmins": { - "message": "Your request has been sent to your admin." + "message": "Το αίτημά σας εστάλη στον διαχειριστή σας." }, "youWillBeNotifiedOnceApproved": { - "message": "You will be notified once approved." + "message": "Θα ειδοποιηθείτε μόλις εγκριθεί." }, "troubleLoggingIn": { - "message": "Trouble logging in?" + "message": "Δεν μπορείτε να συνδεθείτε;" }, "loginApproved": { - "message": "Login approved" + "message": "Η σύνδεση εγκρίθηκε" }, "userEmailMissing": { - "message": "User email missing" + "message": "Το email του χρήστη απουσιάζει" }, "deviceTrusted": { - "message": "Device trusted" + "message": "Αξιόπιστη συσκευή" }, "inputRequired": { - "message": "Input is required." + "message": "Απαιτείται εισαγωγή." }, "required": { - "message": "required" + "message": "απαιτείται" }, "search": { - "message": "Search" + "message": "Αναζήτηση" }, "inputMinLength": { - "message": "Input must be at least $COUNT$ characters long.", + "message": "Η καταχώρηση πρέπει να είναι τουλάχιστον $COUNT$ χαρακτήρες.", "placeholders": { "count": { "content": "$1", @@ -2322,7 +2322,7 @@ } }, "inputMaxLength": { - "message": "Input must not exceed $COUNT$ characters in length.", + "message": "Η καταχώρηση δεν πρέπει να υπερβαίνει τους $COUNT$ χαρακτήρες σε μήκος.", "placeholders": { "count": { "content": "$1", @@ -2331,7 +2331,7 @@ } }, "inputForbiddenCharacters": { - "message": "The following characters are not allowed: $CHARACTERS$", + "message": "Οι ακόλουθοι χαρακτήρες δεν επιτρέπονται: $CHARACTERS$", "placeholders": { "characters": { "content": "$1", @@ -2340,7 +2340,7 @@ } }, "inputMinValue": { - "message": "Input value must be at least $MIN$.", + "message": "Η τιμή καταχώρησης πρέπει να είναι τουλάχιστον $MIN$", "placeholders": { "min": { "content": "$1", @@ -2349,7 +2349,7 @@ } }, "inputMaxValue": { - "message": "Input value must not exceed $MAX$.", + "message": "Η τιμή καταχώρησης δεν πρέπει να υπερβαίνει το $MAX$.", "placeholders": { "max": { "content": "$1", @@ -2358,17 +2358,17 @@ } }, "multipleInputEmails": { - "message": "1 or more emails are invalid" + "message": "1 ή περισσότερα email δεν είναι έγκυρα" }, "inputTrimValidator": { - "message": "Input must not contain only whitespace.", + "message": "Η καταχώρηση δεν πρέπει να περιέχει μόνο κενά.", "description": "Notification to inform the user that a form's input can't contain only whitespace." }, "inputEmail": { - "message": "Input is not an email address." + "message": "Η καταχώρηση δεν είναι διεύθυνση email." }, "fieldsNeedAttention": { - "message": "$COUNT$ field(s) above need your attention.", + "message": "$COUNT$ Το/α παραπάνω πεδίo/α χρειάζονται την προσοχή σας.", "placeholders": { "count": { "content": "$1", @@ -2377,22 +2377,22 @@ } }, "selectPlaceholder": { - "message": "-- Select --" + "message": "-- Επιλογή --" }, "multiSelectPlaceholder": { - "message": "-- Type to filter --" + "message": "-- Πληκτρολογήστε για φιλτράρισμα --" }, "multiSelectLoading": { - "message": "Retrieving options..." + "message": "Ανάκτηση επιλογών..." }, "multiSelectNotFound": { - "message": "No items found" + "message": "Δεν βρέθηκαν αντικείμενα" }, "multiSelectClearAll": { - "message": "Clear all" + "message": "Εκκαθάριση όλων" }, "plusNMore": { - "message": "+ $QUANTITY$ more", + "message": "+ $QUANTITY$ περισσότερα", "placeholders": { "quantity": { "content": "$1", @@ -2401,10 +2401,10 @@ } }, "submenu": { - "message": "Submenu" + "message": "Υπομενού" }, "toggleCollapse": { - "message": "Toggle collapse", + "message": "Εναλλαγή σύμπτυξης", "description": "Toggling an expand/collapse state." } } diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index 1632b31ee03..84fee7b77c3 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Server version" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Third-party" diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index b9f2a3784c3..4bf280ce707 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Server Version" }, - "selfHosted": { - "message": "Self-Hosted" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Third-Party" diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index e43102b483e..50b721348c8 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Versión del servidor" }, - "selfHosted": { - "message": "Autoalojado" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Aplicaciones de terceros" diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index 5fc83e82676..f9a11297795 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Serveri versioon" }, - "selfHosted": { - "message": "Enda majutatud" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Kolmanda osapoole" diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index ac54d0c4ea6..987565d609d 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Zerbitzariaren bertsioa" }, - "selfHosted": { - "message": "Ostatatze propioduna" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Hirugarrenen aplikazioak" diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index e1057038933..ebb75c9f893 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "نسخه سرور" }, - "selfHosted": { - "message": "خود میزبان" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "شخص ثالث" diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 69492afd7f4..2abd01eefa6 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -796,7 +796,7 @@ "message": "1 Gt salattua tallennustilaa tiedostoliitteille." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Omisteiset kaksivaiheisen kirjautumisen vaihtoehdot, kuten YubiKey ja Duo." }, "ppremiumSignUpReports": { "message": "Salasanahygienian, tilin terveyden ja tietovuotojen raportointitoiminnot pitävät holvisi turvassa." @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Palvelimen versio" }, - "selfHosted": { - "message": "Itse ylläpidetty" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Ulkopuolinen taho" @@ -2246,7 +2246,7 @@ "message": "Muista tämä laite" }, "uncheckIfPublicDevice": { - "message": "Poista käytöstä julkisilla laitteilla" + "message": "Poista valinta julkisilla laitteilla" }, "approveFromYourOtherDevice": { "message": "Hyväksy muilta laitteiltasi" @@ -2286,7 +2286,7 @@ "message": "Hyväksyntää pyydetty ylläpidolta" }, "adminApprovalRequestSentToAdmins": { - "message": "Pyyntösi on välitetty ylläpidolle." + "message": "Pyyntösi on välitetty ylläpidollesi." }, "youWillBeNotifiedOnceApproved": { "message": "Saat ilmoituksen kun se on hyväksytty." diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index be101c80b41..3f046898751 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Bersyon ng server" }, - "selfHosted": { - "message": "Auto-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Ika-tatlong-partido" diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index b554c619a5f..d9dcd906c10 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Version du serveur" }, - "selfHosted": { - "message": "Auto-hébergé" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Tierce partie" diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index 6aea5876eac..43c3cc0b68e 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Server version" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Third-party" diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index 752935ce81a..d199a2e8db9 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Server version" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Third-party" diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index d5c465e68bf..e1d89271f21 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Server version" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Third-party" diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index db0fefbbffb..ee296293572 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Verzija poslužitelja" }, - "selfHosted": { - "message": "Vlastiti poslužitelj" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Third-party" diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index 8bc89651d53..62a14307f6e 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Szerver verzió" }, - "selfHosted": { - "message": "Saját kiszolgáló" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Harmadik fél" diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index d8f6698f4ce..5b1d144591d 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Server version" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Third-party" diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index e17ea019409..ad335ccd56c 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -796,7 +796,7 @@ "message": "1 GB di spazio di archiviazione criptato per gli allegati." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Opzioni di verifica in due passaggi proprietarie come YubiKey e Duo." }, "ppremiumSignUpReports": { "message": "Sicurezza delle password, integrità dell'account, e rapporti su violazioni di dati per mantenere sicura la tua cassaforte." @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Versione Server" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Terze parti" diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index e979237988c..629a1644510 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "サーバーのバージョン" }, - "selfHosted": { - "message": "セルフホスト" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "サードパーティー" diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index a619c47eaf4..f950febf1f3 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Server version" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Third-party" diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index 6aea5876eac..43c3cc0b68e 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Server version" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Third-party" diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index fd01869139d..3635ce719ba 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Server version" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Third-party" diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index f982332a503..c8ad9cff366 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Server version" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Third-party" diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index a3ba40c1fd9..b8f9c6cf12a 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Server version" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Trečioji šalis" diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index cbf2c469124..dfe5405e037 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Servera versija" }, - "selfHosted": { - "message": "Pašizvietots" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Trešās puses" diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index 416b35f78ec..dab1da00605 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Server version" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Third-party" diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index e12aa7f8a65..4401946ca5d 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -208,10 +208,10 @@ "message": "संकालन" }, "syncVaultNow": { - "message": "Sync vault now" + "message": "तिजोरी संकालन आता करा" }, "lastSync": { - "message": "Last sync:" + "message": "शेवटचे संकालन:" }, "passGen": { "message": "पासवर्ड जनित्र" @@ -279,7 +279,7 @@ "message": "Avoid ambiguous characters" }, "searchVault": { - "message": "Search vault" + "message": "तिजोरीत शोधा" }, "edit": { "message": "Edit" @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Server version" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Third-party" diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index 6aea5876eac..43c3cc0b68e 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Server version" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Third-party" diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index e806ea0a781..5a3cb1a6675 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Server Versjon" }, - "selfHosted": { - "message": "Selvbetjent" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Tredjepart" diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index 6aea5876eac..43c3cc0b68e 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Server version" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Third-party" diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index f34016f70f0..7149b18ff68 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Serverversie" }, - "selfHosted": { - "message": "Zelfgehost" + "selfHostedServer": { + "message": "zelfgehost" }, "thirdParty": { "message": "van derden" diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index 6aea5876eac..43c3cc0b68e 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Server version" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Third-party" diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index 6aea5876eac..43c3cc0b68e 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Server version" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Third-party" diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index 75f416b14e4..c2ec8abe5d0 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Wersja serwera" }, - "selfHosted": { - "message": "Samodzielnie hostowany" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Inny dostawca" diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index f8db10e49f8..474174a36e9 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Versão do servidor" }, - "selfHosted": { - "message": "Auto-hospedado" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Terceiros" diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index 074ddb150e0..28362dc31ad 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -796,7 +796,7 @@ "message": "1 GB de armazenamento encriptado para anexos de ficheiros." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Opções proprietárias de verificação de dois passos, como YubiKey e Duo." }, "ppremiumSignUpReports": { "message": "Higiene de palavras-passe, saúde da conta e relatórios de violação de dados para manter o seu cofre seguro." @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Versão do servidor" }, - "selfHosted": { - "message": "Auto-hospedado" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "De terceiros" diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 20c44ddcca2..40c86ecf37d 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Versiune server" }, - "selfHosted": { - "message": "Autogăzduit" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Parte terță" diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index 738193fb48a..df0906a0ec1 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -671,7 +671,7 @@ "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, "exportVault": { - "message": "Экспортировать хранилище" + "message": "Экспорт хранилища" }, "fileFormat": { "message": "Формат файла" @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Версия сервера" }, - "selfHosted": { - "message": "Собственный хостинг" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Сторонний" @@ -2141,7 +2141,7 @@ "message": "Фраза отпечатка" }, "fingerprintMatchInfo": { - "message": "Убедитесь, что ваше хранилище разблокировано и фраза отпечатка пальца совпадает на другом устройстве." + "message": "Убедитесь, что ваше хранилище разблокировано и фраза отпечатка совпадает на другом устройстве." }, "resendNotification": { "message": "Отправить уведомление повторно" @@ -2168,7 +2168,7 @@ "message": "Обнаружен слабый пароль, найденный в утечке данных. Используйте надежный и уникальный пароль для защиты вашего аккаунта. Вы уверены, что хотите использовать этот пароль?" }, "checkForBreaches": { - "message": "Проверьте известные случаи утечки данных для этого пароля" + "message": "Проверять известные случаи утечки данных для этого пароля" }, "important": { "message": "Важно:" diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index cf3ac370c6d..1236f44e6eb 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Server version" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Third-party" diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index 1b90bc94053..5778db28f79 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -796,7 +796,7 @@ "message": "1 GB šifrovaného úložiska." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Proprietárne možnosti dvojstupňového prihlásenia ako napríklad YubiKey a Duo." }, "ppremiumSignUpReports": { "message": "Správy o sile hesla, zabezpečení účtov a únikoch dát ktoré vám pomôžu udržať vaše kontá v bezpečí." @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Verzia servera" }, - "selfHosted": { - "message": "Vlastný hosting" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Tretia strana" diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index cae2464dcb1..06967ee2166 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Verzija strežnika" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Third-party" diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index d4e5ee4f2a4..f823208d1dd 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -796,7 +796,7 @@ "message": "1ГБ шифровано складиште за прилоге." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Приоритарне опције пријаве у два корака као што су YubiKey и Duo." }, "ppremiumSignUpReports": { "message": "Извештаји о хигијени лозинки, здравственом стању налога и кршењу података да бисте заштитили сеф." @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Верзија сервера" }, - "selfHosted": { - "message": "Личан хостинг" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Трећа страна" diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 7ab058680c4..2c90c1c8f86 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Serverversion" }, - "selfHosted": { - "message": "Lokalt installerad" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Tredje part" diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index 6aea5876eac..43c3cc0b68e 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Server version" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Third-party" diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index 7fcf7835119..97ce8ca58cc 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Server version" }, - "selfHosted": { - "message": "Self-hosted" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Third-party" diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 4134a27d265..e32dcb4ac81 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Sunucu sürümü" }, - "selfHosted": { - "message": "Barındırılan" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Üçüncü taraf" diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index 2d1a0b66291..c695aaadf2e 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -285,7 +285,7 @@ "message": "Змінити" }, "view": { - "message": "Перегляд" + "message": "Переглянути" }, "noItemsInList": { "message": "Немає записів." @@ -321,7 +321,7 @@ "message": "Видалити запис" }, "viewItem": { - "message": "Перегляд запису" + "message": "Переглянути запис" }, "launch": { "message": "Перейти" @@ -796,7 +796,7 @@ "message": "1 ГБ зашифрованого сховища для файлів." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Додаткові можливості двоетапної авторизації, як-от YubiKey та Duo." }, "ppremiumSignUpReports": { "message": "Гігієна паролів, здоров'я облікового запису, а також звіти про вразливості даних, щоб зберігати ваше сховище в безпеці." @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Версія сервера" }, - "selfHosted": { - "message": "Власне розміщення" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Сторонній" diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index 5d71340db52..95716b3fc36 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "Phiên bản máy chủ" }, - "selfHosted": { - "message": "Tự lưu trữ" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "Bên thứ ba" diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index 1fc419d0d16..eded4b220c7 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -796,7 +796,7 @@ "message": "1 GB 文件附件加密存储。" }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "专有的两步登录选项,如 YubiKey 和 Duo。" }, "ppremiumSignUpReports": { "message": "密码健康、账户体检以及数据泄露报告,保障您的密码库安全。" @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "服务器版本" }, - "selfHosted": { - "message": "自托管" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "第三方" @@ -2135,7 +2135,7 @@ "message": "设备登录" }, "loginWithDeviceEnabledInfo": { - "message": "设备登录必须在 Bitwarden 应用程序的设置中设启用。需要其他选项吗?" + "message": "设备登录必须在 Bitwarden 应用程序的设置中启用。需要其他登录选项吗?" }, "fingerprintPhraseHeader": { "message": "指纹短语" diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index 68eb917021e..96bdfaaa4df 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -2092,8 +2092,8 @@ "serverVersion": { "message": "伺服器版本" }, - "selfHosted": { - "message": "自我裝載" + "selfHostedServer": { + "message": "self-hosted" }, "thirdParty": { "message": "第三方" diff --git a/apps/browser/store/locales/el/copy.resx b/apps/browser/store/locales/el/copy.resx index 496118ddf6e..01def6ea5af 100644 --- a/apps/browser/store/locales/el/copy.resx +++ b/apps/browser/store/locales/el/copy.resx @@ -155,7 +155,7 @@ Ένας ασφαλής και δωρεάν διαχειριστής κωδικών για όλες τις συσκευές σας - Συγχρονίστε και αποκτήστε πρόσβαση στο vault σας από πολλές συσκευές + Συγχρονίστε και αποκτήστε πρόσβαση στο θησαυροφυλάκιό σας από πολλαπλές συσκευές Διαχειριστείτε όλες τις συνδέσεις και τους κωδικούς σας με ασφάλεια From fe354f906339e7f5cb8b6f87e16b8a2f51bbc8d0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 8 Sep 2023 10:29:11 +0000 Subject: [PATCH 43/46] Autosync the updated translations (#6227) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/web/src/locales/af/messages.json | 11 +-- apps/web/src/locales/ar/messages.json | 11 +-- apps/web/src/locales/az/messages.json | 29 +++--- apps/web/src/locales/be/messages.json | 73 ++++++++-------- apps/web/src/locales/bg/messages.json | 17 ++-- apps/web/src/locales/bn/messages.json | 11 +-- apps/web/src/locales/bs/messages.json | 11 +-- apps/web/src/locales/ca/messages.json | 107 +++++++++++------------ apps/web/src/locales/cs/messages.json | 11 +-- apps/web/src/locales/cy/messages.json | 11 +-- apps/web/src/locales/da/messages.json | 11 +-- apps/web/src/locales/de/messages.json | 13 ++- apps/web/src/locales/el/messages.json | 11 +-- apps/web/src/locales/en_GB/messages.json | 11 +-- apps/web/src/locales/en_IN/messages.json | 11 +-- apps/web/src/locales/eo/messages.json | 11 +-- apps/web/src/locales/es/messages.json | 15 ++-- apps/web/src/locales/et/messages.json | 11 +-- apps/web/src/locales/eu/messages.json | 13 ++- apps/web/src/locales/fa/messages.json | 13 ++- apps/web/src/locales/fi/messages.json | 53 ++++++----- apps/web/src/locales/fil/messages.json | 13 ++- apps/web/src/locales/fr/messages.json | 11 +-- apps/web/src/locales/gl/messages.json | 11 +-- apps/web/src/locales/he/messages.json | 11 +-- apps/web/src/locales/hi/messages.json | 11 +-- apps/web/src/locales/hr/messages.json | 13 ++- apps/web/src/locales/hu/messages.json | 13 ++- apps/web/src/locales/id/messages.json | 11 +-- apps/web/src/locales/it/messages.json | 15 ++-- apps/web/src/locales/ja/messages.json | 13 ++- apps/web/src/locales/ka/messages.json | 11 +-- apps/web/src/locales/km/messages.json | 11 +-- apps/web/src/locales/kn/messages.json | 11 +-- apps/web/src/locales/ko/messages.json | 11 +-- apps/web/src/locales/lv/messages.json | 13 ++- apps/web/src/locales/ml/messages.json | 11 +-- apps/web/src/locales/mr/messages.json | 11 +-- apps/web/src/locales/my/messages.json | 11 +-- apps/web/src/locales/nb/messages.json | 13 ++- apps/web/src/locales/ne/messages.json | 11 +-- apps/web/src/locales/nl/messages.json | 13 ++- apps/web/src/locales/nn/messages.json | 11 +-- apps/web/src/locales/or/messages.json | 11 +-- apps/web/src/locales/pl/messages.json | 11 +-- apps/web/src/locales/pt_BR/messages.json | 15 ++-- apps/web/src/locales/pt_PT/messages.json | 19 ++-- apps/web/src/locales/ro/messages.json | 11 +-- apps/web/src/locales/ru/messages.json | 21 ++--- apps/web/src/locales/si/messages.json | 11 +-- apps/web/src/locales/sk/messages.json | 13 ++- apps/web/src/locales/sl/messages.json | 11 +-- apps/web/src/locales/sr/messages.json | 15 ++-- apps/web/src/locales/sr_CS/messages.json | 11 +-- apps/web/src/locales/sv/messages.json | 13 ++- apps/web/src/locales/te/messages.json | 11 +-- apps/web/src/locales/th/messages.json | 11 +-- apps/web/src/locales/tr/messages.json | 13 ++- apps/web/src/locales/uk/messages.json | 21 ++--- apps/web/src/locales/vi/messages.json | 11 +-- apps/web/src/locales/zh_CN/messages.json | 19 ++-- apps/web/src/locales/zh_TW/messages.json | 13 ++- 62 files changed, 400 insertions(+), 586 deletions(-) diff --git a/apps/web/src/locales/af/messages.json b/apps/web/src/locales/af/messages.json index 4b80c3c2119..46c073483aa 100644 --- a/apps/web/src/locales/af/messages.json +++ b/apps/web/src/locales/af/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "Beveilig u rekening deur ’n bykomende stap te vereis wanneer u aanteken." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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.'" }, @@ -7016,12 +7019,6 @@ "next": { "message": "Volgende" }, - "usFlag": { - "message": "VS-vlag" - }, - "euFlag": { - "message": "EU-vlag" - }, "selectedRegionFlag": { "message": "Gekose streekvlag" }, diff --git a/apps/web/src/locales/ar/messages.json b/apps/web/src/locales/ar/messages.json index f43798b93c1..8639957af05 100644 --- a/apps/web/src/locales/ar/messages.json +++ b/apps/web/src/locales/ar/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "Secure your account by requiring an additional step when logging in." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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.'" }, @@ -7016,12 +7019,6 @@ "next": { "message": "Next" }, - "usFlag": { - "message": "US flag" - }, - "euFlag": { - "message": "EU flag" - }, "selectedRegionFlag": { "message": "Selected region flag" }, diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index 6e037753488..18eea5284fe 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -815,7 +815,7 @@ "message": "Təsdiqləmə kodu olan e-poçtu yenidən göndər" }, "useAnotherTwoStepMethod": { - "message": "Başqa bir iki mərhələli giriş metodu istifadə edin" + "message": "Başqa bir iki addımlı giriş üsulu istifadə edin" }, "insertYubiKey": { "message": "\"YubiKey\"i kompüterinizin USB portuna taxın, daha sonra düyməsinə toxunun." @@ -827,13 +827,13 @@ "message": "Giriş edilə bilmir" }, "noTwoStepProviders": { - "message": "Bu hesabın iki mərhələli giriş özəlliyi fəaldır, ancaq, konfiqurasiya edilmiş iki mərhələli provayderlərin heç biri bu brauzer tərəfindən dəstəklənmir." + "message": "Bu hesabda iki addımlı giriş tənzimləməsi var, ancaq, konfiqurasiya edilmiş iki addımlı provayderlərin heç biri bu veb brauzer tərəfindən dəstəklənmir." }, "noTwoStepProviders2": { "message": "Zəhmət olmasa (Chrome kimi) dəstəklənən bir veb brauzer istifadə edin və/və ya veb brauzerlərə (kimlik təsdiqləyici tətbiq kimi) daha yaxşı dəstəklənən provayderlər əlavə edin." }, "twoStepOptions": { - "message": "İki mərhələli giriş seçimləri" + "message": "İki addımlı giriş seçimləri" }, "recoveryCodeDesc": { "message": "İki mərhələli təsdiqləmə provayderlərinə müraciəti itirmisiniz? Bərpa kodunuzu istifadə edərək hesabınızdakı bütün iki mərhələli provayderləri sıradan çıxara bilərsiniz." @@ -1417,23 +1417,26 @@ "message": "Domenlər güncəlləndi" }, "twoStepLogin": { - "message": "İki mərhələli giriş" + "message": "İki addımlı giriş" }, "twoStepLoginEnforcement": { - "message": "İki addımlı girişə məcbur et" + "message": "İki addımlı Giriş Məcburiyyəti" }, "twoStepLoginDesc": { "message": "Giriş edərkən əlavə bir addım tələb edərək hesabınızı qoruyun." }, - "twoStepLoginOrganizationDescStart": { - "message": "Bitwarden iki addım giriş seçimlərini aşağıdakıları istifadə edərək üzvlər üçün tətbiq et ", + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { + "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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": { - "message": "İki mərhələli giriş siyasəti" + "message": "İki addımlı Giriş Siyasəti" }, "twoStepLoginOrganizationDuoDesc": { - "message": "Duo üzərindən iki mərhələli girişi tətbiq etmək üçün aşağıdakı seçimləri istifadə edin." + "message": "Duo üzərindən İki addımlı Girişi tətbiq etmək üçün aşağıdakı seçimləri istifadə edin." }, "twoStepLoginOrganizationSsoDesc": { "message": "SSO quraşdırması etmisinizsə və ya etmək planınız varsa, İki mərhələli giriş, artıq Kimlik Provayderiniz vasitəsilə tətbiq edilmiş ola bilər." @@ -1925,7 +1928,7 @@ "message": "Fayl qoşmaları üçün 1 GB şifrələnmiş saxlama sahəsi." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "YubiKey və Duo kimi mülkiyyətçi iki addımlı giriş seçimləri." }, "premiumSignUpEmergency": { "message": "Fövqəladə vəziyyət müraciəti" @@ -7016,12 +7019,6 @@ "next": { "message": "Növbəti" }, - "usFlag": { - "message": "US bayrağı" - }, - "euFlag": { - "message": "EU bayrağı" - }, "selectedRegionFlag": { "message": "Seçilmiş bölgə bayrağı" }, diff --git a/apps/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index 305dcf75092..449b220591f 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -1425,8 +1425,11 @@ "twoStepLoginDesc": { "message": "Абараніце свой уліковы запіс з дапамогай дадатковага кроку праверкі падчас уваходу." }, - "twoStepLoginOrganizationDescStart": { - "message": "Забяспечыць параметры двухэтапнага ўваходу Bitwarden для ўдзельнікаў выкарыстоўваючы ", + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { + "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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": { @@ -1925,7 +1928,7 @@ "message": "1 ГБ зашыфраванага сховішча для далучаных файлаў." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Прапрыетарныя варыянты двухэтапнага ўваходу, такія як YubiKey, FIDO U2F і Duo." }, "premiumSignUpEmergency": { "message": "Экстранны доступ" @@ -2041,7 +2044,7 @@ } }, "paymentChargedWithUnpaidSubscription": { - "message": "Your payment method will be charged for any unpaid subscriptions." + "message": "Ваш метад аплаты будзе выкарыстаны для любых неаплачаных падпісак." }, "paymentChargedWithTrial": { "message": "У ваш тарыфны план уключаны выпрабавальны перыяд на 7 дзён. У вас не будзе спагнана плата згодна з выбраным спосабам аплаты пакуль не завяршыцца выпрабавальны перыяд. Вы можаце скасаваць яго ў любы момант." @@ -3540,7 +3543,7 @@ "message": "Выберыце дзеянне, якое неабходна выканаць пасля завяршэння часу чакання сховішча." }, "vaultTimeoutLogoutDesc": { - "message": "Choose when your vault will be logged out." + "message": "Выберыце дзеянне пры якім адбудзецца выхад са сховішча." }, "oneMinute": { "message": "1 хвіліна" @@ -6842,58 +6845,58 @@ "message": "Абнавіце налады KDF" }, "loginInitiated": { - "message": "Login initiated" + "message": "Ініцыяваны ўваход" }, "deviceApprovalRequired": { - "message": "Device approval required. Select an approval option below:" + "message": "Патрабуецца ўхваленне прылады. Выберыце параметры ўхвалення ніжэй:" }, "rememberThisDevice": { - "message": "Remember this device" + "message": "Запомніць гэту прыладу" }, "uncheckIfPublicDevice": { - "message": "Uncheck if using a public device" + "message": "Здыміце пазнаку, калі выкарыстоўваеце агульнадаступную прыладу" }, "approveFromYourOtherDevice": { - "message": "Approve from your other device" + "message": "Ухваліць з іншай вашай прылады" }, "requestAdminApproval": { - "message": "Request admin approval" + "message": "Запытаць ухваленне адміністратара" }, "approveWithMasterPassword": { - "message": "Approve with master password" + "message": "Ухваліць з дапамогай асноўнага пароля" }, "trustedDeviceEncryption": { - "message": "Trusted device encryption" + "message": "Шыфраванне даверанай прылады" }, "trustedDevices": { "message": "Давераныя прылады" }, "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", + "message": "Пасля аўтэнтыфікацыі ўдзельнікі расшыфроўваюць даныя сховішча з выкарыстаннем ключа, які захоўваецца на іх прыладах.", "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": "адзіная арганізацыя", "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": { - "message": "policy,", + "message": "палітыка", "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": "патрабуецца 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": { - "message": "policy, and", + "message": "палітыка і", "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": "адміністраванне аднаўлення ўліковага запісу", "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": "палітыка з аўтаматычнай рэгістрацыяй уключаецца пры выкарыстанні гэтага параметра.", "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.'" }, "notFound": { @@ -6984,7 +6987,7 @@ "message": "Выдаленне ўдзельнікаў у якіх адсутнічае асноўны пароль і для якіх ён не быў прызначаны папярэдне можа стаць прычынай абмежавання доступу да іх уліковага запісу." }, "approvedAuthRequest": { - "message": "Approved device for $ID$.", + "message": "Ухваленне прылады для $ID$.", "placeholders": { "id": { "content": "$1", @@ -6993,7 +6996,7 @@ } }, "rejectedAuthRequest": { - "message": "Denied device for $ID$.", + "message": "Адхіленне прылады для $ID$.", "placeholders": { "id": { "content": "$1", @@ -7002,7 +7005,7 @@ } }, "requestedDeviceApproval": { - "message": "Requested device approval." + "message": "Запыт ухвалення прылады." }, "startYour7DayFreeTrialOfBitwardenFor": { "message": "Пачніце 7-дзённую бясплатную пробную версію для $ORG$", @@ -7016,38 +7019,32 @@ "next": { "message": "Далей" }, - "usFlag": { - "message": "Сцяг ЗША" - }, - "euFlag": { - "message": "Сцяг ЕС" - }, "selectedRegionFlag": { "message": "Сцяг выбранага рэгіёна" }, "accountSuccessfullyCreated": { - "message": "Account successfully created!" + "message": "Уліковы запіс паспяхова створаны!" }, "adminApprovalRequested": { - "message": "Admin approval requested" + "message": "Патрабуецца ўхваленне адміністратара" }, "adminApprovalRequestSentToAdmins": { - "message": "Your request has been sent to your admin." + "message": "Ваш запыт адпраўлены адміністратару." }, "youWillBeNotifiedOnceApproved": { - "message": "You will be notified once approved." + "message": "Вы атрымаеце апавяшчэння пасля яго ўхвалення." }, "troubleLoggingIn": { - "message": "Trouble logging in?" + "message": "Праблемы з уваходам?" }, "loginApproved": { - "message": "Login approved" + "message": "Уваход ухвалены" }, "userEmailMissing": { - "message": "User email missing" + "message": "Адсутнічае электронная пошта карыстальніка" }, "deviceTrusted": { - "message": "Device trusted" + "message": "Давераная прылада" }, "sendsNoItemsTitle": { "message": "Няма актыўных Send'аў", @@ -7160,7 +7157,7 @@ "message": "Максімальны патэнцыйны кошт сэрвісных уліковых запісаў" }, "loggedInExclamation": { - "message": "Logged in!" + "message": "Вы ўвайшлі!" }, "smBetaEndedDesc": { "message": "Перыяд бэта-тэсціравання менеджара сакрэтаў завершыцца $BETA_ENDING_DATE$. Дадайце менеджар сакрэтаў у сваю платную падпіску і захавайце доступ да даных (засталося дзён: $DAYS$). Звяжыцеся з нашай службай падтрымкі, каб дадаць менеджар сакрэтаў у сваю платную падпіску.", diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index 9b32d828818..4cf667a626c 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "Допълнителна защита на абонамента чрез изискването на допълнително действие при вписване." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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.'" }, @@ -1925,7 +1928,7 @@ "message": "1 GB пространство за файлове, които се шифрират." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Частно двустепенно удостоверяване чрез YubiKey и Duo." }, "premiumSignUpEmergency": { "message": "Авариен достъп" @@ -4758,10 +4761,10 @@ "message": "Потребителят с най-големи права на достъп, който може да управлява всички настройки на доставчика, както и да има достъп до и да управлява клиентските организации." }, "serviceUser": { - "message": "Service user" + "message": "Сервизен потребител" }, "serviceUserDesc": { - "message": "Service users can access and manage all client organizations." + "message": "Сервизните потребители имат достъп до всички клиентски организации и могат да ги управляват." }, "providerInviteUserDesc": { "message": "Може да поканите потребител към доставчика си, като попълните адреса му за електронна поща, с който е регистриран в Битуорден, по-долу. В случай, че потребителят не е регистриран, той автоматично ще получи покана и да се регистрира." @@ -7016,12 +7019,6 @@ "next": { "message": "Next" }, - "usFlag": { - "message": "Знаме на САЩ" - }, - "euFlag": { - "message": "Знаме на ЕС" - }, "selectedRegionFlag": { "message": "Знаме на избрания регион" }, diff --git a/apps/web/src/locales/bn/messages.json b/apps/web/src/locales/bn/messages.json index c970aacffb7..065e6482322 100644 --- a/apps/web/src/locales/bn/messages.json +++ b/apps/web/src/locales/bn/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "Secure your account by requiring an additional step when logging in." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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.'" }, @@ -7016,12 +7019,6 @@ "next": { "message": "Next" }, - "usFlag": { - "message": "US flag" - }, - "euFlag": { - "message": "EU flag" - }, "selectedRegionFlag": { "message": "Selected region flag" }, diff --git a/apps/web/src/locales/bs/messages.json b/apps/web/src/locales/bs/messages.json index ea7b495a715..bff25d8f84c 100644 --- a/apps/web/src/locales/bs/messages.json +++ b/apps/web/src/locales/bs/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "Secure your account by requiring an additional step when logging in." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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.'" }, @@ -7016,12 +7019,6 @@ "next": { "message": "Next" }, - "usFlag": { - "message": "US flag" - }, - "euFlag": { - "message": "EU flag" - }, "selectedRegionFlag": { "message": "Selected region flag" }, diff --git a/apps/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index 498f1a56615..d3283662601 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -712,7 +712,7 @@ "message": "S'ha produït un error inesperat." }, "expirationDateError": { - "message": "Please select an expiration date that is in the future." + "message": "Seleccioneu una data de caducitat futura." }, "emailAddress": { "message": "Adreça electrònica" @@ -955,7 +955,7 @@ "message": "Copia el codi de verificació" }, "copyUuid": { - "message": "Copy UUID" + "message": "Copia UUID" }, "warning": { "message": "Advertiment" @@ -1294,19 +1294,19 @@ "message": "Error en desxifrar el fitxer exportat. La vostra clau de xifratge no coincideix amb la clau de xifratge utilitzada per exportar les dades." }, "importDestination": { - "message": "Import destination" + "message": "Importa destinació" }, "learnAboutImportOptions": { - "message": "Learn about your import options" + "message": "Obteniu informació sobre les opcions d'importació" }, "selectImportFolder": { - "message": "Select a folder" + "message": "Selecciona una carpeta" }, "selectImportCollection": { - "message": "Select a collection" + "message": "Selecciona una col·lecció" }, "importTargetHint": { - "message": "Select this option if you want the imported file contents moved to a $DESTINATION$", + "message": "Seleccioneu aquesta opció si voleu que el contingut del fitxer importat es desplace a $DESTINATION$", "description": "Located as a hint under the import target. Will be appended by either folder or collection, depending if the user is importing into an individual or an organizational vault.", "placeholders": { "destination": { @@ -1316,7 +1316,7 @@ } }, "importUnassignedItemsError": { - "message": "File contains unassigned items." + "message": "El fitxer conté elements no assignats." }, "selectFormat": { "message": "Seleccioneu el format del fitxer d'importació" @@ -1425,8 +1425,11 @@ "twoStepLoginDesc": { "message": "Protegiu el vostre compte exigint un pas addicional en iniciar sessió." }, - "twoStepLoginOrganizationDescStart": { - "message": "Apliqueu les opcions d'inici de sessió en dos passos de Bitwarden per als membres mitjançant la ", + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { + "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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": { @@ -1925,7 +1928,7 @@ "message": "1 GB d'emmagatzematge xifrat per als fitxers adjunts." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Opcions propietàries de doble factor com ara YubiKey i Duo." }, "premiumSignUpEmergency": { "message": "Accés d’emergència" @@ -2041,7 +2044,7 @@ } }, "paymentChargedWithUnpaidSubscription": { - "message": "Your payment method will be charged for any unpaid subscriptions." + "message": "Es cobrarà al vostre mètode de pagament les subscripcions no pagades." }, "paymentChargedWithTrial": { "message": "El vostre pla inclou una prova gratuïta de 7 dies. El mètode de pagament no es cobrarà fins que no s'haja acabat la prova. Podeu cancel·lar-ho en qualsevol moment." @@ -3540,7 +3543,7 @@ "message": "Trieu quan es tancarà la vostra caixa forta i feu l'acció seleccionada." }, "vaultTimeoutLogoutDesc": { - "message": "Choose when your vault will be logged out." + "message": "Trieu quan es tancarà la sessió de la vostra caixa forta." }, "oneMinute": { "message": "1 minut" @@ -3631,7 +3634,7 @@ "message": "Aquest element té fitxers adjunts antics que s'han de corregir." }, "attachmentFixDescription": { - "message": "This attachment uses outdated encryption. Select 'Fix' to download, re-encrypt, and re-upload the attachment." + "message": "Aquest fitxer adjunt utilitza un xifratge obsolet. Seleccioneu \"Arregla\" per descarregar, tornar a xifrar i tornar a carregar el fitxer adjunt." }, "fix": { "message": "Corregeix", @@ -4939,7 +4942,7 @@ "message": "Inhabilita l'exportació de la caixa forta personal" }, "disablePersonalVaultExportDescription": { - "message": "Do not allow members to export data from their individual vault." + "message": "No permetes que els membres exporten dades des de la seua caixa forta individual." }, "vaultExportDisabled": { "message": "L'exportació de la caixa forta s'ha suprimit" @@ -5443,7 +5446,7 @@ "message": "S'està exportant la caixa forta de l’organització" }, "exportingIndividualVaultDescription": { - "message": "Only the individual vault items associated with $EMAIL$ will be exported. Organization vault items will not be included. Only vault item information will be exported and will not include associated attachments.", + "message": "Només s'exportaran els elements de la caixa de seguretat individuals associats a $EMAIL$. Els elements de la caixa de l'organització no s'inclouran. Només s'exportarà la informació de l'element de la caixa de seguretat i no inclourà els fitxers adjunts associats.", "placeholders": { "email": { "content": "$1", @@ -5452,7 +5455,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": "Només s'exportarà la caixa forta de l'organització associada a $ORGANIZATION$. No s'inclouran els elements de les caixes fortes individuals ni d'altres organitzacions.", "placeholders": { "organization": { "content": "$1", @@ -6842,58 +6845,58 @@ "message": "Actualitza la configuració de KDF" }, "loginInitiated": { - "message": "Login initiated" + "message": "S'ha iniciat la sessió" }, "deviceApprovalRequired": { - "message": "Device approval required. Select an approval option below:" + "message": "Cal l'aprovació del dispositiu. Seleccioneu una opció d'aprovació a continuació:" }, "rememberThisDevice": { - "message": "Remember this device" + "message": "Recorda aquest dispositiu" }, "uncheckIfPublicDevice": { - "message": "Uncheck if using a public device" + "message": "Desmarqueu si utilitzeu un dispositiu públic" }, "approveFromYourOtherDevice": { - "message": "Approve from your other device" + "message": "Aproveu des d'un altre dispositiu vostre" }, "requestAdminApproval": { - "message": "Request admin approval" + "message": "Sol·liciteu l'aprovació de l'administrador" }, "approveWithMasterPassword": { - "message": "Approve with master password" + "message": "Aprova amb contrasenya mestra" }, "trustedDeviceEncryption": { - "message": "Trusted device encryption" + "message": "Encriptació de dispositius de confiança" }, "trustedDevices": { "message": "Dispositius de confiança" }, "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", + "message": "Una vegada autenticats, els membres desxifraran les dades de la caixa forta mitjançant una clau emmagatzemada al seu dispositiu. La", "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": "política d'organització", "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": { - "message": "policy,", + "message": "ú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.'" }, "memberDecryptionOptionTdeDescriptionLinkTwo": { - "message": "SSO required", + "message": "la política de", "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": { - "message": "policy, and", + "message": "requerida y la política", "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": "d'administració de recuperació del compte", "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": "amb inscripció automàtica s'activaran quan s'utilitze aquesta opció.", "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.'" }, "notFound": { @@ -6984,7 +6987,7 @@ "message": "La supressió de membres que no tenen contrasenyes mestres sense establir-ne una pot restringir l'accés al seu compte complet." }, "approvedAuthRequest": { - "message": "Approved device for $ID$.", + "message": "Dispositiu aprovat per $ID$.", "placeholders": { "id": { "content": "$1", @@ -6993,7 +6996,7 @@ } }, "rejectedAuthRequest": { - "message": "Denied device for $ID$.", + "message": "Dispositiu denegat per $ID$.", "placeholders": { "id": { "content": "$1", @@ -7002,7 +7005,7 @@ } }, "requestedDeviceApproval": { - "message": "Requested device approval." + "message": "S'ha sol·licitat l'aprovació del dispositiu." }, "startYour7DayFreeTrialOfBitwardenFor": { "message": "Inicieu la vostra prova gratuïta de 7 dies de Bitwarden per a $ORG$", @@ -7016,38 +7019,32 @@ "next": { "message": "Següent" }, - "usFlag": { - "message": "Bandera EUA" - }, - "euFlag": { - "message": "Bandera UE" - }, "selectedRegionFlag": { "message": "Bandera de la regió seleccionada" }, "accountSuccessfullyCreated": { - "message": "Account successfully created!" + "message": "Compte creat correctament!" }, "adminApprovalRequested": { - "message": "Admin approval requested" + "message": "S'ha sol·licitat l'aprovació de l'administrador" }, "adminApprovalRequestSentToAdmins": { - "message": "Your request has been sent to your admin." + "message": "La vostra sol·licitud s'ha enviat a l'administrador." }, "youWillBeNotifiedOnceApproved": { - "message": "You will be notified once approved." + "message": "Se us notificarà una vegada aprovat." }, "troubleLoggingIn": { - "message": "Trouble logging in?" + "message": "Teniu problemes per iniciar la sessió?" }, "loginApproved": { - "message": "Login approved" + "message": "S'ha aprovat l'inici de sessió" }, "userEmailMissing": { - "message": "User email missing" + "message": "Falta el correu electrònic de l'usuari" }, "deviceTrusted": { - "message": "Device trusted" + "message": "Dispositiu de confiança" }, "sendsNoItemsTitle": { "message": "No hi ha Sends actius", @@ -7118,7 +7115,7 @@ "message": "Comptes de serveis addicionals" }, "includedServiceAccounts": { - "message": "Your plan comes with $COUNT$ service accounts.", + "message": "El vostre pla inclou $COUNT$ comptes de servei.", "placeholders": { "count": { "content": "$1", @@ -7127,7 +7124,7 @@ } }, "addAdditionalServiceAccounts": { - "message": "You can add additional service accounts for $COST$ per month.", + "message": "Podeu afegir comptes de servei addicionals per $COST$ al mes.", "placeholders": { "cost": { "content": "$1", @@ -7160,10 +7157,10 @@ "message": "Cost potencial màxim del compte de servei" }, "loggedInExclamation": { - "message": "Logged in!" + "message": "Connectat!" }, "smBetaEndedDesc": { - "message": "The Secrets Manager Beta ended $BETA_ENDING_DATE$. You have $DAYS$ days left to add Secrets Manager to your paid subscription and maintain access to Secrets Manager data. Contact Customer Success to add Secrets Manager to your subscription.", + "message": "La beta de l'administrador de secrets va finalitzar el dia $BETA_ENDING_DATE$. Et queden $DAYS$ dies per afegir l'administrador de secrets a la teua subscripció de pagament i mantindre l'accés a les dades de l'administrador de secrets. Contacteu amb Customer Success per afegir l'administrador de secrets a la vostra subscripció.", "placeholders": { "beta_ending_date": { "content": "$1", @@ -7176,12 +7173,12 @@ } }, "betaEnding": { - "message": "Beta Ending" + "message": "Beta final" }, "beta": { "message": "Beta" }, "alreadyHaveAccount": { - "message": "Already have an account?" + "message": "Ja tens un compte?" } } diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index c18a9fa2044..b145c4d1950 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "Zabezpečte svůj účet vyžadováním dodatečného kroku při přihlašování." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Povolí dvoufázové přihlášení pro Vaši organizaci." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Vynutí dvoufázové přihlášení členů k Bitwardenu použitím ", "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.'" }, @@ -7016,12 +7019,6 @@ "next": { "message": "Další" }, - "usFlag": { - "message": "Americká vlajka" - }, - "euFlag": { - "message": "Evropská vlajka" - }, "selectedRegionFlag": { "message": "Vlajka zvoleného regionu" }, diff --git a/apps/web/src/locales/cy/messages.json b/apps/web/src/locales/cy/messages.json index a71551e7710..4ed205c1f73 100644 --- a/apps/web/src/locales/cy/messages.json +++ b/apps/web/src/locales/cy/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "Secure your account by requiring an additional step when logging in." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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.'" }, @@ -7016,12 +7019,6 @@ "next": { "message": "Next" }, - "usFlag": { - "message": "US flag" - }, - "euFlag": { - "message": "EU flag" - }, "selectedRegionFlag": { "message": "Selected region flag" }, diff --git a/apps/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index 3ed144bcaf5..e2e0db2e83e 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "Beskyt kontoen ved at kræve et ekstra trin under indlogning." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Aktivér totrins-login for organisationen." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Håndhæv Bitwarden totrins-login indstillinger for medlemmer vha. ", "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.'" }, @@ -7016,12 +7019,6 @@ "next": { "message": "Næste" }, - "usFlag": { - "message": "Amerikansk flag" - }, - "euFlag": { - "message": "EU-flag" - }, "selectedRegionFlag": { "message": "Valgte områdeflag" }, diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index 07f5f1d5e7a..5be4cb32039 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "Sichern Sie Ihr Konto mit Zwei-Faktor-Authentifizierung." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Zwei-Faktor-Authentifizierung für deine Organisation aktivieren." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Erzwinge eine Zwei-Faktor-Authentifizierung in Bitwarden für Mitglieder durch Verwendung der ", "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.'" }, @@ -1925,7 +1928,7 @@ "message": "1 GB verschlüsselter Speicherplatz für Dateianhänge." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Proprietäre Optionen für die Zwei-Faktor Authentifizierung wie YubiKey und Duo." }, "premiumSignUpEmergency": { "message": "Notfallzugriff" @@ -7016,12 +7019,6 @@ "next": { "message": "Weiter" }, - "usFlag": { - "message": "US-Flagge" - }, - "euFlag": { - "message": "EU-Flagge" - }, "selectedRegionFlag": { "message": "Flagge der ausgewählten Region" }, diff --git a/apps/web/src/locales/el/messages.json b/apps/web/src/locales/el/messages.json index 5103325a343..d2d87fb7180 100644 --- a/apps/web/src/locales/el/messages.json +++ b/apps/web/src/locales/el/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "Ασφαλίστε το λογαριασμό σας απαιτώντας ένα επιπλέον βήμα κατά τη σύνδεση." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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.'" }, @@ -7016,12 +7019,6 @@ "next": { "message": "Next" }, - "usFlag": { - "message": "US flag" - }, - "euFlag": { - "message": "EU flag" - }, "selectedRegionFlag": { "message": "Selected region flag" }, diff --git a/apps/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index f342a7e81f3..65abe5b1f72 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "Secure your account by requiring an additional step when logging in." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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.'" }, @@ -7016,12 +7019,6 @@ "next": { "message": "Next" }, - "usFlag": { - "message": "US flag" - }, - "euFlag": { - "message": "EU flag" - }, "selectedRegionFlag": { "message": "Selected region flag" }, diff --git a/apps/web/src/locales/en_IN/messages.json b/apps/web/src/locales/en_IN/messages.json index 5f31a22c729..dc120f301e9 100644 --- a/apps/web/src/locales/en_IN/messages.json +++ b/apps/web/src/locales/en_IN/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "Secure your account by requiring an additional step when logging in." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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.'" }, @@ -7016,12 +7019,6 @@ "next": { "message": "Next" }, - "usFlag": { - "message": "US flag" - }, - "euFlag": { - "message": "EU flag" - }, "selectedRegionFlag": { "message": "Selected region flag" }, diff --git a/apps/web/src/locales/eo/messages.json b/apps/web/src/locales/eo/messages.json index 8034131dcc5..e259d0f0732 100644 --- a/apps/web/src/locales/eo/messages.json +++ b/apps/web/src/locales/eo/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "Sekurigu vian konton postulante plian paŝon kiam vi ensalutas." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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.'" }, @@ -7016,12 +7019,6 @@ "next": { "message": "Next" }, - "usFlag": { - "message": "US flag" - }, - "euFlag": { - "message": "EU flag" - }, "selectedRegionFlag": { "message": "Selected region flag" }, diff --git a/apps/web/src/locales/es/messages.json b/apps/web/src/locales/es/messages.json index 50e743cb655..8f58624a6f7 100644 --- a/apps/web/src/locales/es/messages.json +++ b/apps/web/src/locales/es/messages.json @@ -1425,8 +1425,11 @@ "twoStepLoginDesc": { "message": "Protege tu cuenta requiriendo un paso adicional a la hora de acceder." }, - "twoStepLoginOrganizationDescStart": { - "message": "Forzar opciones de inicio de sesión en dos pasos de Bitwarden para los miembros mediante el uso del ", + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { + "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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": { @@ -1925,7 +1928,7 @@ "message": "1 GB de almacenamiento de archivos cifrados." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Opciones de inicio de sesión con autenticación de dos pasos propietarios como YubiKey y Duo." }, "premiumSignUpEmergency": { "message": "Acceso de emergencia" @@ -7016,12 +7019,6 @@ "next": { "message": "Siguiente" }, - "usFlag": { - "message": "Bandera de EE.UU." - }, - "euFlag": { - "message": "Bandera de EU" - }, "selectedRegionFlag": { "message": "Región seleccionada" }, diff --git a/apps/web/src/locales/et/messages.json b/apps/web/src/locales/et/messages.json index f31e0832366..92ac930999f 100644 --- a/apps/web/src/locales/et/messages.json +++ b/apps/web/src/locales/et/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "Kaitse oma kontot, nõudes sisselogimisel lisakinnitust." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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.'" }, @@ -7016,12 +7019,6 @@ "next": { "message": "Next" }, - "usFlag": { - "message": "US flag" - }, - "euFlag": { - "message": "EU flag" - }, "selectedRegionFlag": { "message": "Selected region flag" }, diff --git a/apps/web/src/locales/eu/messages.json b/apps/web/src/locales/eu/messages.json index 793f4612ca2..55074ebd56f 100644 --- a/apps/web/src/locales/eu/messages.json +++ b/apps/web/src/locales/eu/messages.json @@ -1425,8 +1425,11 @@ "twoStepLoginDesc": { "message": "Ziurtatu zure kontua saioa hastean beste urrats bat egitea eskatuz." }, - "twoStepLoginOrganizationDescStart": { - "message": "Behartu kideei Bitwardenen saioa hasteko bi urratseko aukera, hau erabiliz", + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { + "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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": { @@ -7016,12 +7019,6 @@ "next": { "message": "Next" }, - "usFlag": { - "message": "US flag" - }, - "euFlag": { - "message": "EU flag" - }, "selectedRegionFlag": { "message": "Selected region flag" }, diff --git a/apps/web/src/locales/fa/messages.json b/apps/web/src/locales/fa/messages.json index fe48e771fa0..80c93b7e597 100644 --- a/apps/web/src/locales/fa/messages.json +++ b/apps/web/src/locales/fa/messages.json @@ -1425,8 +1425,11 @@ "twoStepLoginDesc": { "message": "با درخواست یک مرحله اضافی هنگام ورود، حساب خود را ایمن کنید." }, - "twoStepLoginOrganizationDescStart": { - "message": "گزینه‌های ورود دو مرحله ای Bitwarden را برای اعضا اعمال کنید با استفاده از ", + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { + "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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": { @@ -7016,12 +7019,6 @@ "next": { "message": "بعدی" }, - "usFlag": { - "message": "پرچم ایالات متحده" - }, - "euFlag": { - "message": "پرچم اتحادیه اروپا" - }, "selectedRegionFlag": { "message": "پرچم منطقه انتخاب شد" }, diff --git a/apps/web/src/locales/fi/messages.json b/apps/web/src/locales/fi/messages.json index 1606ca35ff6..296d49f7133 100644 --- a/apps/web/src/locales/fi/messages.json +++ b/apps/web/src/locales/fi/messages.json @@ -1306,7 +1306,7 @@ "message": "Valitse kokoelma" }, "importTargetHint": { - "message": "Valitse tämä, jos haluat tuoda tiedoston sisällön kohteesee $DESTINATION$.", + "message": "Valitse tämä, jos haluat tuoda tiedoston sisällön kohteeseen $DESTINATION$.", "description": "Located as a hint under the import target. Will be appended by either folder or collection, depending if the user is importing into an individual or an organizational vault.", "placeholders": { "destination": { @@ -1425,8 +1425,11 @@ "twoStepLoginDesc": { "message": "Suojaa tilisi vaatimalla sisäänkirjautumiseen toinen todennusvaihe." }, - "twoStepLoginOrganizationDescStart": { - "message": "Pakota jäsenille Bitwardenin kaksivaiheisen kirjautumisen valinnat käytännöllä: ", + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { + "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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": { @@ -1925,7 +1928,7 @@ "message": "1 Gt salattua tallennustilaa tiedostoliitteille." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Omisteiset kaksivaiheisen kirjautumisen vaihtoehdot, kuten YubiKey ja Duo." }, "premiumSignUpEmergency": { "message": "Varmuuskäyttö" @@ -2041,7 +2044,7 @@ } }, "paymentChargedWithUnpaidSubscription": { - "message": "Maksutavaltasi veloitetaan kaikki maksamattomat tilauksesta." + "message": "Maksutavaltasi veloitetaan kaikki maksamattomat tilaukset." }, "paymentChargedWithTrial": { "message": "Tilauksesi sisältää ilmaisen 7 päivän kokeilujakson. Maksutapaasi ei veloiteta ennen kokeilujakson päättymistä. Voit irtisanoa tilauksen koska tahansa." @@ -3631,7 +3634,7 @@ "message": "Kohteella on vanhoja tiedostoliitteitä, jotka on korjattava." }, "attachmentFixDescription": { - "message": "Liite käyttää vanhentunutta salausta. Lataa liite, salaa se uudelleen ja lisää se uudestaan valitsemalla \"Korjaa\"." + "message": "Liite on salattu vanhentuneella salauksella. Lataa, salaa uudelleen ja lisää se uudestaan valitsemalla \"Korjaa\"." }, "fix": { "message": "Korjaa", @@ -6851,7 +6854,7 @@ "message": "Muista tämä laite" }, "uncheckIfPublicDevice": { - "message": "Poista käytöstä julkisilla laitteilla" + "message": "Poista valinta julkisilla laitteilla" }, "approveFromYourOtherDevice": { "message": "Hyväksy muilta laitteiltasi" @@ -6869,27 +6872,27 @@ "message": "Luotetut laitteet" }, "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Kun jäsenet on todennettu, he voivat purkaa holvin salauksen heidän laitteellaan säilytettävällä avaimella.", + "message": "Kun jäsenet on todennettu, he voivat purkaa holvin salauksen omalla laitteellaan säilytettävällä avaimella.", "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": "Yksittäinen organisaatio", + "message": "\"Yksittäinen organisaatio\"", "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": { - "message": ",", - "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": "Kertakirjautuminen vaaditaan", - "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": { "message": "ja", "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": "\"Kertakirjautuminen vaaditaan\"", + "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": { + "message": "-käytännöt sekä", + "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": "Tilin palautusavun hallinta", + "message": "\"Tilien palautusavun hallinta\"", "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": { @@ -7016,12 +7019,6 @@ "next": { "message": "Seuraava" }, - "usFlag": { - "message": "Yhdysvaltain lippu" - }, - "euFlag": { - "message": "EU:n lippu" - }, "selectedRegionFlag": { "message": "Valitun alueen lippu" }, @@ -7032,7 +7029,7 @@ "message": "Hyväksyntää pyydetty ylläpidolta" }, "adminApprovalRequestSentToAdmins": { - "message": "Pyyntösi on välitetty ylläpidolle." + "message": "Pyyntösi on välitetty ylläpidollesi." }, "youWillBeNotifiedOnceApproved": { "message": "Saat ilmoituksen kun se on hyväksytty." @@ -7041,7 +7038,7 @@ "message": "Ongelmia kirjautumisessa?" }, "loginApproved": { - "message": "Kirjautuminen hyväksyttiin" + "message": "Kirjautuminen hyväksytty" }, "userEmailMissing": { "message": "Käyttäjän sähköpostiosoite puuttuu" @@ -7061,7 +7058,7 @@ "message": "Kutsu käyttäjiä" }, "secretsManagerForPlan": { - "message": "Salaisuushallinta $PLAN$-tilaukseen", + "message": "Salaisuushallinta $PLAN$ -tilaukseen", "placeholders": { "plan": { "content": "$1", @@ -7070,7 +7067,7 @@ } }, "secretsManagerForPlanDesc": { - "message": "Kehitys ja DevOps-tiimeille salaisuuksien hallintaan ohjelmistokehityksen koko elinkaaren ajaksi." + "message": "Kehitys- ja DevOps-tiimeille ohjelmistokehityksen koko elinkaaren kestävään salaisuuksien hallintaan." }, "free2PersonOrganization": { "message": "Ilmainen kahden hengen organisaatioille" diff --git a/apps/web/src/locales/fil/messages.json b/apps/web/src/locales/fil/messages.json index 1b11567b782..d395fed70d8 100644 --- a/apps/web/src/locales/fil/messages.json +++ b/apps/web/src/locales/fil/messages.json @@ -1425,8 +1425,11 @@ "twoStepLoginDesc": { "message": "Panatilihing ligtas ang account mo sa pamamagitan ng pangangailangan ng ikalawang hakbang kapag nagla-log in." }, - "twoStepLoginOrganizationDescStart": { - "message": "Ipatupad ang mga opsyon sa Dalawang-hakbang na Pag-log in sa Bitwarden para sa mga miyembro gamit ang ", + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { + "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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": { @@ -7016,12 +7019,6 @@ "next": { "message": "Next" }, - "usFlag": { - "message": "US flag" - }, - "euFlag": { - "message": "EU flag" - }, "selectedRegionFlag": { "message": "Selected region flag" }, diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index 6bc5b0a1454..d71064bd50f 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "Sécurisez votre compte en exigeant une étape supplémentaire lors de la connexion." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Activez l'authentification à deux facteurs pour votre organisation." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Imposer les options d'authentification à deux facteurs de Bitwarden pour les membres en utilisant le ", "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.'" }, @@ -7016,12 +7019,6 @@ "next": { "message": "Suivant" }, - "usFlag": { - "message": "Drapeau des États-Unis" - }, - "euFlag": { - "message": "Drapeau de l'Union Européenne" - }, "selectedRegionFlag": { "message": "Drapeau de la région sélectionnée" }, diff --git a/apps/web/src/locales/gl/messages.json b/apps/web/src/locales/gl/messages.json index a71551e7710..4ed205c1f73 100644 --- a/apps/web/src/locales/gl/messages.json +++ b/apps/web/src/locales/gl/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "Secure your account by requiring an additional step when logging in." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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.'" }, @@ -7016,12 +7019,6 @@ "next": { "message": "Next" }, - "usFlag": { - "message": "US flag" - }, - "euFlag": { - "message": "EU flag" - }, "selectedRegionFlag": { "message": "Selected region flag" }, diff --git a/apps/web/src/locales/he/messages.json b/apps/web/src/locales/he/messages.json index 716f2d46071..ee3e68708ec 100644 --- a/apps/web/src/locales/he/messages.json +++ b/apps/web/src/locales/he/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "שפר את אבטחת החשבון שלך על ידי דרישת צעד נוסף עבור כל נסיון חיבור." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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.'" }, @@ -7016,12 +7019,6 @@ "next": { "message": "Next" }, - "usFlag": { - "message": "US flag" - }, - "euFlag": { - "message": "EU flag" - }, "selectedRegionFlag": { "message": "Selected region flag" }, diff --git a/apps/web/src/locales/hi/messages.json b/apps/web/src/locales/hi/messages.json index cb737f84f44..c10d8c37284 100644 --- a/apps/web/src/locales/hi/messages.json +++ b/apps/web/src/locales/hi/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "Secure your account by requiring an additional step when logging in." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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.'" }, @@ -7016,12 +7019,6 @@ "next": { "message": "Next" }, - "usFlag": { - "message": "US flag" - }, - "euFlag": { - "message": "EU flag" - }, "selectedRegionFlag": { "message": "Selected region flag" }, diff --git a/apps/web/src/locales/hr/messages.json b/apps/web/src/locales/hr/messages.json index 3d8d176514c..83621989bf8 100644 --- a/apps/web/src/locales/hr/messages.json +++ b/apps/web/src/locales/hr/messages.json @@ -1425,8 +1425,11 @@ "twoStepLoginDesc": { "message": "Osiguraj svoj račun dodavanjem dodatnog koraka prilikom prijave." }, - "twoStepLoginOrganizationDescStart": { - "message": "Prisili korištenje prijave dvostrukom autentifikacijom koristeći", + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { + "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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": { @@ -7016,12 +7019,6 @@ "next": { "message": "Next" }, - "usFlag": { - "message": "US flag" - }, - "euFlag": { - "message": "EU flag" - }, "selectedRegionFlag": { "message": "Selected region flag" }, diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index c8c8439626f..bec2a62727b 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -1425,8 +1425,11 @@ "twoStepLoginDesc": { "message": "A fiók biztosítása kiegészítő lépéssel bejelentkezéskor." }, - "twoStepLoginOrganizationDescStart": { - "message": "Ezt egy nagyobb mondat részeként használjuk fel,és hivatkozásokat tartalmaznak. A teljes mondat így fog szólni: \"Kétlépcsős bejelentkezési opciók érvényesítése a tagok számára a kétlépcsős bejelentkezési szabályzat használatával.\".", + "twoStepLoginTeamsDesc": { + "message": "A kétlépcsős bejelentkezés engedélyezése a szervezethez." + }, + "twoStepLoginEnterpriseDescStart": { + "message": "A Bitwarden kétlépcsős bejelentkezési opcióinak kényszerítése a tagoknál:", "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": { @@ -7016,12 +7019,6 @@ "next": { "message": "Következő" }, - "usFlag": { - "message": "USA zászló" - }, - "euFlag": { - "message": "EU zászló" - }, "selectedRegionFlag": { "message": "Kiválasztott régió zászló" }, diff --git a/apps/web/src/locales/id/messages.json b/apps/web/src/locales/id/messages.json index a88799f1adc..d3854f8885d 100644 --- a/apps/web/src/locales/id/messages.json +++ b/apps/web/src/locales/id/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "Amankan akun Anda dengan meminta langkah tambahan saat masuk." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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.'" }, @@ -7016,12 +7019,6 @@ "next": { "message": "Next" }, - "usFlag": { - "message": "US flag" - }, - "euFlag": { - "message": "EU flag" - }, "selectedRegionFlag": { "message": "Selected region flag" }, diff --git a/apps/web/src/locales/it/messages.json b/apps/web/src/locales/it/messages.json index c082b3dd9ca..35f6b4d6a3c 100644 --- a/apps/web/src/locales/it/messages.json +++ b/apps/web/src/locales/it/messages.json @@ -1425,8 +1425,11 @@ "twoStepLoginDesc": { "message": "Proteggi il tuo account richiedendo un passaggio aggiuntivo all'accesso." }, - "twoStepLoginOrganizationDescStart": { - "message": "Forza le opzioni di verifica in due passaggi di Bitwarden per i membri usando la ", + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { + "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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": { @@ -1925,7 +1928,7 @@ "message": "1 GB di spazio di archiviazione criptato per gli allegati." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Opzioni di verifica in due passaggi proprietarie come YubiKey e Duo." }, "premiumSignUpEmergency": { "message": "Accesso di emergenza" @@ -7016,12 +7019,6 @@ "next": { "message": "Avanti" }, - "usFlag": { - "message": "Bandiera degli Stati Uniti" - }, - "euFlag": { - "message": "Bandiera dell'Europa" - }, "selectedRegionFlag": { "message": "Bandiera della regione selezionata" }, diff --git a/apps/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index 95647694572..45a9b4db63b 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -1425,8 +1425,11 @@ "twoStepLoginDesc": { "message": "ログイン時に追加の手順を必要としアカウントを保護します。" }, - "twoStepLoginOrganizationDescStart": { - "message": "Bitwarden 2段階ログインの使用をメンバーに強制します", + "twoStepLoginTeamsDesc": { + "message": "組織の2段階認証を有効にする。" + }, + "twoStepLoginEnterpriseDescStart": { + "message": "Bitwardenの2段階認証をメンバーに強制するには、次のようにします。 ", "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": { @@ -7016,12 +7019,6 @@ "next": { "message": "次へ" }, - "usFlag": { - "message": "アメリカ" - }, - "euFlag": { - "message": "EU" - }, "selectedRegionFlag": { "message": "リージョン選択" }, diff --git a/apps/web/src/locales/ka/messages.json b/apps/web/src/locales/ka/messages.json index 5f461f4b70b..4f2b7cc968b 100644 --- a/apps/web/src/locales/ka/messages.json +++ b/apps/web/src/locales/ka/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "Secure your account by requiring an additional step when logging in." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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.'" }, @@ -7016,12 +7019,6 @@ "next": { "message": "Next" }, - "usFlag": { - "message": "US flag" - }, - "euFlag": { - "message": "EU flag" - }, "selectedRegionFlag": { "message": "Selected region flag" }, diff --git a/apps/web/src/locales/km/messages.json b/apps/web/src/locales/km/messages.json index a71551e7710..4ed205c1f73 100644 --- a/apps/web/src/locales/km/messages.json +++ b/apps/web/src/locales/km/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "Secure your account by requiring an additional step when logging in." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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.'" }, @@ -7016,12 +7019,6 @@ "next": { "message": "Next" }, - "usFlag": { - "message": "US flag" - }, - "euFlag": { - "message": "EU flag" - }, "selectedRegionFlag": { "message": "Selected region flag" }, diff --git a/apps/web/src/locales/kn/messages.json b/apps/web/src/locales/kn/messages.json index 94145e99c35..910883c83b0 100644 --- a/apps/web/src/locales/kn/messages.json +++ b/apps/web/src/locales/kn/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "ಲಾಗಿನ್ ಆಗುವಾಗ ಹೆಚ್ಚುವರಿ ಹಂತದ ಅಗತ್ಯವಿರುವ ಮೂಲಕ ನಿಮ್ಮ ಖಾತೆಯನ್ನು ಸುರಕ್ಷಿತಗೊಳಿಸಿ." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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.'" }, @@ -7016,12 +7019,6 @@ "next": { "message": "Next" }, - "usFlag": { - "message": "US flag" - }, - "euFlag": { - "message": "EU flag" - }, "selectedRegionFlag": { "message": "Selected region flag" }, diff --git a/apps/web/src/locales/ko/messages.json b/apps/web/src/locales/ko/messages.json index 9900cfa742e..6d0cbc4bc45 100644 --- a/apps/web/src/locales/ko/messages.json +++ b/apps/web/src/locales/ko/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "로그인할 때 추가 단계를 요구하여 계정을 보호하십시오." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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.'" }, @@ -7016,12 +7019,6 @@ "next": { "message": "Next" }, - "usFlag": { - "message": "US flag" - }, - "euFlag": { - "message": "EU flag" - }, "selectedRegionFlag": { "message": "Selected region flag" }, diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index a1f9a5bc382..3dce02f1ec4 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -859,7 +859,7 @@ "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { - "message": "Verificējiet ar savas organizācijas Duo Security, izmantojot Duo Mobile lietotni, SMS, tālruņa zvanu vai U2F drošības atslēgu.", + "message": "Apliecināšana ar savas apvienības Duo Security, izmantojot Duo Mobile lietotni, īsziņu, tālruņa zvanu vai U2F drošības atslēgu.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "u2fDesc": { @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "Nodrošināt kontu, pieprasot papildu darbību pieteikšanās brīdī." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Iespējo divpakāpju pieteikšanaos savai apvienībai." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Piemērot Bitwarden divpakāpju pieteikšanās iespējas dalībniekiem ar ", "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.'" }, @@ -7016,12 +7019,6 @@ "next": { "message": "Turpināt" }, - "usFlag": { - "message": "ASV karogs" - }, - "euFlag": { - "message": "ES karogs" - }, "selectedRegionFlag": { "message": "Atlasītā apgabala karogs" }, diff --git a/apps/web/src/locales/ml/messages.json b/apps/web/src/locales/ml/messages.json index 1623cf8c2f4..c5265c0ee03 100644 --- a/apps/web/src/locales/ml/messages.json +++ b/apps/web/src/locales/ml/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "Secure your account by requiring an additional step when logging in." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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.'" }, @@ -7016,12 +7019,6 @@ "next": { "message": "Next" }, - "usFlag": { - "message": "US flag" - }, - "euFlag": { - "message": "EU flag" - }, "selectedRegionFlag": { "message": "Selected region flag" }, diff --git a/apps/web/src/locales/mr/messages.json b/apps/web/src/locales/mr/messages.json index a71551e7710..4ed205c1f73 100644 --- a/apps/web/src/locales/mr/messages.json +++ b/apps/web/src/locales/mr/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "Secure your account by requiring an additional step when logging in." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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.'" }, @@ -7016,12 +7019,6 @@ "next": { "message": "Next" }, - "usFlag": { - "message": "US flag" - }, - "euFlag": { - "message": "EU flag" - }, "selectedRegionFlag": { "message": "Selected region flag" }, diff --git a/apps/web/src/locales/my/messages.json b/apps/web/src/locales/my/messages.json index a71551e7710..4ed205c1f73 100644 --- a/apps/web/src/locales/my/messages.json +++ b/apps/web/src/locales/my/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "Secure your account by requiring an additional step when logging in." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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.'" }, @@ -7016,12 +7019,6 @@ "next": { "message": "Next" }, - "usFlag": { - "message": "US flag" - }, - "euFlag": { - "message": "EU flag" - }, "selectedRegionFlag": { "message": "Selected region flag" }, diff --git a/apps/web/src/locales/nb/messages.json b/apps/web/src/locales/nb/messages.json index 5d0b7f5eafd..a41bd2cd34c 100644 --- a/apps/web/src/locales/nb/messages.json +++ b/apps/web/src/locales/nb/messages.json @@ -1425,8 +1425,11 @@ "twoStepLoginDesc": { "message": "Sikre kontoen din ved å kreve et ekstra trinn når du logger på." }, - "twoStepLoginOrganizationDescStart": { - "message": "Håndhev Bitwarden tofaktor-innloggingsalternativer for medlemmer ved å bruke ", + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { + "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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": { @@ -7016,12 +7019,6 @@ "next": { "message": "Next" }, - "usFlag": { - "message": "US flag" - }, - "euFlag": { - "message": "EU flag" - }, "selectedRegionFlag": { "message": "Selected region flag" }, diff --git a/apps/web/src/locales/ne/messages.json b/apps/web/src/locales/ne/messages.json index 4a8c2e06c0c..9813040b78e 100644 --- a/apps/web/src/locales/ne/messages.json +++ b/apps/web/src/locales/ne/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "Secure your account by requiring an additional step when logging in." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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.'" }, @@ -7016,12 +7019,6 @@ "next": { "message": "Next" }, - "usFlag": { - "message": "US flag" - }, - "euFlag": { - "message": "EU flag" - }, "selectedRegionFlag": { "message": "Selected region flag" }, diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index dcead7cd8fd..91387dde607 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -1425,8 +1425,11 @@ "twoStepLoginDesc": { "message": "Beveilig je account door een extra stap te vereisen bij het inloggen." }, - "twoStepLoginOrganizationDescStart": { - "message": "Bitwarden tweestapsaanmeldingsopties afdwingen voor leden door gebruik te maken van de ", + "twoStepLoginTeamsDesc": { + "message": "Tweestapsaanmelding voor je organisatie activeren." + }, + "twoStepLoginEnterpriseDescStart": { + "message": "Bitwarden-tweestapsaanmeldingsinstellingen afdwingen voor leden via het ", "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": { @@ -7016,12 +7019,6 @@ "next": { "message": "Volgende" }, - "usFlag": { - "message": "VS-vlag" - }, - "euFlag": { - "message": "EU-vlag" - }, "selectedRegionFlag": { "message": "Geselecteerde regionale vlag" }, diff --git a/apps/web/src/locales/nn/messages.json b/apps/web/src/locales/nn/messages.json index 22e4a7d30e1..845c218f195 100644 --- a/apps/web/src/locales/nn/messages.json +++ b/apps/web/src/locales/nn/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "Secure your account by requiring an additional step when logging in." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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.'" }, @@ -7016,12 +7019,6 @@ "next": { "message": "Next" }, - "usFlag": { - "message": "US flag" - }, - "euFlag": { - "message": "EU flag" - }, "selectedRegionFlag": { "message": "Selected region flag" }, diff --git a/apps/web/src/locales/or/messages.json b/apps/web/src/locales/or/messages.json index a71551e7710..4ed205c1f73 100644 --- a/apps/web/src/locales/or/messages.json +++ b/apps/web/src/locales/or/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "Secure your account by requiring an additional step when logging in." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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.'" }, @@ -7016,12 +7019,6 @@ "next": { "message": "Next" }, - "usFlag": { - "message": "US flag" - }, - "euFlag": { - "message": "EU flag" - }, "selectedRegionFlag": { "message": "Selected region flag" }, diff --git a/apps/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index 80e7dd695d9..7b543a5f375 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "Zabezpiecz swoje konto poprzez wymóg wykonania dodatkowego kroku podczas logowania." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Włącz dwustopniowe logowanie dla swojej organizacji." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Wymuszaj opcje logowania dwustopniowego Bitwarden dla użytkownikó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.'" }, @@ -7016,12 +7019,6 @@ "next": { "message": "Dalej" }, - "usFlag": { - "message": "Flaga USA" - }, - "euFlag": { - "message": "Flaga UE" - }, "selectedRegionFlag": { "message": "Flaga wybranego regionu" }, diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index 3d931f81d07..107b604585b 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -1425,8 +1425,11 @@ "twoStepLoginDesc": { "message": "Proteja a sua conta exigindo uma etapa adicional ao iniciar sessão." }, - "twoStepLoginOrganizationDescStart": { - "message": "Requerer Login em Duas Etapas no Bitwarden para membros usando o ", + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { + "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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": { @@ -1925,7 +1928,7 @@ "message": "1 GB de armazenamento de arquivos encriptados." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Opções adicionais de login em duas etapas, como YubiKey, FIDO U2F e Duo." }, "premiumSignUpEmergency": { "message": "Acesso de Emergência" @@ -7016,12 +7019,6 @@ "next": { "message": "Avançar" }, - "usFlag": { - "message": "Bandeira dos EUA" - }, - "euFlag": { - "message": "Bandeira Europa" - }, "selectedRegionFlag": { "message": "Sinalização de região selecionada" }, diff --git a/apps/web/src/locales/pt_PT/messages.json b/apps/web/src/locales/pt_PT/messages.json index 0bf7e684824..133373fc64e 100644 --- a/apps/web/src/locales/pt_PT/messages.json +++ b/apps/web/src/locales/pt_PT/messages.json @@ -1425,8 +1425,11 @@ "twoStepLoginDesc": { "message": "Proteja a sua conta exigindo um passo adicional ao iniciar sessão." }, - "twoStepLoginOrganizationDescStart": { - "message": "Reforce as opções de verificação de dois passos do Bitwarden para os membros, utilizando as ", + "twoStepLoginTeamsDesc": { + "message": "Ativar verificação de dois passos para a sua organização." + }, + "twoStepLoginEnterpriseDescStart": { + "message": "Aplicar as opções de verificação de dois passos do Bitwarden para os membros, utilizando as ", "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": { @@ -1925,7 +1928,7 @@ "message": "1 GB de armazenamento encriptado para anexos de ficheiros." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Opções proprietárias de verificação de dois passos, como YubiKey e Duo." }, "premiumSignUpEmergency": { "message": "Acesso de emergência" @@ -3631,7 +3634,7 @@ "message": "Este item tem anexos de ficheiros antigos que precisam de ser corrigidos." }, "attachmentFixDescription": { - "message": "Este anexo utiliza uma encriptação desatualizada. Selecione \"Corrigir\" para transferir, voltar a encriptar e voltar a carregar o anexo." + "message": "Este anexo utiliza uma encriptação desatualizada. Selecione \"Corrigir\" para transferir, reencriptar e recarregar o anexo." }, "fix": { "message": "Corrigir", @@ -7016,12 +7019,6 @@ "next": { "message": "Avançar" }, - "usFlag": { - "message": "Bandeira dos EUA" - }, - "euFlag": { - "message": "Bandeira da UE" - }, "selectedRegionFlag": { "message": "Bandeira da região selecionada" }, @@ -7157,7 +7154,7 @@ "message": "Limite de contas de serviço (opcional)" }, "maxServiceAccountCost": { - "message": "Custo máximo potencial da conta de serviço" + "message": "Custo potencial máximo da conta de serviço" }, "loggedInExclamation": { "message": "Sessão iniciada!" diff --git a/apps/web/src/locales/ro/messages.json b/apps/web/src/locales/ro/messages.json index 27e9d4c17b7..73f07464a8b 100644 --- a/apps/web/src/locales/ro/messages.json +++ b/apps/web/src/locales/ro/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "Vă securizează contul solicitând un pas suplimentar la conectare." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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.'" }, @@ -7016,12 +7019,6 @@ "next": { "message": "Next" }, - "usFlag": { - "message": "US flag" - }, - "euFlag": { - "message": "EU flag" - }, "selectedRegionFlag": { "message": "Selected region flag" }, diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index af9a088993f..f63c97f69f6 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -982,7 +982,7 @@ "message": "Экспорт" }, "exportVault": { - "message": "Экспортировать хранилище" + "message": "Экспорт хранилища" }, "exportSecrets": { "message": "Экспорт секретов" @@ -1319,10 +1319,10 @@ "message": "Файл содержит неназначенные элементы." }, "selectFormat": { - "message": "Выберите формат файла импорта" + "message": "Выберите формат импортируемого файла" }, "selectImportFile": { - "message": "Выберите файл импорта" + "message": "Выберите импортируемый файл" }, "chooseFile": { "message": "Выбрать файл" @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "Защитите свою учетную запись, при помощи дополнительного шага при авторизации." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Включить двухэтапную аутентификацию для вашей организации." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Применить параметры двухэтапной аутентификации Bitwarden для пользователей, используя ", "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.'" }, @@ -3649,7 +3652,7 @@ "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." }, "fingerprintMatchInfo": { - "message": "Убедитесь, что ваше хранилище разблокировано и фраза отпечатка пальца совпадает на другом устройстве." + "message": "Убедитесь, что ваше хранилище разблокировано и фраза отпечатка совпадает на другом устройстве." }, "fingerprintPhraseHeader": { "message": "Фраза отпечатка" @@ -6775,7 +6778,7 @@ "message": "Удалить доступ" }, "checkForBreaches": { - "message": "Проверьте известные случаи утечки данных для этого пароля" + "message": "Проверять известные случаи утечки данных для этого пароля" }, "exposedMasterPassword": { "message": "Мастер-пароль скомпрометирован" @@ -7016,12 +7019,6 @@ "next": { "message": "Далее" }, - "usFlag": { - "message": "Флаг США" - }, - "euFlag": { - "message": "Флаг ЕС" - }, "selectedRegionFlag": { "message": "Флаг выбранного региона" }, diff --git a/apps/web/src/locales/si/messages.json b/apps/web/src/locales/si/messages.json index adff63eb2fc..cf61fe2da62 100644 --- a/apps/web/src/locales/si/messages.json +++ b/apps/web/src/locales/si/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "Secure your account by requiring an additional step when logging in." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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.'" }, @@ -7016,12 +7019,6 @@ "next": { "message": "Next" }, - "usFlag": { - "message": "US flag" - }, - "euFlag": { - "message": "EU flag" - }, "selectedRegionFlag": { "message": "Selected region flag" }, diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index fd1d6d707bf..f666bd0aa1f 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "Zabezpečte svoj účet požadovaním ďalšieho kroku pri prihlasovaní." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Povoľte dvojstupňové prihlásenie pre vašu organizáciu." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Vynútiť Bitwarden dvojstupňové prihlásenie pre členov použitím ", "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.'" }, @@ -1925,7 +1928,7 @@ "message": "1 GB šifrovaného úložiska pre prílohy." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Proprietárne možnosti dvojstupňového prihlásenia ako napríklad YubiKey a Duo." }, "premiumSignUpEmergency": { "message": "Núdzový prístup" @@ -7016,12 +7019,6 @@ "next": { "message": "Ďalej" }, - "usFlag": { - "message": "Vlajka USA" - }, - "euFlag": { - "message": "Vlajka EU" - }, "selectedRegionFlag": { "message": "Selected region flag" }, diff --git a/apps/web/src/locales/sl/messages.json b/apps/web/src/locales/sl/messages.json index c3f3e479f1f..a0f735bc2d3 100644 --- a/apps/web/src/locales/sl/messages.json +++ b/apps/web/src/locales/sl/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "Zavarujte svoj račun z dodatno zaščito pri prijavi." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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.'" }, @@ -7016,12 +7019,6 @@ "next": { "message": "Next" }, - "usFlag": { - "message": "US flag" - }, - "euFlag": { - "message": "EU flag" - }, "selectedRegionFlag": { "message": "Selected region flag" }, diff --git a/apps/web/src/locales/sr/messages.json b/apps/web/src/locales/sr/messages.json index f1ae3e829c1..926c1650e7c 100644 --- a/apps/web/src/locales/sr/messages.json +++ b/apps/web/src/locales/sr/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "Заштитите свој налог захтевањем додатног корака приликом пријављивања." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Омогућите пријаву у два корака за вашу организацију." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Примени Bitwarden опције за пријаву у два корака за чланове користећи ", "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.'" }, @@ -1925,7 +1928,7 @@ "message": "1ГБ шифровано складиште за прилоге." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Приоритарне опције пријаве у два корака као што су YubiKey и Duo." }, "premiumSignUpEmergency": { "message": "Улаз у хитним случајевима" @@ -2041,7 +2044,7 @@ } }, "paymentChargedWithUnpaidSubscription": { - "message": "Your payment method will be charged for any unpaid subscriptions." + "message": "Ваш начин плаћања ће бити наплаћен за све неплаћене претплате." }, "paymentChargedWithTrial": { "message": "Ваш план долази са бесплатним 7-дневним пробним периодом. Начин плаћања неће бити наплаћен док се пробно време не заврши. Наплата ће се вршити периодично, сваки $INTERVAL$. Можете отказати било када." @@ -7016,12 +7019,6 @@ "next": { "message": "Следеће" }, - "usFlag": { - "message": "Америчка застава" - }, - "euFlag": { - "message": "ЕУ застава" - }, "selectedRegionFlag": { "message": "Одабрана застава" }, diff --git a/apps/web/src/locales/sr_CS/messages.json b/apps/web/src/locales/sr_CS/messages.json index a50306355db..b0e7500e183 100644 --- a/apps/web/src/locales/sr_CS/messages.json +++ b/apps/web/src/locales/sr_CS/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "Secure your account by requiring an additional step when logging in." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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.'" }, @@ -7016,12 +7019,6 @@ "next": { "message": "Next" }, - "usFlag": { - "message": "US flag" - }, - "euFlag": { - "message": "EU flag" - }, "selectedRegionFlag": { "message": "Selected region flag" }, diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index 7d2e12239f7..5ca6ab8f146 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -1425,8 +1425,11 @@ "twoStepLoginDesc": { "message": "Säkra ditt konto genom att kräva ett ytterligare steg vid inloggning." }, - "twoStepLoginOrganizationDescStart": { - "message": "Kräv att medlemmar använder tvåstegsverifiering genom att använda ", + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { + "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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": { @@ -7016,12 +7019,6 @@ "next": { "message": "Nästa" }, - "usFlag": { - "message": "US flag" - }, - "euFlag": { - "message": "EU flag" - }, "selectedRegionFlag": { "message": "Selected region flag" }, diff --git a/apps/web/src/locales/te/messages.json b/apps/web/src/locales/te/messages.json index a71551e7710..4ed205c1f73 100644 --- a/apps/web/src/locales/te/messages.json +++ b/apps/web/src/locales/te/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "Secure your account by requiring an additional step when logging in." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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.'" }, @@ -7016,12 +7019,6 @@ "next": { "message": "Next" }, - "usFlag": { - "message": "US flag" - }, - "euFlag": { - "message": "EU flag" - }, "selectedRegionFlag": { "message": "Selected region flag" }, diff --git a/apps/web/src/locales/th/messages.json b/apps/web/src/locales/th/messages.json index 25206db2e37..7948cbeee12 100644 --- a/apps/web/src/locales/th/messages.json +++ b/apps/web/src/locales/th/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "Secure your account by requiring an additional step when logging in." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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.'" }, @@ -7016,12 +7019,6 @@ "next": { "message": "Next" }, - "usFlag": { - "message": "US flag" - }, - "euFlag": { - "message": "EU flag" - }, "selectedRegionFlag": { "message": "Selected region flag" }, diff --git a/apps/web/src/locales/tr/messages.json b/apps/web/src/locales/tr/messages.json index 55b2c51548e..d6b97c70418 100644 --- a/apps/web/src/locales/tr/messages.json +++ b/apps/web/src/locales/tr/messages.json @@ -1425,8 +1425,11 @@ "twoStepLoginDesc": { "message": "Oturum açarken ek bir adım talep ederek hesabınızı güvenceye alabilirsiniz." }, - "twoStepLoginOrganizationDescStart": { - "message": "Üyeler için Bitwarden iki aşamalı giriş seçeneklerini ", + "twoStepLoginTeamsDesc": { + "message": "Kuruluşunuz için iki adımlı oturum açmayı etkinleştirin." + }, + "twoStepLoginEnterpriseDescStart": { + "message": "Bitwarden'ı kullanarak üyeler için İki Adımlı Giriş seçeneklerini zorunlu kılın ", "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": { @@ -7016,12 +7019,6 @@ "next": { "message": "İleri" }, - "usFlag": { - "message": "ABD bayrağı" - }, - "euFlag": { - "message": "AB bayrağı" - }, "selectedRegionFlag": { "message": "Seçilen bölgenin bayrağı" }, diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index 4c336c0b30d..f341ba6b6a8 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -392,7 +392,7 @@ "message": "Редагування" }, "viewItem": { - "message": "Перегляд запису" + "message": "Переглянути запис" }, "new": { "message": "Новий", @@ -1425,8 +1425,11 @@ "twoStepLoginDesc": { "message": "Захистіть обліковий запис, вимагаючи додатковий крок перевірки під час входу." }, - "twoStepLoginOrganizationDescStart": { - "message": "Зобов'язувати учасників використовувати двоетапну перевірку Bitwarden за допомогою ", + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { + "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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": { @@ -1925,7 +1928,7 @@ "message": "1 ГБ зашифрованого сховища для файлів." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Додаткові можливості двоетапної авторизації, як-от YubiKey та Duo." }, "premiumSignUpEmergency": { "message": "Екстрений доступ" @@ -2041,7 +2044,7 @@ } }, "paymentChargedWithUnpaidSubscription": { - "message": "Your payment method will be charged for any unpaid subscriptions." + "message": "Ваш спосіб оплати буди використано для платежів за будь-які несплачені передплати." }, "paymentChargedWithTrial": { "message": "Ваш тарифний план має 7 днів безплатного пробного періоду. З вас не буде стягнуто плату до завершення цього періоду. Ви можете скасувати це в будь-який час." @@ -3084,7 +3087,7 @@ "message": "Пристрій" }, "view": { - "message": "Перегляд" + "message": "Переглянути" }, "invalidDateRange": { "message": "Недійсний проміжок часу." @@ -7016,12 +7019,6 @@ "next": { "message": "Далі" }, - "usFlag": { - "message": "Прапор США" - }, - "euFlag": { - "message": "Прапор ЄС" - }, "selectedRegionFlag": { "message": "Прапор вибраного регіону" }, diff --git a/apps/web/src/locales/vi/messages.json b/apps/web/src/locales/vi/messages.json index fb25525d61a..f7aa1449d12 100644 --- a/apps/web/src/locales/vi/messages.json +++ b/apps/web/src/locales/vi/messages.json @@ -1425,7 +1425,10 @@ "twoStepLoginDesc": { "message": "Bảo mật tài khoản bằng các phương pháp sau khi đăng nhập." }, - "twoStepLoginOrganizationDescStart": { + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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.'" }, @@ -7016,12 +7019,6 @@ "next": { "message": "Next" }, - "usFlag": { - "message": "US flag" - }, - "euFlag": { - "message": "EU flag" - }, "selectedRegionFlag": { "message": "Selected region flag" }, diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index be07ab7e208..7a4ecc7db08 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -609,7 +609,7 @@ "message": "使用设备登录" }, "loginWithDeviceEnabledNote": { - "message": "设备登录必须在 Bitwarden 应用程序的设置中设启用。需要其他选项吗?" + "message": "设备登录必须在 Bitwarden 应用程序的设置中启用。需要其他登录选项吗?" }, "loginWithMasterPassword": { "message": "使用主密码登录" @@ -1425,8 +1425,11 @@ "twoStepLoginDesc": { "message": "在登录时要求使用额外的步骤来保护您的账户。" }, - "twoStepLoginOrganizationDescStart": { - "message": "要为成员实施 Bitwarden 两步登录选项,请使用 ", + "twoStepLoginTeamsDesc": { + "message": "为您的组织启用两步登录。" + }, + "twoStepLoginEnterpriseDescStart": { + "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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": { @@ -1925,7 +1928,7 @@ "message": "1 GB 文件附件加密存储。" }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "专有的两步登录选项,如 YubiKey 和 Duo。" }, "premiumSignUpEmergency": { "message": "紧急访问" @@ -3649,7 +3652,7 @@ "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." }, "fingerprintMatchInfo": { - "message": "请确保您的密码库已解锁,并且指纹短语与其他设备匹配。" + "message": "请确保您的密码库已解锁,并且指纹短语与其他设备上的相匹配。" }, "fingerprintPhraseHeader": { "message": "指纹短语" @@ -7016,12 +7019,6 @@ "next": { "message": "下一步" }, - "usFlag": { - "message": "美国旗帜" - }, - "euFlag": { - "message": "欧盟旗帜" - }, "selectedRegionFlag": { "message": "选择的区域旗帜" }, diff --git a/apps/web/src/locales/zh_TW/messages.json b/apps/web/src/locales/zh_TW/messages.json index acdd1aa1a79..f7692a0a3e4 100644 --- a/apps/web/src/locales/zh_TW/messages.json +++ b/apps/web/src/locales/zh_TW/messages.json @@ -1425,8 +1425,11 @@ "twoStepLoginDesc": { "message": "在登入時執行額外的步驟來保護您的帳戶。" }, - "twoStepLoginOrganizationDescStart": { - "message": "讓成員使用 Bitwarden 兩步驟登入,藉由 ", + "twoStepLoginTeamsDesc": { + "message": "Enable two-step login for your organization." + }, + "twoStepLoginEnterpriseDescStart": { + "message": "Enforce Bitwarden Two-step Login options for members by using the ", "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": { @@ -7016,12 +7019,6 @@ "next": { "message": "Next" }, - "usFlag": { - "message": "US flag" - }, - "euFlag": { - "message": "EU flag" - }, "selectedRegionFlag": { "message": "Selected region flag" }, From 61e1bc1a1c1c36f29426003f8425337dcf8191a4 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Sat, 9 Sep 2023 00:05:37 +1000 Subject: [PATCH 44/46] [AC-1479][BEEEP] Refactor ConfigService to improve observable usage (#5602) * refactor ConfigService to use observables * make environmentService.urls a ReplaySubject --------- Co-authored-by: Hinton --- .../browser/src/background/main.background.ts | 13 +- .../src/background/runtime.background.ts | 4 +- .../services/browser-config.service.ts | 18 +- .../src/popup/services/init.service.ts | 6 +- .../src/popup/services/services.module.ts | 4 +- apps/desktop/src/app/app.component.ts | 2 +- apps/desktop/src/app/services/init.service.ts | 6 +- apps/web/src/app/app.component.ts | 2 +- ...ganization-subscription-cloud.component.ts | 2 +- .../billing/settings/add-credit.component.ts | 3 +- .../settings/organization-plans.component.ts | 2 +- .../environment-selector.component.ts | 2 +- apps/web/src/app/core/init.service.ts | 6 +- .../bit-web/src/app/auth/sso/sso.component.ts | 2 +- .../environment-selector.component.ts | 2 +- .../src/auth/components/sso.component.spec.ts | 2 +- .../src/auth/components/sso.component.ts | 2 +- .../components/two-factor.component.spec.ts | 2 +- .../auth/components/two-factor.component.ts | 2 +- .../directives/if-feature.directive.spec.ts | 23 +-- .../src/directives/if-feature.directive.ts | 17 +- .../src/guard/feature-flag.guard.spec.ts | 8 +- libs/angular/src/guard/feature-flag.guard.ts | 10 +- .../src/services/jslib-services.module.ts | 6 +- libs/common/src/enums/feature-flag.enum.ts | 3 + .../config/config.service.abstraction.ts | 22 ++- .../services/config/config.service.spec.ts | 174 ++++++++++++++++++ .../services/config/config.service.ts | 165 +++++++++-------- .../platform/services/environment.service.ts | 4 +- 29 files changed, 356 insertions(+), 158 deletions(-) create mode 100644 libs/common/src/platform/services/config/config.service.spec.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index f9963bcf7da..e646b98d414 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -35,7 +35,6 @@ import { UserVerificationApiService } from "@bitwarden/common/auth/services/user import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service"; import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; 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"; @@ -53,7 +52,6 @@ import { StateFactory } from "@bitwarden/common/platform/factories/state-factory 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"; -import { ConfigService } from "@bitwarden/common/platform/services/config/config.service"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; @@ -123,6 +121,7 @@ import { flagEnabled } from "../platform/flags"; import { UpdateBadge } from "../platform/listeners/update-badge"; import BrowserPopoutWindowService from "../platform/popup/browser-popout-window.service"; import { BrowserStateService as StateServiceAbstraction } from "../platform/services/abstractions/browser-state.service"; +import { BrowserConfigService } from "../platform/services/browser-config.service"; import { BrowserCryptoService } from "../platform/services/browser-crypto.service"; import { BrowserEnvironmentService } from "../platform/services/browser-environment.service"; import { BrowserI18nService } from "../platform/services/browser-i18n.service"; @@ -200,7 +199,7 @@ export default class MainBackground { avatarUpdateService: AvatarUpdateServiceAbstraction; mainContextMenuHandler: MainContextMenuHandler; cipherContextMenuHandler: CipherContextMenuHandler; - configService: ConfigServiceAbstraction; + configService: BrowserConfigService; configApiService: ConfigApiServiceAbstraction; devicesApiService: DevicesApiServiceAbstraction; devicesService: DevicesServiceAbstraction; @@ -533,12 +532,15 @@ export default class MainBackground { this.authService, this.messagingService ); + this.configApiService = new ConfigApiService(this.apiService, this.authService); - this.configService = new ConfigService( + + this.configService = new BrowserConfigService( this.stateService, this.configApiService, this.authService, - this.environmentService + this.environmentService, + true ); this.browserPopoutWindowService = new BrowserPopoutWindowService(); @@ -682,6 +684,7 @@ export default class MainBackground { await this.notificationBackground.init(); await this.commandsBackground.init(); + this.configService.init(); this.twoFactorService.init(); await this.tabsBackground.init(); diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index c1cfdf0420f..dcf828ef4a0 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -103,7 +103,7 @@ export default class RuntimeBackground { await this.main.refreshMenu(); }, 2000); this.main.avatarUpdateService.loadColorFromState(); - this.configService.fetchServerConfig(); + this.configService.triggerServerConfigFetch(); } break; case "openPopup": @@ -139,7 +139,7 @@ export default class RuntimeBackground { case "triggerAutofillScriptInjection": await this.autofillService.injectAutofillScripts( sender, - await this.configService.getFeatureFlagBool(FeatureFlag.AutofillV2) + await this.configService.getFeatureFlag(FeatureFlag.AutofillV2) ); break; case "bgCollectPageDetails": diff --git a/apps/browser/src/platform/services/browser-config.service.ts b/apps/browser/src/platform/services/browser-config.service.ts index 68237b4c206..f928fdd0726 100644 --- a/apps/browser/src/platform/services/browser-config.service.ts +++ b/apps/browser/src/platform/services/browser-config.service.ts @@ -1,6 +1,10 @@ -import { BehaviorSubject } from "rxjs"; +import { ReplaySubject } from "rxjs"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { ConfigService } from "@bitwarden/common/platform/services/config/config.service"; import { browserSession, sessionSync } from "../decorators/session-sync-observable"; @@ -8,5 +12,15 @@ import { browserSession, sessionSync } from "../decorators/session-sync-observab @browserSession export class BrowserConfigService extends ConfigService { @sessionSync({ initializer: ServerConfig.fromJSON }) - protected _serverConfig: BehaviorSubject; + protected _serverConfig: ReplaySubject; + + constructor( + stateService: StateService, + configApiService: ConfigApiServiceAbstraction, + authService: AuthService, + environmentService: EnvironmentService, + subscribe = false + ) { + super(stateService, configApiService, authService, environmentService, subscribe); + } } diff --git a/apps/browser/src/popup/services/init.service.ts b/apps/browser/src/popup/services/init.service.ts index 23ae6e8e892..c9a1ffb720e 100644 --- a/apps/browser/src/popup/services/init.service.ts +++ b/apps/browser/src/popup/services/init.service.ts @@ -4,6 +4,7 @@ import { AbstractThemingService } from "@bitwarden/angular/services/theming/them 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"; +import { ConfigService } from "@bitwarden/common/platform/services/config/config.service"; import { BrowserStateService as StateServiceAbstraction } from "../../platform/services/abstractions/browser-state.service"; @@ -17,7 +18,8 @@ export class InitService { private popupUtilsService: PopupUtilsService, private stateService: StateServiceAbstraction, private logService: LogServiceAbstraction, - private themingService: AbstractThemingService + private themingService: AbstractThemingService, + private configService: ConfigService ) {} init() { @@ -50,6 +52,8 @@ export class InitService { htmlEl.classList.add("force_redraw"); this.logService.info("Force redraw is on"); } + + this.configService.init(); }; } } diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 261f6abe37d..4e4f914f230 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -36,7 +36,6 @@ import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { LoginService } from "@bitwarden/common/auth/services/login.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -57,6 +56,7 @@ import { } from "@bitwarden/common/platform/abstractions/storage.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; +import { ConfigService } from "@bitwarden/common/platform/services/config/config.service"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { SearchService } from "@bitwarden/common/services/search.service"; @@ -495,7 +495,7 @@ function getBgService(service: keyof MainBackground) { deps: [StateServiceAbstraction, PlatformUtilsService], }, { - provide: ConfigServiceAbstraction, + provide: ConfigService, useClass: BrowserConfigService, deps: [ StateServiceAbstraction, diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index b5321a2bc83..39e2d2f8b11 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -232,7 +232,7 @@ export class AppComponent implements OnInit, OnDestroy { break; case "syncCompleted": await this.updateAppMenu(); - await this.configService.fetchServerConfig(); + this.configService.triggerServerConfigFetch(); break; case "openSettings": await this.openModal(SettingsComponent, this.settingsRef); diff --git a/apps/desktop/src/app/services/init.service.ts b/apps/desktop/src/app/services/init.service.ts index 34300aed931..0d60a1140f8 100644 --- a/apps/desktop/src/app/services/init.service.ts +++ b/apps/desktop/src/app/services/init.service.ts @@ -11,6 +11,7 @@ import { EnvironmentService as EnvironmentServiceAbstraction } from "@bitwarden/ import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; +import { ConfigService } from "@bitwarden/common/platform/services/config/config.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service"; @@ -35,7 +36,8 @@ export class InitService { private cryptoService: CryptoServiceAbstraction, private nativeMessagingService: NativeMessagingService, private themingService: AbstractThemingService, - private encryptService: EncryptService + private encryptService: EncryptService, + private configService: ConfigService ) {} init() { @@ -71,6 +73,8 @@ export class InitService { const containerService = new ContainerService(this.cryptoService, this.encryptService); containerService.attachToGlobal(this.win); + + this.configService.init(); }; } } diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index 3066dbe093c..5b265278423 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -138,7 +138,7 @@ export class AppComponent implements OnDestroy, OnInit { case "syncStarted": break; case "syncCompleted": - await this.configService.fetchServerConfig(); + this.configService.triggerServerConfigFetch(); break; case "upgradeOrganization": { const upgradeConfirmed = await this.dialogService.openSimpleDialog({ 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 dedc99edcb1..b419f743262 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 @@ -135,7 +135,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy this.loading = false; // Remove the remaining lines when the sm-ga-billing flag is deleted - const smBillingEnabled = await this.configService.getFeatureFlagBool( + const smBillingEnabled = await this.configService.getFeatureFlag( FeatureFlag.SecretsManagerBilling ); this.showSecretsManagerSubscribe = this.showSecretsManagerSubscribe && smBillingEnabled; diff --git a/apps/web/src/app/billing/settings/add-credit.component.ts b/apps/web/src/app/billing/settings/add-credit.component.ts index c59886b4b40..2a60e65f2ac 100644 --- a/apps/web/src/app/billing/settings/add-credit.component.ts +++ b/apps/web/src/app/billing/settings/add-credit.component.ts @@ -7,6 +7,7 @@ import { Output, ViewChild, } from "@angular/core"; +import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; @@ -79,7 +80,7 @@ export class AddCreditComponent implements OnInit { this.email = this.subject; this.ppButtonCustomField = "user_id:" + this.userId; } - this.region = await this.configService.getCloudRegion(); + this.region = await firstValueFrom(this.configService.cloudRegion$); this.ppButtonCustomField += ",account_credit:1"; this.ppButtonCustomField += `,region:${this.region}`; this.returnUrl = window.location.href; diff --git a/apps/web/src/app/billing/settings/organization-plans.component.ts b/apps/web/src/app/billing/settings/organization-plans.component.ts index c84d9511a17..6b0bd65d196 100644 --- a/apps/web/src/app/billing/settings/organization-plans.component.ts +++ b/apps/web/src/app/billing/settings/organization-plans.component.ts @@ -169,7 +169,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { this.singleOrgPolicyAppliesToActiveUser = policyAppliesToActiveUser; }); - this.showSecretsManagerSubscribe = await this.configService.getFeatureFlagBool( + this.showSecretsManagerSubscribe = await this.configService.getFeatureFlag( FeatureFlag.SecretsManagerBilling, false ); diff --git a/apps/web/src/app/components/environment-selector/environment-selector.component.ts b/apps/web/src/app/components/environment-selector/environment-selector.component.ts index 99146134b68..3c1c5a19732 100644 --- a/apps/web/src/app/components/environment-selector/environment-selector.component.ts +++ b/apps/web/src/app/components/environment-selector/environment-selector.component.ts @@ -25,7 +25,7 @@ export class EnvironmentSelectorComponent implements OnInit { routeAndParams: string; async ngOnInit() { - this.euServerFlagEnabled = await this.configService.getFeatureFlagBool( + this.euServerFlagEnabled = await this.configService.getFeatureFlag( FeatureFlag.DisplayEuEnvironmentFlag ); const domain = Utils.getDomain(window.location.href); diff --git a/apps/web/src/app/core/init.service.ts b/apps/web/src/app/core/init.service.ts index 3437c4f3e93..f171217d3cd 100644 --- a/apps/web/src/app/core/init.service.ts +++ b/apps/web/src/app/core/init.service.ts @@ -13,6 +13,7 @@ import { } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; +import { ConfigService } from "@bitwarden/common/platform/services/config/config.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service"; @@ -32,7 +33,8 @@ export class InitService { private stateService: StateServiceAbstraction, private cryptoService: CryptoServiceAbstraction, private themingService: AbstractThemingService, - private encryptService: EncryptService + private encryptService: EncryptService, + private configService: ConfigService ) {} init() { @@ -57,6 +59,8 @@ export class InitService { await this.themingService.monitorThemeChanges(); const containerService = new ContainerService(this.cryptoService, this.encryptService); containerService.attachToGlobal(this.win); + + this.configService.init(); }; } } diff --git a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts index e159d4744b4..21cda4bbb05 100644 --- a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts +++ b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts @@ -235,7 +235,7 @@ export class SsoComponent implements OnInit, OnDestroy { ) .subscribe(); - const tdeFeatureFlag = await this.configService.getFeatureFlagBool( + const tdeFeatureFlag = await this.configService.getFeatureFlag( FeatureFlag.TrustedDeviceEncryption ); diff --git a/libs/angular/src/auth/components/environment-selector.component.ts b/libs/angular/src/auth/components/environment-selector.component.ts index 498d72b01ee..19dc95dbcf6 100644 --- a/libs/angular/src/auth/components/environment-selector.component.ts +++ b/libs/angular/src/auth/components/environment-selector.component.ts @@ -89,7 +89,7 @@ export class EnvironmentSelectorComponent implements OnInit, OnDestroy { async updateEnvironmentInfo() { this.selectedEnvironment = this.environmentService.selectedRegion; - this.euServerFlagEnabled = await this.configService.getFeatureFlagBool( + this.euServerFlagEnabled = await this.configService.getFeatureFlag( FeatureFlag.DisplayEuEnvironmentFlag ); } diff --git a/libs/angular/src/auth/components/sso.component.spec.ts b/libs/angular/src/auth/components/sso.component.spec.ts index 68c41b66119..894232af443 100644 --- a/libs/angular/src/auth/components/sso.component.spec.ts +++ b/libs/angular/src/auth/components/sso.component.spec.ts @@ -331,7 +331,7 @@ describe("SsoComponent", () => { describe("Trusted Device Encryption scenarios", () => { beforeEach(() => { - mockConfigService.getFeatureFlagBool.mockResolvedValue(true); // TDE enabled + mockConfigService.getFeatureFlag.mockResolvedValue(true); // TDE enabled }); describe("Given Trusted Device Encryption is enabled and user needs to set a master password", () => { diff --git a/libs/angular/src/auth/components/sso.component.ts b/libs/angular/src/auth/components/sso.component.ts index 8030e880a54..7e6aca7ec07 100644 --- a/libs/angular/src/auth/components/sso.component.ts +++ b/libs/angular/src/auth/components/sso.component.ts @@ -242,7 +242,7 @@ export class SsoComponent { private async isTrustedDeviceEncEnabled( trustedDeviceOption: TrustedDeviceUserDecryptionOption ): Promise { - const trustedDeviceEncryptionFeatureActive = await this.configService.getFeatureFlagBool( + const trustedDeviceEncryptionFeatureActive = await this.configService.getFeatureFlag( FeatureFlag.TrustedDeviceEncryption ); diff --git a/libs/angular/src/auth/components/two-factor.component.spec.ts b/libs/angular/src/auth/components/two-factor.component.spec.ts index 470c9d4eb7c..9e147f33573 100644 --- a/libs/angular/src/auth/components/two-factor.component.spec.ts +++ b/libs/angular/src/auth/components/two-factor.component.spec.ts @@ -376,7 +376,7 @@ describe("TwoFactorComponent", () => { describe("Trusted Device Encryption scenarios", () => { beforeEach(() => { - mockConfigService.getFeatureFlagBool.mockResolvedValue(true); + mockConfigService.getFeatureFlag.mockResolvedValue(true); }); describe("Given Trusted Device Encryption is enabled and user needs to set a master password", () => { diff --git a/libs/angular/src/auth/components/two-factor.component.ts b/libs/angular/src/auth/components/two-factor.component.ts index 0a06d393158..9a6352283a0 100644 --- a/libs/angular/src/auth/components/two-factor.component.ts +++ b/libs/angular/src/auth/components/two-factor.component.ts @@ -257,7 +257,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI trustedDeviceOption: TrustedDeviceUserDecryptionOption ): Promise { const ssoTo2faFlowActive = this.route.snapshot.queryParamMap.get("sso") === "true"; - const trustedDeviceEncryptionFeatureActive = await this.configService.getFeatureFlagBool( + const trustedDeviceEncryptionFeatureActive = await this.configService.getFeatureFlag( FeatureFlag.TrustedDeviceEncryption ); diff --git a/libs/angular/src/directives/if-feature.directive.spec.ts b/libs/angular/src/directives/if-feature.directive.spec.ts index bf73a172a55..9d492b7f01f 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 } from "@bitwarden/common/enums/feature-flag.enum"; +import { FeatureFlag, FeatureFlagValue } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -41,21 +41,12 @@ describe("IfFeatureDirective", () => { let content: HTMLElement; let mockConfigService: MockProxy; - const mockConfigFlagValue = (flag: FeatureFlag, flagValue: any) => { - if (typeof flagValue === "boolean") { - mockConfigService.getFeatureFlagBool.mockImplementation((f, defaultValue = false) => - flag == f ? Promise.resolve(flagValue) : Promise.resolve(defaultValue) - ); - } else if (typeof flagValue === "string") { - mockConfigService.getFeatureFlagString.mockImplementation((f, defaultValue = "") => - flag == f ? Promise.resolve(flagValue) : Promise.resolve(defaultValue) - ); - } else if (typeof flagValue === "number") { - mockConfigService.getFeatureFlagNumber.mockImplementation((f, defaultValue = 0) => - flag == f ? Promise.resolve(flagValue) : Promise.resolve(defaultValue) - ); - } + const mockConfigFlagValue = (flag: FeatureFlag, flagValue: FeatureFlagValue) => { + mockConfigService.getFeatureFlag.mockImplementation((f, defaultValue) => + flag == f ? Promise.resolve(flagValue) : Promise.resolve(defaultValue) + ); }; + const queryContent = (testId: string) => fixture.debugElement.query(By.css(`[data-testid="${testId}"]`))?.nativeElement; @@ -126,7 +117,7 @@ describe("IfFeatureDirective", () => { }); it("hides content when the directive throws an unexpected exception", async () => { - mockConfigService.getFeatureFlagBool.mockImplementation(() => Promise.reject("Some error")); + mockConfigService.getFeatureFlag.mockImplementation(() => Promise.reject("Some error")); fixture.detectChanges(); await fixture.whenStable(); diff --git a/libs/angular/src/directives/if-feature.directive.ts b/libs/angular/src/directives/if-feature.directive.ts index 1a0ee35dc68..e9aca531bb7 100644 --- a/libs/angular/src/directives/if-feature.directive.ts +++ b/libs/angular/src/directives/if-feature.directive.ts @@ -1,12 +1,9 @@ import { Directive, Input, OnInit, TemplateRef, ViewContainerRef } from "@angular/core"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { FeatureFlag, FeatureFlagValue } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -// Replace this with a type safe lookup of the feature flag values in PM-2282 -type FlagValue = boolean | number | string; - /** * Directive that conditionally renders the element when the feature flag is enabled and/or * matches the value specified by {@link appIfFeatureValue}. @@ -26,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: FlagValue = true; + @Input() appIfFeatureValue: FeatureFlagValue = true; private hasView = false; @@ -39,15 +36,7 @@ export class IfFeatureDirective implements OnInit { async ngOnInit() { try { - let flagValue: FlagValue; - - if (typeof this.appIfFeatureValue === "boolean") { - flagValue = await this.configService.getFeatureFlagBool(this.appIfFeature); - } else if (typeof this.appIfFeatureValue === "number") { - flagValue = await this.configService.getFeatureFlagNumber(this.appIfFeature); - } else if (typeof this.appIfFeatureValue === "string") { - flagValue = await this.configService.getFeatureFlagString(this.appIfFeature); - } + const flagValue = await this.configService.getFeatureFlag(this.appIfFeature); if (this.appIfFeatureValue === flagValue) { if (!this.hasView) { diff --git a/libs/angular/src/guard/feature-flag.guard.spec.ts b/libs/angular/src/guard/feature-flag.guard.spec.ts index bba879278ff..1ac2a90ae0d 100644 --- a/libs/angular/src/guard/feature-flag.guard.spec.ts +++ b/libs/angular/src/guard/feature-flag.guard.spec.ts @@ -30,15 +30,15 @@ describe("canAccessFeature", () => { // Mock the correct getter based on the type of flagValue; also mock default values if one is not provided if (typeof flagValue === "boolean") { - mockConfigService.getFeatureFlagBool.mockImplementation((flag, defaultValue = false) => + mockConfigService.getFeatureFlag.mockImplementation((flag, defaultValue = false) => flag == testFlag ? Promise.resolve(flagValue) : Promise.resolve(defaultValue) ); } else if (typeof flagValue === "string") { - mockConfigService.getFeatureFlagString.mockImplementation((flag, defaultValue = "") => + mockConfigService.getFeatureFlag.mockImplementation((flag, defaultValue = "") => flag == testFlag ? Promise.resolve(flagValue) : Promise.resolve(defaultValue) ); } else if (typeof flagValue === "number") { - mockConfigService.getFeatureFlagNumber.mockImplementation((flag, defaultValue = 0) => + mockConfigService.getFeatureFlag.mockImplementation((flag, defaultValue = 0) => flag == testFlag ? Promise.resolve(flagValue) : Promise.resolve(defaultValue) ); } @@ -143,7 +143,7 @@ describe("canAccessFeature", () => { it("fails to navigate when the config service throws an unexpected exception", async () => { const { router } = setup(canAccessFeature(testFlag), true); - mockConfigService.getFeatureFlagBool.mockImplementation(() => Promise.reject("Some error")); + mockConfigService.getFeatureFlag.mockImplementation(() => Promise.reject("Some error")); await router.navigate([featureRoute]); diff --git a/libs/angular/src/guard/feature-flag.guard.ts b/libs/angular/src/guard/feature-flag.guard.ts index f4596f3cf4b..d9297cbd978 100644 --- a/libs/angular/src/guard/feature-flag.guard.ts +++ b/libs/angular/src/guard/feature-flag.guard.ts @@ -29,16 +29,8 @@ export const canAccessFeature = ( const i18nService = inject(I18nService); const logService = inject(LogService); - let flagValue: FlagValue; - try { - if (typeof requiredFlagValue === "boolean") { - flagValue = await configService.getFeatureFlagBool(featureFlag); - } else if (typeof requiredFlagValue === "number") { - flagValue = await configService.getFeatureFlagNumber(featureFlag); - } else if (typeof requiredFlagValue === "string") { - flagValue = await configService.getFeatureFlagString(featureFlag); - } + const flagValue = await configService.getFeatureFlag(featureFlag); if (flagValue === requiredFlagValue) { return true; diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index df64c25c914..5d279657101 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -640,7 +640,7 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction"; useClass: SyncNotifierService, }, { - provide: ConfigServiceAbstraction, + provide: ConfigService, useClass: ConfigService, deps: [ StateServiceAbstraction, @@ -649,6 +649,10 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction"; EnvironmentServiceAbstraction, ], }, + { + provide: ConfigServiceAbstraction, + useExisting: ConfigService, + }, { provide: ConfigApiServiceAbstraction, useClass: ConfigApiService, diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 7016849b3bc..8f30478ced5 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -5,3 +5,6 @@ export enum FeatureFlag { AutofillV2 = "autofill-v2", SecretsManagerBilling = "sm-ga-billing", } + +// Replace this with a type safe lookup of the feature flag values in PM-2282 +export type FeatureFlagValue = number | string | boolean; diff --git a/libs/common/src/platform/abstractions/config/config.service.abstraction.ts b/libs/common/src/platform/abstractions/config/config.service.abstraction.ts index 13e44b9f5eb..59f87b0fa29 100644 --- a/libs/common/src/platform/abstractions/config/config.service.abstraction.ts +++ b/libs/common/src/platform/abstractions/config/config.service.abstraction.ts @@ -1,14 +1,26 @@ import { Observable } from "rxjs"; import { FeatureFlag } from "../../../enums/feature-flag.enum"; +import { Region } from "../environment.service"; import { ServerConfig } from "./server-config"; export abstract class ConfigServiceAbstraction { serverConfig$: Observable; - fetchServerConfig: () => Promise; - getFeatureFlagBool: (key: FeatureFlag, defaultValue?: boolean) => Promise; - getFeatureFlagString: (key: FeatureFlag, defaultValue?: string) => Promise; - getFeatureFlagNumber: (key: FeatureFlag, defaultValue?: number) => Promise; - getCloudRegion: (defaultValue?: string) => Promise; + cloudRegion$: Observable; + getFeatureFlag$: ( + key: FeatureFlag, + defaultValue?: T + ) => Observable; + getFeatureFlag: ( + key: FeatureFlag, + defaultValue?: T + ) => Promise; + + /** + * Force ConfigService to fetch an updated config from the server and emit it from serverConfig$ + * @deprecated The service implementation should subscribe to an observable and use that to trigger a new fetch from + * server instead + */ + triggerServerConfigFetch: () => void; } diff --git a/libs/common/src/platform/services/config/config.service.spec.ts b/libs/common/src/platform/services/config/config.service.spec.ts new file mode 100644 index 00000000000..511ecfd5c86 --- /dev/null +++ b/libs/common/src/platform/services/config/config.service.spec.ts @@ -0,0 +1,174 @@ +import { MockProxy, mock } from "jest-mock-extended"; +import { ReplaySubject, skip, take } from "rxjs"; + +import { AuthService } from "../../../auth/abstractions/auth.service"; +import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; +import { ConfigApiServiceAbstraction } from "../../abstractions/config/config-api.service.abstraction"; +import { ServerConfig } from "../../abstractions/config/server-config"; +import { EnvironmentService } from "../../abstractions/environment.service"; +import { StateService } from "../../abstractions/state.service"; +import { ServerConfigData } from "../../models/data/server-config.data"; +import { + EnvironmentServerConfigResponse, + ServerConfigResponse, + ThirdPartyServerConfigResponse, +} from "../../models/response/server-config.response"; + +import { ConfigService } from "./config.service"; + +describe("ConfigService", () => { + let stateService: MockProxy; + let configApiService: MockProxy; + let authService: MockProxy; + let environmentService: MockProxy; + + let serverResponseCount: number; // increments to track distinct responses received from server + + // Observables will start emitting as soon as this is created, so only create it + // after everything is mocked + const configServiceFactory = () => { + const configService = new ConfigService( + stateService, + configApiService, + authService, + environmentService + ); + configService.init(); + return configService; + }; + + beforeEach(() => { + stateService = mock(); + configApiService = mock(); + authService = mock(); + environmentService = mock(); + environmentService.urls = new ReplaySubject(1); + + serverResponseCount = 1; + configApiService.get.mockImplementation(() => + Promise.resolve(serverConfigResponseFactory("server" + serverResponseCount++)) + ); + + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it("Loads config from storage", (done) => { + const storedConfigData = serverConfigDataFactory("storedConfig"); + stateService.getServerConfig.mockResolvedValueOnce(storedConfigData); + + const configService = configServiceFactory(); + + configService.serverConfig$.pipe(take(1)).subscribe((config) => { + expect(config).toEqual(new ServerConfig(storedConfigData)); + expect(stateService.getServerConfig).toHaveBeenCalledTimes(1); + expect(stateService.setServerConfig).not.toHaveBeenCalled(); + done(); + }); + }); + + describe("Fetches config from server", () => { + beforeEach(() => { + stateService.getServerConfig.mockResolvedValueOnce(null); + }); + + it.each([1, 2, 3])( + "after %p hour/s", + (hours: number, done: jest.DoneCallback) => { + const configService = configServiceFactory(); + + // skip initial load from storage, plus previous hours (if any) + configService.serverConfig$.pipe(skip(hours), take(1)).subscribe((config) => { + try { + expect(config.gitHash).toEqual("server" + hours); + expect(configApiService.get).toHaveBeenCalledTimes(hours); + done(); + } catch (e) { + done(e); + } + }); + + const oneHourInMs = 1000 * 3600; + jest.advanceTimersByTime(oneHourInMs * hours + 1); + } + ); + + it("when environment URLs change", (done) => { + const configService = configServiceFactory(); + + // skip initial load from storage + configService.serverConfig$.pipe(skip(1), take(1)).subscribe((config) => { + try { + expect(config.gitHash).toEqual("server1"); + done(); + } catch (e) { + done(e); + } + }); + + (environmentService.urls as ReplaySubject).next(); + }); + + it("when triggerServerConfigFetch() is called", (done) => { + const configService = configServiceFactory(); + + // skip initial load from storage + configService.serverConfig$.pipe(skip(1), take(1)).subscribe((config) => { + try { + expect(config.gitHash).toEqual("server1"); + done(); + } catch (e) { + done(e); + } + }); + + configService.triggerServerConfigFetch(); + }); + }); + + it("Saves server config to storage when the user is logged in", (done) => { + stateService.getServerConfig.mockResolvedValueOnce(null); + authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.Locked); + const configService = configServiceFactory(); + + // skip initial load from storage + configService.serverConfig$.pipe(skip(1), take(1)).subscribe(() => { + try { + expect(stateService.setServerConfig).toHaveBeenCalledWith( + expect.objectContaining({ gitHash: "server1" }) + ); + done(); + } catch (e) { + done(e); + } + }); + + configService.triggerServerConfigFetch(); + }); +}); + +function serverConfigDataFactory(gitHash: string) { + return new ServerConfigData(serverConfigResponseFactory(gitHash)); +} + +function serverConfigResponseFactory(gitHash: string) { + return new ServerConfigResponse({ + version: "myConfigVersion", + gitHash: gitHash, + server: new ThirdPartyServerConfigResponse({ + name: "myThirdPartyServer", + url: "www.example.com", + }), + environment: new EnvironmentServerConfigResponse({ + vault: "vault.example.com", + }), + featureStates: { + feat1: "off", + feat2: "on", + feat3: "off", + }, + }); +} diff --git a/libs/common/src/platform/services/config/config.service.ts b/libs/common/src/platform/services/config/config.service.ts index 5b0325b68ac..c3a5fab4d70 100644 --- a/libs/common/src/platform/services/config/config.service.ts +++ b/libs/common/src/platform/services/config/config.service.ts @@ -1,103 +1,106 @@ -import { BehaviorSubject, concatMap, from, timer } from "rxjs"; +import { + ReplaySubject, + Subject, + concatMap, + delayWhen, + filter, + firstValueFrom, + from, + map, + merge, + timer, +} from "rxjs"; import { AuthService } from "../../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; -import { FeatureFlag } from "../../../enums/feature-flag.enum"; +import { FeatureFlag, FeatureFlagValue } from "../../../enums/feature-flag.enum"; import { ConfigApiServiceAbstraction } from "../../abstractions/config/config-api.service.abstraction"; import { ConfigServiceAbstraction } from "../../abstractions/config/config.service.abstraction"; import { ServerConfig } from "../../abstractions/config/server-config"; -import { EnvironmentService } from "../../abstractions/environment.service"; +import { EnvironmentService, Region } from "../../abstractions/environment.service"; import { StateService } from "../../abstractions/state.service"; import { ServerConfigData } from "../../models/data/server-config.data"; +const ONE_HOUR_IN_MILLISECONDS = 1000 * 3600; + export class ConfigService implements ConfigServiceAbstraction { - protected _serverConfig = new BehaviorSubject(null); + protected _serverConfig = new ReplaySubject(1); serverConfig$ = this._serverConfig.asObservable(); + private _forceFetchConfig = new Subject(); + private inited = false; + + cloudRegion$ = this.serverConfig$.pipe( + map((config) => config?.environment?.cloudRegion ?? Region.US) + ); constructor( private stateService: StateService, private configApiService: ConfigApiServiceAbstraction, private authService: AuthService, - private environmentService: EnvironmentService - ) { - // Re-fetch the server config every hour - timer(0, 1000 * 3600) - .pipe(concatMap(() => from(this.fetchServerConfig()))) - .subscribe((serverConfig) => { - this._serverConfig.next(serverConfig); - }); + private environmentService: EnvironmentService, - this.environmentService.urls.subscribe(() => { - this.fetchServerConfig(); - }); - } + // Used to avoid duplicate subscriptions, e.g. in browser between the background and popup + private subscribe = true + ) {} - async fetchServerConfig(): Promise { - try { - const response = await this.configApiService.get(); - - if (response == null) { - return; - } - - const data = new ServerConfigData(response); - const serverConfig = new ServerConfig(data); - this._serverConfig.next(serverConfig); - - const userAuthStatus = await this.authService.getAuthStatus(); - if (userAuthStatus !== AuthenticationStatus.LoggedOut) { - // Store the config for offline use if the user is logged in - await this.stateService.setServerConfig(data); - this.environmentService.setCloudWebVaultUrl(data.environment?.cloudRegion); - } - // Always return new server config from server to calling method - // to ensure up to date information - // This change is specifically for the getFeatureFlag > buildServerConfig flow - // for locked or logged in users. - return serverConfig; - } catch { - return null; - } - } - - async getFeatureFlagBool(key: FeatureFlag, defaultValue = false): Promise { - return await this.getFeatureFlag(key, defaultValue); - } - - async getFeatureFlagString(key: FeatureFlag, defaultValue = ""): Promise { - return await this.getFeatureFlag(key, defaultValue); - } - - async getFeatureFlagNumber(key: FeatureFlag, defaultValue = 0): Promise { - return await this.getFeatureFlag(key, defaultValue); - } - - async getCloudRegion(defaultValue = "US"): Promise { - const serverConfig = await this.buildServerConfig(); - return serverConfig.environment?.cloudRegion ?? defaultValue; - } - - private async getFeatureFlag(key: FeatureFlag, defaultValue: T): Promise { - const serverConfig = await this.buildServerConfig(); - if ( - serverConfig == null || - serverConfig.featureStates == null || - serverConfig.featureStates[key] == null - ) { - return defaultValue; - } - return serverConfig.featureStates[key] as T; - } - - private async buildServerConfig(): Promise { - const data = await this.stateService.getServerConfig(); - const domain = data ? new ServerConfig(data) : this._serverConfig.getValue(); - - if (domain == null || !domain.isValid() || domain.expiresSoon()) { - const value = await this.fetchServerConfig(); - return value ?? domain; + init() { + if (!this.subscribe || this.inited) { + return; } - return domain; + // Get config from storage on initial load + const fromStorage = from(this.stateService.getServerConfig()).pipe( + map((data) => (data == null ? null : new ServerConfig(data))) + ); + + fromStorage.subscribe((config) => this._serverConfig.next(config)); + + // Fetch config from server + // If you need to fetch a new config when an event occurs, add an observable that emits on that event here + merge( + timer(ONE_HOUR_IN_MILLISECONDS, ONE_HOUR_IN_MILLISECONDS), // after 1 hour, then every hour + this.environmentService.urls, // when environment URLs change (including when app is started) + this._forceFetchConfig // manual + ) + .pipe( + delayWhen(() => fromStorage), // wait until storage has emitted first to avoid a race condition + concatMap(() => this.configApiService.get()), + filter((response) => response != null), + map((response) => new ServerConfigData(response)), + delayWhen((data) => this.saveConfig(data)), + map((data) => new ServerConfig(data)) + ) + .subscribe((config) => this._serverConfig.next(config)); + + this.inited = true; + } + + getFeatureFlag$(key: FeatureFlag, defaultValue?: T) { + return this.serverConfig$.pipe( + map((serverConfig) => { + if (serverConfig?.featureStates == null || serverConfig.featureStates[key] == null) { + return defaultValue; + } + + return serverConfig.featureStates[key] as T; + }) + ); + } + + async getFeatureFlag(key: FeatureFlag, defaultValue?: T) { + return await firstValueFrom(this.getFeatureFlag$(key, defaultValue)); + } + + triggerServerConfigFetch() { + this._forceFetchConfig.next(); + } + + private async saveConfig(data: ServerConfigData) { + if ((await this.authService.getAuthStatus()) === AuthenticationStatus.LoggedOut) { + return; + } + + await this.stateService.setServerConfig(data); + this.environmentService.setCloudWebVaultUrl(data.environment?.cloudRegion); } } diff --git a/libs/common/src/platform/services/environment.service.ts b/libs/common/src/platform/services/environment.service.ts index 770de20f834..d85341a68c2 100644 --- a/libs/common/src/platform/services/environment.service.ts +++ b/libs/common/src/platform/services/environment.service.ts @@ -1,4 +1,4 @@ -import { concatMap, Observable, Subject } from "rxjs"; +import { concatMap, Observable, ReplaySubject } from "rxjs"; import { EnvironmentUrls } from "../../auth/models/domain/environment-urls"; import { @@ -9,7 +9,7 @@ import { import { StateService } from "../abstractions/state.service"; export class EnvironmentService implements EnvironmentServiceAbstraction { - private readonly urlsSubject = new Subject(); + private readonly urlsSubject = new ReplaySubject(1); urls: Observable = this.urlsSubject.asObservable(); selectedRegion?: Region; initialized = false; From da06f1e5def7fe5f58968992ff47e9f9e829a2ac Mon Sep 17 00:00:00 2001 From: Jonathan Prusik Date: Fri, 8 Sep 2023 10:25:32 -0400 Subject: [PATCH 45/46] [PM-3612] Bug - Reprompt prevents autofill keyboard shortcut from cycling fill ciphers (#6096) * cycle last used cipher on subsequent keyboard shortcut use on a page * incorporate master password existence check * cycle next cipher before reprompt Co-authored-by: Cesar Gonzalez * replace hasMasterPassword with hasMasterPasswordAndMasterKeyHash --------- Co-authored-by: Cesar Gonzalez --- apps/browser/src/autofill/services/autofill.service.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index 9d5dfb8a201..dc2a4918c3b 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -265,9 +265,14 @@ export default class AutofillService implements AutofillServiceInterface { } if ( - cipher.reprompt !== CipherRepromptType.None && + cipher.reprompt === CipherRepromptType.Password && + // If the master password has is not available, reprompt will error (await this.userVerificationService.hasMasterPasswordAndMasterKeyHash()) ) { + if (fromCommand) { + this.cipherService.updateLastUsedIndexForUrl(tab.url); + } + await BrowserApi.tabSendMessageData(tab, "passwordReprompt", { cipherId: cipher.id, action: "autofill", From d149894aadda3f85b4e46003cfc0022030c4991a Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Fri, 8 Sep 2023 18:38:46 +0200 Subject: [PATCH 46/46] [PM-2643] Resolve DUO iframe not being clickable (#6219) --- apps/desktop/src/index.html | 2 +- apps/desktop/src/scss/environment.scss | 26 ------------------- .../src/vault/app/vault/vault.component.ts | 2 -- 3 files changed, 1 insertion(+), 29 deletions(-) diff --git a/apps/desktop/src/index.html b/apps/desktop/src/index.html index 6005361c0c7..23049d40d89 100644 --- a/apps/desktop/src/index.html +++ b/apps/desktop/src/index.html @@ -11,7 +11,7 @@ Bitwarden - +
    diff --git a/apps/desktop/src/scss/environment.scss b/apps/desktop/src/scss/environment.scss index b18032f1a71..eae8b4a2d5b 100644 --- a/apps/desktop/src/scss/environment.scss +++ b/apps/desktop/src/scss/environment.scss @@ -1,30 +1,4 @@ html.os_macos { - body.layout_frontend { - -webkit-app-region: drag; - - button, - a, - i, - b, - span, - input, - p, - h1, - h2, - h3, - h4, - h5, - h6, - img, - select, - textarea, - label, - .box, - .modal-backdrop { - -webkit-app-region: no-drag; - } - } - .vault .header-search { -webkit-app-region: drag; diff --git a/apps/desktop/src/vault/app/vault/vault.component.ts b/apps/desktop/src/vault/app/vault/vault.component.ts index a058f080543..194cee098f7 100644 --- a/apps/desktop/src/vault/app/vault/vault.component.ts +++ b/apps/desktop/src/vault/app/vault/vault.component.ts @@ -207,7 +207,6 @@ export class VaultComponent implements OnInit, OnDestroy { if (!this.syncService.syncInProgress) { await this.load(); } - document.body.classList.remove("layout_frontend"); this.searchBarService.setEnabled(true); this.searchBarService.setPlaceholderText(this.i18nService.t("searchVault")); @@ -226,7 +225,6 @@ export class VaultComponent implements OnInit, OnDestroy { ngOnDestroy() { this.searchBarService.setEnabled(false); this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); - document.body.classList.add("layout_frontend"); } async load() {